mirror of
https://git.ondrovo.com/MightyPork/group-actor.git
synced 2024-11-15 20:51:07 +00:00
add nobot and optout/optin
This commit is contained in:
parent
37a9f323e6
commit
900132970d
7 changed files with 160 additions and 19 deletions
|
@ -148,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:*
|
||||
|
@ -165,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**
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -313,6 +329,28 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -589,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");
|
||||
|
@ -702,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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue