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 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<i64>,
#[description = "Minutes"] minutes: Option<i64>,
#[description = "Hours"] hours: Option<i64>,
@ -204,6 +208,8 @@ pub async fn mute(
#[description = "Months"] months: Option<i64>,
#[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::<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;
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",
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
}
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>(
"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));
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<User>,
#[description = "Member"] user: Option<UserId>,
#[description = "Until message"] until: Option<String>,
#[min = 1]
#[max = 1000]
#[description = "Count"]
count: Option<i64>,
) -> 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<u8>,
#[description = "Reason for the ban"] reason: Option<String>,
) -> 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
}

View File

@ -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
),

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 decancer;
pub mod embed;
pub mod macros;
pub mod media_channel;
pub mod moderation;
pub mod poll;

View File

@ -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<Http>,
cache: &Arc<Cache>,
database: &Arc<Database>,
member: &Member,
guild_id: GuildId,
user_id: UserId,
mute_role_id: u64,
mute_duration: u64,
) -> JoinHandle<Option<Error>> {
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",
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::<u64>().unwrap()))
.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 {
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<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))
}