3 Commits
Author SHA1 Message Date
MightyPork c52147ad4d fixes for new release 2021-08-28 10:24:35 +02:00
MightyPork 31a9d767ae bump elefren 2021-08-27 21:55:27 +02:00
MightyPork 98fe694d47 add /undo, update help, readme and changelog; fix follow to join 2021-08-27 21:52:33 +02:00
9 changed files with 246 additions and 86 deletions
+11
View File
@@ -1,5 +1,16 @@
# 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
- Add `/undo` command
- Fix users joining via follow not marked as members
## v0.2.4 ## v0.2.4
- make account lookup try harder - make account lookup try harder
Generated
+2 -2
View File
@@ -276,7 +276,7 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
[[package]] [[package]]
name = "elefren" name = "elefren"
version = "0.22.0" version = "0.22.0"
source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=a0ebb46#a0ebb46542ede2d235ca6094135a6d6d01d0ecb8" source = "git+https://git.ondrovo.com/MightyPork/elefren-fork.git?rev=7847df0#7847df0e2b2c0f6b9a089e2db2f9b10dfe070a45"
dependencies = [ dependencies = [
"chrono", "chrono",
"doc-comment", "doc-comment",
@@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fedigroups" name = "fedigroups"
version = "0.2.4" version = "0.2.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.2.4" 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
@@ -10,7 +10,7 @@ build = "build.rs"
[dependencies] [dependencies]
#elefren = { path = "../elefren22-fork" } #elefren = { path = "../elefren22-fork" }
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "a0ebb46" } elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "7847df0" }
env_logger = "0.9.0" env_logger = "0.9.0"
+22 -13
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,23 +137,27 @@ 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
- `/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** **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
- `/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.
+49 -17
View File
@@ -7,6 +7,8 @@ pub enum StatusCommand {
Ignore, Ignore,
/// Boost the previous post in the thread /// Boost the previous post in the thread
Boost, Boost,
/// Un-reblog parent post, or delete an announcement
Undo,
/// Admin: Ban a user /// Admin: Ban a user
BanUser(String), BanUser(String),
/// Admin: Un-ban a server /// Admin: Un-ban a server
@@ -80,6 +82,8 @@ 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"(?: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)?)?)?"));
static RE_BAN_USER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ban\s+", p_user!())); static RE_BAN_USER: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ban\s+", p_user!()));
@@ -90,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!()));
@@ -108,21 +112,24 @@ static RE_CLOSE_GROUP: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"cl
static RE_HELP: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"help")); static RE_HELP: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"help"));
static RE_MEMBERS: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:members|who)")); static RE_MEMBERS: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:members|who|admins)"));
static RE_TAGS: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:hashtags|tags)")); static RE_TAGS: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:hashtags|tags)"));
static RE_LEAVE: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:leave)")); static RE_LEAVE: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"leave"));
static RE_JOIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"(?:join)")); 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);
@@ -167,6 +174,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
return vec![StatusCommand::Help]; return vec![StatusCommand::Help];
} }
if RE_UNDO.is_match(&content) {
debug!("UNDO");
return vec![StatusCommand::Undo];
}
// additive commands // additive commands
let mut commands = vec![]; let mut commands = vec![];
@@ -310,11 +322,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
#[cfg(test)] #[cfg(test)]
mod 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_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,
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] #[test]
@@ -405,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");
@@ -434,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");
@@ -512,6 +526,7 @@ mod test {
assert!(!RE_MEMBERS.is_match("/admin lain@pleroma.soykaf.com")); assert!(!RE_MEMBERS.is_match("/admin lain@pleroma.soykaf.com"));
assert!(RE_MEMBERS.is_match("/members")); assert!(RE_MEMBERS.is_match("/members"));
assert!(RE_MEMBERS.is_match("/who")); assert!(RE_MEMBERS.is_match("/who"));
assert!(RE_MEMBERS.is_match("/admins"));
} }
#[test] #[test]
@@ -532,15 +547,22 @@ mod test {
for (i, c) in RE_A_HASHTAG.captures_iter("foo #banana #χαλβάς #ласточка").enumerate() { for (i, c) in RE_A_HASHTAG.captures_iter("foo #banana #χαλβάς #ласточка").enumerate() {
if i == 0 { if i == 0 {
assert_eq!(c.get(1).unwrap().as_str(), "banana"); 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(), "χαλβάς"); assert_eq!(c.get(1).unwrap().as_str(), "χαλβάς");
} } else if i == 2 {
else if i == 2 {
assert_eq!(c.get(1).unwrap().as_str(), "ласточка"); assert_eq!(c.get(1).unwrap().as_str(), "ласточка");
} }
} }
} }
#[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() {
@@ -551,6 +573,16 @@ mod test {
assert!(RE_LEAVE.is_match("/leave z")); 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("/delete"));
assert!(RE_UNDO.is_match("/undo"));
assert!(RE_UNDO.is_match("x /undo"));
assert!(RE_UNDO.is_match("/undo z"));
}
#[test] #[test]
fn test_join() { fn test_join() {
assert!(!RE_JOIN.is_match("/list")); assert!(!RE_JOIN.is_match("/list"));
@@ -595,7 +627,7 @@ mod test {
vec![ vec![
StatusCommand::BanUser("lain".to_string()), StatusCommand::BanUser("lain".to_string()),
StatusCommand::BanUser("piggo@piggo.space".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") parse_slash_commands("let's /ban @lain! /ban @piggo@piggo.space and also /ban soykaf.com")
); );
+89 -26
View File
@@ -10,9 +10,11 @@ use crate::error::GroupError;
use crate::group_handler::GroupHandle; use crate::group_handler::GroupHandle;
use crate::store::data::GroupConfig; use crate::store::data::GroupConfig;
use crate::utils::{LogError, normalize_acct}; use crate::utils::{LogError, normalize_acct};
use elefren::entities::account::Account;
pub struct ProcessMention<'a> { pub struct ProcessMention<'a> {
status: Status, status: Status,
group_account: &'a Account,
config: &'a mut GroupConfig, config: &'a mut GroupConfig,
client: &'a mut FediClient, client: &'a mut FediClient,
group_acct: String, group_acct: String,
@@ -66,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));
} }
} }
@@ -78,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)
}); });
} }
} }
@@ -107,6 +109,7 @@ impl<'a> ProcessMention<'a> {
} }
let pm = Self { let pm = Self {
group_account: &gh.group_account,
status_user_id: status.account.id.to_string(), status_user_id: status.account.id.to_string(),
client: &mut gh.client, client: &mut gh.client,
can_write: gh.config.can_write(&status_acct), can_write: gh.config.can_write(&status_acct),
@@ -130,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> {
@@ -151,6 +154,10 @@ impl<'a> ProcessMention<'a> {
for cmd in commands { for cmd in commands {
match cmd { match cmd {
StatusCommand::Undo => {
self.cmd_undo().await
.log_error("Error handling undo cmd");
}
StatusCommand::Ignore => { StatusCommand::Ignore => {
unreachable!(); // Handled above unreachable!(); // Handled above
} }
@@ -232,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)
@@ -252,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")
@@ -296,6 +312,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> { async fn cmd_ban_user(&mut self, user: &str) -> Result<(), GroupError> {
let u = normalize_acct(user, &self.group_acct)?; let u = normalize_acct(user, &self.group_acct)?;
if self.is_admin { if self.is_admin {
@@ -413,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");
} }
@@ -422,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");
} }
@@ -519,41 +568,46 @@ impl<'a> ProcessMention<'a> {
} }
self.add_reply("\n\ 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.\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\
`/ignore`, `/i` - make the group ignore the post\n\ `/ignore`, `/i` - make the group ignore the post\n\
`/ping` - check the service is alive\n\
`/tags` - show group hashtags\n\ `/tags` - show group hashtags\n\
`/join` - join the group\n\ `/join` - (re-)join the group\n\
`/leave` - leave the group"); `/leave` - leave the group");
if self.config.is_member_only() { if self.is_admin {
self.add_reply("`/members`, `/who` - show group members / admins"); self.add_reply("`/members`, `/who` - show group members / admins");
// undo is listed as an admin command
} else { } 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 { if self.is_admin {
self.add_reply("\n\ self.add_reply("\n\
**Admin commands:**\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\ `/remove user` - remove a member\n\
`/add #hashtag` - add a group hashtag\n\ `/add #hashtag` - add a group hashtag\n\
`/remove #hashtag` - remove 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\ `/ban x` - ban a user or server\n\
`/unban x` - lift a ban\n\ `/unban x` - lift a ban\n\
`/admin user` - grant admin rights\n\ `/admin user` - grant admin rights\n\
`/deadmin user` - revoke admin rights\n\ `/deadmin user` - revoke admin rights\n\
`/opengroup` - make member-only\n\ `/opengroup` - make member-only\n\
`/closegroup` - make public-access\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");
} }
} }
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();
@@ -565,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());
} }
} }
@@ -634,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(" .");
}
}
+27 -24
View File
@@ -18,12 +18,14 @@ use crate::store::ConfigStore;
use crate::store::data::GroupConfig; use crate::store::data::GroupConfig;
use crate::utils::{LogError, normalize_acct, VisExt}; use crate::utils::{LogError, normalize_acct, VisExt};
use crate::command::StatusCommand; use crate::command::StatusCommand;
use elefren::entities::account::Account;
mod handle_mention; mod handle_mention;
/// This is one group's config store capable of persistence /// This is one group's config store capable of persistence
#[derive(Debug)] #[derive(Debug)]
pub struct GroupHandle { pub struct GroupHandle {
pub(crate) group_account: Account,
pub(crate) client: FediClient, pub(crate) client: FediClient,
pub(crate) config: GroupConfig, pub(crate) config: GroupConfig,
pub(crate) store: Arc<ConfigStore>, pub(crate) store: Arc<ConfigStore>,
@@ -248,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)?;
@@ -277,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);
@@ -440,7 +439,7 @@ impl GroupHandle {
admins.sort(); admins.sort();
format!("\ 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\ If you'd like to join, please ask one of the group admins:\n\
{admins}", {admins}",
user = notif_acct, user = notif_acct,
@@ -448,9 +447,13 @@ impl GroupHandle {
) )
} else { } else {
follow_back = true; follow_back = true;
self.config.set_member(notif_acct, true)
.log_error("Fail add a member");
format!("\ format!("\
@{user} Welcome to the group! The group user will now follow you back to complete the sign-up. \ @{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.", Use /help for more info.",
user = notif_acct user = notif_acct
) )
+34 -2
View File
@@ -67,12 +67,27 @@ impl ConfigStore {
let client = elefren::helpers::cli::authenticate(registration).await?; let client = elefren::helpers::cli::authenticate(registration).await?;
let appdata = client.data.clone(); let appdata = client.data.clone();
let data = GroupConfig::new(opts.acct, appdata); let data = GroupConfig::new(opts.acct.clone(), appdata);
// save & persist // save & persist
self.set_group_config(data.clone()).await?; 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 { Ok(GroupHandle {
group_account,
client, client,
config: data, config: data,
store: self.clone(), store: self.clone(),
@@ -101,7 +116,22 @@ impl ConfigStore {
config.set_appdata(appdata); config.set_appdata(appdata);
self.set_group_config(config.clone()).await?; 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 { Ok(GroupHandle {
group_account,
client, client,
config, config,
store: self.clone(), store: self.clone(),
@@ -125,12 +155,13 @@ impl ConfigStore {
let client = FediClient::from(gc.get_appdata().clone()); let client = FediClient::from(gc.get_appdata().clone());
match client.verify_credentials().await { let my_account = match client.verify_credentials().await {
Ok(account) => { Ok(account) => {
info!( info!(
"Group account verified: @{}, \"{}\"", "Group account verified: @{}, \"{}\"",
account.acct, account.display_name account.acct, account.display_name
); );
account
} }
Err(e) => { Err(e) => {
error!("Group @{} auth error: {}", gc.get_acct(), e); error!("Group @{} auth error: {}", gc.get_acct(), e);
@@ -139,6 +170,7 @@ impl ConfigStore {
}; };
Some(GroupHandle { Some(GroupHandle {
group_account: my_account,
client, client,
config: gc, config: gc,
store: self.clone(), store: self.clone(),
+10
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,
}
}
} }