mirror of
https://git.ondrovo.com/MightyPork/group-actor.git
synced 2024-12-18 21:26:37 +00:00
access control and command handling with regex
This commit is contained in:
parent
5a631f785e
commit
99d9b83c68
7 changed files with 793 additions and 113 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -530,12 +530,15 @@ dependencies = [
|
|||
"futures 0.3.16",
|
||||
"log 0.4.14",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smart-default",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"voca_rs",
|
||||
"websocket",
|
||||
]
|
||||
|
||||
|
@ -1777,6 +1780,16 @@ version = "1.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
|
@ -2144,6 +2157,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.6"
|
||||
|
@ -2215,6 +2234,17 @@ version = "0.9.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
|
|
@ -26,6 +26,9 @@ tokio = { version = "1", features = ["full"] }
|
|||
tokio-stream = "0.1.7"
|
||||
thiserror = "1.0.26"
|
||||
futures = "0.3"
|
||||
voca_rs = "1.13.0"
|
||||
regex = "1.5.4"
|
||||
once_cell = "1.8.0"
|
||||
|
||||
native-tls = "0.2.8"
|
||||
websocket = "0.26.2"
|
||||
|
|
443
src/command.rs
Normal file
443
src/command.rs
Normal 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>"#));
|
||||
}
|
||||
}
|
|
@ -9,9 +9,11 @@ use elefren::entities::notification::{Notification, NotificationType};
|
|||
use elefren::status_builder::Visibility;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::command::StatusCommand;
|
||||
use crate::store::{ConfigStore, GroupError};
|
||||
use crate::store::data::GroupConfig;
|
||||
use crate::utils::LogError;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// This is one group's config store capable of persistence
|
||||
#[derive(Debug)]
|
||||
|
@ -125,25 +127,298 @@ impl GroupHandle {
|
|||
}
|
||||
|
||||
async fn handle_notification(&mut self, n: Notification) {
|
||||
const DELAY_BEFORE_ACTION: Duration = Duration::from_millis(500);
|
||||
|
||||
debug!("Handling notif #{}", n.id);
|
||||
let ts = n.timestamp_millis();
|
||||
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 {
|
||||
NotificationType::Mention => {
|
||||
if let Some(status) = n.status {
|
||||
if status.content.contains("/gi") || status.content.contains("\\gi") {
|
||||
info!("Mention ignored by gi");
|
||||
} else if status.content.contains("/gb") || status.content.contains("\\gb") {
|
||||
if let Some(id) = status.in_reply_to_id {
|
||||
info!("Boosting prev post by GB");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// self.client.reblog(&id).await.log_error("Failed to boost");
|
||||
if self.config.is_banned(&status.account.acct) {
|
||||
warn!("Status author {} is banned!", status.account.acct);
|
||||
return;
|
||||
}
|
||||
|
||||
let commands = crate::command::parse_status(&status.content);
|
||||
|
||||
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 {
|
||||
info!("Boosting mention");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// self.client.reblog(&status.id).await.log_error("Failed to boost");
|
||||
let mut reply = vec![];
|
||||
let mut boost_prev = false;
|
||||
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!");
|
||||
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()
|
||||
.status(format!("@{} welcome to the group!", &n.account.acct))
|
||||
.status(text)
|
||||
.content_type("text/markdown")
|
||||
.visibility(Visibility::Unlisted)
|
||||
.build().expect("error build status");
|
||||
|
||||
let _ = self.client.new_status(post).await.log_error("Failed to post");
|
||||
*/
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
98
src/main.rs
98
src/main.rs
|
@ -24,6 +24,7 @@ use elefren::debug::NotificationDisplay;
|
|||
mod store;
|
||||
mod group_handle;
|
||||
mod utils;
|
||||
mod command;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
|
@ -78,104 +79,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
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", ¬ification.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!", ¬ification.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!");
|
||||
|
||||
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)
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -101,6 +101,14 @@ impl GroupConfig {
|
|||
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) {
|
||||
self.last_notif_ts = self.last_notif_ts.max(ts);
|
||||
self.mark_dirty();
|
||||
|
@ -127,10 +135,14 @@ impl GroupConfig {
|
|||
|| 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
|
||||
fn is_users_server_banned(&self, acct: &str) -> bool {
|
||||
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 {
|
||||
|
|
|
@ -170,9 +170,9 @@ impl ConfigStore {
|
|||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GroupError {
|
||||
#[error("Operation refused because the user is admin")]
|
||||
#[error("User is admin")]
|
||||
UserIsAdmin,
|
||||
#[error("Operation refused because the user is banned")]
|
||||
#[error("User is banned")]
|
||||
UserIsBanned,
|
||||
#[error("Server could not be banned because there are admin users on it")]
|
||||
AdminsOnServer,
|
||||
|
|
Loading…
Reference in a new issue