access control and command handling with regex

This commit is contained in:
Ondřej Hruška 2021-08-22 01:50:07 +02:00
parent 5a631f785e
commit 99d9b83c68
No known key found for this signature in database
GPG key ID: 2C5FD5035250423D
7 changed files with 793 additions and 113 deletions

30
Cargo.lock generated
View file

@ -530,12 +530,15 @@ dependencies = [
"futures 0.3.16", "futures 0.3.16",
"log 0.4.14", "log 0.4.14",
"native-tls", "native-tls",
"once_cell",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"smart-default", "smart-default",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"voca_rs",
"websocket", "websocket",
] ]
@ -1777,6 +1780,16 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
[[package]]
name = "stfu8"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf70433e3300a3c395d06606a700cdf4205f4f14dbae2c6833127c6bb22db77"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -2144,6 +2157,12 @@ dependencies = [
"smallvec 0.6.10", "smallvec 0.6.10",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.6" version = "0.1.6"
@ -2215,6 +2234,17 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "voca_rs"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec29ce40c253a1579092852bbea5cb4fbcf34c04b91d8127300202aa17c998fc"
dependencies = [
"regex",
"stfu8",
"unicode-segmentation",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.0" version = "0.3.0"

View file

@ -26,6 +26,9 @@ tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.7" tokio-stream = "0.1.7"
thiserror = "1.0.26" thiserror = "1.0.26"
futures = "0.3" futures = "0.3"
voca_rs = "1.13.0"
regex = "1.5.4"
once_cell = "1.8.0"
native-tls = "0.2.8" native-tls = "0.2.8"
websocket = "0.26.2" websocket = "0.26.2"

443
src/command.rs Normal file
View file

@ -0,0 +1,443 @@
use once_cell::sync::Lazy;
use regex::{Regex, RegexSetBuilder};
#[derive(Debug, Clone, PartialEq)]
pub enum StatusCommand {
Boost,
Ignore,
BanUser(String),
UnbanUser(String),
BanServer(String),
UnbanServer(String),
AddMember(String),
RemoveMember(String),
GrantAdmin(String),
RemoveAdmin(String),
OpenGroup,
CloseGroup,
Help,
ListMembers,
Leave,
}
macro_rules! p_user {
() => {
r"(@?[a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+\.[a-z0-9_-]+|@[a-zA-Z0-9_.-]+)"
}
}
macro_rules! p_server {
() => {
r"([a-zA-Z0-9_.-]+\.[a-zA-Z0-9_-]+)"
}
}
macro_rules! command {
($($val:expr),+) => {
Regex::new(concat!(r"(?:^|\s|>|\n)[\\/]", $($val,)+ r"(?:$|[!,]|\W)")).unwrap()
}
}
static RE_BOOST: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"b(?:oost)?")
});
static RE_IGNORE: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"i(?:gn(?:ore)?)?")
});
static RE_BAN_USER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"ban\s+", p_user!())
});
static RE_UNBAN_USER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"unban\s+", p_user!())
});
static RE_BAN_SERVER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"ban\s+", p_server!())
});
static RE_UNBAN_SERVER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"unban\s+", p_server!())
});
static RE_ADD_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:accept|invite|member|add)\s+", p_user!())
});
static RE_REMOVE_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:expel|kick|remove)\s+", p_user!())
});
static RE_GRANT_ADMIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:op|admin|grant)\s+", p_user!())
});
static RE_REVOKE_ADMIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:deop|unop|deadmin|unadmin|ungrant|revoke)\s+", p_user!())
});
static RE_OPEN_GROUP: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"opengroup")
});
static RE_CLOSE_GROUP: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"closegroup")
});
static RE_HELP: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"help")
});
static RE_MEMBERS: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:list|members)")
});
static RE_LEAVE: once_cell::sync::Lazy<Regex> = Lazy::new(|| {
command!(r"(?:leave|quit)")
});
pub fn parse_status(content: &str) -> Vec<StatusCommand> {
debug!("Raw content: {}", content);
let content = content.replace("<br/>", " ");
// let content = content.replace("<br />", " ");
// let content = content.replace("<BR/>", " ");
// let content = content.replace("<BR />", " ");
let content = voca_rs::strip::strip_tags(&content);
debug!("Stripped tags: {}", content);
// short-circuiting commands
if RE_IGNORE.is_match(&content) {
debug!("IGNORE");
return vec![StatusCommand::Ignore];
}
if RE_HELP.is_match(&content) {
debug!("HELP");
return vec![StatusCommand::Help];
}
// additive commands
let mut commands = vec![];
if RE_BOOST.is_match(&content) {
debug!("BOOST");
commands.push(StatusCommand::Boost);
}
if RE_LEAVE.is_match(&content) {
debug!("LEAVE");
commands.push(StatusCommand::Leave);
}
if RE_MEMBERS.is_match(&content) {
debug!("MEMBERS");
commands.push(StatusCommand::ListMembers);
}
if RE_OPEN_GROUP.is_match(&content) {
debug!("OPEN GROUP");
commands.push(StatusCommand::OpenGroup);
}
if RE_CLOSE_GROUP.is_match(&content) {
debug!("CLOSE GROUP");
commands.push(StatusCommand::CloseGroup);
}
if let Some(c) = RE_BAN_USER.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("BAN USER: {}", s);
commands.push(StatusCommand::BanUser(s.to_owned()));
}
}
if let Some(c) = RE_UNBAN_USER.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("UNBAN USER: {}", s);
commands.push(StatusCommand::UnbanUser(s.to_owned()));
}
}
if let Some(c) = RE_BAN_SERVER.captures(&content) {
if let Some(s) = c.get(1) {
debug!("BAN SERVER: {}", s.as_str());
commands.push(StatusCommand::BanServer(s.as_str().to_owned()));
}
}
if let Some(c) = RE_UNBAN_SERVER.captures(&content) {
if let Some(s) = c.get(1) {
debug!("UNBAN SERVER: {}", s.as_str());
commands.push(StatusCommand::UnbanServer(s.as_str().to_owned()));
}
}
if let Some(c) = RE_ADD_MEMBER.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("ADD MEMBER: {}", s);
commands.push(StatusCommand::AddMember(s.to_owned()));
}
}
if let Some(c) = RE_REMOVE_MEMBER.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("UNBAN USER: {}", s);
commands.push(StatusCommand::RemoveMember(s.to_owned()));
}
}
if let Some(c) = RE_GRANT_ADMIN.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("ADD ADMIN: {}", s);
commands.push(StatusCommand::GrantAdmin(s.to_owned()));
}
}
if let Some(c) = RE_REVOKE_ADMIN.captures(&content) {
if let Some(s) = c.get(1) {
let s = s.as_str();
let s = s.trim_start_matches('@');
debug!("REMOVE ADMIN: {}", s);
commands.push(StatusCommand::RemoveAdmin(s.to_owned()));
}
}
commands
}
#[cfg(test)]
mod test {
use crate::command::{parse_status, StatusCommand};
use super::{RE_ADD_MEMBER, RE_BAN_SERVER, RE_BAN_USER, RE_BOOST, RE_CLOSE_GROUP, RE_GRANT_ADMIN, RE_HELP,
RE_IGNORE, RE_LEAVE, RE_MEMBERS, RE_OPEN_GROUP, RE_REMOVE_MEMBER, RE_REVOKE_ADMIN};
#[test]
fn test_boost() {
assert!(RE_BOOST.is_match("/b"));
assert!(RE_BOOST.is_match(">/b"));
assert!(RE_BOOST.is_match("/b mm"));
assert!(RE_BOOST.is_match("/b."));
assert!(RE_BOOST.is_match("\\b"));
assert!(!RE_BOOST.is_match("boo/b"));
assert!(RE_BOOST.is_match("bla\n/b"));
assert!(RE_BOOST.is_match("/boost"));
assert!(RE_BOOST.is_match("/boost\n"));
assert!(RE_BOOST.is_match("/boost dfdfg"));
assert!(!RE_BOOST.is_match("/boosty"));
assert!(RE_BOOST.is_match("/b\nxxx"));
assert!(!RE_BOOST.is_match("/bleble\n"));
}
#[test]
fn test_ignore() {
assert!(RE_IGNORE.is_match("/i"));
assert!(RE_IGNORE.is_match("/i mm"));
assert!(RE_IGNORE.is_match("/i."));
assert!(RE_IGNORE.is_match("\\i"));
assert!(!RE_IGNORE.is_match("boo/i"));
assert!(RE_IGNORE.is_match("bla\n/i"));
assert!(RE_IGNORE.is_match("/ign"));
assert!(RE_IGNORE.is_match("/ignore"));
assert!(RE_IGNORE.is_match("/ignore x"));
assert!(RE_IGNORE.is_match("/ignore\n"));
assert!(RE_IGNORE.is_match("/ignore dfdfg"));
assert!(!RE_IGNORE.is_match("/ignorey"));
assert!(RE_IGNORE.is_match("/i\nxxx"));
assert!(!RE_IGNORE.is_match("/ileble\n"));
}
#[test]
fn test_ban_user() {
assert!(RE_BAN_USER.is_match("/ban lain@pleroma.soykaf.com"));
assert!(RE_BAN_USER.is_match("/ban lain@stupidname.uk"));
assert!(RE_BAN_USER.is_match("bababababa /ban lain@stupidname.uk lala"));
assert!(!RE_BAN_USER.is_match("/ban stupidname.uk"));
assert!(RE_BAN_USER.is_match("/ban @lain"));
assert!(RE_BAN_USER.is_match("/ban @lain aaa"));
assert!(RE_BAN_USER.is_match("/ban \t lain@pleroma.soykaf.com"));
assert!(RE_BAN_USER.is_match("/ban @lain@pleroma.soykaf.com"));
assert!(RE_BAN_USER.is_match("/ban @l-a_i.n9@xn--999pleroma-weirdname.com"));
assert!(RE_BAN_USER.is_match("/ban @LAIN@PleromA.soykaf.com"));
let c = RE_BAN_USER.captures("/ban lain@pleroma.soykaf.com");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "lain@pleroma.soykaf.com");
let c = RE_BAN_USER.captures("/ban lain@pleroma.soykaf.com xx");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "lain@pleroma.soykaf.com");
let c = RE_BAN_USER.captures("/ban @lain@pleroma.soykaf.com");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain@pleroma.soykaf.com");
let c = RE_BAN_USER.captures("/ban @lain");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain");
let c = RE_BAN_USER.captures("/ban @lain xx");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain");
}
#[test]
fn test_ban_server() {
assert!(!RE_BAN_SERVER.is_match("/ban lain@pleroma.soykaf.com"));
assert!(RE_BAN_SERVER.is_match("/ban pleroma.soykaf.com"));
assert!(RE_BAN_SERVER.is_match("/ban xn--999pleroma-weirdname.com"));
assert!(RE_BAN_SERVER.is_match("/ban \t xn--999pleroma-weirdname.com"));
assert!(RE_BAN_SERVER.is_match("mamama /ban pleroma.soykaf.com momomo"));
assert!(!RE_BAN_SERVER.is_match("/ban @pleroma.soykaf.com"));
let c = RE_BAN_SERVER.captures("/ban pleroma.soykaf.com");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "pleroma.soykaf.com");
let c = RE_BAN_SERVER.captures("/ban pleroma.soykaf.com xx");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "pleroma.soykaf.com");
}
#[test]
fn test_add_member() {
assert!(RE_ADD_MEMBER.is_match("/accept lain@pleroma.soykaf.com"));
assert!(RE_ADD_MEMBER.is_match("/accept @lain@pleroma.soykaf.com"));
assert!(RE_ADD_MEMBER.is_match("\\accept @lain"));
assert!(RE_ADD_MEMBER.is_match("/invite @lain"));
assert!(RE_ADD_MEMBER.is_match("/add @lain"));
assert!(RE_ADD_MEMBER.is_match("/member @lain"));
let c = RE_ADD_MEMBER.captures("/add @lain");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain");
}
#[test]
fn test_remove_member() {
assert!(!RE_REMOVE_MEMBER.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_REMOVE_MEMBER.is_match("/expel lain@pleroma.soykaf.com"));
assert!(RE_REMOVE_MEMBER.is_match("/expel @lain@pleroma.soykaf.com"));
assert!(RE_REMOVE_MEMBER.is_match("\\expel @lain"));
assert!(RE_REMOVE_MEMBER.is_match("/kick @lain"));
assert!(RE_REMOVE_MEMBER.is_match("/remove @lain"));
let c = RE_REMOVE_MEMBER.captures("/kick lain@pleroma.soykaf.com");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "lain@pleroma.soykaf.com");
}
#[test]
fn test_add_admin() {
assert!(!RE_GRANT_ADMIN.is_match("/expel lain@pleroma.soykaf.com"));
assert!(RE_GRANT_ADMIN.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_GRANT_ADMIN.is_match("/grant @lain@pleroma.soykaf.com"));
assert!(RE_GRANT_ADMIN.is_match("\\op @lain"));
let c = RE_GRANT_ADMIN.captures("/op @lain@pleroma.soykaf.com");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain@pleroma.soykaf.com");
}
#[test]
fn test_remove_admin() {
assert!(!RE_REVOKE_ADMIN.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_REVOKE_ADMIN.is_match("/revoke @lain"));
assert!(RE_REVOKE_ADMIN.is_match("/deop @lain"));
assert!(RE_REVOKE_ADMIN.is_match("/unop @lain"));
assert!(RE_REVOKE_ADMIN.is_match("/deadmin @lain"));
assert!(RE_REVOKE_ADMIN.is_match("/unadmin @lain"));
assert!(RE_REVOKE_ADMIN.is_match("/ungrant @lain"));
let c = RE_REVOKE_ADMIN.captures("/ungrant @lain");
assert!(c.is_some());
assert_eq!(c.unwrap().get(1).unwrap().as_str(), "@lain");
}
#[test]
fn test_opengroup() {
assert!(!RE_OPEN_GROUP.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_OPEN_GROUP.is_match("/opengroup"));
assert!(RE_OPEN_GROUP.is_match("x /opengroup"));
assert!(RE_OPEN_GROUP.is_match("/opengroup dfgdfg"));
assert!(RE_OPEN_GROUP.is_match("\n\n/opengroup\n dfgdfg\n\n"));
}
#[test]
fn test_closegroup() {
assert!(!RE_CLOSE_GROUP.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_CLOSE_GROUP.is_match("/closegroup"));
assert!(RE_CLOSE_GROUP.is_match("x /closegroup"));
assert!(RE_CLOSE_GROUP.is_match("/closegroup dfgdfg"));
assert!(RE_CLOSE_GROUP.is_match("\n\n/closegroup\n dfgdfg\n\n"));
}
#[test]
fn test_help() {
assert!(!RE_HELP.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_HELP.is_match("/help"));
assert!(!RE_HELP.is_match("/helpx"));
assert!(!RE_HELP.is_match("a/help"));
assert!(!RE_HELP.is_match("help"));
assert!(RE_HELP.is_match("x /help"));
assert!(RE_HELP.is_match("/help dfgdfg"));
assert!(RE_HELP.is_match("\n\n/help\n dfgdfg\n\n"));
}
#[test]
fn test_members() {
assert!(!RE_MEMBERS.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_MEMBERS.is_match("/members"));
assert!(RE_MEMBERS.is_match("/list"));
}
#[test]
fn test_leave() {
assert!(!RE_LEAVE.is_match("/list"));
assert!(RE_LEAVE.is_match("/leave"));
assert!(RE_LEAVE.is_match("/quit"));
assert!(RE_LEAVE.is_match("x /quit"));
assert!(RE_LEAVE.is_match("/quit z"));
}
#[test]
fn test_real_post() {
assert_eq!(Vec::<StatusCommand>::new(), parse_status("Hello there is nothing here /fake command"));
assert_eq!(vec![StatusCommand::Help], parse_status("lets see some \\help and /ban @lain"));
assert_eq!(vec![StatusCommand::Ignore], parse_status("lets see some /ignore and /ban @lain"));
assert_eq!(vec![StatusCommand::BanUser("lain".to_string()), StatusCommand::BanServer("soykaf.com".to_string())],
parse_status("let's /ban @lain! and also /ban soykaf.com"));
}
#[test]
fn test_strip() {
assert_eq!(vec![StatusCommand::BanUser("betty".to_string())],
parse_status(r#"Let's bad the naughty bot: /ban <span class="h-card"><a class="u-url mention" data-user="9nXpaGZL88fPAiP8xU" href="https://piggo.space/users/betty" rel="ugc">@<span>betty</span></a></span>"#));
assert_eq!(vec![StatusCommand::BanUser("betty@abstarbauze.com".to_string())],
parse_status(r#"Let's bad the naughty bot: /ban <span class="h-card"><a class="u-url mention" data-user="9nXpaGZL88fPAiP8xU" href="https://piggo.space/users/betty" rel="ugc">@<span>betty@abstarbauze.com</span></a></span>"#));
}
}

View file

@ -9,9 +9,11 @@ use elefren::entities::notification::{Notification, NotificationType};
use elefren::status_builder::Visibility; use elefren::status_builder::Visibility;
use futures::StreamExt; use futures::StreamExt;
use crate::command::StatusCommand;
use crate::store::{ConfigStore, GroupError}; use crate::store::{ConfigStore, GroupError};
use crate::store::data::GroupConfig; use crate::store::data::GroupConfig;
use crate::utils::LogError; use crate::utils::LogError;
use std::collections::HashSet;
/// This is one group's config store capable of persistence /// This is one group's config store capable of persistence
#[derive(Debug)] #[derive(Debug)]
@ -125,25 +127,298 @@ impl GroupHandle {
} }
async fn handle_notification(&mut self, n: Notification) { async fn handle_notification(&mut self, n: Notification) {
const DELAY_BEFORE_ACTION: Duration = Duration::from_millis(500);
debug!("Handling notif #{}", n.id); debug!("Handling notif #{}", n.id);
let ts = n.timestamp_millis(); let ts = n.timestamp_millis();
self.config.set_last_notif(ts); self.config.set_last_notif(ts);
let can_write = self.config.can_write(&n.account.acct);
let is_admin = self.config.is_admin(&n.account.acct);
if self.config.is_banned(&n.account.acct) {
warn!("Notification actor {} is banned!", n.account.acct);
return;
}
match n.notification_type { match n.notification_type {
NotificationType::Mention => { NotificationType::Mention => {
if let Some(status) = n.status { if let Some(status) = n.status {
if status.content.contains("/gi") || status.content.contains("\\gi") { if self.config.is_banned(&status.account.acct) {
info!("Mention ignored by gi"); warn!("Status author {} is banned!", status.account.acct);
} else if status.content.contains("/gb") || status.content.contains("\\gb") { return;
if let Some(id) = status.in_reply_to_id { }
info!("Boosting prev post by GB");
tokio::time::sleep(Duration::from_millis(500)).await; let commands = crate::command::parse_status(&status.content);
// self.client.reblog(&id).await.log_error("Failed to boost");
if commands.is_empty() {
debug!("No commands in post");
if !can_write {
warn!("User {} not allowed to post in group", n.account.acct);
return;
}
if status.in_reply_to_id.is_none() {
// Someone tagged the group in OP, boost it.
info!("Boosting OP mention");
tokio::time::sleep(DELAY_BEFORE_ACTION).await;
self.client.reblog(&status.id).await
.log_error("Failed to boost");
} else {
debug!("Not OP, ignore mention")
} }
} else { } else {
info!("Boosting mention"); let mut reply = vec![];
tokio::time::sleep(Duration::from_millis(500)).await; let mut boost_prev = false;
// self.client.reblog(&status.id).await.log_error("Failed to boost"); let mut new_members = vec![];
let mut new_admins = vec![];
let mut removed_admins = vec![];
let mut instance_ban_announcements = vec![];
let mut instance_unban_announcements = vec![];
// TODO normalize local user handles
let mut any_admin_cmd = false;
for cmd in commands {
match cmd {
StatusCommand::Ignore => {
debug!("Notif ignored because of ignore command");
return;
}
StatusCommand::Boost => {
if !can_write {
warn!("User {} not allowed to boost to group", n.account.acct);
} else {
boost_prev = status.in_reply_to_id.is_some();
}
}
StatusCommand::BanUser(u) => {
if is_admin {
if !self.config.is_banned(&u) {
match self.config.ban_user(&u, true) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} banned from group!", u));
}
Err(e) => {
reply.push(format!("Failed to ban user {}: {}", u, e));
}
}
}
} else {
warn!("Not admin, can't manage bans");
}
}
StatusCommand::UnbanUser(u) => {
if is_admin {
if self.config.is_banned(&u) {
match self.config.ban_user(&u, false) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} un-banned!", u));
}
Err(e) => {
unreachable!()
}
}
}
} else {
warn!("Not admin, can't manage bans");
}
}
StatusCommand::BanServer(s) => {
if is_admin {
if !self.config.is_server_banned(&s) {
match self.config.ban_server(&s, true) {
Ok(_) => {
any_admin_cmd = true;
instance_ban_announcements.push(s.clone());
reply.push(format!("Instance {} banned from group!", s));
}
Err(e) => {
reply.push(format!("Failed to ban instance {}: {}", s, e));
}
}
}
} else {
warn!("Not admin, can't manage bans");
}
}
StatusCommand::UnbanServer(s) => {
if is_admin {
if self.config.is_server_banned(&s) {
match self.config.ban_server(&s, false) {
Ok(_) => {
any_admin_cmd = true;
instance_unban_announcements.push(s.clone());
reply.push(format!("Instance {} un-banned!", s));
}
Err(e) => {
unreachable!()
}
}
}
} else {
warn!("Not admin, can't manage bans");
}
}
StatusCommand::AddMember(u) => {
if is_admin {
if !self.config.is_member(&u) {
match self.config.set_member(&u, true) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} added to group!", u));
new_members.push(u);
}
Err(e) => {
reply.push(format!("Failed to add user {} to group: {}", u, e));
}
}
}
} else {
warn!("Not admin, can't manage members");
}
}
StatusCommand::RemoveMember(u) => {
if is_admin {
if self.config.is_member(&u) {
match self.config.set_member(&u, false) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} removed from group.", u));
}
Err(e) => {
unreachable!()
}
}
}
} else {
warn!("Not admin, can't manage members");
}
}
StatusCommand::GrantAdmin(u) => {
if is_admin {
if !self.config.is_admin(&u) {
match self.config.set_admin(&u, true) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} is now a group admin!", u));
new_admins.push(u);
}
Err(e) => {
reply.push(format!("Failed to make user {} a group admin: {}", u, e));
}
}
}
} else {
warn!("Not admin, can't manage admin rights");
}
}
StatusCommand::RemoveAdmin(u) => {
if is_admin {
if self.config.is_admin(&u) {
match self.config.set_admin(&u, false) {
Ok(_) => {
any_admin_cmd = true;
reply.push(format!("User {} is no longer a group admin!", u));
removed_admins.push(u)
}
Err(e) => {
reply.push(format!("Failed to revoke {}'s group admin: {}", u, e));
}
}
}
} else {
warn!("Not admin, can't manage admin rights");
}
}
StatusCommand::OpenGroup => {
if is_admin {
if self.config.is_member_only() {
any_admin_cmd = true;
self.config.set_member_only(false);
reply.push(format!("Group changed to open-access"));
}
} else {
warn!("Not admin, can't manage group mode");
}
}
StatusCommand::CloseGroup => {
if is_admin {
if !self.config.is_member_only() {
any_admin_cmd = true;
self.config.set_member_only(true);
reply.push(format!("Group changed to member-only"));
}
} else {
warn!("Not admin, can't manage group mode");
}
}
StatusCommand::Help => {
reply.push("Mention the group user in a top-level post to share it with the group's members.".to_string());
reply.push("Posts with commands won't be shared. Supported commands:".to_string());
reply.push("/ignore, /ign, /i - don't run any commands in the post".to_string());
reply.push("/boost, /b - boost the replied-to post into the group".to_string());
reply.push("/leave - leave the group as a member".to_string());
if is_admin {
reply.push("/members".to_string());
reply.push("/kick, /remove user - kick a member".to_string());
reply.push("/add user - add a member".to_string());
reply.push("/ban x - ban a user or a server".to_string());
reply.push("/unban x - lift a ban".to_string());
reply.push("/op, /admin user - give admin rights".to_string());
reply.push("/unop, /unadmin user - remove admin rights".to_string());
reply.push("/opengroup, /closegroup - control posting access".to_string());
}
}
StatusCommand::ListMembers => {
if is_admin {
reply.push("Member list:".to_string());
let admins = self.config.get_admins().collect::<HashSet<_>>();
let members = self.config.get_members().collect::<Vec<_>>();
for m in members {
if admins.contains(&m) {
reply.push(format!("{} [admin]", m));
} else {
reply.push(format!("{}", m));
}
}
}
}
StatusCommand::Leave => {
if self.config.is_member(&n.account.acct) {
any_admin_cmd = true;
let _ = self.config.set_member(&n.account.acct, false);
reply.push("You left the group.".to_string());
}
}
}
}
tokio::time::sleep(DELAY_BEFORE_ACTION).await;
if boost_prev {
self.client.reblog(&status.in_reply_to_id.as_ref().unwrap()).await
.log_error("Failed to boost");
}
if !reply.is_empty() {
let r = reply.join("\n");
let post = StatusBuilder::new()
.status(format!("@{user}\n{msg}", user=n.account.acct, msg=r))
.content_type("text/markdown")
.visibility(Visibility::Direct)
.build().expect("error build status");
let _ = self.client.new_status(post).await.log_error("Failed to post");
}
if any_admin_cmd {
self.save_if_needed().await.log_error("Failed to save");
}
} }
} }
} }
@ -151,15 +426,28 @@ impl GroupHandle {
info!("New follower!"); info!("New follower!");
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;
/* let text = if self.config.is_member_only() {
// Admins are listed without @, so they won't become handles here.
// Tagging all admins would be annoying.
let mut admins = self.config.get_admins().cloned().collect::<Vec<_>>();
admins.sort();
format!(
"@{user} welcome to the group! This is a member-only group, you won't be \
able to post. Ask the group admins if you wish to join!\n\n\
Admins: {admins}", user = &n.account.acct, admins = admins.join(", "))
} else {
format!(
"@{user} welcome to the group! This is a public-access group. \
To share a post, tag the group user. Use /help for more info.", user = &n.account.acct)
};
let post = StatusBuilder::new() let post = StatusBuilder::new()
.status(format!("@{} welcome to the group!", &n.account.acct)) .status(text)
.content_type("text/markdown") .content_type("text/markdown")
.visibility(Visibility::Unlisted) .visibility(Visibility::Unlisted)
.build().expect("error build status"); .build().expect("error build status");
let _ = self.client.new_status(post).await.log_error("Failed to post"); let _ = self.client.new_status(post).await.log_error("Failed to post");
*/
} }
_ => {} _ => {}
} }

View file

@ -24,6 +24,7 @@ use elefren::debug::NotificationDisplay;
mod store; mod store;
mod group_handle; mod group_handle;
mod utils; mod utils;
mod command;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@ -78,104 +79,7 @@ async fn main() -> anyhow::Result<()> {
futures::future::join_all(handles).await; futures::future::join_all(handles).await;
/*
let client = if let Ok(data) = elefren::helpers::toml::from_file("group-actor-data.toml") {
FediClient::from(data)
} else {
register().await?
};
let you = client.verify_credentials().await?;
println!("{:#?}", you);
let mut events = client.streaming_user().await?;
while let Some(event) = events.next().await {
match event {
Event::Update(status) => {
info!("Status: {:?}", status);
},
Event::Notification(notification) => {
info!("Notification: {:?}", notification.notification_type);
debug!("{:?}", notification);
/*
match notification.notification_type {
NotificationType::Mention => {
if let Some(status) = notification.status {
if status.content.contains("/gi") {
debug!("GRPIGNORE!");
} else if status.content.contains("/gb") {
debug!("GROUP BOOST PREV!");
if let Some(id) = status.in_reply_to_id {
let _ = client.reblog(&id);
}
}
// else if status.content.contains("/gping") {
// let post = StatusBuilder::new()
// .status(format!("@{} hello", &notification.account.acct))
// .content_type("text/markdown")
// //.in_reply_to(status.id)
// .visibility(Visibility::Unlisted)
// .build().expect("error build status");
//
// let _ = client.new_status(post);
// }
else {
info!("BOOSTING");
let _ = client.reblog(&status.id);
}
}
}
NotificationType::Follow => {
info!("New follower!");
let post = StatusBuilder::new()
.status(format!("@{} welcome to the group!", &notification.account.acct))
.content_type("text/markdown")
.visibility(Visibility::Unlisted)
.build().expect("error build status");
let _ = client.new_status(post).await;
}
_ => {}
}
*/
},
Event::Delete(id) => {
info!("Delete: {}", id);
},
Event::FiltersChanged => {
info!("FiltersChanged");
// ???
},
}
}
*/
println!("Main loop ended!"); println!("Main loop ended!");
Ok(()) Ok(())
} }
/*
async fn register() -> anyhow::Result<FediClient> {
let registration = Registration::new("https://piggo.space")
.client_name("group-actor")
.force_login(true)
.scopes(
Scopes::read(scopes::Read::Accounts)
| Scopes::read(scopes::Read::Notifications)
| Scopes::read(scopes::Read::Statuses)
| Scopes::read(scopes::Read::Follows)
| Scopes::write(scopes::Write::Statuses)
| Scopes::write(scopes::Write::Media)
)
.build().await?;
let client = elefren::helpers::cli::authenticate(registration).await?;
elefren::helpers::toml::to_file(&*client, "group-actor-data.toml")?;
Ok(client)
}
*/

View file

@ -101,6 +101,14 @@ impl GroupConfig {
self.mark_dirty(); self.mark_dirty();
} }
pub(crate) fn get_admins(&self) -> impl Iterator<Item=&String> {
self.admin_users.iter()
}
pub(crate) fn get_members(&self) -> impl Iterator<Item=&String> {
self.member_users.iter()
}
pub(crate) fn set_last_notif(&mut self, ts: u64) { pub(crate) fn set_last_notif(&mut self, ts: u64) {
self.last_notif_ts = self.last_notif_ts.max(ts); self.last_notif_ts = self.last_notif_ts.max(ts);
self.mark_dirty(); self.mark_dirty();
@ -127,10 +135,14 @@ impl GroupConfig {
|| self.is_users_server_banned(acct) || self.is_users_server_banned(acct)
} }
pub(crate) fn is_server_banned(&self, server: &str) -> bool {
self.banned_servers.contains(server)
}
/// Check if the user's server is banned /// Check if the user's server is banned
fn is_users_server_banned(&self, acct: &str) -> bool { fn is_users_server_banned(&self, acct: &str) -> bool {
let server = acct_to_server(acct); let server = acct_to_server(acct);
self.banned_servers.contains(server) self.is_server_banned(server)
} }
pub(crate) fn can_write(&self, acct: &str) -> bool { pub(crate) fn can_write(&self, acct: &str) -> bool {

View file

@ -170,9 +170,9 @@ impl ConfigStore {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum GroupError { pub enum GroupError {
#[error("Operation refused because the user is admin")] #[error("User is admin")]
UserIsAdmin, UserIsAdmin,
#[error("Operation refused because the user is banned")] #[error("User is banned")]
UserIsBanned, UserIsBanned,
#[error("Server could not be banned because there are admin users on it")] #[error("Server could not be banned because there are admin users on it")]
AdminsOnServer, AdminsOnServer,