From c52147ad4ddaef12024b76cddcb44ae19656b697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 28 Aug 2021 10:24:35 +0200 Subject: [PATCH] fixes for new release --- CHANGELOG.md | 7 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 35 +++++++++------ src/command.rs | 31 ++++++++++---- src/group_handler/handle_mention.rs | 66 ++++++++++++++++++++--------- src/group_handler/mod.rs | 39 ++++++++--------- src/utils.rs | 10 +++++ 8 files changed, 127 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd2bb2..11c32fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.2.6 +- Allow boosting group hashtags when they are in a reply, except when it is private/DM + or contains actionable commands +- `/follow` and `/unfollow` are now aliases to `/add` and `/remove` (for users and tags) +- Add workaround for pleroma markdown processor eating trailing hashtags +- Command replies are now always DM again so we don't spam timelines + ## v0.2.5 - Add `/undo` command - Fix users joining via follow not marked as members diff --git a/Cargo.lock b/Cargo.lock index f73d330..744c098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fedigroups" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 842ee89..2bf3eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fedigroups" -version = "0.2.5" +version = "0.2.6" authors = ["Ondřej Hruška "] edition = "2018" publish = false diff --git a/README.md b/README.md index 42990ad..2da58fd 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,11 @@ An example systemd service file is included in the repository as well. Make sure ## Group usage +### Sharing into the group + +The group will boost (reblog) any status meeting these criteria: +- + ### Commands Commands are simple text lines you use when mentioning the group user. DMs work well for this. @@ -132,10 +137,10 @@ For group hashtags to work, the group user must follow all its members; otherwis **Basic commands** - `/help` - show help -- `/ignore`, `/i` - make the group completely ignore the post -- `/members`, `/who` - show group members / admins +- `/ignore` (alias `/i`) - make the group completely ignore the post +- `/members` (alias `/who`) - show group members / admins - `/tags` - show group hashtags -- `/boost`, `/b` - boost the replied-to post into the group +- `/boost` (alias `/b`) - boost the replied-to post into the group - `/ping` - ping the group service to check it's running, it will reply - `/join` - join the group - `/leave` - leave the group @@ -143,14 +148,16 @@ For group hashtags to work, the group user must follow all its members; otherwis **For admins** - `/announce x` - make a public announcement from the rest of the status. Note: this does not preserve any formatting! -- `/ban x` - ban a user or a server from the group -- `/unban x` - lift a ban -- `/op user`, `/admin user` - grant admin rights to the group -- `/deop user`, `/deadmin user` - revoke admin rights -- `/opengroup` - make member-only -- `/closegroup` - make public-access -- `/add user` - add a member (use e-mail style address) -- `/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. +- `/ban user@domain` - ban a user from interacting with the group or having their statuses shared +- `/unban user@domain` - lift a user ban +- `/ban domain.tld` - ban a server (works similar to instance mute) +- `/unban domain.tld` - lift a server ban +- `/op user@domain` (alias `/admin`) - grant admin rights to a user +- `/deop user@domain` (alias `/deadmin`) - revoke admin rights +- `/opengroup` - make the group member-only +- `/closegroup` - make the group public-access +- `/add user@domain` (alias `/follow`) - add a member +- `/remove user@domain` (alias `/remove`) - remove a member +- `/add #hashtag` (alias `/follow`) - add a hashtag to the group +- `/remove #hashtag` (alias `/unfollow`) - remove a hashtag from the group +- `/undo` (alias `/delete`) - 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 57bd164..b0cff03 100644 --- a/src/command.rs +++ b/src/command.rs @@ -82,7 +82,7 @@ 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_UNDO: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:delete|undo)")); static RE_IGNORE: once_cell::sync::Lazy = Lazy::new(|| command!(r"i(?:g(?:n(?:ore)?)?)?")); @@ -94,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|follow)\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|unfollow|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|follow)\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|unfollow)\s+", p_hashtag!())); static RE_GRANT_ADMIN: once_cell::sync::Lazy = Lazy::new(|| command!(r"(?:op|admin)\s+", p_user!())); @@ -123,10 +123,13 @@ 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_ANNOUNCE: once_cell::sync::Lazy = - Lazy::new(|| Regex::new(concat!(r"(?:^|\s|>|\n)[\\/]announce\s+(.*)$")).unwrap()); + Lazy::new(|| Regex::new(r"(?:^|\s|>|\n)[\\/]announce\s+(.*)$").unwrap()); static RE_A_HASHTAG: once_cell::sync::Lazy = - Lazy::new(|| Regex::new(concat!(r"(?:^|\b|\s|>|\n)#(\w+)")).unwrap()); + Lazy::new(|| Regex::new(r"(?:^|\b|\s|>|\n)#(\w+)").unwrap()); + +pub static RE_HASHTAG_TRIGGERING_PLEROMA_BUG: once_cell::sync::Lazy = + Lazy::new(|| Regex::new(r"(?:^|\b|\s|>|\n)#\w+[^\s]*$").unwrap()); pub fn parse_status_tags(content: &str) -> Vec { debug!("Raw content: {}", content); @@ -319,7 +322,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { #[cfg(test)] mod test { - use crate::command::{parse_slash_commands, RE_A_HASHTAG, RE_ADD_TAG, RE_JOIN, StatusCommand}; + use crate::command::{parse_slash_commands, RE_A_HASHTAG, RE_HASHTAG_TRIGGERING_PLEROMA_BUG, 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, @@ -414,6 +417,7 @@ mod test { fn test_add_member() { assert!(RE_ADD_MEMBER.is_match("/add lain@pleroma.soykaf.com")); assert!(RE_ADD_MEMBER.is_match("/add @lain@pleroma.soykaf.com")); + assert!(RE_ADD_MEMBER.is_match("/follow @lain@pleroma.soykaf.com")); assert!(RE_ADD_MEMBER.is_match("\\add @lain")); let c = RE_ADD_MEMBER.captures("/add @lain"); @@ -443,6 +447,7 @@ mod test { assert!(RE_ADD_TAG.is_match("\\add #ласточка")); assert!(RE_ADD_TAG.is_match("/add #nya.")); assert!(RE_ADD_TAG.is_match("/add #nya)")); + assert!(RE_ADD_TAG.is_match("/follow #nya)")); assert!(RE_ADD_TAG.is_match("/add #nya and more)")); let c = RE_ADD_TAG.captures("/add #breadposting"); @@ -549,6 +554,15 @@ mod test { } } } + #[test] + fn test_match_tag_at_end() { + assert!(!RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag sdfsd")); + assert!(!RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag .")); + assert!(RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag")); + assert!(RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag.")); + assert!(RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag...")); + assert!(RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("#tag...")); + } #[test] fn test_leave() { @@ -563,6 +577,7 @@ mod test { fn test_undo() { assert!(!RE_UNDO.is_match("/list")); assert!(RE_UNDO.is_match("/undo")); + assert!(RE_UNDO.is_match("/delete")); assert!(RE_UNDO.is_match("/undo")); assert!(RE_UNDO.is_match("x /undo")); assert!(RE_UNDO.is_match("/undo z")); diff --git a/src/group_handler/handle_mention.rs b/src/group_handler/handle_mention.rs index 17f4011..3a7cf19 100644 --- a/src/group_handler/handle_mention.rs +++ b/src/group_handler/handle_mention.rs @@ -68,7 +68,7 @@ impl<'a> ProcessMention<'a> { let mut admins = self.config.get_admins().collect::>(); admins.sort(); for a in admins { - self.replies.push(a.to_string()); + self.replies.push(format!("- {}", a)); } } @@ -80,9 +80,9 @@ impl<'a> ProcessMention<'a> { members.dedup(); for m in members { self.replies.push(if admins.contains(&m) { - format!("{} [admin]", m) + format!("- {} [admin]", m) } else { - m.to_string() + format!("- {}", m) }); } } @@ -133,12 +133,12 @@ impl<'a> ProcessMention<'a> { .log_error("Failed to reblog status") } - fn add_reply(&mut self, line: impl ToString) { - self.replies.push(line.to_string()) + fn add_reply(&mut self, line: impl AsRef) { + self.replies.push(line.as_ref().trim().to_string()) } - fn add_announcement(&mut self, line: impl ToString) { - self.announcements.push(line.to_string()) + fn add_announcement<'t>(&mut self, line: impl AsRef) { + self.announcements.push(line.as_ref().trim().to_string()) } async fn handle(mut self) -> Result<(), GroupError> { @@ -239,18 +239,21 @@ impl<'a> ProcessMention<'a> { } if !self.replies.is_empty() { - debug!("replies={:?}", self.replies); - let r = self.replies.join("\n"); - debug!("r={}", r); + let mut msg = self.replies.join("\n"); + debug!("r={}", msg); + + if self.want_markdown { + apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); + } if let Ok(post) = StatusBuilder::new() - .status(format!("@{user}\n{msg}", user = self.status_acct, msg = r)) + .status(format!("@{user} {msg}", user = self.status_acct, msg = msg)) .content_type(if self.want_markdown { "text/markdown" } else { "text/plain" }) - .visibility(self.status.visibility) // Copy visibility + .visibility(Visibility::Direct) .build() { let _ = self.client.new_status(post) @@ -259,7 +262,13 @@ impl<'a> ProcessMention<'a> { } if !self.announcements.is_empty() { - let msg = self.announcements.join("\n"); + let mut msg = self.announcements.join("\n"); + debug!("a={}", msg); + + if self.want_markdown { + apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); + } + let post = StatusBuilder::new() .status(format!("**📢 Group announcement**\n{msg}", msg = msg)) .content_type("text/markdown") @@ -445,8 +454,12 @@ impl<'a> ProcessMention<'a> { async fn cmd_add_tag(&mut self, tag: String) { if self.is_admin { - self.config.add_tag(&tag); - self.add_reply(format!("Tag #{} added to the group!", tag)); + if self.config.is_tag_followed(&tag) { + self.add_reply(format!("Tag \"{}\" added to the group!", tag)); + } else { + self.config.add_tag(&tag); + self.add_reply(format!("Tag \"{}\" was already in group!", tag)); + } } else { self.add_reply("Only admins can manage group tags"); } @@ -454,8 +467,12 @@ impl<'a> ProcessMention<'a> { async fn cmd_remove_tag(&mut self, tag: String) { if self.is_admin { - self.config.remove_tag(&tag); - self.add_reply(format!("Tag #{} removed from the group!", tag)); + if self.config.is_tag_followed(&tag) { + self.config.remove_tag(&tag); + self.add_reply(format!("Tag \"{}\" removed from the group!", tag)); + } else { + self.add_reply(format!("Tag \"{}\" was not in group!", tag)); + } } else { self.add_reply("Only admins can manage group tags"); } @@ -551,8 +568,7 @@ impl<'a> ProcessMention<'a> { } self.add_reply("\n\ - To share a post, @ the group user or use a group hashtag. \ - Replies and mentions with commands won't be shared.\n\ + To share a post, @ the group user or use a group hashtag.\n\ \n\ **Supported commands:**\n\ `/boost`, `/b` - boost the replied-to post into the group\n\ @@ -591,6 +607,7 @@ impl<'a> ProcessMention<'a> { } async fn cmd_list_members(&mut self) { + self.want_markdown = true; if self.is_admin { self.add_reply("Group members:"); self.append_member_list_to_reply(); @@ -602,10 +619,11 @@ impl<'a> ProcessMention<'a> { async fn cmd_list_tags(&mut self) { self.add_reply("Group tags:"); + self.want_markdown = true; let mut tags = self.config.get_tags().collect::>(); tags.sort(); for t in tags { - self.replies.push(format!("#{}", t).to_string()); + self.replies.push(format!("- {}", t).to_string()); } } @@ -671,3 +689,11 @@ impl<'a> ProcessMention<'a> { Ok(()) } } + +fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) { + if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(&msg) { + // if a status ends with a hashtag, pleroma will fuck it up + debug!("Adding \" .\" to fix pleroma hashtag eating bug!"); + msg.push_str(" ."); + } +} diff --git a/src/group_handler/mod.rs b/src/group_handler/mod.rs index 3b1e1bd..013f75c 100644 --- a/src/group_handler/mod.rs +++ b/src/group_handler/mod.rs @@ -250,27 +250,17 @@ impl GroupHandle { let ts = s.timestamp_millis(); self.config.set_last_status(ts); + // Short circuit checks if s.visibility.is_private() { debug!("Status is direct/private, discard"); return Ok(()); } - if s.in_reply_to_id.is_some() { - debug!("Status is a reply, discard"); - return Ok(()); - } - if !s.content.contains('#') { debug!("No tags in status, discard"); return Ok(()); } - let commands = crate::command::parse_slash_commands(&s.content); - if commands.contains(&StatusCommand::Ignore) { - debug!("Post has IGNORE command, discard"); - return Ok(()); - } - let group_user = self.config.get_acct(); let status_user = normalize_acct(&s.account.acct, group_user)?; @@ -279,25 +269,32 @@ impl GroupHandle { return Ok(()); } - if s.content.contains("/add ") - || s.content.contains("/remove ") - || s.content.contains("\\add ") - || s.content.contains("\\remove ") - { - debug!("Looks like a hashtag manipulation command, discard"); + if self.config.is_banned(&status_user) { + debug!("Status author @{} is banned, discard", status_user); return Ok(()); } - if self.config.is_banned(&status_user) { - debug!("Status author @{} is banned.", status_user); + if !self.config.is_member_or_admin(&status_user) { + debug!("Status author @{} is not a member, discard", status_user); return Ok(()); } - if !self.config.is_member_or_admin(&status_user) { - debug!("Status author @{} is not a member.", status_user); + let commands = crate::command::parse_slash_commands(&s.content); + if commands.contains(&StatusCommand::Ignore) { + debug!("Post has IGNORE command, discard"); return Ok(()); } + for m in s.mentions { + let mentioned_user = normalize_acct(&m.acct, group_user)?; + if mentioned_user == group_user { + if !commands.is_empty() { + debug!("Detected commands for this group, tags dont apply; discard"); + return Ok(()); + } + } + } + let tags = crate::command::parse_status_tags(&s.content); debug!("Tags in status: {:?}", tags); diff --git a/src/utils.rs b/src/utils.rs index ad64d71..c1f55db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -95,10 +95,20 @@ mod test { pub trait VisExt: Copy { /// Check if is private or direct fn is_private(self) -> bool; + fn make_unlisted(self) -> Self; } impl VisExt for Visibility { fn is_private(self) -> bool { self == Visibility::Direct || self == Visibility::Private } + + fn make_unlisted(self) -> Self { + match self { + Visibility::Public => { + Visibility::Unlisted + } + other => other, + } + } }