From abdc0922eb0f4d647c151bddf51552c39b9524b3 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 30 Aug 2022 02:27:15 +0200 Subject: [PATCH] feat: lock & unlock commands --- src/commands/moderation.rs | 193 +++++++++++++++++++++++++++++++------ src/db/model.rs | 23 ++++- src/main.rs | 2 + src/utils/moderation.rs | 110 ++++++++++++++------- 4 files changed, 260 insertions(+), 68 deletions(-) diff --git a/src/commands/moderation.rs b/src/commands/moderation.rs index d137810..194e912 100644 --- a/src/commands/moderation.rs +++ b/src/commands/moderation.rs @@ -1,10 +1,17 @@ use bson::{doc, Document}; use chrono::{Duration, Utc}; use mongodb::options::{UpdateModifications, UpdateOptions}; -use poise::serenity_prelude::{self as serenity, Member, RoleId, User}; +use poise::serenity_prelude::{ + self as serenity, + Member, + PermissionOverwrite, + Permissions, + RoleId, + User, +}; use tracing::{debug, trace}; -use crate::db::model::Muted; +use crate::db::model::{LockedChannel, Muted}; use crate::utils::moderation::{ ban_moderation, queue_unmute_member, @@ -14,6 +21,135 @@ use crate::utils::moderation::{ }; use crate::{Context, Error}; +/// Lock a channel. +#[poise::command(slash_command)] +pub async fn lock(ctx: Context<'_>) -> Result<(), Error> { + let data = &ctx.data().read().await; + let configuration = &data.configuration; + let database = &data.database; + let discord = &ctx.discord(); + let cache = &discord.cache; + let http = &discord.http; + + let channel_id = ctx.channel_id().0; + let channel = &cache.guild_channel(channel_id).unwrap(); + + let query: Document = LockedChannel { + channel_id: Some(channel_id.to_string()), + ..Default::default() + } + .into(); + + // Check if channel is already muted, if so succeed. + if let Ok(mut cursor) = database + .find::("locked", query.clone(), None) + .await + { + if cursor.advance().await.unwrap() { + respond_moderation( + &ctx, + &ModerationKind::Lock( + channel.name.clone(), + Some(Error::from("Channel already locked")), + ), + configuration, + ) + .await?; + return Ok(()); + } + } + + // accumulate all roles with write permissions + let permission_overwrites: Vec<_> = channel + .permission_overwrites + .iter() + .filter_map(|r| { + if r.allow.send_messages() || !r.deny.send_messages() { + Some(r.clone()) + } else { + None + } + }) + .collect(); + + // lock the channel by and creating the new permission overwrite + for permission_overwrite in &permission_overwrites { + let permission = Permissions::SEND_MESSAGES & Permissions::ADD_REACTIONS; + channel + .create_permission(http, &PermissionOverwrite { + allow: permission_overwrite.allow & !permission, + deny: permission_overwrite.deny | permission, + kind: permission_overwrite.kind, + }) + .await?; + } + + // save the original overwrites + let updated: Document = LockedChannel { + overwrites: Some(permission_overwrites), + ..Default::default() + } + .into(); + + database + .update::( + "locked", + query, + UpdateModifications::Document(doc! { "$set": updated}), + Some(UpdateOptions::builder().upsert(true).build()), + ) + .await?; + + respond_moderation( + &ctx, + &ModerationKind::Lock(channel.name.clone(), None), + configuration, + ) + .await +} + +/// Unlock a channel. +#[poise::command(slash_command)] +pub async fn unlock(ctx: Context<'_>) -> Result<(), Error> { + let data = &ctx.data().read().await; + let configuration = &data.configuration; + let database = &data.database; + let discord = &ctx.discord(); + let cache = &discord.cache; + let http = &discord.http; + + let channel_id = ctx.channel_id().0; + + let delete_result = database + .find_and_delete::( + "locked", + LockedChannel { + channel_id: Some(channel_id.to_string()), + ..Default::default() + } + .into(), + None, + ) + .await; + + let channel = cache.guild_channel(channel_id).unwrap(); + let mut error = None; + if let Ok(Some(locked_channel)) = delete_result { + for overwrite in &locked_channel.overwrites.unwrap() { + channel.create_permission(http, overwrite).await?; + } + } else { + error = Some(Error::from("Channel already unlocked")) + } + + respond_moderation( + &ctx, + &ModerationKind::Unlock(channel.name.clone(), error), // TODO: handle error + configuration, + ) + .await +} + /// Unmute a member. #[poise::command(slash_command)] pub async fn unmute( @@ -30,21 +166,20 @@ pub async fn unmute( pending_unmute.abort(); } + let queue = queue_unmute_member( + &ctx.discord().http, + &data.database, + &member, + configuration.general.mute.role, + 0, + ) + .await + .unwrap(); + respond_moderation( &ctx, - &ModerationKind::Unmute( - queue_unmute_member( - &ctx.discord().http, - &data.database, - &member, - configuration.general.mute.role, - 0, - ) - .await - .unwrap(), - ), - &member.user, - &configuration, + &ModerationKind::Unmute(member.user, queue), + configuration, ) .await } @@ -175,9 +310,13 @@ pub async fn mute( respond_moderation( &ctx, - &ModerationKind::Mute(reason, format!("", unmute_time.timestamp()), result), - &member.user, - &configuration, + &ModerationKind::Mute( + member.user, + reason, + format!("", unmute_time.timestamp()), + result, + ), + configuration, ) .await } @@ -207,9 +346,7 @@ pub async fn purge( let too_old_timestamp = Utc::now().timestamp() - MAX_BULK_DELETE_AGO_SECS; let current_user = ctx.discord().http.get_current_user().await?; - let image = current_user - .avatar_url() - .unwrap_or_else(|| current_user.default_avatar_url()); + let image = current_user.face(); let handle = ctx .send(|f| { @@ -314,22 +451,16 @@ pub async fn unban(ctx: Context<'_>, #[description = "User"] user: User) -> Resu async fn handle_ban(ctx: &Context<'_>, kind: &BanKind) -> Result<(), Error> { let data = ctx.data().read().await; - let ban_result = ban_moderation(&ctx, &kind).await; + let ban_result = ban_moderation(ctx, kind).await; - let moderated_user; respond_moderation( - &ctx, + ctx, &match kind { BanKind::Ban(user, _, reason) => { - moderated_user = user; - ModerationKind::Ban(reason.clone(), ban_result) - }, - BanKind::Unban(user) => { - moderated_user = user; - ModerationKind::Unban(ban_result) + ModerationKind::Ban(user.clone(), reason.clone(), ban_result) }, + BanKind::Unban(user) => ModerationKind::Unban(user.clone(), ban_result), }, - &moderated_user, &data.configuration, ) .await diff --git a/src/db/model.rs b/src/db/model.rs index c4c12b3..4e5e4d3 100644 --- a/src/db/model.rs +++ b/src/db/model.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use bson::Document; +use poise::serenity_prelude::{PermissionOverwrite}; use serde::{Deserialize, Serialize}; use serde_with_macros::skip_serializing_none; @@ -15,12 +16,32 @@ pub struct Muted { pub reason: Option, } +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct LockedChannel { + pub channel_id: Option, + pub overwrites: Option>, +} + impl From for Document { fn from(muted: Muted) -> Self { - bson::to_document(&muted).unwrap() + to_document(&muted) } } +impl From for Document { + fn from(locked: LockedChannel) -> Self { + to_document(&locked) + } +} + +fn to_document(t: &T) -> Document +where + T: Serialize, +{ + bson::to_document(t).unwrap() +} + // Display trait impl Display for Muted { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/main.rs b/src/main.rs index b53ac1e..54c52b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,8 @@ async fn main() { moderation::purge(), moderation::ban(), moderation::unban(), + moderation::lock(), + moderation::unlock(), misc::reply(), ]; poise::set_qualified_names(&mut commands); diff --git a/src/utils/moderation.rs b/src/utils/moderation.rs index 61a507e..4819f4c 100644 --- a/src/utils/moderation.rs +++ b/src/utils/moderation.rs @@ -15,10 +15,12 @@ use crate::serenity::SerenityError; use crate::{Context, Error}; pub enum ModerationKind { - Mute(String, String, Option), // Reason, Expires, Error - Unmute(Option), // Error - Ban(Option, Option), // Reason, Error - Unban(Option), // Error + Mute(User, String, String, Option), // User, Reason, Expires, Error + Unmute(User, Option), // User, Error + Ban(User, Option, Option), // User, Reason, Error + Unban(User, Option), // User, Error + Lock(String, Option), // Channel name, Error + Unlock(String, Option), // Channel name, Error } pub enum BanKind { Ban(User, Option, Option), // User, Amount of days to delete messages, Reason @@ -117,38 +119,48 @@ pub fn queue_unmute_member( pub async fn respond_moderation<'a>( ctx: &Context<'_>, moderation: &ModerationKind, - user: &serenity::User, configuration: &Configuration, ) -> Result<(), Error> { + let current_user = ctx.discord().http.get_current_user().await?; + let create_embed = |f: &mut serenity::CreateEmbed| { - let tag = user.tag(); - match moderation { - ModerationKind::Mute(reason, expires, error) => match error { - Some(err) => f.title(format!("Failed to mute {}", tag)).field( - "Exception", - err.to_string(), - false, - ), - None => f.title(format!("Muted {}", tag)), - } - .field("Reason", reason, false) - .field("Expires", expires, false), - ModerationKind::Unmute(error) => match error { - Some(err) => f.title(format!("Failed to unmute {}", tag)).field( - "Exception", - err.to_string(), - false, - ), - None => f.title(format!("Unmuted {}", tag)), + let mut moderated_user: Option<&User> = None; + + let result = match moderation { + ModerationKind::Mute(user, reason, expires, error) => { + moderated_user = Some(user); + + match error { + Some(err) => f.title(format!("Failed to mute {}", user.tag())).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Muted {}", user.tag())), + } + .field("Reason", reason, false) + .field("Expires", expires, false) + }, + ModerationKind::Unmute(user, error) => { + moderated_user = Some(user); + match error { + Some(err) => f.title(format!("Failed to unmute {}", user.tag())).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Unmuted {}", user.tag())), + } }, - ModerationKind::Ban(reason, error) => { + ModerationKind::Ban(user, reason, error) => { + moderated_user = Some(user); let f = match error { - Some(err) => f.title(format!("Failed to ban {}", tag)).field( + Some(err) => f.title(format!("Failed to ban {}", user.tag())).field( "Exception", err.to_string(), false, ), - None => f.title(format!("Banned {}", tag)), + None => f.title(format!("Banned {}", user.tag())), }; if let Some(reason) = reason { f.field("Reason", reason, false) @@ -156,21 +168,47 @@ pub async fn respond_moderation<'a>( f } }, - ModerationKind::Unban(error) => match error { - Some(err) => f.title(format!("Failed to unban {}", tag)).field( + ModerationKind::Unban(user, error) => { + moderated_user = Some(user); + match error { + Some(err) => f.title(format!("Failed to unban {}", user.tag())).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Unbanned {}", user.tag())), + } + }, + ModerationKind::Lock(channel, error) => match error { + Some(err) => f.title(format!("Failed to lock {} ", channel)).field( "Exception", err.to_string(), false, ), - None => f.title(format!("Unbanned {}", tag)), + None => f.title(format!("Locked {}", channel)).description( + "Unlocking the channel will restore the original permission overwrites.", + ), + }, + ModerationKind::Unlock(channel, error) => match error { + Some(err) => f.title(format!("Failed to unlock {}", channel)).field( + "Exception", + err.to_string(), + false, + ), + None => f + .title(format!("Unlocked {}", channel)) + .description("Restored original permission overwrites."), }, } - .color(configuration.general.embed_color) - .thumbnail( - &user - .avatar_url() - .unwrap_or_else(|| user.default_avatar_url()), - ); + .color(configuration.general.embed_color); + + let user = if let Some(user) = moderated_user { + user.face() + } else { + current_user.face() + }; + + result.thumbnail(&user); }; let reply = ctx