fixes for new release

This commit is contained in:
Ondřej Hruška 2021-08-28 10:24:35 +02:00
parent 31a9d767ae
commit c52147ad4d
No known key found for this signature in database
GPG key ID: 2C5FD5035250423D
8 changed files with 128 additions and 66 deletions

View file

@ -1,5 +1,12 @@
# Changelog # 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 ## v0.2.5
- Add `/undo` command - Add `/undo` command
- Fix users joining via follow not marked as members - Fix users joining via follow not marked as members

2
Cargo.lock generated
View file

@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fedigroups" name = "fedigroups"
version = "0.2.5" version = "0.2.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.2.5" version = "0.2.6"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
publish = false publish = false

View file

@ -93,6 +93,11 @@ An example systemd service file is included in the repository as well. Make sure
## Group usage ## Group usage
### Sharing into the group
The group will boost (reblog) any status meeting these criteria:
-
### Commands ### Commands
Commands are simple text lines you use when mentioning the group user. DMs work well for this. 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** **Basic commands**
- `/help` - show help - `/help` - show help
- `/ignore`, `/i` - make the group completely ignore the post - `/ignore` (alias `/i`) - make the group completely ignore the post
- `/members`, `/who` - show group members / admins - `/members` (alias `/who`) - show group members / admins
- `/tags` - show group hashtags - `/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 - `/ping` - ping the group service to check it's running, it will reply
- `/join` - join the group - `/join` - join the group
- `/leave` - leave 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** **For admins**
- `/announce x` - make a public announcement from the rest of the status. Note: this does not preserve any formatting! - `/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 - `/ban user@domain` - ban a user from interacting with the group or having their statuses shared
- `/unban x` - lift a ban - `/unban user@domain` - lift a user ban
- `/op user`, `/admin user` - grant admin rights to the group - `/ban domain.tld` - ban a server (works similar to instance mute)
- `/deop user`, `/deadmin user` - revoke admin rights - `/unban domain.tld` - lift a server ban
- `/opengroup` - make member-only - `/op user@domain` (alias `/admin`) - grant admin rights to a user
- `/closegroup` - make public-access - `/deop user@domain` (alias `/deadmin`) - revoke admin rights
- `/add user` - add a member (use e-mail style address) - `/opengroup` - make the group member-only
- `/kick user, /remove user` - kick a member - `/closegroup` - make the group public-access
- `/add #hashtag` - add a hasgtag to the group - `/add user@domain` (alias `/follow`) - add a member
- `/remove #hashtag` - remove a hasgtag from the group - `/remove user@domain` (alias `/remove`) - remove a member
- `/undo` - when used by an admin, this command can un-boost any status. It can also delete an announcement made in error. - `/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.

View file

@ -82,7 +82,7 @@ macro_rules! command {
static RE_BOOST: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"b(?:oost)?")); static RE_BOOST: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"b(?:oost)?"));
static RE_UNDO: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"undo")); static RE_UNDO: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:delete|undo)"));
static RE_IGNORE: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"i(?:g(?:n(?:ore)?)?)?")); static RE_IGNORE: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"i(?:g(?:n(?:ore)?)?)?"));
@ -94,13 +94,13 @@ static RE_BAN_SERVER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ban
static RE_UNBAN_SERVER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"unban\s+", p_server!())); static RE_UNBAN_SERVER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"unban\s+", p_server!()));
static RE_ADD_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"add\s+", p_user!())); static RE_ADD_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:add|follow)\s+", p_user!()));
static RE_REMOVE_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:kick|remove)\s+", p_user!())); static RE_REMOVE_MEMBER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:kick|unfollow|remove)\s+", p_user!()));
static RE_ADD_TAG: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"add\s+", p_hashtag!())); static RE_ADD_TAG: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:add|follow)\s+", p_hashtag!()));
static RE_REMOVE_TAG: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"remove\s+", p_hashtag!())); static RE_REMOVE_TAG: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:remove|unfollow)\s+", p_hashtag!()));
static RE_GRANT_ADMIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:op|admin)\s+", p_user!())); static RE_GRANT_ADMIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:op|admin)\s+", p_user!()));
@ -123,10 +123,13 @@ static RE_JOIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"join"));
static RE_PING: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ping")); static RE_PING: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ping"));
static RE_ANNOUNCE: once_cell::sync::Lazy<Regex> = static RE_ANNOUNCE: once_cell::sync::Lazy<Regex> =
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<Regex> = static RE_A_HASHTAG: once_cell::sync::Lazy<Regex> =
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<Regex> =
Lazy::new(|| Regex::new(r"(?:^|\b|\s|>|\n)#\w+[^\s]*$").unwrap());
pub fn parse_status_tags(content: &str) -> Vec<String> { pub fn parse_status_tags(content: &str) -> Vec<String> {
debug!("Raw content: {}", content); debug!("Raw content: {}", content);
@ -319,7 +322,7 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
#[cfg(test)] #[cfg(test)]
mod 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::{ use super::{
RE_ADD_MEMBER, RE_ANNOUNCE, RE_BAN_SERVER, RE_BAN_USER, RE_BOOST, RE_CLOSE_GROUP, RE_GRANT_ADMIN, RE_HELP, 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() { 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("/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")); assert!(RE_ADD_MEMBER.is_match("\\add @lain"));
let c = RE_ADD_MEMBER.captures("/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 #ласточка"));
assert!(RE_ADD_TAG.is_match("/add #nya.")); assert!(RE_ADD_TAG.is_match("/add #nya."));
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)")); assert!(RE_ADD_TAG.is_match("/add #nya and more)"));
let c = RE_ADD_TAG.captures("/add #breadposting"); 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] #[test]
fn test_leave() { fn test_leave() {
@ -563,6 +577,7 @@ mod test {
fn test_undo() { fn test_undo() {
assert!(!RE_UNDO.is_match("/list")); assert!(!RE_UNDO.is_match("/list"));
assert!(RE_UNDO.is_match("/undo")); assert!(RE_UNDO.is_match("/undo"));
assert!(RE_UNDO.is_match("/delete"));
assert!(RE_UNDO.is_match("/undo")); assert!(RE_UNDO.is_match("/undo"));
assert!(RE_UNDO.is_match("x /undo")); assert!(RE_UNDO.is_match("x /undo"));
assert!(RE_UNDO.is_match("/undo z")); assert!(RE_UNDO.is_match("/undo z"));

View file

@ -68,7 +68,7 @@ impl<'a> ProcessMention<'a> {
let mut admins = self.config.get_admins().collect::<Vec<_>>(); let mut admins = self.config.get_admins().collect::<Vec<_>>();
admins.sort(); admins.sort();
for a in admins { 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(); members.dedup();
for m in members { for m in members {
self.replies.push(if admins.contains(&m) { self.replies.push(if admins.contains(&m) {
format!("{} [admin]", m) format!("- {} [admin]", m)
} else { } else {
m.to_string() format!("- {}", m)
}); });
} }
} }
@ -133,12 +133,12 @@ impl<'a> ProcessMention<'a> {
.log_error("Failed to reblog status") .log_error("Failed to reblog status")
} }
fn add_reply(&mut self, line: impl ToString) { fn add_reply(&mut self, line: impl AsRef<str>) {
self.replies.push(line.to_string()) self.replies.push(line.as_ref().trim().to_string())
} }
fn add_announcement(&mut self, line: impl ToString) { fn add_announcement<'t>(&mut self, line: impl AsRef<str>) {
self.announcements.push(line.to_string()) self.announcements.push(line.as_ref().trim().to_string())
} }
async fn handle(mut self) -> Result<(), GroupError> { async fn handle(mut self) -> Result<(), GroupError> {
@ -239,18 +239,21 @@ impl<'a> ProcessMention<'a> {
} }
if !self.replies.is_empty() { if !self.replies.is_empty() {
debug!("replies={:?}", self.replies); let mut msg = self.replies.join("\n");
let r = self.replies.join("\n"); debug!("r={}", msg);
debug!("r={}", r);
if self.want_markdown {
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
}
if let Ok(post) = StatusBuilder::new() 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 { .content_type(if self.want_markdown {
"text/markdown" "text/markdown"
} else { } else {
"text/plain" "text/plain"
}) })
.visibility(self.status.visibility) // Copy visibility .visibility(Visibility::Direct)
.build() .build()
{ {
let _ = self.client.new_status(post) let _ = self.client.new_status(post)
@ -259,7 +262,13 @@ impl<'a> ProcessMention<'a> {
} }
if !self.announcements.is_empty() { 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() let post = StatusBuilder::new()
.status(format!("**📢 Group announcement**\n{msg}", msg = msg)) .status(format!("**📢 Group announcement**\n{msg}", msg = msg))
.content_type("text/markdown") .content_type("text/markdown")
@ -445,8 +454,12 @@ impl<'a> ProcessMention<'a> {
async fn cmd_add_tag(&mut self, tag: String) { async fn cmd_add_tag(&mut self, tag: String) {
if self.is_admin { if self.is_admin {
self.config.add_tag(&tag); if self.config.is_tag_followed(&tag) {
self.add_reply(format!("Tag #{} added to the group!", 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 { } else {
self.add_reply("Only admins can manage group tags"); 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) { async fn cmd_remove_tag(&mut self, tag: String) {
if self.is_admin { if self.is_admin {
self.config.remove_tag(&tag); if self.config.is_tag_followed(&tag) {
self.add_reply(format!("Tag #{} removed from the group!", 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 { } else {
self.add_reply("Only admins can manage group tags"); self.add_reply("Only admins can manage group tags");
} }
@ -551,8 +568,7 @@ impl<'a> ProcessMention<'a> {
} }
self.add_reply("\n\ self.add_reply("\n\
To share a post, @ the group user or use a group hashtag. \ To share a post, @ the group user or use a group hashtag.\n\
Replies and mentions with commands won't be shared.\n\
\n\ \n\
**Supported commands:**\n\ **Supported commands:**\n\
`/boost`, `/b` - boost the replied-to post into the group\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) { async fn cmd_list_members(&mut self) {
self.want_markdown = true;
if self.is_admin { if self.is_admin {
self.add_reply("Group members:"); self.add_reply("Group members:");
self.append_member_list_to_reply(); self.append_member_list_to_reply();
@ -602,10 +619,11 @@ impl<'a> ProcessMention<'a> {
async fn cmd_list_tags(&mut self) { async fn cmd_list_tags(&mut self) {
self.add_reply("Group tags:"); self.add_reply("Group tags:");
self.want_markdown = true;
let mut tags = self.config.get_tags().collect::<Vec<_>>(); let mut tags = self.config.get_tags().collect::<Vec<_>>();
tags.sort(); tags.sort();
for t in tags { 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(()) 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(" .");
}
}

View file

@ -250,27 +250,17 @@ impl GroupHandle {
let ts = s.timestamp_millis(); let ts = s.timestamp_millis();
self.config.set_last_status(ts); self.config.set_last_status(ts);
// Short circuit checks
if s.visibility.is_private() { if s.visibility.is_private() {
debug!("Status is direct/private, discard"); debug!("Status is direct/private, discard");
return Ok(()); return Ok(());
} }
if s.in_reply_to_id.is_some() {
debug!("Status is a reply, discard");
return Ok(());
}
if !s.content.contains('#') { if !s.content.contains('#') {
debug!("No tags in status, discard"); debug!("No tags in status, discard");
return Ok(()); 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 group_user = self.config.get_acct();
let status_user = normalize_acct(&s.account.acct, group_user)?; let status_user = normalize_acct(&s.account.acct, group_user)?;
@ -279,25 +269,32 @@ impl GroupHandle {
return Ok(()); 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");
return Ok(());
}
if self.config.is_banned(&status_user) { if self.config.is_banned(&status_user) {
debug!("Status author @{} is banned.", status_user); debug!("Status author @{} is banned, discard", status_user);
return Ok(()); return Ok(());
} }
if !self.config.is_member_or_admin(&status_user) { if !self.config.is_member_or_admin(&status_user) {
debug!("Status author @{} is not a member.", status_user); debug!("Status author @{} is not a member, discard", status_user);
return Ok(()); return Ok(());
} }
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); let tags = crate::command::parse_status_tags(&s.content);
debug!("Tags in status: {:?}", tags); debug!("Tags in status: {:?}", tags);

View file

@ -95,10 +95,20 @@ mod test {
pub trait VisExt: Copy { pub trait VisExt: Copy {
/// Check if is private or direct /// Check if is private or direct
fn is_private(self) -> bool; fn is_private(self) -> bool;
fn make_unlisted(self) -> Self;
} }
impl VisExt for Visibility { impl VisExt for Visibility {
fn is_private(self) -> bool { fn is_private(self) -> bool {
self == Visibility::Direct || self == Visibility::Private self == Visibility::Direct || self == Visibility::Private
} }
fn make_unlisted(self) -> Self {
match self {
Visibility::Public => {
Visibility::Unlisted
}
other => other,
}
}
} }