Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c52147ad4d
|
||
|
|
31a9d767ae
|
||
|
|
98fe694d47
|
@@ -1,5 +1,16 @@
|
||||
# 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
|
||||
- make account lookup try harder
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -276,7 +276,7 @@ checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
|
||||
[[package]]
|
||||
name = "elefren"
|
||||
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 = [
|
||||
"chrono",
|
||||
"doc-comment",
|
||||
@@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||
|
||||
[[package]]
|
||||
name = "fedigroups"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fedigroups"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||
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 = "a0ebb46" }
|
||||
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "7847df0" }
|
||||
|
||||
env_logger = "0.9.0"
|
||||
|
||||
|
||||
@@ -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,23 +137,27 @@ 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
|
||||
- `/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!
|
||||
- `/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
|
||||
- `/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.
|
||||
|
||||
+49
-17
@@ -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<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_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_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!()));
|
||||
|
||||
@@ -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_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_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> =
|
||||
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> =
|
||||
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> {
|
||||
debug!("Raw content: {}", content);
|
||||
@@ -167,6 +174,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
|
||||
return vec![StatusCommand::Help];
|
||||
}
|
||||
|
||||
if RE_UNDO.is_match(&content) {
|
||||
debug!("UNDO");
|
||||
return vec![StatusCommand::Undo];
|
||||
}
|
||||
|
||||
// additive commands
|
||||
|
||||
let mut commands = vec![];
|
||||
@@ -310,11 +322,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
|
||||
|
||||
#[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_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,
|
||||
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]
|
||||
@@ -405,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");
|
||||
@@ -434,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");
|
||||
@@ -512,6 +526,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,15 +547,22 @@ 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(), "ласточка");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[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() {
|
||||
@@ -551,6 +573,16 @@ 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("/delete"));
|
||||
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 +627,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")
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
@@ -66,7 +68,7 @@ impl<'a> ProcessMention<'a> {
|
||||
let mut admins = self.config.get_admins().collect::<Vec<_>>();
|
||||
admins.sort();
|
||||
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();
|
||||
for m in members {
|
||||
self.replies.push(if admins.contains(&m) {
|
||||
format!("{} [admin]", m)
|
||||
format!("- {} [admin]", m)
|
||||
} else {
|
||||
m.to_string()
|
||||
format!("- {}", m)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
@@ -130,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<str>) {
|
||||
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<str>) {
|
||||
self.announcements.push(line.as_ref().trim().to_string())
|
||||
}
|
||||
|
||||
async fn handle(mut self) -> Result<(), GroupError> {
|
||||
@@ -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
|
||||
}
|
||||
@@ -232,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)
|
||||
@@ -252,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")
|
||||
@@ -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> {
|
||||
let u = normalize_acct(user, &self.group_acct)?;
|
||||
if self.is_admin {
|
||||
@@ -413,8 +454,12 @@ impl<'a> ProcessMention<'a> {
|
||||
|
||||
async fn cmd_add_tag(&mut self, tag: String) {
|
||||
if self.is_admin {
|
||||
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 #{} added to the group!", tag));
|
||||
self.add_reply(format!("Tag \"{}\" was already in group!", tag));
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
if self.is_admin {
|
||||
if self.config.is_tag_followed(&tag) {
|
||||
self.config.remove_tag(&tag);
|
||||
self.add_reply(format!("Tag #{} removed from the group!", 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");
|
||||
}
|
||||
@@ -519,41 +568,46 @@ impl<'a> ProcessMention<'a> {
|
||||
}
|
||||
|
||||
self.add_reply("\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\
|
||||
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\
|
||||
`/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");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -565,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::<Vec<_>>();
|
||||
tags.sort();
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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<ConfigStore>,
|
||||
@@ -248,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)?;
|
||||
|
||||
@@ -277,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");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.config.is_banned(&status_user) {
|
||||
debug!("Status author @{} is banned.", status_user);
|
||||
debug!("Status author @{} is banned, discard", status_user);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -440,7 +439,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 +447,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
|
||||
)
|
||||
|
||||
+34
-2
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user