5 Commits
10 changed files with 257 additions and 73 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## v0.2.7
- Fix some wrong responses to admin commands
- Remove automatic announcements from some admin commands
- Send no reply to unauthorized admin commands
- Fix swapped `/opengroup` and `/closegroup` descriptions in help
- Add `/optout` and `/optin`
- Add `#nobot` checking when using the `/boost` command
## v0.2.6
- Allow boosting group hashtags when they are in a reply, except when it is private/DM
or contains actionable commands
Generated
+1 -1
View File
@@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fedigroups"
version = "0.2.6"
version = "0.2.7"
dependencies = [
"anyhow",
"clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "fedigroups"
version = "0.2.6"
version = "0.2.7"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
publish = false
+33 -4
View File
@@ -1,4 +1,5 @@
# Fedi Groups
## How it works
This is an approximation of groups you can use right now with existing fedi software that implements the Mastodon client API.
@@ -8,6 +9,8 @@ Groups implement moderation (banning users and instances, member-only mode with
Group admins can issue group announcements that are posted poublicly by the group user, such as when there is a planned maintenance. The group will attempt to catch up with posts missed during the outage.
*Note: In this document, "reblog" and "boost" are used interchangeably.*
### Advantages of emulated groups
Unlike some other attempts at group implementation (namely gup.pe or the mythical WIP Pleroma Groups), this works with current Pleroma and Mastodon. There's no need for interoperability in different server implementations, since it uses existing follow/mention/reblog features that are already cross-compatible. Mastodon users can join a group running on Pleroma and vice-versa.
@@ -95,8 +98,26 @@ An example systemd service file is included in the repository as well. Make sure
### Sharing into the group
The group will boost (reblog) any status meeting these criteria:
-
The group will boost any status meeting these criteria:
- The visiblity is public or unlisted
- It's not a command request (i.e. mentions the group user and contains valid command(s))
- Either:
- it mentions the group user and is not a reply
- or, it contains one of the group hashtags
Examples of posts that will be shared:
- `@group Look at this duck` (public or unlisted)
- `Look at this duck @group` (public or unlisted)
- `I love #ducks` (public or unlisted, if #ducks is a group hashtag)
- `@otheruser tell me about #ducks` (in a thread, public or unlisted, if #ducks is a group hashtag)
- `Ducks are cool @otheruser @group` (original post)
These won't be shared:
- `ducks suck`
- `@group #ducks /i` (anything with the "ignore" command is ignored)
- `@group /remove #ducks` (admin command, even if it includes a group hashtag)
- `@otheruser tell me about ducks` (in a thread)
- `@otheruser @group tell me about ducks` (in a thread)
### Commands
@@ -127,6 +148,12 @@ When a *group member* posts one of the group hashtags, the group will reblog it.
For group hashtags to work, the group user must follow all its members; otherwise the posts might not federate to the group's server.
### Opting-out and #nobot
The group service respects the `#nobot` tag in users' profiles. When it's detected, the user's posts can't be shared to the group using the `/boost` command, unless they explicitly join.
To prevent individual groups from boosting your posts, use the `/optout` command.
### List of commands
*Note on command arguments:*
@@ -144,6 +171,8 @@ For group hashtags to work, the group user must follow all its members; otherwis
- `/ping` - ping the group service to check it's running, it will reply
- `/join` - join the group
- `/leave` - leave the group
- `/optout` - forbid sharing of your posts to the group (no effect for admins and members)
- `/optin` - reverse an opt-out
- `/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**
@@ -154,8 +183,8 @@ For group hashtags to work, the group user must follow all its members; otherwis
- `/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
- `/closegroup` - make the group member-only
- `/opengroup` - 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
+54 -10
View File
@@ -1,5 +1,6 @@
use once_cell::sync::Lazy;
use regex::Regex;
use crate::utils;
#[derive(Debug, Clone, PartialEq)]
pub enum StatusCommand {
@@ -31,6 +32,10 @@ pub enum StatusCommand {
RemoveAdmin(String),
/// Admin: Send a public announcement
Announce(String),
/// Opt out of boosts
OptOut,
/// Opt in to boosts
OptIn,
/// Admin: Make the group open-access
OpenGroup,
/// Admin: Make the group member-only, this effectively disables posting from non-members
@@ -120,6 +125,10 @@ 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_OPTOUT: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"optout"));
static RE_OPTIN: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"optin"));
static RE_PING: once_cell::sync::Lazy<Regex> = Lazy::new(|| command!(r"ping"));
static RE_ANNOUNCE: once_cell::sync::Lazy<Regex> =
@@ -128,14 +137,15 @@ static RE_ANNOUNCE: once_cell::sync::Lazy<Regex> =
static RE_A_HASHTAG: once_cell::sync::Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?:^|\b|\s|>|\n)#(\w+)").unwrap());
pub static RE_NOBOT_TAG: once_cell::sync::Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?:^|\b|\s|>|\n)#nobot(?:\b|$)").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);
let content = content.replace("<br/>", "<br/> ");
let content = content.replace("</p>", "</p> ");
let content = voca_rs::strip::strip_tags(&content);
let content = utils::strip_html(content);
debug!("Stripped tags: {}", content);
let mut tags = vec![];
@@ -150,11 +160,7 @@ pub fn parse_status_tags(content: &str) -> Vec<String> {
pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
debug!("Raw content: {}", content);
let content = content.replace("<br/>", "<br/> ");
let content = content.replace("</p>", "</p> ");
let content = voca_rs::strip::strip_tags(&content);
let content = utils::strip_html(content);
debug!("Stripped tags: {}", content);
if !content.contains('/') && !content.contains('\\') {
@@ -198,6 +204,14 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
commands.push(StatusCommand::Join);
}
if RE_OPTOUT.is_match(&content) {
debug!("OPT-OUT");
commands.push(StatusCommand::OptOut);
} else if RE_OPTIN.is_match(&content) {
debug!("OPT-IN");
commands.push(StatusCommand::OptIn);
}
if RE_PING.is_match(&content) {
debug!("PING");
commands.push(StatusCommand::Ping);
@@ -322,11 +336,11 @@ pub fn parse_slash_commands(content: &str) -> Vec<StatusCommand> {
#[cfg(test)]
mod test {
use crate::command::{parse_slash_commands, RE_A_HASHTAG, RE_HASHTAG_TRIGGERING_PLEROMA_BUG, RE_ADD_TAG, RE_JOIN, StatusCommand};
use crate::command::{parse_slash_commands, RE_A_HASHTAG, RE_HASHTAG_TRIGGERING_PLEROMA_BUG, RE_NOBOT_TAG, 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_OPEN_GROUP, RE_REMOVE_MEMBER, RE_REVOKE_ADMIN, RE_TAGS, RE_UNDO,
RE_IGNORE, RE_LEAVE, RE_OPTOUT, RE_OPTIN, RE_MEMBERS, RE_OPEN_GROUP, RE_REMOVE_MEMBER, RE_REVOKE_ADMIN, RE_TAGS, RE_UNDO,
};
#[test]
@@ -554,6 +568,7 @@ mod test {
}
}
}
#[test]
fn test_match_tag_at_end() {
assert!(!RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("banana #tag sdfsd"));
@@ -564,6 +579,17 @@ mod test {
assert!(RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match("#tag..."));
}
#[test]
fn test_match_tag_nobot() {
assert!(!RE_NOBOT_TAG.is_match("banana #tag sdfsd"));
assert!(!RE_NOBOT_TAG.is_match("banana #nobotanicals sdfsd"));
assert!(RE_NOBOT_TAG.is_match("#nobot"));
assert!(RE_NOBOT_TAG.is_match("aaa#nobot"));
assert!(RE_NOBOT_TAG.is_match("aaa #nobot"));
assert!(RE_NOBOT_TAG.is_match("#nobot xxx"));
assert!(RE_NOBOT_TAG.is_match("#nobot\nxxx"));
}
#[test]
fn test_leave() {
assert!(!RE_LEAVE.is_match("/list"));
@@ -573,6 +599,24 @@ mod test {
assert!(RE_LEAVE.is_match("/leave z"));
}
#[test]
fn test_optout() {
assert!(!RE_OPTOUT.is_match("/list"));
assert!(!RE_OPTOUT.is_match("/optoutaaa"));
assert!(RE_OPTOUT.is_match("/optout"));
assert!(RE_OPTOUT.is_match("x /optout"));
assert!(RE_OPTOUT.is_match("/optout z"));
}
#[test]
fn test_optin() {
assert!(!RE_OPTIN.is_match("/list"));
assert!(!RE_OPTIN.is_match("/optinaaa"));
assert!(RE_OPTIN.is_match("/optin"));
assert!(RE_OPTIN.is_match("x /optin"));
assert!(RE_OPTIN.is_match("/optin z"));
}
#[test]
fn test_undo() {
assert!(!RE_UNDO.is_match("/list"));
+2
View File
@@ -6,6 +6,8 @@ pub enum GroupError {
UserIsAdmin,
#[error("User is banned")]
UserIsBanned,
#[error("User opted out from the group")]
UserOptedOut,
#[error("Server could not be banned because there are admin users on it")]
AdminsOnServer,
#[error("Group config is missing in the config store")]
+129 -56
View File
@@ -2,15 +2,16 @@ use std::collections::HashSet;
use std::time::Duration;
use elefren::{FediClient, SearchType, StatusBuilder};
use elefren::entities::account::Account;
use elefren::entities::prelude::Status;
use elefren::status_builder::Visibility;
use crate::command::StatusCommand;
use crate::command::{RE_NOBOT_TAG, StatusCommand};
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;
use crate::utils;
pub struct ProcessMention<'a> {
status: Status,
@@ -52,7 +53,7 @@ impl<'a> ProcessMention<'a> {
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
if acct_normalized == acct {
debug!("Search done, account found: {}", item.acct);
return Ok(Some(item.id))
return Ok(Some(item.id));
} else {
warn!("Found wrong account: {}", item.acct);
}
@@ -134,11 +135,11 @@ impl<'a> ProcessMention<'a> {
}
fn add_reply(&mut self, line: impl AsRef<str>) {
self.replies.push(line.as_ref().trim().to_string())
self.replies.push(line.as_ref().trim_matches(' ').to_string())
}
fn add_announcement<'t>(&mut self, line: impl AsRef<str>) {
self.announcements.push(line.as_ref().trim().to_string())
self.announcements.push(line.as_ref().trim_matches(' ').to_string())
}
async fn handle(mut self) -> Result<(), GroupError> {
@@ -175,6 +176,12 @@ impl<'a> ProcessMention<'a> {
self.cmd_unban_user(&u).await
.log_error("Error handling unban-user cmd");
}
StatusCommand::OptOut => {
self.cmd_optout().await;
}
StatusCommand::OptIn => {
self.cmd_optin().await;
}
StatusCommand::BanServer(s) => {
self.cmd_ban_server(&s).await;
}
@@ -196,7 +203,7 @@ impl<'a> ProcessMention<'a> {
self.cmd_remove_tag(tag).await;
}
StatusCommand::GrantAdmin(u) => {
self.cmd_grant_member(&u).await
self.cmd_grant_admin(&u).await
.log_error("Error handling grant-admin cmd");
}
StatusCommand::RemoveAdmin(u) => {
@@ -232,10 +239,19 @@ impl<'a> ProcessMention<'a> {
}
if self.do_boost_prev_post {
self.client
.reblog(self.status.in_reply_to_id.as_ref().unwrap())
.await
.log_error("Failed to boost");
if let (Some(prev_acct_id), Some(prev_status_id)) = (self.status.in_reply_to_account_id.as_ref(), self.status.in_reply_to_id.as_ref()) {
match self.id_to_acct_check_boostable(prev_acct_id).await {
Ok(_acct) => {
self.client
.reblog(prev_status_id)
.await
.log_error("Failed to boost");
}
Err(e) => {
warn!("Can't reblog: {}", e);
}
}
}
}
if !self.replies.is_empty() {
@@ -292,8 +308,9 @@ impl<'a> ProcessMention<'a> {
// tokio::time::sleep(DELAY_BEFORE_ACTION).await;
self.reblog_status().await;
} else {
self.add_reply("You are not allowed to post to this group");
warn!("User @{} can't post to group!", self.status_acct);
}
// Otherwise, don't react
} else {
debug!("Not OP, ignore mention");
}
@@ -308,7 +325,29 @@ impl<'a> ProcessMention<'a> {
if self.can_write {
self.do_boost_prev_post = self.status.in_reply_to_id.is_some();
} else {
self.add_reply("You are not allowed to share to this group");
warn!("User @{} can't share to group!", self.status_acct);
}
}
async fn cmd_optout(&mut self) {
if self.is_admin {
self.add_reply("Group admins can't opt-out.");
} else if self.config.is_member(&self.status_acct) {
self.add_reply("Group members can't opt-out. You have to leave first.");
} else {
self.config.set_optout(&self.status_acct, true);
self.add_reply("Your posts will no longer be shared to the group.");
}
}
async fn cmd_optin(&mut self) {
if self.is_admin {
self.add_reply("Opt-in has no effect for admins.");
} else if self.config.is_member(&self.status_acct) {
self.add_reply("Opt-in has no effect for members.");
} else {
self.config.set_optout(&self.status_acct, false);
self.add_reply("Your posts can now be shared to the group.");
}
}
@@ -321,7 +360,7 @@ impl<'a> ProcessMention<'a> {
info!("Deleting group post #{}", parent_status_id);
self.client.delete_status(parent_status_id).await?;
} else {
warn!("Only admin can delete announcements.");
warn!("Only admin can delete posts made by the group user");
}
} else {
if self.is_admin || parent_account_id == &self.status_user_id {
@@ -329,7 +368,9 @@ impl<'a> ProcessMention<'a> {
// 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.");
warn!("Only the author and admins can undo reblogs");
// XXX this means when someone /b's someone else's post to a group,
// they then can't reverse that (only admin or the post's author can)
}
}
}
@@ -351,9 +392,11 @@ impl<'a> ProcessMention<'a> {
self.add_reply(format!("Failed to ban user {}: {}", u, e));
}
}
} else {
self.add_reply(format!("No action, user {} is already banned", u));
}
} else {
self.add_reply("Only admins can manage user bans");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
@@ -371,9 +414,11 @@ impl<'a> ProcessMention<'a> {
unreachable!()
}
}
} else {
self.add_reply(format!("No action, user {} is not banned", u));
}
} else {
self.add_reply("Only admins can manage user bans");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
@@ -383,16 +428,17 @@ impl<'a> ProcessMention<'a> {
if !self.config.is_server_banned(s) {
match self.config.ban_server(s, true) {
Ok(_) => {
self.add_announcement(format!("Server \"{}\" has been banned.", s));
self.add_reply(format!("Server {} banned from group!", s));
}
Err(e) => {
self.add_reply(format!("Failed to ban server {}: {}", s, e));
}
}
} else {
self.add_reply(format!("No action, server {} already banned", s));
}
} else {
self.add_reply("Only admins can manage server bans");
warn!("Ignore cmd, user not admin");
}
}
@@ -401,22 +447,24 @@ impl<'a> ProcessMention<'a> {
if self.config.is_server_banned(s) {
match self.config.ban_server(s, false) {
Ok(_) => {
self.add_announcement(format!("Server \"{}\" has been un-banned.", s));
self.add_reply(format!("Server {} un-banned!", s));
}
Err(_) => {
unreachable!()
Err(e) => {
self.add_reply(format!("Unexpected error occured: {}", e));
}
}
} else {
self.add_reply(format!("No action, server {} is not banned", s));
}
} else {
self.add_reply("Only admins can manage server bans");
warn!("Ignore cmd, user not admin");
}
}
async fn cmd_add_member(&mut self, user: &str) -> Result<(), GroupError> {
let u = normalize_acct(user, &self.group_acct)?;
if self.is_admin {
// Allow even if the user is already a member - that will trigger re-follow
match self.config.set_member(&u, true) {
Ok(_) => {
self.add_reply(format!("User {} added to the group!", u));
@@ -428,7 +476,7 @@ impl<'a> ProcessMention<'a> {
}
}
} else {
self.add_reply("Only admins can manage members");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
@@ -442,26 +490,26 @@ impl<'a> ProcessMention<'a> {
self.unfollow_by_acct(&u).await
.log_error("Failed to unfollow removed user");
}
Err(_) => {
unreachable!()
Err(e) => {
self.add_reply(format!("Unexpected error occured: {}", e));
}
}
} else {
self.add_reply("Only admins can manage members");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
async fn cmd_add_tag(&mut self, tag: String) {
if self.is_admin {
if self.config.is_tag_followed(&tag) {
if !self.config.is_tag_followed(&tag) {
self.config.add_tag(&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));
self.add_reply(format!("No action, \"{}\" is already a group tag", tag));
}
} else {
self.add_reply("Only admins can manage group tags");
warn!("Ignore cmd, user not admin");
}
}
@@ -471,14 +519,14 @@ impl<'a> ProcessMention<'a> {
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));
self.add_reply(format!("No action, \"{}\" is not a group tag", tag));
}
} else {
self.add_reply("Only admins can manage group tags");
warn!("Ignore cmd, user not admin");
}
}
async fn cmd_grant_member(&mut self, user: &str) -> Result<(), GroupError> {
async fn cmd_grant_admin(&mut self, user: &str) -> Result<(), GroupError> {
let u = normalize_acct(user, &self.group_acct)?;
if self.is_admin {
if !self.config.is_admin(&u) {
@@ -488,7 +536,6 @@ impl<'a> ProcessMention<'a> {
let _ = self.config.set_member(&u, true);
self.add_reply(format!("User {} is now a group admin!", u));
self.add_announcement(format!("User @{} can now manage this group!", u));
}
Err(e) => {
self.add_reply(format!(
@@ -497,9 +544,11 @@ impl<'a> ProcessMention<'a> {
));
}
}
} else {
self.add_reply(format!("No action, \"{}\" is admin already", u));
}
} else {
self.add_reply("Only admins can manage admins");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
@@ -511,15 +560,16 @@ impl<'a> ProcessMention<'a> {
match self.config.set_admin(&u, false) {
Ok(_) => {
self.add_reply(format!("User {} is no longer a group admin!", u));
self.add_announcement(format!("User @{} no longer manages this group.", u));
}
Err(e) => {
self.add_reply(format!("Failed to revoke {}'s group admin: {}", u, e));
}
}
} else {
self.add_reply(format!("No action, user {} is not admin", u));
}
} else {
self.add_reply("Only admins can manage admins");
warn!("Ignore cmd, user not admin");
}
Ok(())
}
@@ -529,10 +579,11 @@ impl<'a> ProcessMention<'a> {
if self.config.is_member_only() {
self.config.set_member_only(false);
self.add_reply("Group changed to open-access");
self.add_announcement("This group is now open-access!");
} else {
self.add_reply("No action, group is open-access already");
}
} else {
self.add_reply("Only admins can set group options");
warn!("Ignore cmd, user not admin");
}
}
@@ -541,33 +592,34 @@ impl<'a> ProcessMention<'a> {
if !self.config.is_member_only() {
self.config.set_member_only(true);
self.add_reply("Group changed to member-only");
self.add_announcement("This group is now member-only!");
} else {
self.add_reply("No action, group is member-only already");
}
} else {
self.add_reply("Only admins can set group options");
warn!("Ignore cmd, user not admin");
}
}
async fn cmd_help(&mut self) {
self.want_markdown = true;
if self.config.is_member_only() {
self.add_reply("This is a member-only group. ");
} else {
self.add_reply("This is a public-access group. ");
}
if self.is_admin {
self.add_reply("*You are an admin.*");
let membership_line = if self.is_admin {
"*You are an admin.*"
} else if self.config.is_member(&self.status_acct) {
self.add_reply("*You are a member.*");
"*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.*");
"*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.*");
"*You are not a member, follow or use /join to join the group.*"
};
if self.config.is_member_only() {
self.add_reply(format!("This is a member-only group. {}", membership_line));
} else {
self.add_reply(format!("This is a public-access group. {}", membership_line));
}
self.add_reply("\n\
self.add_reply("\
To share a post, @ the group user or use a group hashtag.\n\
\n\
**Supported commands:**\n\
@@ -575,7 +627,8 @@ impl<'a> ProcessMention<'a> {
`/ignore`, `/i` - make the group ignore the post\n\
`/tags` - show group hashtags\n\
`/join` - (re-)join the group\n\
`/leave` - leave the group");
`/leave` - leave the group\n\
`/optout` - forbid sharing of your posts");
if self.is_admin {
self.add_reply("`/members`, `/who` - show group members / admins");
@@ -600,8 +653,8 @@ impl<'a> ProcessMention<'a> {
`/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\
`/closegroup` - make member-only\n\
`/opengroup` - make public-access\n\
`/announce x` - make a public announcement");
}
}
@@ -688,6 +741,26 @@ impl<'a> ProcessMention<'a> {
}
Ok(())
}
/// Convert ID to account, checking if the user is boostable
async fn id_to_acct_check_boostable(&self, id: &str) -> Result<String, GroupError> {
// Try to unfollow
let account = self.client.get_account(id).await?;
let bio = utils::strip_html(&account.note);
if RE_NOBOT_TAG.is_match(&bio) {
// #nobot
Err(GroupError::UserOptedOut)
} else {
let normalized = normalize_acct(&account.acct, &self.group_acct)?;
if self.config.is_banned(&normalized) {
return Err(GroupError::UserIsBanned);
} else if self.config.is_optout(&normalized) {
return Err(GroupError::UserOptedOut);
} else {
Ok(normalized)
}
}
}
}
fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) {
+5 -1
View File
@@ -274,6 +274,8 @@ impl GroupHandle {
return Ok(());
}
// optout does not work for members and admins, so don't check it
if !self.config.is_member_or_admin(&status_user) {
debug!("Status author @{} is not a member, discard", status_user);
return Ok(());
@@ -300,10 +302,12 @@ impl GroupHandle {
'tags: for t in tags {
if self.config.is_tag_followed(&t) {
info!("REBLOG #{} STATUS", &t);
info!("REBLOG #{} STATUS", t);
self.client.reblog(&s.id).await
.log_error("Failed to reblog");
break 'tags; // do not reblog multiple times!
} else {
debug!("#{} is not a group tag", t);
}
}
+18
View File
@@ -41,6 +41,8 @@ pub(crate) struct GroupConfig {
member_users: HashSet<String>,
/// List of users banned from posting to the group
banned_users: HashSet<String>,
/// Users who decided they don't want to be shared to the group (does not apply to members)
optout_users: HashSet<String>,
/// True if only members should be allowed to write
member_only: bool,
/// Banned domain names, e.g. kiwifarms.cc
@@ -69,6 +71,7 @@ impl Default for GroupConfig {
admin_users: Default::default(),
member_users: Default::default(),
banned_users: Default::default(),
optout_users: Default::default(),
member_only: false,
banned_servers: Default::default(),
last_notif_ts: 0,
@@ -147,6 +150,10 @@ impl GroupConfig {
&self.acct
}
pub(crate) fn is_optout(&self, acct: &str) -> bool {
self.optout_users.contains(acct)
}
pub(crate) fn is_admin(&self, acct: &str) -> bool {
self.admin_users.contains(acct)
}
@@ -212,6 +219,17 @@ impl GroupConfig {
Ok(())
}
pub(crate) fn set_optout(&mut self, acct: &str, optout: bool) {
let change = if optout {
self.optout_users.insert(acct.to_owned())
} else {
self.optout_users.remove(acct)
};
if change {
self.mark_dirty();
}
}
pub(crate) fn ban_user(&mut self, acct: &str, ban: bool) -> Result<(), GroupError> {
let mut change = false;
if ban {
+6
View File
@@ -112,3 +112,9 @@ impl VisExt for Visibility {
}
}
}
pub(crate) fn strip_html(content: &str) -> String {
let content = content.replace("<br/>", "<br/> ");
let content = content.replace("</p>", "</p> ");
voca_rs::strip::strip_tags(&content)
}