|
|
@ -1,3 +1,4 @@ |
|
|
|
|
|
|
|
use std::collections::HashSet; |
|
|
|
use std::sync::Arc; |
|
|
|
use std::sync::Arc; |
|
|
|
use std::time::{Duration, Instant}; |
|
|
|
use std::time::{Duration, Instant}; |
|
|
|
|
|
|
|
|
|
|
@ -10,10 +11,10 @@ use elefren::status_builder::Visibility; |
|
|
|
use futures::StreamExt; |
|
|
|
use futures::StreamExt; |
|
|
|
|
|
|
|
|
|
|
|
use crate::command::StatusCommand; |
|
|
|
use crate::command::StatusCommand; |
|
|
|
use crate::store::{ConfigStore, GroupError}; |
|
|
|
use crate::error::GroupError; |
|
|
|
|
|
|
|
use crate::store::ConfigStore; |
|
|
|
use crate::store::data::GroupConfig; |
|
|
|
use crate::store::data::GroupConfig; |
|
|
|
use crate::utils::LogError; |
|
|
|
use crate::utils::{LogError, normalize_acct}; |
|
|
|
use std::collections::HashSet; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// This is one group's config store capable of persistence
|
|
|
|
/// This is one group's config store capable of persistence
|
|
|
|
#[derive(Debug)] |
|
|
|
#[derive(Debug)] |
|
|
@ -23,6 +24,12 @@ pub struct GroupHandle { |
|
|
|
pub(crate) store: Arc<ConfigStore>, |
|
|
|
pub(crate) store: Arc<ConfigStore>, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DELAY_BEFORE_ACTION: Duration = Duration::from_millis(250); |
|
|
|
|
|
|
|
const DELAY_REOPEN_STREAM: Duration = Duration::from_millis(1000); |
|
|
|
|
|
|
|
const MAX_CATCHUP_NOTIFS: usize = 25; |
|
|
|
|
|
|
|
const PERIODIC_SAVE: Duration = Duration::from_secs(60); |
|
|
|
|
|
|
|
const PING_INTERVAL: Duration = Duration::from_secs(15); // must be < periodic save!
|
|
|
|
|
|
|
|
|
|
|
|
impl GroupHandle { |
|
|
|
impl GroupHandle { |
|
|
|
pub async fn save(&mut self) -> Result<(), GroupError> { |
|
|
|
pub async fn save(&mut self) -> Result<(), GroupError> { |
|
|
|
debug!("Saving group config & status"); |
|
|
|
debug!("Saving group config & status"); |
|
|
@ -60,9 +67,6 @@ impl NotifTimestamp for Notification { |
|
|
|
|
|
|
|
|
|
|
|
impl GroupHandle { |
|
|
|
impl GroupHandle { |
|
|
|
pub async fn run(&mut self) -> Result<(), GroupError> { |
|
|
|
pub async fn run(&mut self) -> Result<(), GroupError> { |
|
|
|
const PERIODIC_SAVE: Duration = Duration::from_secs(60); |
|
|
|
|
|
|
|
const PING_INTERVAL: Duration = Duration::from_secs(15); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(PERIODIC_SAVE >= PING_INTERVAL); |
|
|
|
assert!(PERIODIC_SAVE >= PING_INTERVAL); |
|
|
|
|
|
|
|
|
|
|
|
let mut next_save = Instant::now() + PERIODIC_SAVE; // so we save at start
|
|
|
|
let mut next_save = Instant::now() + PERIODIC_SAVE; // so we save at start
|
|
|
@ -102,7 +106,8 @@ impl GroupHandle { |
|
|
|
match event { |
|
|
|
match event { |
|
|
|
Event::Update(_status) => {} |
|
|
|
Event::Update(_status) => {} |
|
|
|
Event::Notification(n) => { |
|
|
|
Event::Notification(n) => { |
|
|
|
self.handle_notification(n).await; |
|
|
|
self.handle_notification(n).await |
|
|
|
|
|
|
|
.log_error("Error handling a notification"); |
|
|
|
} |
|
|
|
} |
|
|
|
Event::Delete(_id) => {} |
|
|
|
Event::Delete(_id) => {} |
|
|
|
Event::FiltersChanged => {} |
|
|
|
Event::FiltersChanged => {} |
|
|
@ -122,100 +127,112 @@ impl GroupHandle { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
warn!("Notif stream closed, will reopen"); |
|
|
|
warn!("Notif stream closed, will reopen"); |
|
|
|
tokio::time::sleep(Duration::from_millis(1000)).await; |
|
|
|
tokio::time::sleep(DELAY_REOPEN_STREAM).await; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fn handle_notification(&mut self, n: Notification) { |
|
|
|
async fn handle_notification(&mut self, n: Notification) -> Result<(), GroupError> { |
|
|
|
const DELAY_BEFORE_ACTION: Duration = Duration::from_millis(500); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug!("Handling notif #{}", n.id); |
|
|
|
debug!("Handling notif #{}", n.id); |
|
|
|
let ts = n.timestamp_millis(); |
|
|
|
let ts = n.timestamp_millis(); |
|
|
|
self.config.set_last_notif(ts); |
|
|
|
self.config.set_last_notif(ts); |
|
|
|
|
|
|
|
|
|
|
|
let can_write = self.config.can_write(&n.account.acct); |
|
|
|
let group_acct = self.config.get_acct().to_string(); |
|
|
|
let is_admin = self.config.is_admin(&n.account.acct); |
|
|
|
let notif_acct = normalize_acct(&n.account.acct, &group_acct)?; |
|
|
|
|
|
|
|
|
|
|
|
if self.config.is_banned(&n.account.acct) { |
|
|
|
let can_write = self.config.can_write(¬if_acct); |
|
|
|
warn!("Notification actor {} is banned!", n.account.acct); |
|
|
|
let is_admin = self.config.is_admin(¬if_acct); |
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
if self.config.is_banned(¬if_acct) { |
|
|
|
|
|
|
|
warn!("Notification actor {} is banned!", notif_acct); |
|
|
|
|
|
|
|
return Ok(()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
match n.notification_type { |
|
|
|
match n.notification_type { |
|
|
|
NotificationType::Mention => { |
|
|
|
NotificationType::Mention => { |
|
|
|
if let Some(status) = n.status { |
|
|
|
if let Some(status) = n.status { |
|
|
|
if self.config.is_banned(&status.account.acct) { |
|
|
|
let status_acct = normalize_acct(&status.account.acct, &group_acct)?; |
|
|
|
warn!("Status author {} is banned!", status.account.acct); |
|
|
|
|
|
|
|
return; |
|
|
|
if self.config.is_banned(&status_acct) { |
|
|
|
|
|
|
|
warn!("Status author {} is banned!", status_acct); |
|
|
|
|
|
|
|
return Ok(()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let commands = crate::command::parse_status(&status.content); |
|
|
|
let commands = crate::command::parse_status(&status.content); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut replies = vec![]; |
|
|
|
|
|
|
|
let mut announcements = vec![]; |
|
|
|
|
|
|
|
let mut do_boost_prev_post = false; |
|
|
|
|
|
|
|
// let mut new_members = vec![];
|
|
|
|
|
|
|
|
// let mut new_admins = vec![];
|
|
|
|
|
|
|
|
// let mut removed_admins = vec![];
|
|
|
|
|
|
|
|
// let mut instance_ban_announcements = vec![];
|
|
|
|
|
|
|
|
// let mut instance_unban_announcements = vec![];
|
|
|
|
|
|
|
|
let mut any_admin_cmd = false; |
|
|
|
|
|
|
|
|
|
|
|
if commands.is_empty() { |
|
|
|
if commands.is_empty() { |
|
|
|
debug!("No commands in post"); |
|
|
|
debug!("No commands in post"); |
|
|
|
if !can_write { |
|
|
|
|
|
|
|
warn!("User {} not allowed to post in group", n.account.acct); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if status.in_reply_to_id.is_none() { |
|
|
|
if status.in_reply_to_id.is_none() { |
|
|
|
|
|
|
|
if can_write { |
|
|
|
// Someone tagged the group in OP, boost it.
|
|
|
|
// Someone tagged the group in OP, boost it.
|
|
|
|
info!("Boosting OP mention"); |
|
|
|
info!("Boosting OP mention"); |
|
|
|
tokio::time::sleep(DELAY_BEFORE_ACTION).await; |
|
|
|
tokio::time::sleep(DELAY_BEFORE_ACTION).await; |
|
|
|
self.client.reblog(&status.id).await |
|
|
|
self.client.reblog(&status.id).await |
|
|
|
.log_error("Failed to boost"); |
|
|
|
.log_error("Failed to boost"); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
debug!("Not OP, ignore mention") |
|
|
|
replies.push(format!("You are not allowed to post to this group")); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
debug!("Not OP, ignore mention"); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
let mut reply = vec![]; |
|
|
|
|
|
|
|
let mut boost_prev = false; |
|
|
|
|
|
|
|
let mut new_members = vec![]; |
|
|
|
|
|
|
|
let mut new_admins = vec![]; |
|
|
|
|
|
|
|
let mut removed_admins = vec![]; |
|
|
|
|
|
|
|
let mut instance_ban_announcements = vec![]; |
|
|
|
|
|
|
|
let mut instance_unban_announcements = vec![]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO normalize local user handles
|
|
|
|
|
|
|
|
let mut any_admin_cmd = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for cmd in commands { |
|
|
|
for cmd in commands { |
|
|
|
match cmd { |
|
|
|
match cmd { |
|
|
|
|
|
|
|
// ignore is first on purpose
|
|
|
|
StatusCommand::Ignore => { |
|
|
|
StatusCommand::Ignore => { |
|
|
|
debug!("Notif ignored because of ignore command"); |
|
|
|
debug!("Notif ignored because of ignore command"); |
|
|
|
return; |
|
|
|
return Ok(()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
StatusCommand::Announce(a) => { |
|
|
|
|
|
|
|
debug!("Sending PSA"); |
|
|
|
|
|
|
|
announcements.push(a); |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::Boost => { |
|
|
|
StatusCommand::Boost => { |
|
|
|
if !can_write { |
|
|
|
if can_write { |
|
|
|
warn!("User {} not allowed to boost to group", n.account.acct); |
|
|
|
do_boost_prev_post = status.in_reply_to_id.is_some(); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
boost_prev = status.in_reply_to_id.is_some(); |
|
|
|
replies.push(format!("You are not allowed to share to this group")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::BanUser(u) => { |
|
|
|
StatusCommand::BanUser(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if !self.config.is_banned(&u) { |
|
|
|
if !self.config.is_banned(&u) { |
|
|
|
match self.config.ban_user(&u, true) { |
|
|
|
match self.config.ban_user(&u, true) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} banned from group!", u)); |
|
|
|
replies.push(format!("User {} banned from group!", u)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// no announcement here
|
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
reply.push(format!("Failed to ban user {}: {}", u, e)); |
|
|
|
replies.push(format!("Failed to ban user {}: {}", u, e)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage bans"); |
|
|
|
replies.push(format!("Only admins can manage user bans")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::UnbanUser(u) => { |
|
|
|
StatusCommand::UnbanUser(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if self.config.is_banned(&u) { |
|
|
|
if self.config.is_banned(&u) { |
|
|
|
match self.config.ban_user(&u, false) { |
|
|
|
match self.config.ban_user(&u, false) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} un-banned!", u)); |
|
|
|
replies.push(format!("User {} un-banned!", u)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// no announcement here
|
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
unreachable!() |
|
|
|
unreachable!() |
|
|
@ -223,7 +240,7 @@ impl GroupHandle { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage bans"); |
|
|
|
replies.push(format!("Only admins can manage user bans")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::BanServer(s) => { |
|
|
|
StatusCommand::BanServer(s) => { |
|
|
@ -232,16 +249,16 @@ impl GroupHandle { |
|
|
|
match self.config.ban_server(&s, true) { |
|
|
|
match self.config.ban_server(&s, true) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
instance_ban_announcements.push(s.clone()); |
|
|
|
announcements.push(format!("Server \"{}\" has been banned.", s)); |
|
|
|
reply.push(format!("Instance {} banned from group!", s)); |
|
|
|
replies.push(format!("Server {} banned from group!", s)); |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
reply.push(format!("Failed to ban instance {}: {}", s, e)); |
|
|
|
replies.push(format!("Failed to ban server {}: {}", s, e)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage bans"); |
|
|
|
replies.push(format!("Only admins can manage server bans")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::UnbanServer(s) => { |
|
|
|
StatusCommand::UnbanServer(s) => { |
|
|
@ -250,8 +267,8 @@ impl GroupHandle { |
|
|
|
match self.config.ban_server(&s, false) { |
|
|
|
match self.config.ban_server(&s, false) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
instance_unban_announcements.push(s.clone()); |
|
|
|
announcements.push(format!("Server \"{}\" has been un-banned.", s)); |
|
|
|
reply.push(format!("Instance {} un-banned!", s)); |
|
|
|
replies.push(format!("Server {} un-banned!", s)); |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
unreachable!() |
|
|
|
unreachable!() |
|
|
@ -259,34 +276,39 @@ impl GroupHandle { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage bans"); |
|
|
|
replies.push(format!("Only admins can manage server bans")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::AddMember(u) => { |
|
|
|
StatusCommand::AddMember(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if !self.config.is_member(&u) { |
|
|
|
if !self.config.is_member(&u) { |
|
|
|
match self.config.set_member(&u, true) { |
|
|
|
match self.config.set_member(&u, true) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} added to group!", u)); |
|
|
|
replies.push(format!("User {} added to the group!", u)); |
|
|
|
new_members.push(u); |
|
|
|
|
|
|
|
|
|
|
|
if self.config.is_member_only() { |
|
|
|
|
|
|
|
announcements.push(format!("Welcome new member @{} to the group!", u)); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
reply.push(format!("Failed to add user {} to group: {}", u, e)); |
|
|
|
replies.push(format!("Failed to add user {} to group: {}", u, e)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage members"); |
|
|
|
replies.push(format!("Only admins can manage members")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::RemoveMember(u) => { |
|
|
|
StatusCommand::RemoveMember(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if self.config.is_member(&u) { |
|
|
|
if self.config.is_member(&u) { |
|
|
|
match self.config.set_member(&u, false) { |
|
|
|
match self.config.set_member(&u, false) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} removed from group.", u)); |
|
|
|
replies.push(format!("User {} removed from the group.", u)); |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
unreachable!() |
|
|
|
unreachable!() |
|
|
@ -294,43 +316,45 @@ impl GroupHandle { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage members"); |
|
|
|
replies.push(format!("Only admins can manage members")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::GrantAdmin(u) => { |
|
|
|
StatusCommand::GrantAdmin(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if !self.config.is_admin(&u) { |
|
|
|
if !self.config.is_admin(&u) { |
|
|
|
match self.config.set_admin(&u, true) { |
|
|
|
match self.config.set_admin(&u, true) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} is now a group admin!", u)); |
|
|
|
replies.push(format!("User {} is now a group admin!", u)); |
|
|
|
new_admins.push(u); |
|
|
|
announcements.push(format!("User @{} can now manage this group!", u)); |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
reply.push(format!("Failed to make user {} a group admin: {}", u, e)); |
|
|
|
replies.push(format!("Failed to make user {} a group admin: {}", u, e)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage admin rights"); |
|
|
|
replies.push(format!("Only admins can manage admins")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::RemoveAdmin(u) => { |
|
|
|
StatusCommand::RemoveAdmin(u) => { |
|
|
|
|
|
|
|
let u = normalize_acct(&u, &group_acct)?; |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
if self.config.is_admin(&u) { |
|
|
|
if self.config.is_admin(&u) { |
|
|
|
match self.config.set_admin(&u, false) { |
|
|
|
match self.config.set_admin(&u, false) { |
|
|
|
Ok(_) => { |
|
|
|
Ok(_) => { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
reply.push(format!("User {} is no longer a group admin!", u)); |
|
|
|
replies.push(format!("User {} is no longer a group admin!", u)); |
|
|
|
removed_admins.push(u) |
|
|
|
announcements.push(format!("User @{} no longer manages this group.", u)); |
|
|
|
} |
|
|
|
} |
|
|
|
Err(e) => { |
|
|
|
Err(e) => { |
|
|
|
reply.push(format!("Failed to revoke {}'s group admin: {}", u, e)); |
|
|
|
replies.push(format!("Failed to revoke {}'s group admin: {}", u, e)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage admin rights"); |
|
|
|
replies.push(format!("Only admins can manage admins")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::OpenGroup => { |
|
|
|
StatusCommand::OpenGroup => { |
|
|
@ -338,10 +362,11 @@ impl GroupHandle { |
|
|
|
if self.config.is_member_only() { |
|
|
|
if self.config.is_member_only() { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
self.config.set_member_only(false); |
|
|
|
self.config.set_member_only(false); |
|
|
|
reply.push(format!("Group changed to open-access")); |
|
|
|
replies.push(format!("Group changed to open-access")); |
|
|
|
|
|
|
|
announcements.push(format!("This group is now open-access!")); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage group mode"); |
|
|
|
replies.push(format!("Only admins can set group options")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::CloseGroup => { |
|
|
|
StatusCommand::CloseGroup => { |
|
|
@ -349,66 +374,80 @@ impl GroupHandle { |
|
|
|
if !self.config.is_member_only() { |
|
|
|
if !self.config.is_member_only() { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
self.config.set_member_only(true); |
|
|
|
self.config.set_member_only(true); |
|
|
|
reply.push(format!("Group changed to member-only")); |
|
|
|
replies.push(format!("Group changed to member-only")); |
|
|
|
|
|
|
|
announcements.push(format!("This group is now member-only!")); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
warn!("Not admin, can't manage group mode"); |
|
|
|
replies.push(format!("Only admins can set group options")); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::Help => { |
|
|
|
StatusCommand::Help => { |
|
|
|
reply.push("Mention the group user in a top-level post to share it with the group's members.".to_string()); |
|
|
|
replies.push("Mention the group user in a top-level post to share it with the group's members.".to_string()); |
|
|
|
reply.push("Posts with commands won't be shared. Supported commands:".to_string()); |
|
|
|
replies.push("Posts with commands won't be shared. Supported commands:".to_string()); |
|
|
|
reply.push("/ignore, /ign, /i - don't run any commands in the post".to_string()); |
|
|
|
replies.push("/ignore, /ign, /i - don't run any commands in the post".to_string()); |
|
|
|
reply.push("/boost, /b - boost the replied-to post into the group".to_string()); |
|
|
|
replies.push("/boost, /b - boost the replied-to post into the group".to_string()); |
|
|
|
reply.push("/leave - leave the group as a member".to_string()); |
|
|
|
replies.push("/leave - leave the group as a member".to_string()); |
|
|
|
|
|
|
|
|
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
reply.push("/members".to_string()); |
|
|
|
replies.push("/members".to_string()); |
|
|
|
reply.push("/kick, /remove user - kick a member".to_string()); |
|
|
|
replies.push("/kick, /remove user - kick a member".to_string()); |
|
|
|
reply.push("/add user - add a member".to_string()); |
|
|
|
replies.push("/add user - add a member".to_string()); |
|
|
|
reply.push("/ban x - ban a user or a server".to_string()); |
|
|
|
replies.push("/ban x - ban a user or a server".to_string()); |
|
|
|
reply.push("/unban x - lift a ban".to_string()); |
|
|
|
replies.push("/unban x - lift a ban".to_string()); |
|
|
|
reply.push("/op, /admin user - give admin rights".to_string()); |
|
|
|
replies.push("/op, /admin user - give admin rights".to_string()); |
|
|
|
reply.push("/unop, /unadmin user - remove admin rights".to_string()); |
|
|
|
replies.push("/unop, /unadmin user - remove admin rights".to_string()); |
|
|
|
reply.push("/opengroup, /closegroup - control posting access".to_string()); |
|
|
|
replies.push("/opengroup, /closegroup - control posting access".to_string()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::ListMembers => { |
|
|
|
StatusCommand::ListMembers => { |
|
|
|
if is_admin { |
|
|
|
if is_admin { |
|
|
|
reply.push("Member list:".to_string()); |
|
|
|
if self.config.is_member_only() { |
|
|
|
|
|
|
|
replies.push("Group members:".to_string()); |
|
|
|
let admins = self.config.get_admins().collect::<HashSet<_>>(); |
|
|
|
let admins = self.config.get_admins().collect::<HashSet<_>>(); |
|
|
|
let members = self.config.get_members().collect::<Vec<_>>(); |
|
|
|
let mut members = self.config.get_members().collect::<Vec<_>>(); |
|
|
|
|
|
|
|
members.extend(admins.iter()); |
|
|
|
|
|
|
|
members.sort(); |
|
|
|
|
|
|
|
members.dedup(); |
|
|
|
for m in members { |
|
|
|
for m in members { |
|
|
|
if admins.contains(&m) { |
|
|
|
if admins.contains(&m) { |
|
|
|
reply.push(format!("{} [admin]", m)); |
|
|
|
replies.push(format!("{} [admin]", m)); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
replies.push(format!("{}", m)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
reply.push(format!("{}", m)); |
|
|
|
replies.push("Group admins:".to_string()); |
|
|
|
|
|
|
|
let mut admins = self.config.get_admins().collect::<Vec<_>>(); |
|
|
|
|
|
|
|
admins.sort(); |
|
|
|
|
|
|
|
for a in admins { |
|
|
|
|
|
|
|
replies.push(format!("{}", a)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
StatusCommand::Leave => { |
|
|
|
StatusCommand::Leave => { |
|
|
|
if self.config.is_member(&n.account.acct) { |
|
|
|
if self.config.is_member(¬if_acct) { |
|
|
|
any_admin_cmd = true; |
|
|
|
any_admin_cmd = true; |
|
|
|
let _ = self.config.set_member(&n.account.acct, false); |
|
|
|
let _ = self.config.set_member(¬if_acct, false); |
|
|
|
reply.push("You left the group.".to_string()); |
|
|
|
replies.push("You left the group.".to_string()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(DELAY_BEFORE_ACTION).await; |
|
|
|
tokio::time::sleep(DELAY_BEFORE_ACTION).await; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if boost_prev { |
|
|
|
if do_boost_prev_post { |
|
|
|
self.client.reblog(&status.in_reply_to_id.as_ref().unwrap()).await |
|
|
|
self.client.reblog(&status.in_reply_to_id.as_ref().unwrap()).await |
|
|
|
.log_error("Failed to boost"); |
|
|
|
.log_error("Failed to boost"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if !reply.is_empty() { |
|
|
|
if !replies.is_empty() { |
|
|
|
let r = reply.join("\n"); |
|
|
|
let r = replies.join("\n"); |
|
|
|
|
|
|
|
|
|
|
|
let post = StatusBuilder::new() |
|
|
|
let post = StatusBuilder::new() |
|
|
|
.status(format!("@{user}\n{msg}", user=n.account.acct, msg=r)) |
|
|
|
.status(format!("@{user}\n{msg}", user = notif_acct, msg = r)) |
|
|
|
.content_type("text/markdown") |
|
|
|
.content_type("text/markdown") |
|
|
|
.visibility(Visibility::Direct) |
|
|
|
.visibility(Visibility::Direct) |
|
|
|
.build().expect("error build status"); |
|
|
|
.build().expect("error build status"); |
|
|
@ -416,12 +455,22 @@ impl GroupHandle { |
|
|
|
let _ = self.client.new_status(post).await.log_error("Failed to post"); |
|
|
|
let _ = self.client.new_status(post).await.log_error("Failed to post"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !announcements.is_empty() { |
|
|
|
|
|
|
|
let msg = announcements.join("\n"); |
|
|
|
|
|
|
|
let post = StatusBuilder::new() |
|
|
|
|
|
|
|
.status(format!("**📢 Group announcement**\n{msg}", msg = msg)) |
|
|
|
|
|
|
|
.content_type("text/markdown") |
|
|
|
|
|
|
|
.visibility(Visibility::Unlisted) |
|
|
|
|
|
|
|
.build().expect("error build status"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let _ = self.client.new_status(post).await.log_error("Failed to post"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if any_admin_cmd { |
|
|
|
if any_admin_cmd { |
|
|
|
self.save_if_needed().await.log_error("Failed to save"); |
|
|
|
self.save_if_needed().await.log_error("Failed to save"); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
NotificationType::Follow => { |
|
|
|
NotificationType::Follow => { |
|
|
|
info!("New follower!"); |
|
|
|
info!("New follower!"); |
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await; |
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await; |
|
|
@ -434,11 +483,11 @@ impl GroupHandle { |
|
|
|
format!( |
|
|
|
format!( |
|
|
|
"@{user} welcome to the group! This is a member-only group, you won't be \ |
|
|
|
"@{user} welcome to the group! This is a member-only group, you won't be \ |
|
|
|
able to post. Ask the group admins if you wish to join!\n\n\ |
|
|
|
able to post. Ask the group admins if you wish to join!\n\n\ |
|
|
|
Admins: {admins}", user = &n.account.acct, admins = admins.join(", ")) |
|
|
|
Admins: {admins}", user = notif_acct, admins = admins.join(", ")) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
format!( |
|
|
|
format!( |
|
|
|
"@{user} welcome to the group! This is a public-access group. \ |
|
|
|
"@{user} welcome to the group! This is a public-access group. \ |
|
|
|
To share a post, tag the group user. Use /help for more info.", user = &n.account.acct) |
|
|
|
To share a post, tag the group user. Use /help for more info.", user = notif_acct) |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
let post = StatusBuilder::new() |
|
|
|
let post = StatusBuilder::new() |
|
|
@ -451,11 +500,12 @@ impl GroupHandle { |
|
|
|
} |
|
|
|
} |
|
|
|
_ => {} |
|
|
|
_ => {} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Catch up with missed notifications, returns true if any were handled
|
|
|
|
/// Catch up with missed notifications, returns true if any were handled
|
|
|
|
async fn catch_up_with_missed_notifications(&mut self) -> Result<bool, GroupError> { |
|
|
|
async fn catch_up_with_missed_notifications(&mut self) -> Result<bool, GroupError> { |
|
|
|
const MAX_CATCHUP_NOTIFS: usize = 25; |
|
|
|
|
|
|
|
let last_notif = self.config.get_last_notif(); |
|
|
|
let last_notif = self.config.get_last_notif(); |
|
|
|
|
|
|
|
|
|
|
|
let notifications = self.client.notifications().await?; |
|
|
|
let notifications = self.client.notifications().await?; |
|
|
@ -489,7 +539,8 @@ impl GroupHandle { |
|
|
|
|
|
|
|
|
|
|
|
for n in notifs_to_handle { |
|
|
|
for n in notifs_to_handle { |
|
|
|
debug!("Handling missed notification: {}", NotificationDisplay(&n)); |
|
|
|
debug!("Handling missed notification: {}", NotificationDisplay(&n)); |
|
|
|
self.handle_notification(n).await; |
|
|
|
self.handle_notification(n).await |
|
|
|
|
|
|
|
.log_error("Error handling a notification"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return Ok(true); |
|
|
|
return Ok(true); |
|
|
|