diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 99e4919..45c23d6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,7 +4,6 @@ use color_eyre::eyre::Report; use poise::Command; mod general; -pub mod moderation; pub fn to_global_commands() -> Vec> { vec![ @@ -15,8 +14,5 @@ pub fn to_global_commands() -> Vec> { general::say(), general::stars(), general::tag(), - moderation::ban_user(), - moderation::mass_ban(), - moderation::kick_user(), ] } diff --git a/src/commands/moderation/actions.rs b/src/commands/moderation/actions.rs deleted file mode 100644 index 0513238..0000000 --- a/src/commands/moderation/actions.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::{consts::COLORS, Context}; - -use async_trait::async_trait; -use color_eyre::eyre::{eyre, Context as _, Result}; -use log::*; -use poise::serenity_prelude::{CacheHttp, GuildId, Http, Timestamp, User}; - -type Fields<'a> = Vec<(&'a str, String, bool)>; - -#[async_trait] -pub trait ModActionInfo { - fn to_fields(&self) -> Option; - fn description(&self) -> String; - async fn run_action( - &self, - http: impl CacheHttp + AsRef, - user: &User, - guild_id: &GuildId, - reason: String, - ) -> Result<()>; -} - -pub struct ModAction -where - T: ModActionInfo, -{ - pub reason: Option, - pub data: T, -} - -impl ModAction { - fn get_all_fields(&self) -> Fields { - let mut fields = vec![]; - - if let Some(reason) = self.reason.clone() { - fields.push(("Reason:", reason, false)); - } - - if let Some(mut action_fields) = self.data.to_fields() { - fields.append(&mut action_fields); - } - - fields - } - - /// internal mod logging - pub async fn log_action(&self, ctx: &Context<'_>) -> Result<()> { - let channel_id = ctx - .data() - .config - .discord - .channels - .say_log_channel_id - .ok_or_else(|| eyre!("Couldn't find say_log_channel_id! Unable to log mod action"))?; - - let channel = ctx - .http() - .get_channel(channel_id.into()) - .await - .wrap_err_with(|| "Couldn't resolve say_log_channel_id as a Channel! Are you sure you sure you used the right one?")?; - - let channel = channel - .guild() - .ok_or_else(|| eyre!("Couldn't resolve say_log_channel_id as a GuildChannel! Are you sure you used the right one?"))?; - - let fields = self.get_all_fields(); - let title = format!("{} user!", self.data.description()); - - channel - .send_message(ctx, |m| { - m.embed(|e| e.title(title).fields(fields).color(COLORS["red"])) - }) - .await?; - - Ok(()) - } - - /// public facing message - pub async fn reply(&self, ctx: &Context<'_>, user: &User, dm_user: Option) -> Result<()> { - let mut resp = format!("{} {}!", self.data.description(), user.name); - - if dm_user.unwrap_or_default() { - resp = format!("{resp} (user notified with direct message)"); - } - - ctx.reply(resp).await?; - - Ok(()) - } - - pub async fn dm_user(&self, ctx: &Context<'_>, user: &User, guild_id: &GuildId) -> Result<()> { - let guild = ctx.http().get_guild(*guild_id.as_u64()).await?; - let title = format!("{} from {}!", self.data.description(), guild.name); - - user.dm(ctx, |m| { - m.embed(|e| { - e.title(title).color(COLORS["red"]); - - if let Some(reason) = &self.reason { - e.description(format!("Reason: {}", reason)); - } - - e - }) - }) - .await?; - - Ok(()) - } - - pub async fn handle( - &self, - ctx: &Context<'_>, - user: &User, - quiet: Option, - dm_user: Option, - handle_reply: bool, - ) -> Result<()> { - if quiet.unwrap_or_default() { - ctx.defer_ephemeral().await?; - } else { - ctx.defer().await?; - } - - let guild_id = ctx - .guild_id() - .ok_or_else(|| eyre!("Couldn't get GuildId for context!"))?; - let actual_reason = self.reason.clone().unwrap_or("".to_string()); - - if dm_user.unwrap_or_default() { - self.dm_user(ctx, user, &guild_id).await?; - } - - self.data - .run_action(ctx, user, &guild_id, actual_reason) - .await?; - - self.log_action(ctx).await?; - - if handle_reply { - self.reply(ctx, user, dm_user).await?; - } - - Ok(()) - } -} - -pub struct Ban { - pub purge_messages_days: u8, -} - -#[async_trait] -impl ModActionInfo for Ban { - fn to_fields(&self) -> Option { - let fields = vec![( - "Purged messages:", - format!("Last {} day(s)", self.purge_messages_days), - false, - )]; - - Some(fields) - } - - fn description(&self) -> String { - "Banned".to_string() - } - - async fn run_action( - &self, - http: impl CacheHttp + AsRef, - user: &User, - guild_id: &GuildId, - reason: String, - ) -> Result<()> { - debug!("Banning user {user} with reason: \"{reason}\""); - - guild_id - .ban_with_reason(http, user, self.purge_messages_days, reason) - .await?; - - Ok(()) - } -} - -pub struct Timeout { - pub time_until: Timestamp, -} - -#[async_trait] -impl ModActionInfo for Timeout { - fn to_fields(&self) -> Option { - let fields = vec![("Timed out until:", self.time_until.to_string(), false)]; - - Some(fields) - } - - fn description(&self) -> String { - "Timed out".to_string() - } - - #[allow(unused_variables)] - async fn run_action( - &self, - http: impl CacheHttp + AsRef, - user: &User, - guild_id: &GuildId, - reason: String, - ) -> Result<()> { - todo!() - } -} - -pub struct Kick {} - -#[async_trait] -impl ModActionInfo for Kick { - fn to_fields(&self) -> Option { - None - } - - fn description(&self) -> String { - "Kicked".to_string() - } - - async fn run_action( - &self, - http: impl CacheHttp + AsRef, - user: &User, - guild_id: &GuildId, - reason: String, - ) -> Result<()> { - debug!("Kicked user {user} with reason: \"{reason}\""); - - guild_id.kick_with_reason(http, user, &reason).await?; - - Ok(()) - } -} diff --git a/src/commands/moderation/mod.rs b/src/commands/moderation/mod.rs deleted file mode 100644 index 21259f7..0000000 --- a/src/commands/moderation/mod.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::Context; -use std::error::Error; - -use color_eyre::eyre::{eyre, Result}; -use poise::serenity_prelude::{ArgumentConvert, ChannelId, GuildId, User}; - -pub mod actions; -use actions::{Ban, Kick, ModAction}; - -async fn split_argument( - ctx: &Context<'_>, - guild_id: Option, - channel_id: Option, - list: String, -) -> Result> -where - T: ArgumentConvert, - T::Err: Error + Send + Sync + 'static, -{ - // yes i should be using something like `filter_map()` here. async closures - // are unstable though so woooooo - let mut res: Vec = vec![]; - for item in list.split(',') { - let item = T::convert(ctx.serenity_context(), guild_id, channel_id, item.trim()).await?; - - res.push(item); - } - - Ok(res) -} - -/// Ban a user -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "BAN_MEMBERS", - required_permissions = "BAN_MEMBERS", - aliases("ban") -)] -pub async fn ban_user( - ctx: Context<'_>, - #[description = "User to ban"] user: User, - #[description = "Reason to ban"] reason: Option, - #[description = "Number of days to purge their messages from (defaults to 0)"] - purge_messages_days: Option, - #[description = "If true, the reply from the bot will be ephemeral"] quiet: Option, - #[description = "If true, the affected user will be sent a DM"] dm_user: Option, -) -> Result<()> { - let dmd = purge_messages_days.unwrap_or(0); - - let action = ModAction { - reason, - data: Ban { - purge_messages_days: dmd, - }, - }; - - action.handle(&ctx, &user, quiet, dm_user, true).await?; - - Ok(()) -} - -/// Ban multiple users -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "BAN_MEMBERS", - required_permissions = "BAN_MEMBERS", - aliases("ban_multi") -)] -pub async fn mass_ban( - ctx: Context<'_>, - #[description = "Comma separated list of users to ban"] users: String, - #[description = "Reason to ban"] reason: Option, - #[description = "Number of days to purge their messages from (defaults to 0)"] - purge_messages_days: Option, - #[description = "If true, the reply from the bot will be ephemeral"] quiet: Option, - #[description = "If true, the affected user will be sent a DM"] dm_user: Option, -) -> Result<()> { - let gid = ctx - .guild_id() - .ok_or_else(|| eyre!("Couldn't get GuildId!"))?; - - let dmd = purge_messages_days.unwrap_or(0); - let users: Vec = split_argument(&ctx, Some(gid), None, users).await?; - - for user in &users { - let action = ModAction { - reason: reason.clone(), - data: Ban { - purge_messages_days: dmd, - }, - }; - - action.handle(&ctx, user, quiet, dm_user, false).await?; - } - - let resp = format!("{} users banned!", users.len()); - ctx.reply(resp).await?; - - Ok(()) -} - -/// Kick a user -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "KICK_MEMBERS", - required_permissions = "KICK_MEMBERS", - aliases("kick") -)] -pub async fn kick_user( - ctx: Context<'_>, - #[description = "User to kick"] user: User, - #[description = "Reason to kick"] reason: Option, - #[description = "If true, the reply from the bot will be ephemeral"] quiet: Option, - #[description = "If true, the affected user will be sent a DM"] dm_user: Option, -) -> Result<()> { - let action = ModAction { - reason, - data: Kick {}, - }; - - action.handle(&ctx, &user, quiet, dm_user, true).await?; - - Ok(()) -} - -/// Kick multiple users -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "KICK_MEMBERS", - required_permissions = "KICK_MEMBERS", - aliases("multi_kick") -)] -pub async fn mass_kick( - ctx: Context<'_>, - #[description = "Comma separated list of users to kick"] users: String, - #[description = "Reason to kick"] reason: Option, - #[description = "If true, the reply from the bot will be ephemeral"] quiet: Option, - #[description = "If true, the affected user will be sent a DM"] dm_user: Option, -) -> Result<()> { - let gid = ctx - .guild_id() - .ok_or_else(|| eyre!("Couldn't get GuildId!"))?; - let users: Vec = split_argument(&ctx, Some(gid), None, users).await?; - - for user in &users { - let action = ModAction { - reason: reason.clone(), - data: Kick {}, - }; - - action.handle(&ctx, user, quiet, dm_user, false).await?; - } - - let resp = format!("{} users kicked!", users.len()); - ctx.reply(resp).await?; - - Ok(()) -} diff --git a/src/handlers/event/message_logger.rs b/src/handlers/event/message_logger.rs deleted file mode 100644 index d4f00e4..0000000 --- a/src/handlers/event/message_logger.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::consts::COLORS; -use crate::Data; - -use color_eyre::eyre::{eyre, Result}; -use log::debug; -use poise::serenity_prelude::{ - ChannelId, Colour, Http, Message, MessageId, MessageUpdateEvent, User, -}; - -#[allow(unused_variables)] -pub async fn log_msg(user: &User, content: String, color: T) -> Result<()> -where - T: Into, -{ - todo!() -} - -pub async fn handle_create(data: &Data, msg: &Message) -> Result<()> { - let channel_id = msg.channel_id; - let message_id = msg.id; - let content = &msg.content; - let author_id = msg.author.id; - - debug!("Logging message {message_id}"); - data.storage - .store_message(&channel_id, &message_id, content.to_string(), author_id) - .await?; - - Ok(()) -} - -pub async fn handle_update(data: &Data, event: &MessageUpdateEvent) -> Result<()> { - let stored = data - .storage - .get_message(&event.channel_id, &event.id) - .await?; - - let new_content = event.content.as_ref().ok_or_else(|| { - eyre!("Couldn't get content from event! Is the MESSAGE_CONTENT intent enabled?") - })?; - - let author = event - .author - .as_ref() - .ok_or_else(|| eyre!("Couldn't get author from message!"))?; - - if new_content != &stored.content { - log_msg(author, new_content.to_string(), COLORS["yellow"]).await?; - - debug!("Updating message {}", event.id); - data.storage - .store_message( - &event.channel_id, - &event.id, - new_content.to_string(), - author.id, - ) - .await?; - } - - Ok(()) -} - -pub async fn handle_delete( - http: impl AsRef, - data: &Data, - channel_id: &ChannelId, - message_id: &MessageId, -) -> Result<()> { - let stored = data.storage.get_message(channel_id, message_id).await?; - let user = http.as_ref().get_user(*stored.author.as_u64()).await?; - - log_msg(&user, stored.content, COLORS["red"]).await?; - - debug!("Deleting message {message_id}"); - data.storage.delete_message(channel_id, message_id).await?; - - Ok(()) -} diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index a0db8c2..57f614a 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -9,7 +9,6 @@ mod analyze_logs; mod delete_on_reaction; mod eta; mod expand_link; -mod message_logger; pub mod pluralkit; mod support_onboard; @@ -48,28 +47,11 @@ pub async fn handle( return Ok(()); } - // store all new messages to monitor edits and deletes - message_logger::handle_create(data, new_message).await?; - eta::handle(ctx, new_message).await?; expand_link::handle(ctx, new_message).await?; analyze_logs::handle(ctx, new_message, data).await?; } - Event::MessageDelete { - channel_id, - deleted_message_id, - guild_id: _, - } => message_logger::handle_delete(ctx, data, channel_id, deleted_message_id).await?, - - Event::MessageUpdate { - old_if_available: _, - new: _, - event, - } => { - message_logger::handle_update(data, event).await?; - } - Event::ReactionAdd { add_reaction } => { delete_on_reaction::handle(ctx, add_reaction).await? } diff --git a/src/storage/message_logger.rs b/src/storage/message_logger.rs deleted file mode 100644 index 0ee2407..0000000 --- a/src/storage/message_logger.rs +++ /dev/null @@ -1,11 +0,0 @@ -use poise::serenity_prelude::UserId; -use redis_macros::{FromRedisValue, ToRedisArgs}; -use serde::{Deserialize, Serialize}; - -pub const MSG_LOG_KEY: &str = "message-log-v1"; - -#[derive(Clone, Debug, Deserialize, Serialize, FromRedisValue, ToRedisArgs)] -pub struct MessageLog { - pub author: UserId, - pub content: String, -} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 885817a..2cfbe33 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,12 +2,9 @@ use std::fmt::Debug; use color_eyre::eyre::Result; use log::*; -use poise::serenity_prelude::{ChannelId, MessageId, UserId}; +use poise::serenity_prelude::UserId; use redis::{AsyncCommands as _, Client, FromRedisValue, ToRedisArgs}; -pub mod message_logger; -use message_logger::*; - const PK_KEY: &str = "pluralkit-v1"; const LAUNCHER_VERSION_KEY: &str = "launcher-version-v1"; @@ -97,45 +94,6 @@ impl Storage { self.key_exists(&key).await } - pub async fn store_message( - &self, - channel_id: &ChannelId, - message_id: &MessageId, - content: String, - author: UserId, - ) -> Result<()> { - let key = format!("{MSG_LOG_KEY}:{channel_id}:{message_id}"); - - let val = MessageLog { author, content }; - - self.set_key(&key, val).await?; - self.expire_key(&key, 30 * 24 * 60 * 60).await?; // only store for 30 days - - Ok(()) - } - - pub async fn get_message( - &self, - channel_id: &ChannelId, - message_id: &MessageId, - ) -> Result { - let key = format!("{MSG_LOG_KEY}:{channel_id}:{message_id}"); - let res = self.get_key(&key).await?; - - Ok(res) - } - - pub async fn delete_message( - &self, - channel_id: &ChannelId, - message_id: &MessageId, - ) -> Result<()> { - let key = format!("{MSG_LOG_KEY}:{channel_id}:{message_id}"); - self.delete_key(&key).await?; - - Ok(()) - } - pub async fn cache_launcher_version(&self, version: &str) -> Result<()> { self.set_key(LAUNCHER_VERSION_KEY, version).await?;