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 { 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, /// 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, /// 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 last_notif_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(), }, admin_users: Default::default(), member_users: Default::default(), banned_users: Default::default(), member_only: false, banned_servers: Default::default(), last_notif_ts: 0, dirty: false, } } } impl GroupConfig { pub(crate) fn new(acct: String, appdata: AppData) -> Self { Self { acct, appdata, ..Default::default() } } 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) { self.appdata = appdata; self.mark_dirty(); } 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 set_last_notif(&mut self, ts: u64) { self.last_notif_ts = self.last_notif_ts.max(ts); self.mark_dirty(); } pub(crate) fn get_last_notif(&self) -> u64 { self.last_notif_ts } pub(crate) fn get_acct(&self) -> &str { &self.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_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> { if admin { if self.is_banned(acct) { return Err(GroupError::UserIsBanned); } self.admin_users.insert(acct.to_owned()); } else { self.admin_users.remove(acct); } self.mark_dirty(); Ok(()) } pub(crate) fn set_member(&mut self, acct: &str, member: bool) -> Result<(), GroupError> { if member { if self.is_banned(acct) { return Err(GroupError::UserIsBanned); } self.member_users.insert(acct.to_owned()); } else { self.member_users.remove(acct); } self.mark_dirty(); Ok(()) } pub(crate) fn ban_user(&mut self, acct: &str, ban: bool) -> Result<(), GroupError> { if ban { if self.is_admin(acct) { return Err(GroupError::UserIsAdmin); } self.banned_users.insert(acct.to_owned()); } else { self.banned_users.remove(acct); } Ok(()) } pub(crate) fn ban_server(&mut self, server: &str, ban: bool) -> Result<(), GroupError> { 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); } self.mark_dirty(); Ok(()) } pub(crate) fn set_member_only(&mut self, member_only: bool) { self.member_only = member_only; self.mark_dirty(); } 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" ); } }