use std ::cmp ::Ordering ;
use std ::collections ::HashSet ;
use std ::time ::Duration ;
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 ::error ::GroupError ;
use crate ::group_handler ::GroupHandle ;
use crate ::store ::group_config ::GroupConfig ;
use crate ::store ::CommonConfig ;
use crate ::tr ::TranslationTable ;
use crate ::utils ;
use crate ::utils ::{ normalize_acct , LogError , VisExt } ;
use crate ::{ grp_debug , grp_info , grp_warn } ;
pub struct ProcessMention < ' a > {
status : Status ,
group_account : & ' a Account ,
config : & ' a mut GroupConfig ,
cc : & ' a CommonConfig ,
client : & ' a mut FediClient ,
group_acct : String ,
status_acct : String ,
status_user_id : String ,
can_write : bool ,
is_admin : bool ,
replies : String ,
announcements : String ,
do_boost_prev_post : bool ,
}
impl < ' a > ProcessMention < ' a > {
fn tr ( & self ) -> & TranslationTable {
self . config . tr ( )
}
async fn lookup_acct_id ( & self , acct : & str , followed : bool ) -> Result < Option < String > , GroupError > {
grp_debug ! ( self , "Looking up user ID by acct: {}" , acct ) ;
match tokio ::time ::timeout (
Duration ::from_secs ( 5 ) ,
self . client
. search_v2 ( acct , ! followed , Some ( SearchType ::Accounts ) , Some ( 1 ) , followed ) ,
)
. await
{
Err ( _ ) = > {
grp_warn ! ( self , "Account lookup timeout!" ) ;
Err ( GroupError ::ApiTimeout )
}
Ok ( Err ( e ) ) = > {
// Elefren error
Err ( e . into ( ) )
}
Ok ( Ok ( res ) ) = > {
for item in res . accounts {
// XXX limit is 1!
let acct_normalized = normalize_acct ( & item . acct , & self . group_acct ) ? ;
if acct_normalized = = acct {
grp_debug ! ( self , "Search done, account found: {}" , item . acct ) ;
return Ok ( Some ( item . id ) ) ;
} else {
grp_warn ! ( self , "Found wrong account: {}" , item . acct ) ;
}
}
grp_debug ! ( self , "Search done, nothing found" ) ;
Ok ( None )
}
}
}
fn append_admin_list_to_reply ( & mut self ) {
let mut admins = self . config . get_admins ( ) . collect ::< Vec < _ > > ( ) ;
admins . sort ( ) ;
let mut to_add = String ::new ( ) ;
for a in admins {
to_add . push_str ( & crate ::tr ! ( self , "user_list_entry" , user = a ) ) ;
}
self . add_reply ( & to_add ) ;
}
fn append_member_list_to_reply ( & mut self ) {
let admins = self . config . get_admins ( ) . collect ::< HashSet < _ > > ( ) ;
let mut members = self . config . get_members ( ) . collect ::< Vec < _ > > ( ) ;
members . extend ( admins . iter ( ) ) ;
members . sort ( ) ;
members . dedup ( ) ;
let mut to_add = String ::new ( ) ;
for m in members {
to_add . push_str ( & if admins . contains ( & m ) {
crate ::tr ! ( self , "user_list_entry_admin" , user = m )
} else {
crate ::tr ! ( self , "user_list_entry" , user = m )
} ) ;
}
self . add_reply ( & to_add ) ;
}
async fn follow_user_by_id ( & self , id : & str ) -> Result < ( ) , GroupError > {
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 > {
grp_debug ! ( self , "Trying to unfollow user #{}" , id ) ;
self . client . unfollow ( id ) . await ? ;
self . delay_after_post ( ) . await ;
Ok ( ( ) )
}
pub ( crate ) async fn run ( gh : & ' a mut GroupHandle , status : Status ) -> Result < ( ) , GroupError > {
let group_acct = gh . config . get_acct ( ) . to_string ( ) ;
let status_acct = normalize_acct ( & status . account . acct , & group_acct ) ? . to_string ( ) ;
if gh . config . is_banned ( & status_acct ) {
grp_warn ! ( gh , "Status author {} is banned!" , status_acct ) ;
return Ok ( ( ) ) ;
}
let pm = Self {
group_account : & gh . group_account ,
status_user_id : status . account . id . to_string ( ) ,
client : & mut gh . client ,
cc : & gh . cc ,
can_write : gh . config . can_write ( & status_acct ) ,
is_admin : gh . config . is_admin ( & status_acct ) ,
replies : String ::new ( ) ,
announcements : String ::new ( ) ,
do_boost_prev_post : false ,
group_acct ,
status_acct ,
status ,
config : & mut gh . config ,
} ;
pm . handle ( ) . await
}
async fn reblog_status ( & self ) {
self . client . reblog ( & self . status . id ) . await . log_error ( "Failed to reblog status" ) ;
self . delay_after_post ( ) . await ;
}
fn add_reply ( & mut self , line : impl AsRef < str > ) {
self . replies . push_str ( line . as_ref ( ) )
}
fn add_announcement ( & mut self , line : impl AsRef < str > ) {
self . announcements . push_str ( line . as_ref ( ) )
}
async fn handle ( mut self ) -> Result < ( ) , GroupError > {
let commands = crate ::command ::parse_slash_commands ( & self . status . content ) ;
if commands . is_empty ( ) {
self . handle_post_with_no_commands ( ) . await ;
} else {
if commands . contains ( & StatusCommand ::Ignore ) {
grp_debug ! ( self , "Notif ignored because of ignore command" ) ;
return Ok ( ( ) ) ;
}
for cmd in commands {
if ! self . replies . is_empty ( ) {
self . replies . push ( '\n' ) ; // make sure there's a newline between batched commands.
}
match cmd {
StatusCommand ::Undo = > {
self . cmd_undo ( ) . await . log_error ( "Error handling undo cmd" ) ;
}
StatusCommand ::Ignore = > {
unreachable! ( ) ; // Handled above
}
StatusCommand ::Announce ( a ) = > {
self . cmd_announce ( a ) . await ;
}
StatusCommand ::Boost = > {
self . cmd_boost ( ) . await ;
}
StatusCommand ::BanUser ( u ) = > {
self . cmd_ban_user ( & u ) . await . log_error ( "Error handling ban-user cmd" ) ;
}
StatusCommand ::UnbanUser ( u ) = > {
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 ;
}
StatusCommand ::UnbanServer ( s ) = > {
self . cmd_unban_server ( & s ) . await ;
}
StatusCommand ::AddMember ( u ) = > {
self . cmd_add_member ( & u ) . await . log_error ( "Error handling add-member cmd" ) ;
}
StatusCommand ::RemoveMember ( u ) = > {
self . cmd_remove_member ( & u ) . await . log_error ( "Error handling remove-member cmd" ) ;
}
StatusCommand ::AddTag ( tag ) = > {
self . cmd_add_tag ( tag ) . await ;
}
StatusCommand ::RemoveTag ( tag ) = > {
self . cmd_remove_tag ( tag ) . await ;
}
StatusCommand ::GrantAdmin ( u ) = > {
self . cmd_grant_admin ( & u ) . await . log_error ( "Error handling grant-admin cmd" ) ;
}
StatusCommand ::RemoveAdmin ( u ) = > {
self . cmd_revoke_admin ( & u ) . await . log_error ( "Error handling grant-admin cmd" ) ;
}
StatusCommand ::OpenGroup = > {
self . cmd_open_group ( ) . await ;
}
StatusCommand ::CloseGroup = > {
self . cmd_close_group ( ) . await ;
}
StatusCommand ::Help = > {
self . cmd_help ( ) . await ;
}
StatusCommand ::ListMembers = > {
self . cmd_list_members ( ) . await ;
}
StatusCommand ::ListTags = > {
self . cmd_list_tags ( ) . await ;
}
StatusCommand ::Leave = > {
self . cmd_leave ( ) . await ;
}
StatusCommand ::Join = > {
self . cmd_join ( ) . await ;
}
StatusCommand ::Ping = > {
self . cmd_ping ( ) . await ;
}
}
}
}
if self . do_boost_prev_post {
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" ) ;
self . delay_after_post ( ) . await ;
}
Err ( e ) = > {
grp_warn ! ( self , "Can't reblog: {}" , e ) ;
}
}
}
}
if ! self . replies . is_empty ( ) {
let mut msg = std ::mem ::take ( & mut self . replies ) ;
grp_debug ! ( self , "r={}" , 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 ? ;
}
if ! self . announcements . is_empty ( ) {
let mut msg = std ::mem ::take ( & mut self . announcements ) ;
grp_debug ! ( self , "a={}" , 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 ? ;
}
Ok ( ( ) )
}
async fn send_reply_multipart ( & self , mention : String , msg : String ) -> Result < ( ) , GroupError > {
let parts = smart_split ( & msg , Some ( mention ) , self . config . get_character_limit ( ) ) ;
let mut parent = self . status . id . clone ( ) ;
for p in parts {
if let Ok ( post ) = StatusBuilder ::new ( )
. status ( p )
. content_type ( "text/markdown" )
. in_reply_to ( & parent )
. visibility ( Visibility ::Direct )
. build ( )
{
let status = self . client . new_status ( post ) . await ? ;
self . delay_after_post ( ) . await ;
parent = status . id ;
}
// Sleep a bit to avoid throttling
tokio ::time ::sleep ( Duration ::from_secs ( 1 ) ) . await ;
}
Ok ( ( ) )
}
async fn send_announcement_multipart ( & self , msg : & str ) -> Result < ( ) , GroupError > {
let parts = smart_split ( msg , None , self . config . get_character_limit ( ) ) ;
let mut parent = None ;
for p in parts {
let mut builder = StatusBuilder ::new ( ) ;
builder . status ( p ) . content_type ( "text/markdown" ) . visibility ( Visibility ::Public ) ;
if let Some ( p ) = parent . as_ref ( ) {
builder . in_reply_to ( p ) ;
}
let post = builder . build ( ) . expect ( "error build status" ) ;
let status = self . client . new_status ( post ) . await ? ;
self . delay_after_post ( ) . await ;
parent = Some ( status . id ) ;
// Sleep a bit to avoid throttling
tokio ::time ::sleep ( Duration ::from_secs ( 1 ) ) . await ;
}
Ok ( ( ) )
}
async fn handle_post_with_no_commands ( & mut self ) {
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.
grp_info ! ( self , "Boosting OP mention" ) ;
// tokio::time::sleep(DELAY_BEFORE_ACTION).await;
self . reblog_status ( ) . await ;
// Otherwise, don't react
} else {
// 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" ) ;
}
} else {
grp_warn ! ( self , "User @{} can't post to group!" , self . status_acct ) ;
}
}
async fn cmd_announce ( & mut self , msg : String ) {
grp_info ! ( self , "Sending PSA" ) ;
self . add_announcement ( msg ) ;
}
async fn cmd_boost ( & mut self ) {
if self . can_write {
self . do_boost_prev_post = self . status . in_reply_to_id . is_some ( ) ;
} else {
grp_warn ! ( self , "User @{} can't share to group!" , self . status_acct ) ;
}
}
async fn cmd_optout ( & mut self ) {
if self . is_admin {
self . add_reply ( crate ::tr ! ( self , "cmd_optout_fail_admin_cant" ) ) ;
} else if self . config . is_member ( & self . status_acct ) {
self . add_reply ( crate ::tr ! ( self , "cmd_optout_fail_member_cant" ) ) ;
} else {
self . config . set_optout ( & self . status_acct , true ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_optout_ok" ) ) ;
}
}
async fn cmd_optin ( & mut self ) {
if self . is_admin {
self . add_reply ( crate ::tr ! ( self , "cmd_optin_fail_admin_cant" ) ) ;
} else if self . config . is_member ( & self . status_acct ) {
self . add_reply ( crate ::tr ! ( self , "cmd_optin_fail_member_cant" ) ) ;
} else {
self . config . set_optout ( & self . status_acct , false ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_optin_ok" ) ) ;
}
}
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 {
grp_info ! ( self , "Deleting group post #{}" , parent_status_id ) ;
self . client . delete_status ( parent_status_id ) . await ? ;
self . delay_after_post ( ) . await ;
} else {
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 {
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 {
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)
}
}
Ok ( ( ) )
}
async fn cmd_ban_user ( & mut self , user : & str ) -> Result < ( ) , GroupError > {
let u = normalize_acct ( user , & self . group_acct ) ? ;
if self . is_admin {
if ! self . config . is_banned ( & u ) {
match self . config . ban_user ( & u , true ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_ban_user_ok" , user = & u ) ) ;
self . unfollow_by_acct ( & u ) . await . log_error ( "Failed to unfollow banned user" ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_ban_user_fail_already" , user = & u ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
Ok ( ( ) )
}
async fn cmd_unban_user ( & mut self , user : & str ) -> Result < ( ) , GroupError > {
let u = normalize_acct ( user , & self . group_acct ) ? ;
if self . is_admin {
if self . config . is_banned ( & u ) {
match self . config . ban_user ( & u , false ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_unban_user_ok" , user = & u ) ) ;
// no announcement here
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_unban_user_fail_already" , user = & u ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
Ok ( ( ) )
}
async fn cmd_ban_server ( & mut self , s : & str ) {
if self . is_admin {
if ! self . config . is_server_banned ( s ) {
match self . config . ban_server ( s , true ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_ban_server_ok" , server = s ) ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_ban_server_fail_already" , server = s ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
}
async fn cmd_unban_server ( & mut self , s : & str ) {
if self . is_admin {
if self . config . is_server_banned ( s ) {
match self . config . ban_server ( s , false ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_unban_server_ok" , server = s ) ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_unban_server_fail_already" , server = s ) ) ;
}
} else {
grp_warn ! ( self , "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 ( crate ::tr ! ( self , "cmd_add_user_ok" , user = & u ) ) ;
// marked as member, now also follow the user
self . follow_by_acct ( & u ) . await . log_error ( "Failed to follow" ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
Ok ( ( ) )
}
async fn cmd_remove_member ( & mut self , user : & str ) -> Result < ( ) , GroupError > {
let u = normalize_acct ( user , & self . group_acct ) ? ;
if self . is_admin {
match self . config . set_member ( & u , false ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_remove_user_ok" , user = & u ) ) ;
self . unfollow_by_acct ( & u ) . await . log_error ( "Failed to unfollow removed user" ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
grp_warn ! ( self , "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 ) {
self . config . add_tag ( & tag ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_add_tag_ok" , tag = & tag ) ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_add_tag_fail_already" , tag = & tag ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
}
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 ( crate ::tr ! ( self , "cmd_remove_tag_ok" , tag = & tag ) ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_remove_tag_fail_already" , tag = & tag ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
}
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 ) {
match self . config . set_admin ( & u , true ) {
Ok ( _ ) = > {
// try to make the config a little more sane, admins should be members
let _ = self . config . set_member ( & u , true ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_admin_ok" , user = & u ) ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_admin_fail_already" , user = & u ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
Ok ( ( ) )
}
async fn cmd_revoke_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 ) {
match self . config . set_admin ( & u , false ) {
Ok ( _ ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_unadmin_ok" , user = & u ) ) ;
}
Err ( e ) = > {
self . add_reply ( crate ::tr ! ( self , "cmd_error" , cause = & e . to_string ( ) ) ) ;
}
}
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_unadmin_fail_already" , user = & u ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
Ok ( ( ) )
}
async fn cmd_open_group ( & mut self ) {
if self . is_admin {
if self . config . is_member_only ( ) {
self . config . set_member_only ( false ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_open_resp" ) ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_open_resp_already" ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
}
async fn cmd_close_group ( & mut self ) {
if self . is_admin {
if ! self . config . is_member_only ( ) {
self . config . set_member_only ( true ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_close_resp" ) ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "cmd_close_resp_already" ) ) ;
}
} else {
grp_warn ! ( self , "Ignore cmd, user not admin" ) ;
}
}
async fn cmd_help ( & mut self ) {
let membership_line = if self . is_admin {
crate ::tr ! ( self , "help_membership_admin" )
} else if self . config . is_member ( & self . status_acct ) {
crate ::tr ! ( self , "help_membership_member" )
} else if self . config . is_member_only ( ) {
crate ::tr ! ( self , "help_membership_guest_closed" )
} else {
crate ::tr ! ( self , "help_membership_guest_open" )
} ;
if self . config . is_member_only ( ) {
self . add_reply ( crate ::tr ! (
self ,
"help_group_info_closed" ,
membership = & membership_line
) ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "help_group_info_open" , membership = & membership_line ) ) ;
}
self . add_reply ( crate ::tr ! ( self , "help_basic_commands" ) ) ;
if ! self . is_admin {
self . add_reply ( crate ::tr ! ( self , "help_member_commands" ) ) ;
}
if self . is_admin {
self . add_reply ( crate ::tr ! ( self , "help_admin_commands" ) ) ;
}
}
async fn cmd_list_members ( & mut self ) {
if self . is_admin {
self . add_reply ( crate ::tr ! ( self , "member_list_heading" ) ) ;
self . append_member_list_to_reply ( ) ;
} else {
self . add_reply ( crate ::tr ! ( self , "admin_list_heading" ) ) ;
self . append_admin_list_to_reply ( ) ;
}
}
async fn cmd_list_tags ( & mut self ) {
self . add_reply ( crate ::tr ! ( self , "tag_list_heading" ) ) ;
let mut tags = self . config . get_tags ( ) . collect ::< Vec < _ > > ( ) ;
tags . sort ( ) ;
let mut to_add = String ::new ( ) ;
for t in tags {
to_add . push_str ( & crate ::tr ! ( self , "tag_list_entry" , tag = t ) ) ;
}
self . add_reply ( to_add ) ;
}
async fn cmd_leave ( & mut self ) {
if self . config . is_member_or_admin ( & self . status_acct ) {
// admin can leave but that's a bad idea
let _ = self . config . set_member ( & self . status_acct , false ) ;
self . add_reply ( crate ::tr ! ( self , "cmd_leave_resp" ) ) ;
}
self . unfollow_user_by_id ( & self . status_user_id )
. await
. log_error ( "Failed to unfollow" ) ;
}
async fn cmd_join ( & mut self ) {
if self . config . is_member_or_admin ( & self . status_acct ) {
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" ) ;
} else {
// Not a member yet
if self . config . is_member_only ( ) {
// No you can't
self . add_reply ( crate ::tr ! ( self , "welcome_closed" ) ) ;
self . append_admin_list_to_reply ( ) ;
} else {
// Open access, try to follow back
self . follow_user_by_id ( & self . status_user_id ) . await . log_error ( "Failed to follow" ) ;
// This only fails if the user is banned, but that is filtered above
let _ = self . config . set_member ( & self . status_acct , true ) ;
self . add_reply ( crate ::tr ! ( self , "welcome_join_cmd" ) ) ;
}
}
}
async fn cmd_ping ( & mut self ) {
self . add_reply ( crate ::tr ! ( self , "ping_response" , version = env! ( "CARGO_PKG_VERSION" ) ) ) ;
}
async fn unfollow_by_acct ( & self , acct : & str ) -> Result < ( ) , GroupError > {
// Try to unfollow
if let Ok ( Some ( id ) ) = self . lookup_acct_id ( acct , true ) . await {
self . unfollow_user_by_id ( & id ) . await ? ;
}
Ok ( ( ) )
}
async fn follow_by_acct ( & self , acct : & str ) -> Result < ( ) , GroupError > {
// Try to unfollow
if let Ok ( Some ( id ) ) = self . lookup_acct_id ( acct , false ) . await {
self . follow_user_by_id ( & id ) . await ? ;
}
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 ) ;
let normalized = normalize_acct ( & account . acct , & self . group_acct ) ? ;
if RE_NOBOT_TAG . is_match ( & bio ) & & ! self . config . is_member ( & normalized ) {
// #nobot in a non-member account
Err ( GroupError ::UserOptedOutNobot )
} else {
if self . config . is_banned ( & normalized ) {
Err ( GroupError ::UserIsBanned )
} else if self . config . is_optout ( & normalized ) {
Err ( GroupError ::UserOptedOut )
} else {
Ok ( normalized )
}
}
}
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 ( & 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 ( " ." ) ;
}
}
}
fn smart_split ( msg : & str , prefix : Option < String > , limit : usize ) -> Vec < String > {
let prefix = prefix . unwrap_or_default ( ) ;
if msg . len ( ) + prefix . len ( ) < limit {
return vec! [ format! ( "{}{}" , prefix , msg ) ] ;
}
let mut parts_to_send = vec! [ ] ;
let mut this_piece = prefix . clone ( ) ;
for l in msg . split ( '\n' ) {
// println!("* Line: {:?}", l);
match ( this_piece . len ( ) + l . len ( ) ) . cmp ( & limit ) {
Ordering ::Less = > {
// println!("append line");
// this line still fits comfortably
this_piece . push_str ( l ) ;
this_piece . push ( '\n' ) ;
}
Ordering ::Equal = > {
// 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());
// line too long to append
if this_piece ! = prefix {
let trimmed = this_piece . trim ( ) ;
if ! trimmed . is_empty ( ) {
// println!("flush buffer: {:?}", trimmed);
parts_to_send . push ( trimmed . to_owned ( ) ) ;
}
}
// start new piece with the line. If the line is too long, break it up.
this_piece = format! ( "{}{}" , prefix , l ) ;
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");
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)");
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);
parts_to_send . push ( part_trimmed . to_owned ( ) ) ;
this_piece = format! ( "{}{}" , prefix , this_piece . trim ( ) ) ;
}
this_piece . push ( '\n' ) ;
}
}
}
if this_piece ! = prefix {
let leftover_trimmed = this_piece . trim ( ) ;
if ! leftover_trimmed . is_empty ( ) {
// println!("flush buffer: {:?}", leftover_trimmed);
parts_to_send . push ( leftover_trimmed . to_owned ( ) ) ;
}
}
parts_to_send
}
#[ cfg(test) ]
mod test {
#[ test ]
fn test_smart_split_lines ( ) {
let to_split = "a234567890\nb234567890\nc234567890\nd234\n67890\ne234567890\n" ;
let parts = super ::smart_split ( to_split , None , 10 ) ;
assert_eq! (
vec! [
"a234567890" . to_string ( ) ,
"b234567890" . to_string ( ) ,
"c234567890" . to_string ( ) ,
"d234\n67890" . to_string ( ) ,
"e234567890" . to_string ( ) ,
] ,
parts
) ;
}
#[ test ]
fn test_smart_split_nosplit ( ) {
let to_split = "foo\nbar\nbaz" ;
let parts = super ::smart_split ( to_split , None , 1000 ) ;
assert_eq! ( vec! [ "foo\nbar\nbaz" . to_string ( ) ] , parts ) ;
}
#[ test ]
fn test_smart_split_nosplit_prefix ( ) {
let to_split = "foo\nbar\nbaz" ;
let parts = super ::smart_split ( to_split , Some ( "PREFIX" . to_string ( ) ) , 1000 ) ;
assert_eq! ( vec! [ "PREFIXfoo\nbar\nbaz" . to_string ( ) ] , parts ) ;
}
#[ test ]
fn test_smart_split_prefix_each ( ) {
let to_split = "1234\n56\n7" ;
let parts = super ::smart_split ( to_split , Some ( "PREFIX" . to_string ( ) ) , 10 ) ;
assert_eq! ( vec! [ "PREFIX1234" . to_string ( ) , "PREFIX56\n7" . to_string ( ) ] , parts ) ;
}
#[ test ]
fn test_smart_split_words ( ) {
let to_split = "one two three four five six seven eight nine ten" ;
let parts = super ::smart_split ( to_split , None , 10 ) ;
assert_eq! (
vec! [
"one two" . to_string ( ) ,
"three four" . to_string ( ) ,
"five six" . to_string ( ) ,
"seven" . to_string ( ) ,
"eight nine" . to_string ( ) ,
"ten" . to_string ( ) ,
] ,
parts
) ;
}
#[ test ]
fn test_smart_split_words_multispace ( ) {
let to_split = "one two three four five six seven eight nine ten " ;
let parts = super ::smart_split ( to_split , None , 10 ) ;
assert_eq! (
vec! [
"one two" . to_string ( ) ,
"three four" . to_string ( ) ,
"five six" . to_string ( ) ,
"seven" . to_string ( ) ,
"eight nine" . to_string ( ) ,
"ten" . to_string ( ) ,
] ,
parts
) ;
}
#[ test ]
fn test_smart_split_words_longword ( ) {
let to_split = "one two threefourfive six" ;
let parts = super ::smart_split ( to_split , None , 10 ) ;
assert_eq! (
vec! [ "one two" . to_string ( ) , "threefourf" . to_string ( ) , "ive six" . to_string ( ) ] ,
parts
) ;
}
#[ test ]
fn test_smart_split_words_prefix ( ) {
let to_split = "one two three four five six seven eight nine ten" ;
let parts = super ::smart_split ( to_split , Some ( "PREFIX" . to_string ( ) ) , 15 ) ;
assert_eq! (
vec! [
"PREFIXone two" . to_string ( ) ,
"PREFIXthree" . to_string ( ) ,
"PREFIXfour five" . to_string ( ) ,
"PREFIXsix seven" . to_string ( ) ,
"PREFIXeight" . to_string ( ) ,
"PREFIXnine ten" . to_string ( ) ,
] ,
parts
) ;
}
#[ test ]
fn test_smart_split_realistic ( ) {
let to_split = " \
Lorem ipsum dolor sit amet , consectetur adipiscing elit . \ n \
Aenean venenatis libero ac ex suscipit , nec efficitur arcu convallis . \ n \
Nulla ante neque , efficitur nec fermentum a , fermentum nec nisl . \ n \
Sed dolor ex , vestibulum at malesuada ut , faucibus ac ante . \ n \
Nullam scelerisque magna dui , id tempor purus faucibus sit amet . \ n \
Curabitur pretium condimentum pharetra . \ n \
Aenean dictum , tortor et ultrices fermentum , mauris erat vehicula lectus . \ n \
Nec varius mauris sem sollicitudin dolor . Nunc porta in urna nec vulputate . " ;
let parts = super ::smart_split ( to_split , Some ( "@pepa@pig.club " . to_string ( ) ) , 140 ) ;
assert_eq! ( vec! [
"@pepa@pig.club Lorem ipsum dolor sit amet, consectetur adipiscing elit." . to_string ( ) ,
"@pepa@pig.club Aenean venenatis libero ac ex suscipit, nec efficitur arcu convallis." . to_string ( ) ,
"@pepa@pig.club Nulla ante neque, efficitur nec fermentum a, fermentum nec nisl.\nSed dolor ex, vestibulum at malesuada ut, faucibus ac ante." . to_string ( ) ,
"@pepa@pig.club Nullam scelerisque magna dui, id tempor purus faucibus sit amet.\nCurabitur pretium condimentum pharetra." . to_string ( ) ,
"@pepa@pig.club Aenean dictum, tortor et ultrices fermentum, mauris erat vehicula lectus." . to_string ( ) ,
"@pepa@pig.club Nec varius mauris sem sollicitudin dolor. Nunc porta in urna nec vulputate." . to_string ( ) ,
] , parts ) ;
}
}