Skip to content

Commit

Permalink
refactor: create ModAction struct
Browse files Browse the repository at this point in the history
  • Loading branch information
getchoo committed Dec 8, 2023
1 parent ed496f5 commit 78c8aa7
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 125 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ serde = "1.0.193"
serde_json = "1.0.108"

[dependencies]
async-trait = "0.1.74"
color-eyre = "0.6.2"
dotenvy = "0.15.7"
env_logger = "0.10.0"
Expand Down
5 changes: 3 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ pub fn to_global_commands() -> Vec<Command<Data, Report>> {
general::say(),
general::stars(),
general::tag(),
moderation::ban(),
moderation::kick(),
moderation::ban_user(),
moderation::mass_ban(),
moderation::kick_user(),
]
}
331 changes: 209 additions & 122 deletions src/commands/moderation/actions.rs
Original file line number Diff line number Diff line change
@@ -1,146 +1,233 @@
use crate::{consts::COLORS, Context};

use color_eyre::eyre::{eyre, Result};
use poise::serenity_prelude::{
futures::TryFutureExt, CreateEmbed, CreateMessage, FutureExt, Guild, Timestamp, User, UserId,
};

struct Action {
reason: String,
data: ActionData,
use async_trait::async_trait;
use color_eyre::eyre::{eyre, Context as _, Result};
use log::*;
use poise::serenity_prelude::{CacheHttp, Http, Member, Timestamp};

type Fields<'a> = Vec<(&'a str, String, bool)>;

#[async_trait]
pub trait ModActionInfo {
fn to_fields(&self) -> Option<Fields>;
fn description(&self) -> String;
async fn run_action(
&self,
http: impl CacheHttp + AsRef<Http>,
user: &Member,
reason: String,
) -> Result<()>;
}

enum ActionData {
Kick,
Ban { purge: u8 },
Timeout { until: Timestamp },
pub struct ModAction<T>
where
T: ModActionInfo,
{
pub reason: Option<String>,
pub data: T,
}

fn build_dm<'a, 'b>(
message: &'b mut CreateMessage<'a>,
guild: &Guild,
action: &Action,
) -> &'b mut CreateMessage<'a> {
let description = match &action.data {
ActionData::Kick => "kicked from".to_string(),
ActionData::Ban { purge: _ } => "banned from".to_string(),
ActionData::Timeout { until } => {
format!("timed out until <t:{}> in", until.unix_timestamp())
impl<T: ModActionInfo> ModAction<T> {
fn get_all_fields(&self) -> Fields {
let mut fields = vec![];

if let Some(reason) = self.reason.clone() {
fields.push(("Reason:", reason, false));
}
};
let guild_name = &guild.name;
let reason = &action.reason;
message.content(format!(
"You have been {description} {guild_name}.\nReason: {reason}"
))
}

async fn moderate(
ctx: &Context<'_>,
users: &Vec<UserId>,
action: &Action,
quiet: bool,
) -> Result<()> {
let guild = ctx
.guild()
.ok_or_else(|| eyre!("Couldn't get guild from message!"))?;
let reason = &action.reason;

let mut count = 0;

for user in users {
if quiet {
if let Ok(channel) = user.create_dm_channel(ctx.http()).await {
let _ = channel
.send_message(ctx.http(), |message| build_dm(message, &guild, action))
.await;
}
if let Some(mut action_fields) = self.data.to_fields() {
fields.append(&mut action_fields);
}

let success = match action.data {
ActionData::Kick => guild
.kick_with_reason(ctx.http(), user, reason)
.await
.is_ok(),
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?"))?;

ActionData::Ban { purge } => guild
.ban_with_reason(ctx.http(), user, purge, reason)
.await
.is_ok(),
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: &Member,
dm_user: Option<bool>,
) -> Result<()> {
let mut resp = format!("{} {}!", self.data.description(), user.user.name);

if dm_user.unwrap_or_default() {
resp = format!("{resp} (user notified with direct message)");
}

ActionData::Timeout { until } => guild
.edit_member(ctx.http(), user, |member| {
member.disable_communication_until_datetime(until)
ctx.reply(resp).await?;

Ok(())
}

pub async fn dm_user(&self, ctx: &Context<'_>, user: &Member) -> Result<()> {
let guild = ctx.http().get_guild(*user.guild_id.as_u64()).await?;
let title = format!("{} from {}!", self.data.description(), guild.name);

user.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
.is_ok(),
};
if success {
count += 1;
})
.await?;

Ok(())
}

pub async fn handle(
&self,
ctx: &Context<'_>,
user: &Member,
quiet: Option<bool>,
dm_user: Option<bool>,
handle_reply: bool,
) -> Result<()> {
let actual_reason = self.reason.clone().unwrap_or("".to_string());
self.data.run_action(ctx, user, actual_reason).await?;

if quiet.unwrap_or_default() {
ctx.defer_ephemeral().await?;
} else {
ctx.defer().await?;
}

self.log_action(ctx).await?;

if dm_user.unwrap_or_default() {
self.dm_user(ctx, user).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<Fields> {
let fields = vec![(
"Purged messages:",
format!("Last {} day(s)", self.purge_messages_days),
false,
)];

Some(fields)
}

fn description(&self) -> String {
"Banned".to_string()
}

let total = users.len();
if count == total {
ctx.reply("✅ Done!").await?;
} else {
ctx.reply(format!("⚠️ {count}/{total} succeeded!"))
async fn run_action(
&self,
http: impl CacheHttp + AsRef<Http>,
user: &Member,
reason: String,
) -> Result<()> {
debug!("Banning user {user} with reason: \"{reason}\"");

user.ban_with_reason(http, self.purge_messages_days, reason)
.await?;

Ok(())
}
}

Ok(())
pub struct Timeout {
pub time_until: Timestamp,
}

/// Ban a user
#[poise::command(
slash_command,
prefix_command,
default_member_permissions = "BAN_MEMBERS",
required_permissions = "BAN_MEMBERS",
aliases("ban")
)]
pub async fn ban(
ctx: Context<'_>,
users: Vec<UserId>,
purge: Option<u8>,
reason: Option<String>,
quiet: Option<bool>,
) -> Result<()> {
moderate(
&ctx,
&users,
&Action {
reason: reason.unwrap_or_default(),
data: ActionData::Ban {
purge: purge.unwrap_or(0),
},
},
quiet.unwrap_or(false),
)
.await
#[async_trait]
impl ModActionInfo for Timeout {
fn to_fields(&self) -> Option<Fields> {
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<Http>,
user: &Member,
reason: String,
) -> Result<()> {
todo!()
}
}

/// Kick a user
#[poise::command(
slash_command,
prefix_command,
default_member_permissions = "KICK_MEMBERS",
required_permissions = "KICK_MEMBERS"
)]
pub async fn kick(
ctx: Context<'_>,
users: Vec<UserId>,
reason: Option<String>,
quiet: Option<bool>,
) -> Result<()> {
moderate(
&ctx,
&users,
&Action {
reason: reason.unwrap_or_default(),
data: ActionData::Kick {},
},
quiet.unwrap_or(false),
)
.await
pub struct Kick {}

#[async_trait]
impl ModActionInfo for Kick {
fn to_fields(&self) -> Option<Fields> {
None
}

fn description(&self) -> String {
"Kicked".to_string()
}

async fn run_action(
&self,
http: impl CacheHttp + AsRef<Http>,
user: &Member,
reason: String,
) -> Result<()> {
debug!("Kicked user {user} with reason: \"{reason}\"");

user.kick_with_reason(http, &reason).await?;

Ok(())
}
}
Loading

0 comments on commit 78c8aa7

Please sign in to comment.