From 5fcad81483286a9ed664a82705e50743ddeea19d Mon Sep 17 00:00:00 2001 From: Ax333l Date: Tue, 6 Dec 2022 19:04:17 +0100 Subject: [PATCH] fix(moderation): UserId instead of Member parameter Context: https://github.com/revanced/revanced-discord-bot/issues/38 --- src/commands/moderation.rs | 183 +++++++++++++++++-------------------- src/events/ready.rs | 4 +- src/utils/macros.rs | 7 ++ src/utils/mod.rs | 1 + src/utils/moderation.rs | 53 +++++++++-- 5 files changed, 140 insertions(+), 108 deletions(-) create mode 100644 src/utils/macros.rs diff --git a/src/commands/moderation.rs b/src/commands/moderation.rs index 098e808..a3c8782 100644 --- a/src/commands/moderation.rs +++ b/src/commands/moderation.rs @@ -2,12 +2,12 @@ use bson::{doc, Document}; use chrono::{Duration, Utc}; use mongodb::options::{UpdateModifications, UpdateOptions}; use poise::serenity_prelude::{ - self as serenity, Member, Mentionable, PermissionOverwrite, Permissions, RoleId, User, + self as serenity, Mentionable, PermissionOverwrite, Permissions, UserId, }; -use tracing::log::error; -use tracing::{debug, trace, warn}; +use tracing::{debug, error, trace}; use crate::db::model::{LockedChannel, Muted}; +use crate::utils::macros::to_user; use crate::utils::moderation::{ ban_moderation, queue_unmute_member, respond_moderation, BanKind, ModerationKind, }; @@ -159,15 +159,17 @@ pub async fn unlock(ctx: Context<'_>) -> Result<(), Error> { #[poise::command(slash_command)] pub async fn unmute( ctx: Context<'_>, - #[description = "The member to unmute"] member: Member, + #[description = "The member to unmute"] member: UserId, ) -> Result<(), Error> { + let user = to_user!(member, ctx); + let id = user.id; ctx.defer().await.expect("Failed to defer"); let data = &ctx.data().read().await; let configuration = &data.configuration; - if let Some(pending_unmute) = data.pending_unmutes.get(&member.user.id.0) { - trace!("Cancelling pending unmute for {}", member.user.id.0); + if let Some(pending_unmute) = data.pending_unmutes.get(&id.0) { + trace!("Cancelling pending unmute for {}", id.0); pending_unmute.abort(); } @@ -175,8 +177,10 @@ pub async fn unmute( let queue = queue_unmute_member( &ctx.discord().http, + &ctx.discord().cache, &data.database, - &member, + ctx.guild_id().unwrap(), + id, configuration.general.mute.role, 0, ) @@ -185,7 +189,7 @@ pub async fn unmute( respond_moderation( &ctx, - &ModerationKind::Unmute(member.user, author.clone(), queue), + &ModerationKind::Unmute(user, author.clone(), queue), configuration, ) .await @@ -196,7 +200,7 @@ pub async fn unmute( #[poise::command(slash_command)] pub async fn mute( ctx: Context<'_>, - #[description = "The member to mute"] mut member: Member, + #[description = "The member to mute"] member: UserId, #[description = "Seconds"] seconds: Option, #[description = "Minutes"] minutes: Option, #[description = "Hours"] hours: Option, @@ -204,6 +208,8 @@ pub async fn mute( #[description = "Months"] months: Option, #[description = "The reason of the mute"] reason: String, ) -> Result<(), Error> { + let user = to_user!(member, ctx); + let id = user.id; let now = Utc::now(); let mut mute_duration = Duration::zero(); @@ -232,26 +238,26 @@ pub async fn mute( let data = &mut *ctx.data().write().await; let configuration = &data.configuration; - let mute = &configuration.general.mute; - let mute_role_id = mute.role; - let take = &mute.take; - let is_currently_muted = member.roles.iter().any(|r| r.0 == mute_role_id); - let author = ctx.author(); - if let Some(pending_unmute) = data.pending_unmutes.get(&member.user.id.0) { - trace!("Cancelling pending unmute for {}", member.user.id.0); + let mute = &configuration.general.mute; + let guild_id = ctx.guild_id().unwrap(); + + if let Some(pending_unmute) = data.pending_unmutes.get(&id.0) { + trace!("Cancelling pending unmute for {}", id.0); pending_unmute.abort(); } let unmute_time = if !mute_duration.is_zero() { data.pending_unmutes.insert( - member.user.id.0, + id.0, queue_unmute_member( &ctx.discord().http, + &ctx.discord().cache, &data.database, - &member, - mute_role_id, + guild_id, + id, + mute.role, mute_duration.num_seconds() as u64, ), ); @@ -260,107 +266,81 @@ pub async fn mute( None }; - let result = - if let Err(add_role_result) = member.add_role(&ctx.discord().http, mute_role_id).await { - Some(Error::from(add_role_result)) - } else { - // accumulate all roles to take from the member - let removed_roles = member - .roles - .iter() - .filter(|r| take.contains(&r.0)) - .map(|r| r.to_string()) - .collect::>(); - // take them from the member, get remaining roles - let remaining_roles = member - .remove_roles( - &ctx.discord().http, - &take.iter().map(|&r| RoleId::from(r)).collect::>(), - ) - .await; + let mut updated = Muted { + guild_id: Some(guild_id.0.to_string()), + expires: unmute_time, + reason: Some(reason.clone()), + ..Default::default() + }; - if let Err(remove_role_result) = remaining_roles { - Some(Error::from(remove_role_result)) - } else { - // Roles which were removed from the user - let updated: Document = Muted { - guild_id: Some(member.guild_id.0.to_string()), - expires: unmute_time, - reason: Some(reason.clone()), - taken_roles: if is_currently_muted { - // Prevent the bot from overriding the "take" field. - // This would happen otherwise, because the bot would accumulate the users roles and then override the value in the database - // resulting in the user being muted to have no roles to add back later. - None - } else { - Some(removed_roles) - }, - ..Default::default() - } - .into(); - - if let Err(database_update_result) = data - .database - .update::( - "muted", - Muted { - user_id: Some(member.user.id.0.to_string()), - ..Default::default() - } - .into(), - UpdateModifications::Document(doc! { "$set": updated}), - Some(UpdateOptions::builder().upsert(true).build()), - ) - .await - { - Some(database_update_result) - } else if unmute_time.is_none() { - data.database - .update::( - "muted", - Muted { - user_id: Some(member.user.id.0.to_string()), - ..Default::default() - } - .into(), - UpdateModifications::Document(doc! { "$unset": { "expires": "" } }), - None, - ) - .await - .err() - } else { - None - } + let result = async { + if let Some(mut member) = ctx.discord().cache.member(guild_id, id) { + let (is_currently_muted, removed_roles) = + crate::utils::moderation::mute_moderation(&ctx, &mut member, mute).await?; + // Prevent the bot from overriding the "take" field. + // This would happen otherwise, because the bot would accumulate the users roles and then override the value in the database + // resulting in the user being muted to have no roles to add back later. + if !is_currently_muted { + updated.taken_roles = Some(removed_roles.iter().map(ToString::to_string).collect()); } - }; - - if result.is_none() { - if let Err(e) = member.disconnect_from_voice(&ctx.discord().http).await { - warn!("Could not disconnect member from voice channel: {}", e); } + + let query: Document = Muted { + user_id: Some(id.to_string()), + ..Default::default() + } + .into(); + + let updated: Document = updated.into(); + data.database + .update::( + "muted", + query.clone(), + UpdateModifications::Document(doc! { "$set": updated }), + Some(UpdateOptions::builder().upsert(true).build()), + ) + .await?; + + if unmute_time.is_none() { + data.database + .update::( + "muted", + query, + UpdateModifications::Document(doc! { "$unset": { "expires": "" } }), + None, + ) + .await?; + } + Ok(()) } + .await; let duration = unmute_time.map(|time| format!("", time)); respond_moderation( &ctx, - &ModerationKind::Mute(member.user, author.clone(), reason, duration, result), + &ModerationKind::Mute(user, author.clone(), reason, duration, result.err()), configuration, ) .await } -/// Delete recent messages of a user. Cannot delete messages older than 14 days. +/// Delete recent messages of a member. Cannot delete messages older than 14 days. #[poise::command(slash_command)] pub async fn purge( ctx: Context<'_>, - #[description = "User"] user: Option, + #[description = "Member"] user: Option, #[description = "Until message"] until: Option, #[min = 1] #[max = 1000] #[description = "Count"] count: Option, ) -> Result<(), Error> { + let user = if let Some(id) = user { + Some(to_user!(id, ctx)) + } else { + None + }; // The maximum amount of times to page through messages. If paged over MAX_PAGES amount of times without deleting messages, break. const MAX_PAGES: i8 = 2; // The maximal amount of messages that we can fetch at all @@ -467,20 +447,23 @@ pub async fn purge( Ok(()) } -/// Ban a user. +/// Ban a member. #[poise::command(slash_command)] pub async fn ban( ctx: Context<'_>, - #[description = "User"] user: User, + #[description = "Member"] user: UserId, #[description = "Amount of days to delete messages"] dmd: Option, #[description = "Reason for the ban"] reason: Option, ) -> Result<(), Error> { + // We cannot use `User` as a parameter for the moderation commands because of a bug in serenity. See: https://github.com/revanced/revanced-discord-bot/issues/38 + let user = to_user!(user, ctx); handle_ban(&ctx, &BanKind::Ban(user, dmd, reason)).await } /// Unban a user. #[poise::command(slash_command)] -pub async fn unban(ctx: Context<'_>, #[description = "User"] user: User) -> Result<(), Error> { +pub async fn unban(ctx: Context<'_>, #[description = "User"] user: UserId) -> Result<(), Error> { + let user = to_user!(user, ctx); handle_ban(&ctx, &BanKind::Unban(user)).await } diff --git a/src/events/ready.rs b/src/events/ready.rs index 0e7de57..85de711 100644 --- a/src/events/ready.rs +++ b/src/events/ready.rs @@ -40,8 +40,10 @@ pub async fn load_muted_members(ctx: &serenity::Context, _: &serenity::Ready) { member.user.id.0, queue_unmute_member( &ctx.http, + &ctx.cache, &data.database, - &member, + member.guild_id, + member.user.id, mute_role_id, amount_left as u64, // i64 as u64 is handled properly here ), diff --git a/src/utils/macros.rs b/src/utils/macros.rs new file mode 100644 index 0000000..e5fc801 --- /dev/null +++ b/src/utils/macros.rs @@ -0,0 +1,7 @@ +macro_rules! to_user { + ($user:ident, $ctx:ident) => {{ + $user.to_user(&$ctx.discord().http).await? + }}; +} + +pub(crate) use to_user; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ea197f7..7ed888c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ pub mod autorespond; pub mod bot; pub mod decancer; pub mod embed; +pub mod macros; pub mod media_channel; pub mod moderation; pub mod poll; diff --git a/src/utils/moderation.rs b/src/utils/moderation.rs index c61dc4f..a555d8a 100644 --- a/src/utils/moderation.rs +++ b/src/utils/moderation.rs @@ -2,15 +2,17 @@ use std::cmp; use std::sync::Arc; use mongodb::options::FindOptions; -use poise::serenity_prelude::{ChannelId, GuildChannel, Http, Mentionable, User}; +use poise::serenity_prelude::{ + Cache, ChannelId, GuildChannel, GuildId, Http, Mentionable, User, UserId, +}; use tokio::task::JoinHandle; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; use super::bot::get_data_lock; use super::*; use crate::db::database::Database; use crate::db::model::Muted; -use crate::model::application::Configuration; +use crate::model::application::{Configuration, Mute}; use crate::serenity::SerenityError; use crate::{Context, Error}; @@ -72,23 +74,24 @@ pub async fn mute_on_join(ctx: &serenity::Context, new_member: &mut serenity::Me pub fn queue_unmute_member( http: &Arc, + cache: &Arc, database: &Arc, - member: &Member, + guild_id: GuildId, + user_id: UserId, mute_role_id: u64, mute_duration: u64, ) -> JoinHandle> { + let cache = cache.clone(); let http = http.clone(); let database = database.clone(); - let mut member = member.clone(); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(mute_duration)).await; - let delete_result = database .find_and_delete::( "muted", Muted { - user_id: Some(member.user.id.0.to_string()), + user_id: Some(user_id.0.to_string()), ..Default::default() } .into(), @@ -106,6 +109,8 @@ pub fn queue_unmute_member( .map(|r| RoleId::from(r.parse::().unwrap())) .collect::>(); + // Update roles if they didn't leave the guild. + let mut member = cache.member(guild_id, user_id)?; if let Err(add_role_result) = member.add_roles(&http, &taken_roles).await { Some(Error::from(add_role_result)) } else if let Err(remove_result) = member.remove_role(http, mute_role_id).await { @@ -363,3 +368,37 @@ pub async fn ban_moderation(ctx: &Context<'_>, kind: &BanKind) -> Option, + member: &mut Member, + config: &Mute, +) -> Result<(bool, Vec), SerenityError> { + let mute_role_id = config.role; + let take = &config.take; + + let is_currently_muted = member.roles.iter().any(|r| r.0 == mute_role_id); + + member.add_role(&ctx.discord().http, mute_role_id).await?; + + // accumulate all roles to take from the member + let removed_roles = member + .roles + .iter() + .filter(|r| take.contains(&r.0)) + .copied() + .collect::>(); + // take them from the member. + member + .remove_roles( + &ctx.discord().http, + &take.iter().map(|&r| RoleId::from(r)).collect::>(), + ) + .await?; + + if let Err(e) = member.disconnect_from_voice(&ctx.discord().http).await { + warn!("Could not disconnect member from voice channel: {}", e); + } + + Ok((is_currently_muted, removed_roles)) +}