|
|
|
@ -2,19 +2,25 @@ use std::cmp::Ordering; |
|
|
|
|
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 elefren::{FediClient, SearchType, StatusBuilder}; |
|
|
|
|
|
|
|
|
|
use crate::command::{StatusCommand, RE_NOBOT_TAG}; |
|
|
|
|
use crate::command::{RE_NOBOT_TAG, StatusCommand}; |
|
|
|
|
use crate::error::GroupError; |
|
|
|
|
use crate::group_handler::GroupHandle; |
|
|
|
|
use crate::store::group_config::GroupConfig; |
|
|
|
|
use crate::store::CommonConfig; |
|
|
|
|
use crate::store::group_config::GroupConfig; |
|
|
|
|
use crate::tr::TranslationTable; |
|
|
|
|
use crate::utils; |
|
|
|
|
use crate::utils::{normalize_acct, LogError}; |
|
|
|
|
use crate::utils::{LogError, normalize_acct, VisExt}; |
|
|
|
|
|
|
|
|
|
use crate::{ |
|
|
|
|
grp_debug, |
|
|
|
|
grp_warn, |
|
|
|
|
grp_info |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
pub struct ProcessMention<'a> { |
|
|
|
|
status: Status, |
|
|
|
@ -38,7 +44,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result<Option<String>, GroupError> { |
|
|
|
|
debug!("Looking up user ID by acct: {}", acct); |
|
|
|
|
grp_debug!(self, "Looking up user ID by acct: {}", acct); |
|
|
|
|
|
|
|
|
|
match tokio::time::timeout( |
|
|
|
|
Duration::from_secs(5), |
|
|
|
@ -48,7 +54,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
.await |
|
|
|
|
{ |
|
|
|
|
Err(_) => { |
|
|
|
|
warn!("Account lookup timeout!"); |
|
|
|
|
grp_warn!(self, "Account lookup timeout!"); |
|
|
|
|
Err(GroupError::ApiTimeout) |
|
|
|
|
} |
|
|
|
|
Ok(Err(e)) => { |
|
|
|
@ -60,14 +66,14 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
// XXX limit is 1!
|
|
|
|
|
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?; |
|
|
|
|
if acct_normalized == acct { |
|
|
|
|
debug!("Search done, account found: {}", item.acct); |
|
|
|
|
grp_debug!(self, "Search done, account found: {}", item.acct); |
|
|
|
|
return Ok(Some(item.id)); |
|
|
|
|
} else { |
|
|
|
|
warn!("Found wrong account: {}", item.acct); |
|
|
|
|
grp_warn!(self, "Found wrong account: {}", item.acct); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
debug!("Search done, nothing found"); |
|
|
|
|
grp_debug!(self, "Search done, nothing found"); |
|
|
|
|
Ok(None) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -101,14 +107,14 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> { |
|
|
|
|
debug!("Trying to follow user #{}", id); |
|
|
|
|
grp_debug!(self, "Trying to follow user #{}", id); |
|
|
|
|
self.client.follow(id).await?; |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> { |
|
|
|
|
debug!("Trying to unfollow user #{}", id); |
|
|
|
|
grp_debug!(self, "Trying to unfollow user #{}", id); |
|
|
|
|
self.client.unfollow(id).await?; |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
Ok(()) |
|
|
|
@ -119,7 +125,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string(); |
|
|
|
|
|
|
|
|
|
if gh.config.is_banned(&status_acct) { |
|
|
|
|
warn!("Status author {} is banned!", status_acct); |
|
|
|
|
grp_warn!(gh, "Status author {} is banned!", status_acct); |
|
|
|
|
return Ok(()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -163,7 +169,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.handle_post_with_no_commands().await; |
|
|
|
|
} else { |
|
|
|
|
if commands.contains(&StatusCommand::Ignore) { |
|
|
|
|
debug!("Notif ignored because of ignore command"); |
|
|
|
|
grp_debug!(self, "Notif ignored because of ignore command"); |
|
|
|
|
return Ok(()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -259,7 +265,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
} |
|
|
|
|
Err(e) => { |
|
|
|
|
warn!("Can't reblog: {}", e); |
|
|
|
|
grp_warn!(self, "Can't reblog: {}", e); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -267,9 +273,9 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
|
|
|
|
|
if !self.replies.is_empty() { |
|
|
|
|
let mut msg = std::mem::take(&mut self.replies); |
|
|
|
|
debug!("r={}", msg); |
|
|
|
|
grp_debug!(self, "r={}", msg); |
|
|
|
|
|
|
|
|
|
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); |
|
|
|
|
self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); |
|
|
|
|
|
|
|
|
|
let mention = crate::tr!(self, "mention_prefix", user = &self.status_acct); |
|
|
|
|
self.send_reply_multipart(mention, msg).await?; |
|
|
|
@ -277,9 +283,9 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
|
|
|
|
|
if !self.announcements.is_empty() { |
|
|
|
|
let mut msg = std::mem::take(&mut self.announcements); |
|
|
|
|
debug!("a={}", msg); |
|
|
|
|
grp_debug!(self, "a={}", msg); |
|
|
|
|
|
|
|
|
|
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); |
|
|
|
|
self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); |
|
|
|
|
|
|
|
|
|
let msg = crate::tr!(self, "group_announcement", message = &msg); |
|
|
|
|
self.send_announcement_multipart(&msg).await?; |
|
|
|
@ -339,24 +345,45 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn handle_post_with_no_commands(&mut self) { |
|
|
|
|
debug!("No commands in post"); |
|
|
|
|
if self.status.in_reply_to_id.is_none() { |
|
|
|
|
if self.can_write { |
|
|
|
|
grp_debug!(self, "No commands in post"); |
|
|
|
|
|
|
|
|
|
if self.status.visibility.is_private() { |
|
|
|
|
grp_debug!(self, "Mention is private, discard"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self.can_write { |
|
|
|
|
if self.status.in_reply_to_id.is_none() { |
|
|
|
|
// Someone tagged the group in OP, boost it.
|
|
|
|
|
info!("Boosting OP mention"); |
|
|
|
|
grp_info!(self, "Boosting OP mention"); |
|
|
|
|
// tokio::time::sleep(DELAY_BEFORE_ACTION).await;
|
|
|
|
|
self.reblog_status().await; |
|
|
|
|
// Otherwise, don't react
|
|
|
|
|
} else { |
|
|
|
|
warn!("User @{} can't post to group!", self.status_acct); |
|
|
|
|
// Check for tags
|
|
|
|
|
let tags = crate::command::parse_status_tags(&self.status.content); |
|
|
|
|
grp_debug!(self, "Tags in mention: {:?}", tags); |
|
|
|
|
|
|
|
|
|
for t in tags { |
|
|
|
|
if self.config.is_tag_followed(&t) { |
|
|
|
|
grp_info!(self, "REBLOG #{} STATUS", t); |
|
|
|
|
self.client.reblog(&self.status.id).await.log_error("Failed to reblog"); |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
return; |
|
|
|
|
} else { |
|
|
|
|
grp_debug!(self, "#{} is not a group tag", t); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
grp_debug!(self, "Not OP & no tags, ignore mention"); |
|
|
|
|
} |
|
|
|
|
// Otherwise, don't react
|
|
|
|
|
} else { |
|
|
|
|
debug!("Not OP, ignore mention"); |
|
|
|
|
grp_warn!(self, "User @{} can't post to group!", self.status_acct); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn cmd_announce(&mut self, msg: String) { |
|
|
|
|
info!("Sending PSA"); |
|
|
|
|
grp_info!(self, "Sending PSA"); |
|
|
|
|
self.add_announcement(msg); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -364,7 +391,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
if self.can_write { |
|
|
|
|
self.do_boost_prev_post = self.status.in_reply_to_id.is_some(); |
|
|
|
|
} else { |
|
|
|
|
warn!("User @{} can't share to group!", self.status_acct); |
|
|
|
|
grp_warn!(self, "User @{} can't share to group!", self.status_acct); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -398,19 +425,19 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
// 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); |
|
|
|
|
grp_info!(self, "Deleting group post #{}", parent_status_id); |
|
|
|
|
self.client.delete_status(parent_status_id).await?; |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
} else { |
|
|
|
|
warn!("Only admin can delete posts made by the group user"); |
|
|
|
|
grp_warn!(self, "Only admin can delete posts made by the group user"); |
|
|
|
|
} |
|
|
|
|
} else if self.is_admin || parent_account_id == &self.status_user_id { |
|
|
|
|
info!("Un-reblogging post #{}", parent_status_id); |
|
|
|
|
grp_info!(self, "Un-reblogging post #{}", parent_status_id); |
|
|
|
|
// User unboosting own post boosted by accident, or admin doing it
|
|
|
|
|
self.client.unreblog(parent_status_id).await?; |
|
|
|
|
self.delay_after_post().await; |
|
|
|
|
} else { |
|
|
|
|
warn!("Only the author and admins can undo reblogs"); |
|
|
|
|
grp_warn!(self, "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)
|
|
|
|
|
} |
|
|
|
@ -436,7 +463,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_ban_user_fail_already", user = &u)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -458,7 +485,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -478,7 +505,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -497,7 +524,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_unban_server_fail_already", server = s)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -516,7 +543,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -534,7 +561,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -548,7 +575,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -561,7 +588,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_remove_tag_fail_already", tag = &tag)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -584,7 +611,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_admin_fail_already", user = &u)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -605,7 +632,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -619,7 +646,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_open_resp_already")); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -632,7 +659,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
self.add_reply(crate::tr!(self, "cmd_close_resp_already")); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
warn!("Ignore cmd, user not admin"); |
|
|
|
|
grp_warn!(self, "Ignore cmd, user not admin"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -701,7 +728,7 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
|
|
|
|
|
async fn cmd_join(&mut self) { |
|
|
|
|
if self.config.is_member_or_admin(&self.status_acct) { |
|
|
|
|
debug!("Already member or admin, try to follow-back again"); |
|
|
|
|
grp_debug!(self, "Already member or admin, try to follow-back again"); |
|
|
|
|
// Already a member, so let's try to follow the user
|
|
|
|
|
// again, maybe first time it failed
|
|
|
|
|
self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow"); |
|
|
|
@ -765,13 +792,13 @@ impl<'a> ProcessMention<'a> { |
|
|
|
|
async fn delay_after_post(&self) { |
|
|
|
|
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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(" ."); |
|
|
|
|
fn apply_trailing_hashtag_pleroma_bug_workaround(&self, 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
|
|
|
|
|
grp_debug!(self, "Adding \" .\" to fix pleroma hashtag eating bug!"); |
|
|
|
|
msg.push_str(" ."); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -785,29 +812,29 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> { |
|
|
|
|
let mut parts_to_send = vec![]; |
|
|
|
|
let mut this_piece = prefix.clone(); |
|
|
|
|
for l in msg.split('\n') { |
|
|
|
|
println!("* Line: {:?}", l); |
|
|
|
|
// println!("* Line: {:?}", l);
|
|
|
|
|
|
|
|
|
|
match (this_piece.len() + l.len()).cmp(&limit) { |
|
|
|
|
Ordering::Less => { |
|
|
|
|
println!("append line"); |
|
|
|
|
// println!("append line");
|
|
|
|
|
// this line still fits comfortably
|
|
|
|
|
this_piece.push_str(l); |
|
|
|
|
this_piece.push('\n'); |
|
|
|
|
} |
|
|
|
|
Ordering::Equal => { |
|
|
|
|
println!("exactly fits within limit"); |
|
|
|
|
// println!("exactly fits within limit");
|
|
|
|
|
// this line exactly reaches the limit
|
|
|
|
|
this_piece.push_str(l); |
|
|
|
|
parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned()); |
|
|
|
|
this_piece.push_str(&prefix); |
|
|
|
|
} |
|
|
|
|
Ordering::Greater => { |
|
|
|
|
println!("too long to append (already {} + new {})", this_piece.len(), l.len()); |
|
|
|
|
// println!("too long to append (already {} + new {})", this_piece.len(), l.len());
|
|
|
|
|
// line too long to append
|
|
|
|
|
if this_piece != prefix { |
|
|
|
|
let trimmed = this_piece.trim(); |
|
|
|
|
if !trimmed.is_empty() { |
|
|
|
|
println!("flush buffer: {:?}", trimmed); |
|
|
|
|
// println!("flush buffer: {:?}", trimmed);
|
|
|
|
|
parts_to_send.push(trimmed.to_owned()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -818,18 +845,18 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> { |
|
|
|
|
while this_piece.len() > limit { |
|
|
|
|
// line too long, try splitting at the last space, if any
|
|
|
|
|
let to_send = if let Some(last_space) = (&this_piece[..=limit]).rfind(' ') { |
|
|
|
|
println!("line split at word boundary"); |
|
|
|
|
// println!("line split at word boundary");
|
|
|
|
|
let mut p = this_piece.split_off(last_space + 1); |
|
|
|
|
std::mem::swap(&mut p, &mut this_piece); |
|
|
|
|
p |
|
|
|
|
} else { |
|
|
|
|
println!("line split at exact len (no word boundary found)"); |
|
|
|
|
// println!("line split at exact len (no word boundary found)");
|
|
|
|
|
let mut p = this_piece.split_off(limit); |
|
|
|
|
std::mem::swap(&mut p, &mut this_piece); |
|
|
|
|
p |
|
|
|
|
}; |
|
|
|
|
let part_trimmed = to_send.trim(); |
|
|
|
|
println!("flush buffer: {:?}", part_trimmed); |
|
|
|
|
// println!("flush buffer: {:?}", part_trimmed);
|
|
|
|
|
parts_to_send.push(part_trimmed.to_owned()); |
|
|
|
|
this_piece = format!("{}{}", prefix, this_piece.trim()); |
|
|
|
|
} |
|
|
|
@ -841,7 +868,7 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> { |
|
|
|
|
if this_piece != prefix { |
|
|
|
|
let leftover_trimmed = this_piece.trim(); |
|
|
|
|
if !leftover_trimmed.is_empty() { |
|
|
|
|
println!("flush buffer: {:?}", leftover_trimmed); |
|
|
|
|
// println!("flush buffer: {:?}", leftover_trimmed);
|
|
|
|
|
parts_to_send.push(leftover_trimmed.to_owned()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|