From 957f0dbb3be5a0253a2b812936eec2a35938b0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 27 Aug 2021 00:19:43 +0200 Subject: [PATCH] readme, some fixes --- CHANGELOG.md | 11 +++- Cargo.lock | 4 +- Cargo.toml | 4 +- README.md | 2 +- src/command.rs | 27 ++++++---- src/group_handler/handle_mention.rs | 81 ++++++++++++++++++----------- src/group_handler/mod.rs | 14 ++++- src/main.rs | 6 +++ src/store/mod.rs | 9 ++-- src/utils.rs | 19 ++++--- 10 files changed, 118 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df49b5..43b9893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # Changelog -## v0.2 +## v0.2.2 +- All hashtags, server names and handles are now lowercased = case-insensitive +- Prevent the `-a` flag overwriting existing group in the config +- Update the help text +- `/i` now works in hashtag posts +- `/add user` and `/remove user` now correctly follow/unfollow +## v0.2.1 +- More reliable websocket reconnect, workaround for pleroma socket going silent + +## v0.2.0 - Add hashtag boosting and back-follow/unfollow - Add hashtag commands - Code reorganization diff --git a/Cargo.lock b/Cargo.lock index 9f37efc..49848d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,7 +276,7 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" [[package]] name = "elefren" version = "0.22.0" -source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=54a0e55#54a0e55964784368864f36580c5630f730bf72dc" +source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=a0ebb46#a0ebb46542ede2d235ca6094135a6d6d01d0ecb8" dependencies = [ "chrono", "doc-comment", @@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fedigroups" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6533fa4..6430c81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fedigroups" -version = "0.2.1" +version = "0.2.2" authors = ["Ondřej Hruška "] edition = "2018" publish = false @@ -10,7 +10,7 @@ build = "build.rs" [dependencies] #elefren = { path = "../elefren22-fork" } -elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "54a0e55" } +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 76658bf..cf8b097 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Any user (member in member-only groups) can post to the group by mentioning the ### Group hashtags -Admins can add hashtags to the group config (`/add #hashtag`, remove the same way: `/remove #hashtag`). +Admins can add hashtags to the group config (`/add #hashtag`, remove the same way: `/remove #hashtag`). Hashtags are case-insensitive. When a *group member* posts one of the group hashtags, the group will reblog it. This is a nicer way to share posts, you don't have to mention the group user at all. diff --git a/src/command.rs b/src/command.rs index 97046bb..fdf6e82 100644 --- a/src/command.rs +++ b/src/command.rs @@ -134,7 +134,7 @@ pub fn parse_status_tags(content: &str) -> Vec { let mut tags = vec![]; for c in RE_A_HASHTAG.captures_iter(&content) { if let Some(s) = c.get(1) { - tags.push(s.as_str().to_string()) + tags.push(s.as_str().to_lowercase()) } } @@ -150,6 +150,11 @@ pub fn parse_slash_commands(content: &str) -> Vec { let content = voca_rs::strip::strip_tags(&content); debug!("Stripped tags: {}", content); + if !content.contains('/') && !content.contains('\\') { + // No slash = no command + return vec![]; + } + // short-circuiting commands if RE_IGNORE.is_match(&content) { @@ -221,7 +226,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("BAN USER: {}", s); - commands.push(StatusCommand::BanUser(s.to_owned())); + commands.push(StatusCommand::BanUser(s.to_lowercase())); } } @@ -230,21 +235,21 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("UNBAN USER: {}", s); - commands.push(StatusCommand::UnbanUser(s.to_owned())); + commands.push(StatusCommand::UnbanUser(s.to_lowercase())); } } for c in RE_BAN_SERVER.captures_iter(&content) { if let Some(s) = c.get(1) { debug!("BAN SERVER: {}", s.as_str()); - commands.push(StatusCommand::BanServer(s.as_str().to_owned())); + commands.push(StatusCommand::BanServer(s.as_str().to_lowercase())); } } for c in RE_UNBAN_SERVER.captures_iter(&content) { if let Some(s) = c.get(1) { debug!("UNBAN SERVER: {}", s.as_str()); - commands.push(StatusCommand::UnbanServer(s.as_str().to_owned())); + commands.push(StatusCommand::UnbanServer(s.as_str().to_lowercase())); } } @@ -253,7 +258,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("ADD MEMBER: {}", s); - commands.push(StatusCommand::AddMember(s.to_owned())); + commands.push(StatusCommand::AddMember(s.to_lowercase())); } } @@ -262,7 +267,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("REMOVE USER: {}", s); - commands.push(StatusCommand::RemoveMember(s.to_owned())); + commands.push(StatusCommand::RemoveMember(s.to_lowercase())); } } @@ -270,7 +275,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { if let Some(s) = c.get(1) { let s = s.as_str(); debug!("ADD TAG: {}", s); - commands.push(StatusCommand::AddTag(s.to_owned())); + commands.push(StatusCommand::AddTag(s.to_lowercase())); } } @@ -278,7 +283,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { if let Some(s) = c.get(1) { let s = s.as_str(); debug!("REMOVE TAG: {}", s); - commands.push(StatusCommand::RemoveTag(s.to_owned())); + commands.push(StatusCommand::RemoveTag(s.to_lowercase())); } } @@ -287,7 +292,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("ADD ADMIN: {}", s); - commands.push(StatusCommand::GrantAdmin(s.to_owned())); + commands.push(StatusCommand::GrantAdmin(s.to_lowercase())); } } @@ -296,7 +301,7 @@ pub fn parse_slash_commands(content: &str) -> Vec { let s = s.as_str(); let s = s.trim_start_matches('@'); debug!("REMOVE ADMIN: {}", s); - commands.push(StatusCommand::RemoveAdmin(s.to_owned())); + commands.push(StatusCommand::RemoveAdmin(s.to_lowercase())); } } diff --git a/src/group_handler/handle_mention.rs b/src/group_handler/handle_mention.rs index e145adf..c2647be 100644 --- a/src/group_handler/handle_mention.rs +++ b/src/group_handler/handle_mention.rs @@ -46,9 +46,17 @@ impl<'a> ProcessMention<'a> { Err(e.into()) } Ok(Ok(res)) => { + debug!("Result: {:#?}", res); + if let Some(item) = res.accounts.into_iter().next() { - debug!("Search done, account found"); - Ok(Some(item.id)) + let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?; + if acct_normalized == acct { + debug!("Search done, account found: {}", item.acct); + Ok(Some(item.id)) + } else { + warn!("Search done but found wrong account: {}", item.acct); + Ok(None) + } } else { debug!("Search done, nothing found"); Ok(None) @@ -80,12 +88,14 @@ impl<'a> ProcessMention<'a> { } } - async fn follow_user(&self, id: &str) -> Result<(), GroupError> { + async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> { + debug!("Trying to follow user #{}", id); self.client.follow(id).await?; Ok(()) } - async fn unfollow_user(&self, id: &str) -> Result<(), GroupError> { + async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> { + debug!("Trying to unfollow user #{}", id); self.client.unfollow(id).await?; Ok(()) } @@ -373,13 +383,15 @@ impl<'a> ProcessMention<'a> { match self.config.set_member(&u, true) { Ok(_) => { self.add_reply(format!("User {} added to the group!", u)); - self.follow_user(&self.status_user_id) + self.follow_by_acct(&u) .await.log_error("Failed to follow"); } Err(e) => { self.add_reply(format!("Failed to add user {} to group: {}", u, e)); } } + } else { + debug!("User was already a member"); } } else { self.add_reply("Only admins can manage members"); @@ -505,49 +517,48 @@ impl<'a> ProcessMention<'a> { self.add_reply("This is a public-access group. "); } - if self.config.can_write(&self.status_acct) { - if self.is_admin { - self.add_reply("*You are an admin.*"); - } else { - self.add_reply("*You are a member.*"); - } + if self.is_admin { + self.add_reply("*You are an admin.*"); + } else if self.config.is_member(&self.status_acct) { + self.add_reply("*You are a member.*"); + } else if self.config.is_member_only() { + self.add_reply("*You are not a member, ask one of the admins to add you.*"); } else { - if self.config.is_member_only() { - self.add_reply("*You are not a member, ask one of the admins to add you.*"); - } else { - self.add_reply("*You are not a member, follow or use /join to join the group.*"); - } + self.add_reply("*You are not a member, follow or use /join to join the group.*"); } self.add_reply("\n\ - To share an original post, mention the group user.\n\ + To share a post, mention the group user or use one of the group hashtags. \ 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 completely ignore the post\n\ - `/ping` - check that the service is alive\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\ `/leave` - leave the group"); if self.config.is_member_only() { - self.add_reply("`/members, /who` - show group members / admins"); + self.add_reply("`/members`, `/who` - show group members / admins"); } else { - self.add_reply("`/members, /who` - show group admins"); + self.add_reply("`/members`, `/who` - show group admins"); } if self.is_admin { self.add_reply("\n\ **Admin commands:**\n\ `/add user` - add a member (use e-mail style address)\n\ - `/kick, /remove user` - kick a member\n\ - `/ban x` - ban a user or a server\n\ + `/remove user` - remove a member\n\ + `/add #hashtag` - add a group hashtag\n\ + `/remove #hashtag` - remove a group hashtag\n\ + `/ban x` - ban a user or server\n\ `/unban x` - lift a ban\n\ - `/op, /admin user` - grant admin rights\n\ - `/deop, /deadmin user` - revoke admin rights\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"); + `/announce x` - make a public announcement from the rest of the status (without formatting)"); } } @@ -575,7 +586,7 @@ impl<'a> ProcessMention<'a> { // admin can leave but that's a bad idea let _ = self.config.set_member(&self.status_acct, false); self.add_reply("You're no longer a group member. Unfollow the group user to stop receiving group messages."); - self.unfollow_user(&self.status_user_id).await + self.unfollow_user_by_id(&self.status_user_id).await .log_error("Failed to unfollow"); } } @@ -585,7 +596,7 @@ impl<'a> ProcessMention<'a> { debug!("Already member or admin, try to follow-back again"); // Already a member, so let's try to follow the user // again, maybe first time it failed - self.follow_user(&self.status_user_id).await + self.follow_user_by_id(&self.status_user_id).await .log_error("Failed to follow"); } else { // Not a member yet @@ -598,7 +609,7 @@ impl<'a> ProcessMention<'a> { self.append_admin_list_to_reply(); } else { // Open access, try to follow back - self.follow_user(&self.status_user_id).await + self.follow_user_by_id(&self.status_user_id).await .log_error("Failed to follow"); // This only fails if the user is banned, but that is filtered above @@ -618,7 +629,15 @@ impl<'a> ProcessMention<'a> { async fn unfollow_by_acct(&self, acct: &str) -> Result<(), GroupError> { // Try to unfollow if let Ok(Some(id)) = self.lookup_acct_id(acct, true).await { - self.unfollow_user(&id).await?; + self.unfollow_user_by_id(&id).await?; + } + Ok(()) + } + + async fn follow_by_acct(&self, acct: &str) -> Result<(), GroupError> { + // Try to unfollow + if let Ok(Some(id)) = self.lookup_acct_id(acct, false).await { + self.follow_user_by_id(&id).await?; } Ok(()) } diff --git a/src/group_handler/mod.rs b/src/group_handler/mod.rs index ca1879b..a88e67e 100644 --- a/src/group_handler/mod.rs +++ b/src/group_handler/mod.rs @@ -17,6 +17,7 @@ use crate::error::GroupError; use crate::store::ConfigStore; use crate::store::data::GroupConfig; use crate::utils::{LogError, normalize_acct, VisExt}; +use crate::command::StatusCommand; mod handle_mention; @@ -241,7 +242,7 @@ impl GroupHandle { Ok(()) } - /// Handle a non-mention status + /// Handle a non-mention status for tags async fn handle_status(&mut self, s: Status) -> Result<(), GroupError> { debug!("Handling status #{}", s.id); let ts = s.timestamp_millis(); @@ -252,11 +253,22 @@ impl GroupHandle { 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)?; diff --git a/src/main.rs b/src/main.rs index a13c054..ce7121d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,13 @@ async fn main() -> anyhow::Result<()> { .await?; if let Some(handle) = args.value_of("auth") { + let handle = handle.to_lowercase(); let acct = handle.trim_start_matches('@'); + + if store.group_exists(acct).await { + anyhow::bail!("Group already exists in config!"); + } + if let Some(server) = acct_to_server(acct) { let g = store .auth_new_group(NewGroupOptions { diff --git a/src/store/mod.rs b/src/store/mod.rs index 9d9ba84..4c52bf2 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -152,6 +152,10 @@ impl ConfigStore { .collect() } + pub async fn group_exists(&self, acct : &str) -> bool { + self.data.read().await.groups.contains_key(acct) + } + /* pub(crate) async fn get_group_config(&self, group: &str) -> Option { let c = self.data.read().await; @@ -191,10 +195,7 @@ impl ConfigStore { } fn make_scopes() -> Scopes { - Scopes::read(scopes::Read::Accounts) - | Scopes::read(scopes::Read::Notifications) - | Scopes::read(scopes::Read::Statuses) - | Scopes::read(scopes::Read::Follows) + Scopes::read_all() | Scopes::write(scopes::Write::Statuses) | Scopes::write(scopes::Write::Media) | Scopes::write(scopes::Write::Follows) diff --git a/src/utils.rs b/src/utils.rs index 88ce3b3..ad64d71 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::error::Error; use elefren::status_builder::Visibility; @@ -24,14 +23,14 @@ 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> { - let acct = acct.trim_start_matches('@'); - if acct_to_server(acct).is_some() { +pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result { + let acct = acct.trim_start_matches('@').to_lowercase(); + if acct_to_server(&acct).is_some() { // already has server - Ok(Cow::Borrowed(acct)) + Ok(acct) } else if let Some(gs) = acct_to_server(group) { // attach server from the group actor - Ok(Cow::Owned(format!("{}@{}", acct, gs))) + Ok(format!("{}@{}", acct, gs)) } else { Err(GroupError::BadConfig( format!("Group acct {} is missing server!", group).into(), @@ -81,6 +80,14 @@ mod test { Ok("piggo@piggo.space".into()), normalize_acct("piggo@piggo.space", "uhh") ); + assert_eq!( + Ok("piggo@piggo.space".into()), + normalize_acct("piGGgo@pIggo.spaCe", "uhh") + ); + assert_eq!( + Ok("piggo@banana.nana".into()), + normalize_acct("piGGgo", "foo@baNANA.nana") + ); assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh")); } }