diff --git a/CHANGELOG.md b/CHANGELOG.md index c2dfe61..5dd2bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.2.5 +- Add `/undo` command +- Fix users joining via follow not marked as members + ## v0.2.4 - make account lookup try harder diff --git a/Cargo.lock b/Cargo.lock index 0999925..a0cef16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,7 +276,6 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" [[package]] name = "elefren" version = "0.22.0" -source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=a0ebb46#a0ebb46542ede2d235ca6094135a6d6d01d0ecb8" dependencies = [ "chrono", "doc-comment", @@ -328,7 +327,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fedigroups" -version = "0.2.4" +version = "0.2.5" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6195eb9..7fba33e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fedigroups" -version = "0.2.4" +version = "0.2.5" authors = ["Ondřej Hruška "] edition = "2018" publish = false @@ -9,8 +9,8 @@ build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -#elefren = { path = "../elefren22-fork" } -elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "a0ebb46" } +elefren = { path = "../elefren22-fork" } +#elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "a0ebb46" } env_logger = "0.9.0" diff --git a/README.md b/README.md index cf8b097..42990ad 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ For group hashtags to work, the group user must follow all its members; otherwis - `/ping` - ping the group service to check it's running, it will reply - `/join` - join the group - `/leave` - leave the group +- `/undo` - undo a boost of your post into the group, e.g. when you triggered it unintentionally. Use in a reply to the boosted post, tagging the group user. You can also un-boost your status when someone else shared it into the group using `/boost`, this works even if you're not a member. **For admins** - `/announce x` - make a public announcement from the rest of the status. Note: this does not preserve any formatting! @@ -152,3 +153,4 @@ For group hashtags to work, the group user must follow all its members; otherwis - `/kick user, /remove user` - kick a member - `/add #hashtag` - add a hasgtag to the group - `/remove #hashtag` - remove a hasgtag from the group +- `/undo` - when used by an admin, this command can un-boost any status. It can also delete an announcement made in error. diff --git a/src/command.rs b/src/command.rs index fdf6e82..57bd164 100644 --- a/src/command.rs +++ b/src/command.rs @@ -7,6 +7,8 @@ pub enum StatusCommand { Ignore, /// Boost the previous post in the thread Boost, + /// Un-reblog parent post, or delete an announcement + Undo, /// Admin: Ban a user BanUser(String), /// Admin: Un-ban a server @@ -80,6 +82,8 @@ macro_rules! command { static RE_BOOST: once_cell::sync::Lazy = Lazy::new(|| command!(r"b(?:oost)?")); +static RE_UNDO: once_cell::sync::Lazy = Lazy::new(|| command!(r"undo")); + 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!())); @@ -90,13 +94,13 @@ static RE_BAN_SERVER: once_cell::sync::Lazy = Lazy::new(|| command!(r"ban 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_ADD_TAG: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:add)\s+", p_hashtag!())); +static RE_ADD_TAG: once_cell::sync::Lazy = Lazy::new(|| command!(r"add\s+", p_hashtag!())); -static RE_REMOVE_TAG: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:remove)\s+", p_hashtag!())); +static RE_REMOVE_TAG: once_cell::sync::Lazy = Lazy::new(|| command!(r"remove\s+", p_hashtag!())); static RE_GRANT_ADMIN: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:op|admin)\s+", p_user!())); @@ -108,15 +112,15 @@ static RE_CLOSE_GROUP: once_cell::sync::Lazy = Lazy::new(|| command!(r"cl 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|admins)")); static RE_TAGS: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:hashtags|tags)")); -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_JOIN: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:join)")); +static RE_JOIN: once_cell::sync::Lazy = Lazy::new(|| command!(r"join")); -static RE_PING: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:ping)")); +static RE_PING: once_cell::sync::Lazy = Lazy::new(|| command!(r"ping")); static RE_ANNOUNCE: once_cell::sync::Lazy = Lazy::new(|| Regex::new(concat!(r"(?:^|\s|>|\n)[\\/]announce\s+(.*)$")).unwrap()); @@ -167,6 +171,11 @@ pub fn parse_slash_commands(content: &str) -> Vec { return vec![StatusCommand::Help]; } + if RE_UNDO.is_match(&content) { + debug!("UNDO"); + return vec![StatusCommand::Undo]; + } + // additive commands let mut commands = vec![]; @@ -310,11 +319,11 @@ pub fn parse_slash_commands(content: &str) -> Vec { #[cfg(test)] mod test { - use crate::command::{parse_slash_commands, StatusCommand, RE_JOIN, RE_ADD_TAG, RE_A_HASHTAG}; + use crate::command::{parse_slash_commands, RE_A_HASHTAG, RE_ADD_TAG, RE_JOIN, 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_TAGS, RE_OPEN_GROUP, RE_REMOVE_MEMBER, RE_REVOKE_ADMIN, + RE_IGNORE, RE_LEAVE, RE_MEMBERS, RE_OPEN_GROUP, RE_REMOVE_MEMBER, RE_REVOKE_ADMIN, RE_TAGS, RE_UNDO, }; #[test] @@ -512,6 +521,7 @@ mod test { assert!(!RE_MEMBERS.is_match("/admin lain@pleroma.soykaf.com")); assert!(RE_MEMBERS.is_match("/members")); assert!(RE_MEMBERS.is_match("/who")); + assert!(RE_MEMBERS.is_match("/admins")); } #[test] @@ -532,11 +542,9 @@ mod test { for (i, c) in RE_A_HASHTAG.captures_iter("foo #banana #χαλβάς #ласточка").enumerate() { if i == 0 { assert_eq!(c.get(1).unwrap().as_str(), "banana"); - } - else if i == 1 { + } else if i == 1 { assert_eq!(c.get(1).unwrap().as_str(), "χαλβάς"); - } - else if i == 2 { + } else if i == 2 { assert_eq!(c.get(1).unwrap().as_str(), "ласточка"); } } @@ -551,6 +559,15 @@ mod test { assert!(RE_LEAVE.is_match("/leave z")); } + #[test] + fn test_undo() { + assert!(!RE_UNDO.is_match("/list")); + assert!(RE_UNDO.is_match("/undo")); + assert!(RE_UNDO.is_match("/undo")); + assert!(RE_UNDO.is_match("x /undo")); + assert!(RE_UNDO.is_match("/undo z")); + } + #[test] fn test_join() { assert!(!RE_JOIN.is_match("/list")); @@ -595,7 +612,7 @@ mod test { vec![ StatusCommand::BanUser("lain".to_string()), StatusCommand::BanUser("piggo@piggo.space".to_string()), - StatusCommand::BanServer("soykaf.com".to_string()) + StatusCommand::BanServer("soykaf.com".to_string()), ], parse_slash_commands("let's /ban @lain! /ban @piggo@piggo.space and also /ban soykaf.com") ); diff --git a/src/group_handler/handle_mention.rs b/src/group_handler/handle_mention.rs index 48661c9..17f4011 100644 --- a/src/group_handler/handle_mention.rs +++ b/src/group_handler/handle_mention.rs @@ -10,9 +10,11 @@ use crate::error::GroupError; use crate::group_handler::GroupHandle; use crate::store::data::GroupConfig; use crate::utils::{LogError, normalize_acct}; +use elefren::entities::account::Account; pub struct ProcessMention<'a> { status: Status, + group_account: &'a Account, config: &'a mut GroupConfig, client: &'a mut FediClient, group_acct: String, @@ -107,6 +109,7 @@ impl<'a> ProcessMention<'a> { } let pm = Self { + group_account: &gh.group_account, status_user_id: status.account.id.to_string(), client: &mut gh.client, can_write: gh.config.can_write(&status_acct), @@ -151,6 +154,10 @@ impl<'a> ProcessMention<'a> { for cmd in commands { match cmd { + StatusCommand::Undo => { + self.cmd_undo().await + .log_error("Error handling undo cmd"); + } StatusCommand::Ignore => { unreachable!(); // Handled above } @@ -296,6 +303,31 @@ impl<'a> ProcessMention<'a> { } } + async fn cmd_undo(&mut self) -> Result<(), GroupError> { + if let (Some(ref parent_account_id), Some(ref parent_status_id)) = (&self.status.in_reply_to_account_id, &self.status.in_reply_to_id) { + if parent_account_id == &self.group_account.id { + // This is a post sent by the group user, likely an announcement. + // Undo here means delete it. + if self.is_admin { + info!("Deleting group post #{}", parent_status_id); + self.client.delete_status(parent_status_id).await?; + } else { + warn!("Only admin can delete announcements."); + } + } else { + if self.is_admin || parent_account_id == &self.status_user_id { + info!("Un-reblogging post #{}", parent_status_id); + // User unboosting own post boosted by accident, or admin doing it + self.client.unreblog(parent_status_id).await?; + } else { + self.add_reply("You don't have rights to do that."); + } + } + } + + Ok(()) + } + async fn cmd_ban_user(&mut self, user: &str) -> Result<(), GroupError> { let u = normalize_acct(user, &self.group_acct)?; if self.is_admin { @@ -519,37 +551,42 @@ impl<'a> ProcessMention<'a> { } self.add_reply("\n\ - To share a post, mention the group user or use one of the group hashtags. \ + To share a post, @ the group user or use a group hashtag. \ Replies and mentions with commands won't be shared.\n\ \n\ **Supported commands:**\n\ `/boost`, `/b` - boost the replied-to post into the group\n\ `/ignore`, `/i` - make the group ignore the post\n\ - `/ping` - check the service is alive\n\ `/tags` - show group hashtags\n\ - `/join` - join the group\n\ + `/join` - (re-)join the group\n\ `/leave` - leave the group"); - if self.config.is_member_only() { + if self.is_admin { self.add_reply("`/members`, `/who` - show group members / admins"); + // undo is listed as an admin command } else { - self.add_reply("`/members`, `/who` - show group admins"); + self.add_reply("`/admins` - show group admins"); + self.add_reply("`/undo` - un-boost your post (use in a reply)"); } + // XXX when used on instance with small character limit, this won't fit! + if self.is_admin { self.add_reply("\n\ **Admin commands:**\n\ - `/add user` - add a member (use e-mail style address)\n\ + `/ping` - check the group works\n\ + `/add user` - add a member (user@domain)\n\ `/remove user` - remove a member\n\ `/add #hashtag` - add a group hashtag\n\ `/remove #hashtag` - remove a group hashtag\n\ + `/undo` - un-boost a replied-to post, delete an announcement\n\ `/ban x` - ban a user or server\n\ `/unban x` - lift a ban\n\ `/admin user` - grant admin rights\n\ `/deadmin user` - revoke admin rights\n\ `/opengroup` - make member-only\n\ `/closegroup` - make public-access\n\ - `/announce x` - make a public announcement from the rest of the status (without formatting)"); + `/announce x` - make a public announcement"); } } diff --git a/src/group_handler/mod.rs b/src/group_handler/mod.rs index a88e67e..3b1e1bd 100644 --- a/src/group_handler/mod.rs +++ b/src/group_handler/mod.rs @@ -18,12 +18,14 @@ use crate::store::ConfigStore; use crate::store::data::GroupConfig; use crate::utils::{LogError, normalize_acct, VisExt}; use crate::command::StatusCommand; +use elefren::entities::account::Account; mod handle_mention; /// This is one group's config store capable of persistence #[derive(Debug)] pub struct GroupHandle { + pub(crate) group_account: Account, pub(crate) client: FediClient, pub(crate) config: GroupConfig, pub(crate) store: Arc, @@ -440,7 +442,7 @@ impl GroupHandle { admins.sort(); format!("\ - @{user} Welcome! This group has posting restricted to members. \ + @{user} Welcome to the group! This group has posting restricted to members. \ If you'd like to join, please ask one of the group admins:\n\ {admins}", user = notif_acct, @@ -448,9 +450,13 @@ impl GroupHandle { ) } else { follow_back = true; + + self.config.set_member(notif_acct, true) + .log_error("Fail add a member"); + format!("\ @{user} Welcome to the group! The group user will now follow you back to complete the sign-up. \ - To share a post, tag the group user or use one of the group hashtags.\n\n\ + To share a post, @ the group user or use a group hashtag.\n\n\ Use /help for more info.", user = notif_acct ) diff --git a/src/store/mod.rs b/src/store/mod.rs index 4c52bf2..5a72065 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -67,12 +67,27 @@ impl ConfigStore { let client = elefren::helpers::cli::authenticate(registration).await?; let appdata = client.data.clone(); - let data = GroupConfig::new(opts.acct, appdata); + let data = GroupConfig::new(opts.acct.clone(), appdata); // save & persist self.set_group_config(data.clone()).await?; + let group_account = match client.verify_credentials().await { + Ok(account) => { + info!( + "Group account verified: @{}, \"{}\"", + account.acct, account.display_name + ); + account + } + Err(e) => { + error!("Group @{} auth error: {}", opts.acct, e); + return Err(e.into()); + } + }; + Ok(GroupHandle { + group_account, client, config: data, store: self.clone(), @@ -101,7 +116,22 @@ impl ConfigStore { config.set_appdata(appdata); self.set_group_config(config.clone()).await?; + let group_account = match client.verify_credentials().await { + Ok(account) => { + info!( + "Group account verified: @{}, \"{}\"", + account.acct, account.display_name + ); + account + } + Err(e) => { + error!("Group @{} auth error: {}", acct, e); + return Err(e.into()); + } + }; + Ok(GroupHandle { + group_account, client, config, store: self.clone(), @@ -125,12 +155,13 @@ impl ConfigStore { let client = FediClient::from(gc.get_appdata().clone()); - match client.verify_credentials().await { + let my_account = match client.verify_credentials().await { Ok(account) => { info!( "Group account verified: @{}, \"{}\"", account.acct, account.display_name ); + account } Err(e) => { error!("Group @{} auth error: {}", gc.get_acct(), e); @@ -139,6 +170,7 @@ impl ConfigStore { }; Some(GroupHandle { + group_account: my_account, client, config: gc, store: self.clone(),