fix(moderation): UserId instead of Member parameter

Context: https://github.com/revanced/revanced-discord-bot/issues/38
This commit is contained in:
Ax333l 2022-12-06 19:04:17 +01:00
parent 5e7939a512
commit 5fcad81483
5 changed files with 140 additions and 108 deletions

View File

@ -2,12 +2,12 @@ use bson::{doc, Document};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use mongodb::options::{UpdateModifications, UpdateOptions}; use mongodb::options::{UpdateModifications, UpdateOptions};
use poise::serenity_prelude::{ 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, error, trace};
use tracing::{debug, trace, warn};
use crate::db::model::{LockedChannel, Muted}; use crate::db::model::{LockedChannel, Muted};
use crate::utils::macros::to_user;
use crate::utils::moderation::{ use crate::utils::moderation::{
ban_moderation, queue_unmute_member, respond_moderation, BanKind, ModerationKind, 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)] #[poise::command(slash_command)]
pub async fn unmute( pub async fn unmute(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "The member to unmute"] member: Member, #[description = "The member to unmute"] member: UserId,
) -> Result<(), Error> { ) -> Result<(), Error> {
let user = to_user!(member, ctx);
let id = user.id;
ctx.defer().await.expect("Failed to defer"); ctx.defer().await.expect("Failed to defer");
let data = &ctx.data().read().await; let data = &ctx.data().read().await;
let configuration = &data.configuration; let configuration = &data.configuration;
if let Some(pending_unmute) = data.pending_unmutes.get(&member.user.id.0) { if let Some(pending_unmute) = data.pending_unmutes.get(&id.0) {
trace!("Cancelling pending unmute for {}", member.user.id.0); trace!("Cancelling pending unmute for {}", id.0);
pending_unmute.abort(); pending_unmute.abort();
} }
@ -175,8 +177,10 @@ pub async fn unmute(
let queue = queue_unmute_member( let queue = queue_unmute_member(
&ctx.discord().http, &ctx.discord().http,
&ctx.discord().cache,
&data.database, &data.database,
&member, ctx.guild_id().unwrap(),
id,
configuration.general.mute.role, configuration.general.mute.role,
0, 0,
) )
@ -185,7 +189,7 @@ pub async fn unmute(
respond_moderation( respond_moderation(
&ctx, &ctx,
&ModerationKind::Unmute(member.user, author.clone(), queue), &ModerationKind::Unmute(user, author.clone(), queue),
configuration, configuration,
) )
.await .await
@ -196,7 +200,7 @@ pub async fn unmute(
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn mute( pub async fn mute(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "The member to mute"] mut member: Member, #[description = "The member to mute"] member: UserId,
#[description = "Seconds"] seconds: Option<i64>, #[description = "Seconds"] seconds: Option<i64>,
#[description = "Minutes"] minutes: Option<i64>, #[description = "Minutes"] minutes: Option<i64>,
#[description = "Hours"] hours: Option<i64>, #[description = "Hours"] hours: Option<i64>,
@ -204,6 +208,8 @@ pub async fn mute(
#[description = "Months"] months: Option<i64>, #[description = "Months"] months: Option<i64>,
#[description = "The reason of the mute"] reason: String, #[description = "The reason of the mute"] reason: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let user = to_user!(member, ctx);
let id = user.id;
let now = Utc::now(); let now = Utc::now();
let mut mute_duration = Duration::zero(); let mut mute_duration = Duration::zero();
@ -232,26 +238,26 @@ pub async fn mute(
let data = &mut *ctx.data().write().await; let data = &mut *ctx.data().write().await;
let configuration = &data.configuration; 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(); let author = ctx.author();
if let Some(pending_unmute) = data.pending_unmutes.get(&member.user.id.0) { let mute = &configuration.general.mute;
trace!("Cancelling pending unmute for {}", member.user.id.0); 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(); pending_unmute.abort();
} }
let unmute_time = if !mute_duration.is_zero() { let unmute_time = if !mute_duration.is_zero() {
data.pending_unmutes.insert( data.pending_unmutes.insert(
member.user.id.0, id.0,
queue_unmute_member( queue_unmute_member(
&ctx.discord().http, &ctx.discord().http,
&ctx.discord().cache,
&data.database, &data.database,
&member, guild_id,
mute_role_id, id,
mute.role,
mute_duration.num_seconds() as u64, mute_duration.num_seconds() as u64,
), ),
); );
@ -260,107 +266,81 @@ pub async fn mute(
None None
}; };
let result = let mut updated = Muted {
if let Err(add_role_result) = member.add_role(&ctx.discord().http, mute_role_id).await { guild_id: Some(guild_id.0.to_string()),
Some(Error::from(add_role_result)) expires: unmute_time,
} else { reason: Some(reason.clone()),
// accumulate all roles to take from the member ..Default::default()
let removed_roles = member };
.roles
.iter()
.filter(|r| take.contains(&r.0))
.map(|r| r.to_string())
.collect::<Vec<_>>();
// 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::<Vec<_>>(),
)
.await;
if let Err(remove_role_result) = remaining_roles { let result = async {
Some(Error::from(remove_role_result)) if let Some(mut member) = ctx.discord().cache.member(guild_id, id) {
} else { let (is_currently_muted, removed_roles) =
// Roles which were removed from the user crate::utils::moderation::mute_moderation(&ctx, &mut member, mute).await?;
let updated: Document = Muted { // Prevent the bot from overriding the "take" field.
guild_id: Some(member.guild_id.0.to_string()), // This would happen otherwise, because the bot would accumulate the users roles and then override the value in the database
expires: unmute_time, // resulting in the user being muted to have no roles to add back later.
reason: Some(reason.clone()), if !is_currently_muted {
taken_roles: if is_currently_muted { updated.taken_roles = Some(removed_roles.iter().map(ToString::to_string).collect());
// 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",
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",
Muted {
user_id: Some(member.user.id.0.to_string()),
..Default::default()
}
.into(),
UpdateModifications::Document(doc! { "$unset": { "expires": "" } }),
None,
)
.await
.err()
} else {
None
}
} }
};
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>(
"muted",
query.clone(),
UpdateModifications::Document(doc! { "$set": updated }),
Some(UpdateOptions::builder().upsert(true).build()),
)
.await?;
if unmute_time.is_none() {
data.database
.update::<Muted>(
"muted",
query,
UpdateModifications::Document(doc! { "$unset": { "expires": "" } }),
None,
)
.await?;
}
Ok(())
} }
.await;
let duration = unmute_time.map(|time| format!("<t:{}:F>", time)); let duration = unmute_time.map(|time| format!("<t:{}:F>", time));
respond_moderation( respond_moderation(
&ctx, &ctx,
&ModerationKind::Mute(member.user, author.clone(), reason, duration, result), &ModerationKind::Mute(user, author.clone(), reason, duration, result.err()),
configuration, configuration,
) )
.await .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)] #[poise::command(slash_command)]
pub async fn purge( pub async fn purge(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "User"] user: Option<User>, #[description = "Member"] user: Option<UserId>,
#[description = "Until message"] until: Option<String>, #[description = "Until message"] until: Option<String>,
#[min = 1] #[min = 1]
#[max = 1000] #[max = 1000]
#[description = "Count"] #[description = "Count"]
count: Option<i64>, count: Option<i64>,
) -> Result<(), Error> { ) -> 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. // 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; const MAX_PAGES: i8 = 2;
// The maximal amount of messages that we can fetch at all // The maximal amount of messages that we can fetch at all
@ -467,20 +447,23 @@ pub async fn purge(
Ok(()) Ok(())
} }
/// Ban a user. /// Ban a member.
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn ban( pub async fn ban(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "User"] user: User, #[description = "Member"] user: UserId,
#[description = "Amount of days to delete messages"] dmd: Option<u8>, #[description = "Amount of days to delete messages"] dmd: Option<u8>,
#[description = "Reason for the ban"] reason: Option<String>, #[description = "Reason for the ban"] reason: Option<String>,
) -> Result<(), Error> { ) -> 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 handle_ban(&ctx, &BanKind::Ban(user, dmd, reason)).await
} }
/// Unban a user. /// Unban a user.
#[poise::command(slash_command)] #[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 handle_ban(&ctx, &BanKind::Unban(user)).await
} }

View File

@ -40,8 +40,10 @@ pub async fn load_muted_members(ctx: &serenity::Context, _: &serenity::Ready) {
member.user.id.0, member.user.id.0,
queue_unmute_member( queue_unmute_member(
&ctx.http, &ctx.http,
&ctx.cache,
&data.database, &data.database,
&member, member.guild_id,
member.user.id,
mute_role_id, mute_role_id,
amount_left as u64, // i64 as u64 is handled properly here amount_left as u64, // i64 as u64 is handled properly here
), ),

7
src/utils/macros.rs Normal file
View File

@ -0,0 +1,7 @@
macro_rules! to_user {
($user:ident, $ctx:ident) => {{
$user.to_user(&$ctx.discord().http).await?
}};
}
pub(crate) use to_user;

View File

@ -4,6 +4,7 @@ pub mod autorespond;
pub mod bot; pub mod bot;
pub mod decancer; pub mod decancer;
pub mod embed; pub mod embed;
pub mod macros;
pub mod media_channel; pub mod media_channel;
pub mod moderation; pub mod moderation;
pub mod poll; pub mod poll;

View File

@ -2,15 +2,17 @@ use std::cmp;
use std::sync::Arc; use std::sync::Arc;
use mongodb::options::FindOptions; 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 tokio::task::JoinHandle;
use tracing::{debug, error}; use tracing::{debug, error, warn};
use super::bot::get_data_lock; use super::bot::get_data_lock;
use super::*; use super::*;
use crate::db::database::Database; use crate::db::database::Database;
use crate::db::model::Muted; use crate::db::model::Muted;
use crate::model::application::Configuration; use crate::model::application::{Configuration, Mute};
use crate::serenity::SerenityError; use crate::serenity::SerenityError;
use crate::{Context, Error}; 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( pub fn queue_unmute_member(
http: &Arc<Http>, http: &Arc<Http>,
cache: &Arc<Cache>,
database: &Arc<Database>, database: &Arc<Database>,
member: &Member, guild_id: GuildId,
user_id: UserId,
mute_role_id: u64, mute_role_id: u64,
mute_duration: u64, mute_duration: u64,
) -> JoinHandle<Option<Error>> { ) -> JoinHandle<Option<Error>> {
let cache = cache.clone();
let http = http.clone(); let http = http.clone();
let database = database.clone(); let database = database.clone();
let mut member = member.clone();
tokio::spawn(async move { tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(mute_duration)).await; tokio::time::sleep(std::time::Duration::from_secs(mute_duration)).await;
let delete_result = database let delete_result = database
.find_and_delete::<Muted>( .find_and_delete::<Muted>(
"muted", "muted",
Muted { Muted {
user_id: Some(member.user.id.0.to_string()), user_id: Some(user_id.0.to_string()),
..Default::default() ..Default::default()
} }
.into(), .into(),
@ -106,6 +109,8 @@ pub fn queue_unmute_member(
.map(|r| RoleId::from(r.parse::<u64>().unwrap())) .map(|r| RoleId::from(r.parse::<u64>().unwrap()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// 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 { if let Err(add_role_result) = member.add_roles(&http, &taken_roles).await {
Some(Error::from(add_role_result)) Some(Error::from(add_role_result))
} else if let Err(remove_result) = member.remove_role(http, mute_role_id).await { } 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<Serenit
}, },
} }
} }
pub async fn mute_moderation(
ctx: &Context<'_>,
member: &mut Member,
config: &Mute,
) -> Result<(bool, Vec<serenity::RoleId>), 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::<Vec<_>>();
// take them from the member.
member
.remove_roles(
&ctx.discord().http,
&take.iter().map(|&r| RoleId::from(r)).collect::<Vec<_>>(),
)
.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))
}