use std::collections::{HashMap, HashSet}; use elefren::AppData; use crate::error::GroupError; /// This is the inner data struct holding the config #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub(crate) struct Config { pub(crate) groups: HashMap, } impl Config { // pub(crate) fn iter_groups(&self) -> impl Iterator { // self.groups.values() // } 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) { self.groups.insert(grp.acct.clone(), grp); } } /// This is the inner data struct holding a group's config #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub(crate) struct GroupConfig { enabled: bool, /// Group actor's acct acct: String, /// elefren data appdata: AppData, /// Server's character limit character_limit: usize, /// Hashtags the group will auto-boost from it's members group_tags: HashSet, /// 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 member_users: HashSet, /// List of users banned from posting to the group banned_users: HashSet, /// Users who decided they don't want to be shared to the group (does not apply to members) optout_users: HashSet, /// True if only members should be allowed to write member_only: bool, /// Banned domain names, e.g. kiwifarms.cc banned_servers: HashSet, /// Last seen notification timestamp (millis) last_notif_ts: u64, /// Last seen status timestamp (millis) last_status_ts: u64, #[serde(skip)] dirty: bool, } impl Default for GroupConfig { fn default() -> Self { Self { enabled: true, acct: "".to_string(), appdata: AppData { base: Default::default(), client_id: Default::default(), client_secret: Default::default(), redirect: Default::default(), token: Default::default(), }, character_limit: 5000, group_tags: Default::default(), admin_users: Default::default(), member_users: Default::default(), banned_users: Default::default(), optout_users: Default::default(), member_only: false, banned_servers: Default::default(), last_notif_ts: 0, last_status_ts: 0, dirty: false, } } } impl GroupConfig { pub(crate) fn new(acct: String, appdata: AppData) -> Self { Self { acct, appdata, ..Default::default() } } pub(crate) fn get_character_limit(&self) -> usize { self.character_limit } pub(crate) fn is_enabled(&self) -> bool { self.enabled } /* pub(crate) fn set_enabled(&mut self, ena: bool) { self.enabled = ena; self.mark_dirty(); } */ pub(crate) fn get_appdata(&self) -> &AppData { &self.appdata } pub(crate) fn set_appdata(&mut self, appdata: AppData) { if self.appdata != appdata { self.mark_dirty(); } self.appdata = appdata; } pub(crate) fn get_admins(&self) -> impl Iterator { self.admin_users.iter() } pub(crate) fn get_members(&self) -> impl Iterator { self.member_users.iter() } pub(crate) fn get_tags(&self) -> impl Iterator { self.group_tags.iter() } pub(crate) fn set_last_notif(&mut self, ts: u64) { if self.last_notif_ts != ts { self.mark_dirty(); } self.last_notif_ts = self.last_notif_ts.max(ts); } pub(crate) fn get_last_notif(&self) -> u64 { self.last_notif_ts } pub(crate) fn set_last_status(&mut self, ts: u64) { if self.last_status_ts != ts { self.mark_dirty(); } self.last_status_ts = self.last_status_ts.max(ts); } pub(crate) fn get_last_status(&self) -> u64 { self.last_status_ts } pub(crate) fn get_acct(&self) -> &str { &self.acct } pub(crate) fn is_optout(&self, acct: &str) -> bool { self.optout_users.contains(acct) } pub(crate) fn is_admin(&self, acct: &str) -> bool { self.admin_users.contains(acct) } pub(crate) fn is_member(&self, acct: &str) -> bool { self.member_users.contains(acct) } pub(crate) fn is_member_or_admin(&self, acct: &str) -> bool { self.is_member(acct) || self.is_admin(acct) } pub(crate) fn is_banned(&self, acct: &str) -> bool { self.banned_users.contains(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 fn is_users_server_banned(&self, acct: &str) -> bool { let server = acct_to_server(acct); self.is_server_banned(server) } pub(crate) fn can_write(&self, acct: &str) -> bool { if self.is_admin(acct) { true } else { !self.is_banned(acct) && (!self.is_member_only() || self.is_member(acct)) } } pub(crate) fn set_admin(&mut self, acct: &str, admin: bool) -> Result<(), GroupError> { let change = if admin { if self.is_banned(acct) { return Err(GroupError::UserIsBanned); } self.admin_users.insert(acct.to_owned()) } else { self.admin_users.remove(acct) }; if change { self.mark_dirty(); } Ok(()) } pub(crate) fn set_member(&mut self, acct: &str, member: bool) -> Result<(), GroupError> { let change = if member { if self.is_banned(acct) { return Err(GroupError::UserIsBanned); } self.member_users.insert(acct.to_owned()) } else { self.member_users.remove(acct) }; if change { self.mark_dirty(); } Ok(()) } pub(crate) fn set_optout(&mut self, acct: &str, optout: bool) { let change = if optout { self.optout_users.insert(acct.to_owned()) } else { self.optout_users.remove(acct) }; if change { self.mark_dirty(); } } pub(crate) fn ban_user(&mut self, acct: &str, ban: bool) -> Result<(), GroupError> { let mut change = false; if ban { if self.is_admin(acct) { return Err(GroupError::UserIsAdmin); } // Banned user is also kicked change |= self.member_users.remove(acct); change |= self.banned_users.insert(acct.to_owned()); } else { change |= self.banned_users.remove(acct); } if change { self.mark_dirty(); } Ok(()) } pub(crate) fn ban_server(&mut self, server: &str, ban: bool) -> Result<(), GroupError> { let changed = if ban { for acct in &self.admin_users { let acct_server = acct_to_server(acct); if acct_server == server { return Err(GroupError::AdminsOnServer); } } self.banned_servers.insert(server.to_owned()) } else { self.banned_servers.remove(server) }; if changed { self.mark_dirty(); } Ok(()) } pub(crate) fn add_tag(&mut self, tag: &str) { if self.group_tags.insert(tag.to_string()) { self.mark_dirty(); } } pub(crate) fn remove_tag(&mut self, tag: &str) { if self.group_tags.remove(tag) { self.mark_dirty(); } } pub(crate) fn is_tag_followed(&self, tag: &str) -> bool { self.group_tags.contains(tag) } pub(crate) fn set_member_only(&mut self, member_only: bool) { if self.member_only != member_only { self.mark_dirty(); } self.member_only = member_only; } pub(crate) fn is_member_only(&self) -> bool { self.member_only } pub(crate) fn mark_dirty(&mut self) { self.dirty = true; } pub(crate) fn is_dirty(&self) -> bool { self.dirty } pub(crate) fn clear_dirty_status(&mut self) { self.dirty = false; } } fn acct_to_server(acct: &str) -> &str { acct.split('@').nth(1).unwrap_or_default() } #[cfg(test)] mod tests { use crate::error::GroupError; use crate::store::data::{acct_to_server, GroupConfig}; #[test] fn test_acct_to_server() { assert_eq!("pikachu.rocks", acct_to_server("raichu@pikachu.rocks")); assert_eq!("pikachu.rocks", acct_to_server("m@pikachu.rocks")); assert_eq!("", acct_to_server("what")); } #[test] fn test_default_rules() { 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"); } #[test] fn test_member_only() { let mut group = GroupConfig::default(); 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" ); // 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" ); group.set_admin("piggo@piggo.space", false).unwrap(); 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" ); group.set_admin("piggo@piggo.space", true).unwrap(); assert!( group.can_write("piggo@piggo.space"), "member+admin can post in member-only group" ); } #[test] fn test_banned_users() { // 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"); group.ban_user("piggo@piggo.space", false).unwrap(); assert!(group.can_write("piggo@piggo.space"), "un-ban works"); } #[test] fn test_banned_members() { // Banning single user let mut group = GroupConfig::default(); group.set_member_only(true); 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"); 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"); // unban group.ban_user("piggo@piggo.space", false).unwrap(); assert!(group.can_write("piggo@piggo.space"), "un-ban works"); } #[test] fn test_server_ban() { let mut group = GroupConfig::default(); 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"); group.ban_server("nazi.camp", false).unwrap(); assert!(group.can_write("hitler@nazi.camp"), "server unban works"); } #[test] fn test_sanity() { 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"); 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" ); 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!(group.is_admin("piggo@piggo.space")); assert_eq!( Err(GroupError::AdminsOnServer), group.ban_server("piggo.space", true), "can't bad server with admins" ); } }