From 8822d8d11d752bc5e02772a43bf69e2dc860f49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 22 Aug 2021 18:55:06 +0200 Subject: [PATCH] formatting & lints, one fix for member-only groups --- Cargo.lock | 12 --- Cargo.toml | 2 +- build.rs | 9 +- src/command.rs | 163 +++++++++++++++------------- src/error.rs | 16 +-- src/group_handle.rs | 253 +++++++++++++++++++++++++++++++------------- src/main.rs | 94 ++++++++-------- src/store/data.rs | 138 +++++++++++++++++------- src/store/mod.rs | 99 +++++++++-------- src/utils.rs | 73 +++++++++---- 10 files changed, 532 insertions(+), 327 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e2cb11..9a1f5ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,7 +341,6 @@ dependencies = [ "regex", "serde", "serde_json", - "smart-default", "thiserror", "tokio", "tokio-stream", @@ -1747,17 +1746,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" -[[package]] -name = "smart-default" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "socket2" version = "0.3.19" diff --git a/Cargo.toml b/Cargo.toml index eaf3a54..c924c39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ env_logger = "0.9.0" log = "0.4.14" serde = "1" serde_json = "1" -smart-default = "0.6.0" +#smart-default = "0.6.0" anyhow = "1" clap = "2.33.0" tokio = { version = "1", features = ["full"] } diff --git a/build.rs b/build.rs index 542db5b..cd60b5c 100644 --- a/build.rs +++ b/build.rs @@ -2,11 +2,12 @@ use std::process::Command; use std::str; fn main() { - let desc_c = Command::new("git").args(&["describe", "--all", "--long"]).output().unwrap(); + let desc_c = Command::new("git") + .args(&["describe", "--all", "--long"]) + .output() + .unwrap(); - let desc = unsafe { - str::from_utf8_unchecked( &desc_c.stdout ) - }; + let desc = unsafe { str::from_utf8_unchecked(&desc_c.stdout) }; println!("cargo:rustc-env=GIT_REV={}", desc); } diff --git a/src/command.rs b/src/command.rs index 5df16a2..f05ba7d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,5 +1,5 @@ use once_cell::sync::Lazy; -use regex::{Regex, RegexSetBuilder}; +use regex::Regex; #[derive(Debug, Clone, PartialEq)] pub enum StatusCommand { @@ -24,12 +24,12 @@ pub enum StatusCommand { 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 { @@ -38,69 +38,43 @@ macro_rules! command { } } -static RE_BOOST: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"b(?:oost)?") -}); +static RE_BOOST: once_cell::sync::Lazy = Lazy::new(|| command!(r"b(?:oost)?")); -static RE_IGNORE: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"i(?:g(?:n(?:ore)?)?)?") -}); +static RE_IGNORE: once_cell::sync::Lazy = Lazy::new(|| command!(r"i(?:g(?:n(?:ore)?)?)?")); -static RE_BAN_USER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"ban\s+", p_user!()) -}); +static RE_BAN_USER: once_cell::sync::Lazy = Lazy::new(|| command!(r"ban\s+", p_user!())); -static RE_UNBAN_USER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"unban\s+", p_user!()) -}); +static RE_UNBAN_USER: once_cell::sync::Lazy = Lazy::new(|| command!(r"unban\s+", p_user!())); -static RE_BAN_SERVER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"ban\s+", p_server!()) -}); +static RE_BAN_SERVER: once_cell::sync::Lazy = Lazy::new(|| command!(r"ban\s+", p_server!())); -static RE_UNBAN_SERVER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"unban\s+", p_server!()) -}); +static RE_UNBAN_SERVER: once_cell::sync::Lazy = + Lazy::new(|| command!(r"unban\s+", p_server!())); -static RE_ADD_MEMBER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:add)\s+", p_user!()) -}); +static RE_ADD_MEMBER: once_cell::sync::Lazy = + Lazy::new(|| command!(r"(?:add)\s+", p_user!())); -static RE_REMOVE_MEMBER: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:kick|remove)\s+", p_user!()) -}); +static RE_REMOVE_MEMBER: once_cell::sync::Lazy = + Lazy::new(|| command!(r"(?:kick|remove)\s+", p_user!())); -static RE_GRANT_ADMIN: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:op|admin)\s+", p_user!()) -}); +static RE_GRANT_ADMIN: once_cell::sync::Lazy = + Lazy::new(|| command!(r"(?:op|admin)\s+", p_user!())); -static RE_REVOKE_ADMIN: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:deop|deadmin)\s+", p_user!()) -}); +static RE_REVOKE_ADMIN: once_cell::sync::Lazy = + Lazy::new(|| command!(r"(?:deop|deadmin)\s+", p_user!())); -static RE_OPEN_GROUP: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"opengroup") -}); +static RE_OPEN_GROUP: once_cell::sync::Lazy = Lazy::new(|| command!(r"opengroup")); -static RE_CLOSE_GROUP: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"closegroup") -}); +static RE_CLOSE_GROUP: once_cell::sync::Lazy = Lazy::new(|| command!(r"closegroup")); -static RE_HELP: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"help") -}); +static RE_HELP: once_cell::sync::Lazy = Lazy::new(|| command!(r"help")); -static RE_MEMBERS: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:members|who)") -}); +static RE_MEMBERS: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:members|who)")); -static RE_LEAVE: once_cell::sync::Lazy = Lazy::new(|| { - command!(r"(?:leave)") -}); +static RE_LEAVE: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:leave)")); -static RE_ANNOUNCE: once_cell::sync::Lazy = Lazy::new(|| { - Regex::new(concat!(r"(?:^|\s|>|\n)[\\/]announce\s+(.*)$")).unwrap() -}); +static RE_ANNOUNCE: once_cell::sync::Lazy = + Lazy::new(|| Regex::new(concat!(r"(?:^|\s|>|\n)[\\/]announce\s+(.*)$")).unwrap()); pub fn parse_status(content: &str) -> Vec { debug!("Raw content: {}", content); @@ -241,8 +215,11 @@ pub fn parse_status(content: &str) -> Vec { mod test { use crate::command::{parse_status, StatusCommand}; - use super::{RE_ADD_MEMBER, RE_ANNOUNCE, 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}; + use super::{ + RE_ADD_MEMBER, RE_ANNOUNCE, 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() { @@ -298,15 +275,24 @@ mod test { 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"); + 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"); + 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"); + 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()); @@ -359,7 +345,10 @@ mod test { 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"); + assert_eq!( + c.unwrap().get(1).unwrap().as_str(), + "lain@pleroma.soykaf.com" + ); } #[test] @@ -372,7 +361,10 @@ mod test { 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"); + assert_eq!( + c.unwrap().get(1).unwrap().as_str(), + "@lain@pleroma.soykaf.com" + ); } #[test] @@ -441,27 +433,54 @@ mod test { assert!(RE_ANNOUNCE.is_match("sdfsdffsd /announce b")); assert!(RE_ANNOUNCE.is_match("/announce bla bla bla")); assert!(RE_ANNOUNCE.is_match("sdfsdffsd /announce bla bla bla")); - assert_eq!("bla bla bla", RE_ANNOUNCE.captures("sdfsdffsd /announce bla bla bla").unwrap().get(1).unwrap().as_str()); + assert_eq!( + "bla bla bla", + RE_ANNOUNCE + .captures("sdfsdffsd /announce bla bla bla") + .unwrap() + .get(1) + .unwrap() + .as_str() + ); } #[test] fn test_real_post() { - assert_eq!(Vec::::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::BanUser("piggo@piggo.space".to_string()), - StatusCommand::BanServer("soykaf.com".to_string()) - ], - parse_status("let's /ban @lain! /ban @piggo@piggo.space and also /ban soykaf.com")); + assert_eq!( + Vec::::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::BanUser("piggo@piggo.space".to_string()), + StatusCommand::BanServer("soykaf.com".to_string()) + ], + parse_status("let's /ban @lain! /ban @piggo@piggo.space 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 @betty"#)); - assert_eq!(vec![StatusCommand::BanUser("betty@abstarbauze.com".to_string())], - parse_status(r#"Let's bad the naughty bot: /ban @betty@abstarbauze.com"#)); + assert_eq!( + vec![StatusCommand::BanUser("betty".to_string())], + parse_status( + r#"Let's bad the naughty bot: /ban @betty"# + ) + ); + assert_eq!( + vec![StatusCommand::BanUser("betty@abstarbauze.com".to_string())], + parse_status( + r#"Let's bad the naughty bot: /ban @betty@abstarbauze.com"# + ) + ); } } diff --git a/src/error.rs b/src/error.rs index 619070d..29f109f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,13 +23,13 @@ pub enum GroupError { // this is for tests impl PartialEq for GroupError { fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::UserIsAdmin, Self::UserIsAdmin) => true, - (Self::UserIsBanned, Self::UserIsBanned) => true, - (Self::AdminsOnServer, Self::AdminsOnServer) => true, - (Self::GroupNotExist, Self::GroupNotExist) => true, - (Self::BadConfig(_), Self::BadConfig(_)) => true, - _ => false, - } + matches!( + (self, other), + (Self::UserIsAdmin, Self::UserIsAdmin) + | (Self::UserIsBanned, Self::UserIsBanned) + | (Self::AdminsOnServer, Self::AdminsOnServer) + | (Self::GroupNotExist, Self::GroupNotExist) + | (Self::BadConfig(_), Self::BadConfig(_)) + ) } } diff --git a/src/group_handle.rs b/src/group_handle.rs index c1f9076..1952c0b 100644 --- a/src/group_handle.rs +++ b/src/group_handle.rs @@ -2,19 +2,19 @@ use std::collections::HashSet; use std::sync::Arc; use std::time::{Duration, Instant}; -use elefren::{FediClient, StatusBuilder}; use elefren::debug::EventDisplay; use elefren::debug::NotificationDisplay; use elefren::entities::event::Event; use elefren::entities::notification::{Notification, NotificationType}; use elefren::status_builder::Visibility; +use elefren::{FediClient, StatusBuilder}; use futures::StreamExt; use crate::command::StatusCommand; use crate::error::GroupError; -use crate::store::ConfigStore; use crate::store::data::GroupConfig; -use crate::utils::{LogError, normalize_acct}; +use crate::store::ConfigStore; +use crate::utils::{normalize_acct, LogError}; /// This is one group's config store capable of persistence #[derive(Debug)] @@ -91,22 +91,29 @@ impl GroupHandle { loop { if next_save < Instant::now() { - self.save_if_needed().await + self.save_if_needed() + .await .log_error("Failed to save group"); next_save = Instant::now() + PERIODIC_SAVE; } - let timeout = next_save.saturating_duration_since(Instant::now()) + let timeout = next_save + .saturating_duration_since(Instant::now()) .min(PING_INTERVAL) .max(Duration::from_secs(1)); match tokio::time::timeout(timeout, events.next()).await { Ok(Some(event)) => { - debug!("(@{}) Event: {}", self.config.get_acct(), EventDisplay(&event)); + debug!( + "(@{}) Event: {}", + self.config.get_acct(), + EventDisplay(&event) + ); match event { Event::Update(_status) => {} Event::Notification(n) => { - self.handle_notification(n).await + self.handle_notification(n) + .await .log_error("Error handling a notification"); } Event::Delete(_id) => {} @@ -114,7 +121,10 @@ impl GroupHandle { } } Ok(None) => { - warn!("Group @{} socket closed, restarting...", self.config.get_acct()); + warn!( + "Group @{} socket closed, restarting...", + self.config.get_acct() + ); break; } Err(_) => { @@ -176,10 +186,13 @@ impl GroupHandle { // 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 + self.client + .reblog(&status.id) + .await .log_error("Failed to boost"); } else { - replies.push(format!("You are not allowed to post to this group")); + replies + .push("You are not allowed to post to this group".to_string()); } } else { debug!("Not OP, ignore mention"); @@ -200,7 +213,10 @@ impl GroupHandle { if can_write { do_boost_prev_post = status.in_reply_to_id.is_some(); } else { - replies.push(format!("You are not allowed to share to this group")); + replies.push( + "You are not allowed to share to this group" + .to_string(), + ); } } StatusCommand::BanUser(u) => { @@ -210,17 +226,24 @@ impl GroupHandle { match self.config.ban_user(&u, true) { Ok(_) => { any_admin_cmd = true; - replies.push(format!("User {} banned from group!", u)); + replies.push(format!( + "User {} banned from group!", + u + )); // no announcement here } Err(e) => { - replies.push(format!("Failed to ban user {}: {}", u, e)); + replies.push(format!( + "Failed to ban user {}: {}", + u, e + )); } } } } else { - replies.push(format!("Only admins can manage user bans")); + replies + .push("Only admins can manage user bans".to_string()); } } StatusCommand::UnbanUser(u) => { @@ -234,13 +257,14 @@ impl GroupHandle { // no announcement here } - Err(e) => { + Err(_) => { unreachable!() } } } } else { - replies.push(format!("Only admins can manage user bans")); + replies + .push("Only admins can manage user bans".to_string()); } } StatusCommand::BanServer(s) => { @@ -249,16 +273,26 @@ impl GroupHandle { match self.config.ban_server(&s, true) { Ok(_) => { any_admin_cmd = true; - announcements.push(format!("Server \"{}\" has been banned.", s)); - replies.push(format!("Server {} banned from group!", s)); + announcements.push(format!( + "Server \"{}\" has been banned.", + s + )); + replies.push(format!( + "Server {} banned from group!", + s + )); } Err(e) => { - replies.push(format!("Failed to ban server {}: {}", s, e)); + replies.push(format!( + "Failed to ban server {}: {}", + s, e + )); } } } } else { - replies.push(format!("Only admins can manage server bans")); + replies + .push("Only admins can manage server bans".to_string()); } } StatusCommand::UnbanServer(s) => { @@ -267,16 +301,21 @@ impl GroupHandle { match self.config.ban_server(&s, false) { Ok(_) => { any_admin_cmd = true; - announcements.push(format!("Server \"{}\" has been un-banned.", s)); - replies.push(format!("Server {} un-banned!", s)); + announcements.push(format!( + "Server \"{}\" has been un-banned.", + s + )); + replies + .push(format!("Server {} un-banned!", s)); } - Err(e) => { + Err(_) => { unreachable!() } } } } else { - replies.push(format!("Only admins can manage server bans")); + replies + .push("Only admins can manage server bans".to_string()); } } StatusCommand::AddMember(u) => { @@ -286,19 +325,28 @@ impl GroupHandle { match self.config.set_member(&u, true) { Ok(_) => { any_admin_cmd = true; - replies.push(format!("User {} added to the group!", u)); + replies.push(format!( + "User {} added to the group!", + u + )); if self.config.is_member_only() { - announcements.push(format!("Welcome new member @{} to the group!", u)); + announcements.push(format!( + "Welcome new member @{} to the group!", + u + )); } } Err(e) => { - replies.push(format!("Failed to add user {} to group: {}", u, e)); + replies.push(format!( + "Failed to add user {} to group: {}", + u, e + )); } } } } else { - replies.push(format!("Only admins can manage members")); + replies.push("Only admins can manage members".to_string()); } } StatusCommand::RemoveMember(u) => { @@ -308,15 +356,18 @@ impl GroupHandle { match self.config.set_member(&u, false) { Ok(_) => { any_admin_cmd = true; - replies.push(format!("User {} removed from the group.", u)); + replies.push(format!( + "User {} removed from the group.", + u + )); } - Err(e) => { + Err(_) => { unreachable!() } } } } else { - replies.push(format!("Only admins can manage members")); + replies.push("Only admins can manage members".to_string()); } } StatusCommand::GrantAdmin(u) => { @@ -326,16 +377,25 @@ impl GroupHandle { match self.config.set_admin(&u, true) { Ok(_) => { any_admin_cmd = true; - replies.push(format!("User {} is now a group admin!", u)); - announcements.push(format!("User @{} can now manage this group!", u)); + replies.push(format!( + "User {} is now a group admin!", + u + )); + announcements.push(format!( + "User @{} can now manage this group!", + u + )); } Err(e) => { - replies.push(format!("Failed to make user {} a group admin: {}", u, e)); + replies.push(format!( + "Failed to make user {} a group admin: {}", + u, e + )); } } } } else { - replies.push(format!("Only admins can manage admins")); + replies.push("Only admins can manage admins".to_string()); } } StatusCommand::RemoveAdmin(u) => { @@ -345,16 +405,25 @@ impl GroupHandle { match self.config.set_admin(&u, false) { Ok(_) => { any_admin_cmd = true; - replies.push(format!("User {} is no longer a group admin!", u)); - announcements.push(format!("User @{} no longer manages this group.", u)); + replies.push(format!( + "User {} is no longer a group admin!", + u + )); + announcements.push(format!( + "User @{} no longer manages this group.", + u + )); } Err(e) => { - replies.push(format!("Failed to revoke {}'s group admin: {}", u, e)); + replies.push(format!( + "Failed to revoke {}'s group admin: {}", + u, e + )); } } } } else { - replies.push(format!("Only admins can manage admins")); + replies.push("Only admins can manage admins".to_string()); } } StatusCommand::OpenGroup => { @@ -362,11 +431,14 @@ impl GroupHandle { if self.config.is_member_only() { any_admin_cmd = true; self.config.set_member_only(false); - replies.push(format!("Group changed to open-access")); - announcements.push(format!("This group is now open-access!")); + replies + .push("Group changed to open-access".to_string()); + announcements + .push("This group is now open-access!".to_string()); } } else { - replies.push(format!("Only admins can set group options")); + replies + .push("Only admins can set group options".to_string()); } } StatusCommand::CloseGroup => { @@ -374,24 +446,27 @@ impl GroupHandle { if !self.config.is_member_only() { any_admin_cmd = true; self.config.set_member_only(true); - replies.push(format!("Group changed to member-only")); - announcements.push(format!("This group is now member-only!")); + replies + .push("Group changed to member-only".to_string()); + announcements + .push("This group is now member-only!".to_string()); } } else { - replies.push(format!("Only admins can set group options")); + replies + .push("Only admins can set group options".to_string()); } } StatusCommand::Help => { if self.config.is_member_only() { let mut s = "This is a member-only group. ".to_string(); if self.config.can_write(¬if_acct) { - s.push_str("*You are not a member, ask one of the admins to add you.*"); - } else { if is_admin { s.push_str("*You are an admin.*"); } else { s.push_str("*You are a member.*"); } + } else { + s.push_str("*You are not a member, ask one of the admins to add you.*"); } replies.push(s); } else { @@ -409,7 +484,9 @@ impl GroupHandle { **Supported commands:**\n\ `/ignore, /i` - make the group completely ignore the post\n\ `/members, /who` - show group members / admins\n\ - `/boost, /b` - boost the replied-to post into the group".to_string()); + `/boost, /b` - boost the replied-to post into the group" + .to_string(), + ); if self.config.is_member_only() { replies.push("`/leave` - leave the group".to_string()); @@ -433,8 +510,10 @@ impl GroupHandle { if is_admin { if self.config.is_member_only() { replies.push("Group members:".to_string()); - let admins = self.config.get_admins().collect::>(); - let mut members = self.config.get_members().collect::>(); + let admins = + self.config.get_admins().collect::>(); + let mut members = + self.config.get_members().collect::>(); members.extend(admins.iter()); members.sort(); members.dedup(); @@ -442,7 +521,7 @@ impl GroupHandle { if admins.contains(&m) { replies.push(format!("{} [admin]", m)); } else { - replies.push(format!("{}", m)); + replies.push(m.to_string()); } } } else { @@ -454,10 +533,11 @@ impl GroupHandle { if show_admins { replies.push("Group admins:".to_string()); - let mut admins = self.config.get_admins().collect::>(); + let mut admins = + self.config.get_admins().collect::>(); admins.sort(); for a in admins { - replies.push(format!("{}", a)); + replies.push(a.to_string()); } } } @@ -475,7 +555,9 @@ impl GroupHandle { } if do_boost_prev_post { - self.client.reblog(&status.in_reply_to_id.as_ref().unwrap()).await + self.client + .reblog(&status.in_reply_to_id.as_ref().unwrap()) + .await .log_error("Failed to boost"); } @@ -486,9 +568,14 @@ impl GroupHandle { .status(format!("@{user}\n{msg}", user = notif_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"); + .build() + .expect("error build status"); + + let _ = self + .client + .new_status(post) + .await + .log_error("Failed to post"); } if !announcements.is_empty() { @@ -497,9 +584,14 @@ impl GroupHandle { .status(format!("**📢 Group announcement**\n{msg}", msg = msg)) .content_type("text/markdown") .visibility(Visibility::Unlisted) - .build().expect("error build status"); - - let _ = self.client.new_status(post).await.log_error("Failed to post"); + .build() + .expect("error build status"); + + let _ = self + .client + .new_status(post) + .await + .log_error("Failed to post"); } if any_admin_cmd { @@ -511,28 +603,36 @@ 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::>(); - admins.sort(); - format!( + 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::>(); + 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 = notif_acct, admins = admins.join(", ")) - } else { - format!( - "@{user} welcome to the group! \ - To share a post, tag the group user. Use /help for more info.", user = notif_acct) - }; + } else { + format!( + "@{user} welcome to the group! \ + To share a post, tag the group user. Use /help for more info.", + user = notif_acct + ) + }; let post = StatusBuilder::new() .status(text) .content_type("text/markdown") .visibility(Visibility::Direct) - .build().expect("error build status"); - - let _ = self.client.new_status(post).await.log_error("Failed to post"); + .build() + .expect("error build status"); + + let _ = self + .client + .new_status(post) + .await + .log_error("Failed to post"); } _ => {} } @@ -575,10 +675,11 @@ impl GroupHandle { for n in notifs_to_handle { debug!("Handling missed notification: {}", NotificationDisplay(&n)); - self.handle_notification(n).await + self.handle_notification(n) + .await .log_error("Error handling a notification"); } - return Ok(true); + Ok(true) } } diff --git a/src/main.rs b/src/main.rs index 0db72a0..36721d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,67 +5,62 @@ extern crate elefren; extern crate log; #[macro_use] extern crate serde; -#[macro_use] -extern crate smart_default; +// #[macro_use] +// extern crate smart_default; #[macro_use] extern crate thiserror; use clap::Arg; -use elefren::{FediClient, Registration, Scopes, StatusBuilder}; -use elefren::debug::NotificationDisplay; -use elefren::entities::account::Account; -use elefren::entities::event::Event; -use elefren::entities::notification::NotificationType; -use elefren::scopes; -use elefren::status_builder::Visibility; use log::LevelFilter; -use tokio_stream::{Stream, StreamExt}; use crate::store::{NewGroupOptions, StoreOptions}; use crate::utils::acct_to_server; -mod store; -mod group_handle; -mod utils; mod command; mod error; +mod group_handle; +mod store; +mod utils; #[tokio::main] async fn main() -> anyhow::Result<()> { let args = clap::App::new("groups") - .arg(Arg::with_name("verbose") - .short("v") - .multiple(true) - .help("increase logging, can be repeated")) - .arg(Arg::with_name("config") - .short("c") - .long("config") - .takes_value(true) - .help("set custom storage file, defaults to groups.json")) - .arg(Arg::with_name("auth") - .short("a") - .long("auth") - .takes_value(true) - .value_name("HANDLE") - .help("authenticate to a new server (always using https)")) - .arg(Arg::with_name("reauth") - .short("A") - .long("reauth") - .takes_value(true) - .value_name("HANDLE") - .help("authenticate to a new server (always using https)")) + .arg( + Arg::with_name("verbose") + .short("v") + .multiple(true) + .help("increase logging, can be repeated"), + ) + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .takes_value(true) + .help("set custom storage file, defaults to groups.json"), + ) + .arg( + Arg::with_name("auth") + .short("a") + .long("auth") + .takes_value(true) + .value_name("HANDLE") + .help("authenticate to a new server (always using https)"), + ) + .arg( + Arg::with_name("reauth") + .short("A") + .long("reauth") + .takes_value(true) + .value_name("HANDLE") + .help("authenticate to a new server (always using https)"), + ) .get_matches(); - const LEVELS : [LevelFilter; 5] = [ - /// Corresponds to the `Error` log level. + const LEVELS: [LevelFilter; 5] = [ LevelFilter::Error, - /// Corresponds to the `Warn` log level. LevelFilter::Warn, - /// Corresponds to the `Info` log level. LevelFilter::Info, - /// Corresponds to the `Debug` log level. LevelFilter::Debug, - /// Corresponds to the `Trace` log level. LevelFilter::Trace, ]; @@ -83,15 +78,18 @@ async fn main() -> anyhow::Result<()> { let store = store::ConfigStore::new(StoreOptions { store_path: args.value_of("config").unwrap_or("groups.json").to_string(), save_pretty: true, - }).await?; + }) + .await?; if let Some(handle) = args.value_of("auth") { let acct = handle.trim_start_matches('@'); if let Some(server) = acct_to_server(acct) { - let g = store.auth_new_group(NewGroupOptions { - server: format!("https://{}", server), - acct: acct.to_string() - }).await?; + let g = store + .auth_new_group(NewGroupOptions { + server: format!("https://{}", server), + acct: acct.to_string(), + }) + .await?; eprintln!("New group @{} added to config!", g.config.get_acct()); return Ok(()); @@ -108,13 +106,11 @@ async fn main() -> anyhow::Result<()> { } // Start - let mut groups = store.spawn_groups().await; + let groups = store.spawn_groups().await; let mut handles = vec![]; for mut g in groups { - handles.push(tokio::spawn(async move { - g.run().await - })); + handles.push(tokio::spawn(async move { g.run().await })); } futures::future::join_all(handles).await; diff --git a/src/store/data.rs b/src/store/data.rs index fc5a4a3..e09547a 100644 --- a/src/store/data.rs +++ b/src/store/data.rs @@ -3,7 +3,6 @@ use std::collections::{HashMap, HashSet}; use elefren::AppData; use crate::error::GroupError; -use crate::store; /// This is the inner data struct holding the config #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -12,18 +11,17 @@ pub(crate) struct Config { } impl Config { - pub(crate) fn iter_groups(&self) -> impl Iterator{ + pub(crate) fn iter_groups(&self) -> impl Iterator { self.groups.values() } - pub(crate) fn get_group_config(&self, acct : &str) -> Option<&GroupConfig> { + pub(crate) fn get_group_config(&self, acct: &str) -> Option<&GroupConfig> { self.groups.get(acct) } - pub(crate) fn set_group_config(&mut self, grp : GroupConfig) { + pub(crate) fn set_group_config(&mut self, grp: GroupConfig) { self.groups.insert(grp.acct.clone(), grp); } - } /// This is the inner data struct holding a group's config @@ -34,7 +32,7 @@ pub(crate) struct GroupConfig { /// Group actor's acct acct: String, /// elefren data - appdata : AppData, + appdata: AppData, /// List of admin account "acct" names, e.g. piggo@piggo.space admin_users: HashSet, /// List of users allowed to post to the group, if it is member-only @@ -62,7 +60,7 @@ impl Default for GroupConfig { client_id: Default::default(), client_secret: Default::default(), redirect: Default::default(), - token: Default::default() + token: Default::default(), }, admin_users: Default::default(), member_users: Default::default(), @@ -76,7 +74,7 @@ impl Default for GroupConfig { } impl GroupConfig { - pub(crate) fn new(acct : String, appdata: AppData) -> Self { + pub(crate) fn new(acct: String, appdata: AppData) -> Self { Self { acct, appdata, @@ -88,7 +86,7 @@ impl GroupConfig { self.enabled } - pub(crate) fn set_enabled(&mut self, ena: bool){ + pub(crate) fn set_enabled(&mut self, ena: bool) { self.enabled = ena; self.mark_dirty(); } @@ -102,11 +100,11 @@ impl GroupConfig { self.mark_dirty(); } - pub(crate) fn get_admins(&self) -> impl Iterator { + pub(crate) fn get_admins(&self) -> impl Iterator { self.admin_users.iter() } - pub(crate) fn get_members(&self) -> impl Iterator { + pub(crate) fn get_members(&self) -> impl Iterator { self.member_users.iter() } @@ -132,8 +130,7 @@ impl GroupConfig { } pub(crate) fn is_banned(&self, acct: &str) -> bool { - self.banned_users.contains(acct) - || self.is_users_server_banned(acct) + self.banned_users.contains(acct) || self.is_users_server_banned(acct) } pub(crate) fn is_server_banned(&self, server: &str) -> bool { @@ -150,10 +147,7 @@ impl GroupConfig { if self.is_admin(acct) { true } else { - !self.is_banned(acct) && ( - !self.is_member_only() - || self.is_member(acct) - ) + !self.is_banned(acct) && (!self.is_member_only() || self.is_member(acct)) } } @@ -234,8 +228,7 @@ impl GroupConfig { } fn acct_to_server(acct: &str) -> &str { - acct.split('@').nth(1) - .unwrap_or_default() + acct.split('@').nth(1).unwrap_or_default() } #[cfg(test)] @@ -252,32 +245,53 @@ mod tests { #[test] fn test_default_rules() { - let mut group = GroupConfig::default(); + let group = GroupConfig::default(); assert!(!group.is_member_only()); assert!(!group.is_member("piggo@piggo.space")); assert!(!group.is_admin("piggo@piggo.space")); - assert!(group.can_write("piggo@piggo.space"), "anyone can post by default"); + assert!( + group.can_write("piggo@piggo.space"), + "anyone can post by default" + ); } #[test] fn test_member_only() { let mut group = GroupConfig::default(); - assert!(group.can_write("piggo@piggo.space"), "rando can write in public group"); + assert!( + group.can_write("piggo@piggo.space"), + "rando can write in public group" + ); group.set_member_only(true); - assert!(!group.can_write("piggo@piggo.space"), "rando can't write in member-only group"); + assert!( + !group.can_write("piggo@piggo.space"), + "rando can't write in member-only group" + ); // Admin in member only group.set_admin("piggo@piggo.space", true).unwrap(); - assert!(group.can_write("piggo@piggo.space"), "admin non-member can write in member-only group"); + assert!( + group.can_write("piggo@piggo.space"), + "admin non-member can write in member-only group" + ); group.set_admin("piggo@piggo.space", false).unwrap(); - assert!(!group.can_write("piggo@piggo.space"), "removed admin removes privileged write access"); + assert!( + !group.can_write("piggo@piggo.space"), + "removed admin removes privileged write access" + ); // Member in member only group.set_member("piggo@piggo.space", true).unwrap(); - assert!(group.can_write("piggo@piggo.space"), "member can post in member-only group"); + assert!( + group.can_write("piggo@piggo.space"), + "member can post in member-only group" + ); group.set_admin("piggo@piggo.space", true).unwrap(); - assert!(group.can_write("piggo@piggo.space"), "member+admin can post in member-only group"); + assert!( + group.can_write("piggo@piggo.space"), + "member+admin can post in member-only group" + ); } #[test] @@ -285,7 +299,10 @@ mod tests { // Banning single user let mut group = GroupConfig::default(); group.ban_user("piggo@piggo.space", true).unwrap(); - assert!(!group.can_write("piggo@piggo.space"), "banned user can't post"); + assert!( + !group.can_write("piggo@piggo.space"), + "banned user can't post" + ); group.ban_user("piggo@piggo.space", false).unwrap(); assert!(group.can_write("piggo@piggo.space"), "un-ban works"); } @@ -299,13 +316,25 @@ mod tests { group.set_member("piggo@piggo.space", true).unwrap(); assert!(group.can_write("piggo@piggo.space"), "member can write"); assert!(group.is_member("piggo@piggo.space"), "member is member"); - assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default"); + assert!( + !group.is_banned("piggo@piggo.space"), + "user not banned by default" + ); group.ban_user("piggo@piggo.space", true).unwrap(); - assert!(group.is_member("piggo@piggo.space"), "still member even if banned"); - assert!(group.is_banned("piggo@piggo.space"), "banned user is banned"); - - assert!(!group.can_write("piggo@piggo.space"), "banned member can't post"); + assert!( + group.is_member("piggo@piggo.space"), + "still member even if banned" + ); + assert!( + group.is_banned("piggo@piggo.space"), + "banned user is banned" + ); + + assert!( + !group.can_write("piggo@piggo.space"), + "banned member can't post" + ); // unban group.ban_user("piggo@piggo.space", false).unwrap(); @@ -318,9 +347,18 @@ mod tests { assert!(group.can_write("hitler@nazi.camp"), "randos can write"); group.ban_server("nazi.camp", true).unwrap(); - assert!(!group.can_write("hitler@nazi.camp"), "users from banned server can't write"); - assert!(!group.can_write("1488@nazi.camp"), "users from banned server can't write"); - assert!(group.can_write("troll@freezepeach.xyz"), "other users can still write"); + assert!( + !group.can_write("hitler@nazi.camp"), + "users from banned server can't write" + ); + assert!( + !group.can_write("1488@nazi.camp"), + "users from banned server can't write" + ); + assert!( + group.can_write("troll@freezepeach.xyz"), + "other users can still write" + ); group.ban_server("nazi.camp", false).unwrap(); assert!(group.can_write("hitler@nazi.camp"), "server unban works"); @@ -331,16 +369,34 @@ mod tests { let mut group = GroupConfig::default(); group.set_admin("piggo@piggo.space", true).unwrap(); - assert_eq!(Err(GroupError::UserIsAdmin), group.ban_user("piggo@piggo.space", true), "can't bad admin users"); - group.ban_user("piggo@piggo.space", false).expect("can unbad admin"); + assert_eq!( + Err(GroupError::UserIsAdmin), + group.ban_user("piggo@piggo.space", true), + "can't bad admin users" + ); + group + .ban_user("piggo@piggo.space", false) + .expect("can unbad admin"); group.ban_user("hitler@nazi.camp", true).unwrap(); - assert_eq!(Err(GroupError::UserIsBanned), group.set_admin("hitler@nazi.camp", true), "can't make banned users admins"); + assert_eq!( + Err(GroupError::UserIsBanned), + group.set_admin("hitler@nazi.camp", true), + "can't make banned users admins" + ); group.ban_server("freespeechextremist.com", true).unwrap(); - assert_eq!(Err(GroupError::UserIsBanned), group.set_admin("nibber@freespeechextremist.com", true), "can't make server-banned users admins"); + assert_eq!( + Err(GroupError::UserIsBanned), + group.set_admin("nibber@freespeechextremist.com", true), + "can't make server-banned users admins" + ); assert!(group.is_admin("piggo@piggo.space")); - assert_eq!(Err(GroupError::AdminsOnServer), group.ban_server("piggo.space", true), "can't bad server with admins"); + assert_eq!( + Err(GroupError::AdminsOnServer), + group.ban_server("piggo.space", true), + "can't bad server with admins" + ); } } diff --git a/src/store/mod.rs b/src/store/mod.rs index 37a2257..8ebe623 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,10 +1,7 @@ -use std::collections::{HashMap, HashSet}; -use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use elefren::{FediClient, Registration, Scopes, scopes}; -use elefren::entities::event::Event; +use elefren::{scopes, FediClient, Registration, Scopes}; use futures::StreamExt; use tokio::sync::RwLock; @@ -57,12 +54,16 @@ impl ConfigStore { } /// Spawn a new group - pub async fn auth_new_group(self: &Arc, opts: NewGroupOptions) -> Result { + pub async fn auth_new_group( + self: &Arc, + opts: NewGroupOptions, + ) -> Result { let registration = Registration::new(&opts.server) .client_name("group-actor") .force_login(true) .scopes(make_scopes()) - .build().await?; + .build() + .await?; println!("--- Authenticating NEW bot user @{} ---", opts.acct); let client = elefren::helpers::cli::authenticate(registration).await?; @@ -83,14 +84,18 @@ impl ConfigStore { /// Re-auth an existing group pub async fn reauth_group(self: &Arc, acct: &str) -> Result { let groups = self.data.read().await; - let mut config = groups.get_group_config(acct).ok_or(GroupError::GroupNotExist)?.clone(); + let mut config = groups + .get_group_config(acct) + .ok_or(GroupError::GroupNotExist)? + .clone(); println!("--- Re-authenticating bot user @{} ---", acct); let registration = Registration::new(config.get_appdata().base.to_string()) .client_name("group-actor") .force_login(true) .scopes(make_scopes()) - .build().await?; + .build() + .await?; let client = elefren::helpers::cli::authenticate(registration).await?; let appdata = client.data.clone(); @@ -111,45 +116,52 @@ impl ConfigStore { let groups_iter = groups.iter_groups().cloned(); // Connect in parallel - futures::stream::iter(groups_iter).map(|gc| async { - if !gc.is_enabled() { - debug!("Group @{} is DISABLED", gc.get_acct()); - return None; - } - - debug!("Connecting to @{}", gc.get_acct()); - - let client = FediClient::from(gc.get_appdata().clone()); - - match client.verify_credentials().await { - Ok(account) => { - info!("Group account verified: @{}, {}", account.acct, account.display_name); - } - Err(e) => { - error!("Group @{} auth error: {}", gc.get_acct(), e); + futures::stream::iter(groups_iter) + .map(|gc| async { + if !gc.is_enabled() { + debug!("Group @{} is DISABLED", gc.get_acct()); return None; } - }; - Some(GroupHandle { - client, - config: gc, - store: self.clone(), + debug!("Connecting to @{}", gc.get_acct()); + + let client = FediClient::from(gc.get_appdata().clone()); + + match client.verify_credentials().await { + Ok(account) => { + info!( + "Group account verified: @{}, {}", + account.acct, account.display_name + ); + } + Err(e) => { + error!("Group @{} auth error: {}", gc.get_acct(), e); + return None; + } + }; + + Some(GroupHandle { + client, + config: gc, + store: self.clone(), + }) }) - }).buffer_unordered(8).collect::>().await - .into_iter().flatten().collect() + .buffer_unordered(8) + .collect::>() + .await + .into_iter() + .flatten() + .collect() } pub(crate) async fn get_group_config(&self, group: &str) -> Option { let c = self.data.read().await; - c.get_group_config(group).map(|inner| { - inner.clone() - }) + c.get_group_config(group).cloned() } //noinspection RsSelfConvention /// Set group config to the store. The store then saved. - pub(crate) async fn set_group_config<'a>(&'a self, config: GroupConfig) -> Result<(), GroupError> { + pub(crate) async fn set_group_config(&self, config: GroupConfig) -> Result<(), GroupError> { let mut data = self.data.write().await; data.set_group_config(config); self.persist(&data).await?; @@ -158,13 +170,16 @@ impl ConfigStore { /// Persist the store async fn persist(&self, data: &Config) -> Result<(), GroupError> { - tokio::fs::write(&self.store_path, - if self.save_pretty { - serde_json::to_string_pretty(&data) - } else { - serde_json::to_string(&data) - }?.as_bytes()) - .await?; + tokio::fs::write( + &self.store_path, + if self.save_pretty { + serde_json::to_string_pretty(&data) + } else { + serde_json::to_string(&data) + }? + .as_bytes(), + ) + .await?; Ok(()) } } diff --git a/src/utils.rs b/src/utils.rs index 57e155d..00fcf24 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,44 +1,49 @@ -use std::error::Error; use std::borrow::Cow; +use std::error::Error; + use crate::error::GroupError; pub trait LogError { - fn log_error>(self, msg: S); + fn log_error>(self, msg: S); } -impl LogError for Result { - fn log_error>(self, msg: S) { +impl LogError for Result { + fn log_error>(self, msg: S) { match self { Ok(_) => {} Err(e) => { - error!("{}: {}", msg.as_ref(), e); + error!("{}: {}", msg.as_ref(), e); } } } } -pub(crate) fn acct_to_server(acct : &str) -> Option<&str> { +pub(crate) fn acct_to_server(acct: &str) -> Option<&str> { acct.trim_start_matches('@').split('@').nth(1) } -pub(crate) fn normalize_acct<'a, 'g>(acct : &'a str, group: &'g str) -> Result, GroupError> { +pub(crate) fn normalize_acct<'a, 'g>( + acct: &'a str, + group: &'g str, +) -> Result, GroupError> { let acct = acct.trim_start_matches('@'); if acct_to_server(acct).is_some() { + // already has server Ok(Cow::Borrowed(acct)) + } else if let Some(gs) = acct_to_server(group) { + // attach server from the group actor + Ok(Cow::Owned(format!("{}@{}", acct, gs))) } else { - if let Some(gs) = acct_to_server(group) { - Ok(Cow::Owned(format!("{}@{}", acct, gs))) - } else { - Err(GroupError::BadConfig(format!("Group acct {} is missing server!", group).into())) - } + Err(GroupError::BadConfig( + format!("Group acct {} is missing server!", group).into(), + )) } } #[cfg(test)] mod test { - use crate::utils::{acct_to_server, normalize_acct}; use crate::error::GroupError; - use std::borrow::Cow; + use crate::utils::{acct_to_server, normalize_acct}; #[test] fn test_acct_to_server() { @@ -49,13 +54,37 @@ mod test { #[test] fn test_normalize_acct() { - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("piggo", "betty@piggo.space")); - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("@piggo", "betty@piggo.space")); - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("@piggo@piggo.space", "betty@piggo.space")); - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("@piggo@piggo.space", "oggip@mastodon.social")); - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("piggo@piggo.space", "oggip@mastodon.social")); - assert_eq!(Ok("piggo@mastodon.social".into()), normalize_acct("@piggo", "oggip@mastodon.social")); - assert_eq!(Ok("piggo@piggo.space".into()), normalize_acct("piggo@piggo.space", "uhh")); - assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh")); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("piggo", "betty@piggo.space") + ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("@piggo", "betty@piggo.space") + ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("@piggo@piggo.space", "betty@piggo.space") + ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("@piggo@piggo.space", "oggip@mastodon.social") + ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("piggo@piggo.space", "oggip@mastodon.social") + ); + assert_eq!( + Ok("piggo@mastodon.social".into()), + normalize_acct("@piggo", "oggip@mastodon.social") + ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("piggo@piggo.space", "uhh") + ); + assert_eq!( + Err(GroupError::BadConfig("_".into())), + normalize_acct("piggo", "uhh") + ); } }