mirror of
https://github.com/revanced/revanced-discord-bot.git
synced 2025-05-01 06:54:27 +02:00
fix(moderation): UserId instead of Member parameter
Context: https://github.com/revanced/revanced-discord-bot/issues/38
This commit is contained in:
parent
5e7939a512
commit
5fcad81483
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
7
src/utils/macros.rs
Normal 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;
|
@ -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;
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user