refactor: create ModAction struct
This commit is contained in:
parent
ed496f5cdc
commit
78c8aa7a18
5 changed files with 382 additions and 129 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1224,6 +1224,7 @@ dependencies = [
|
||||||
name = "refraction"
|
name = "refraction"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|
|
@ -15,6 +15,7 @@ serde = "1.0.193"
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = "0.1.74"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
|
|
|
@ -15,7 +15,8 @@ pub fn to_global_commands() -> Vec<Command<Data, Report>> {
|
||||||
general::say(),
|
general::say(),
|
||||||
general::stars(),
|
general::stars(),
|
||||||
general::tag(),
|
general::tag(),
|
||||||
moderation::ban(),
|
moderation::ban_user(),
|
||||||
moderation::kick(),
|
moderation::mass_ban(),
|
||||||
|
moderation::kick_user(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,146 +1,233 @@
|
||||||
use crate::{consts::COLORS, Context};
|
use crate::{consts::COLORS, Context};
|
||||||
|
|
||||||
use color_eyre::eyre::{eyre, Result};
|
use async_trait::async_trait;
|
||||||
use poise::serenity_prelude::{
|
use color_eyre::eyre::{eyre, Context as _, Result};
|
||||||
futures::TryFutureExt, CreateEmbed, CreateMessage, FutureExt, Guild, Timestamp, User, UserId,
|
use log::*;
|
||||||
};
|
use poise::serenity_prelude::{CacheHttp, Http, Member, Timestamp};
|
||||||
|
|
||||||
struct Action {
|
type Fields<'a> = Vec<(&'a str, String, bool)>;
|
||||||
reason: String,
|
|
||||||
data: ActionData,
|
#[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 {
|
pub struct ModAction<T>
|
||||||
Kick,
|
where
|
||||||
Ban { purge: u8 },
|
T: ModActionInfo,
|
||||||
Timeout { until: Timestamp },
|
{
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_dm<'a, 'b>(
|
impl<T: ModActionInfo> ModAction<T> {
|
||||||
message: &'b mut CreateMessage<'a>,
|
fn get_all_fields(&self) -> Fields {
|
||||||
guild: &Guild,
|
let mut fields = vec![];
|
||||||
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())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let guild_name = &guild.name;
|
|
||||||
let reason = &action.reason;
|
|
||||||
message.content(format!(
|
|
||||||
"You have been {description} {guild_name}.\nReason: {reason}"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn moderate(
|
if let Some(reason) = self.reason.clone() {
|
||||||
ctx: &Context<'_>,
|
fields.push(("Reason:", reason, false));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = match action.data {
|
if let Some(mut action_fields) = self.data.to_fields() {
|
||||||
ActionData::Kick => guild
|
fields.append(&mut action_fields);
|
||||||
.kick_with_reason(ctx.http(), user, reason)
|
|
||||||
.await
|
|
||||||
.is_ok(),
|
|
||||||
|
|
||||||
ActionData::Ban { purge } => guild
|
|
||||||
.ban_with_reason(ctx.http(), user, purge, reason)
|
|
||||||
.await
|
|
||||||
.is_ok(),
|
|
||||||
|
|
||||||
ActionData::Timeout { until } => guild
|
|
||||||
.edit_member(ctx.http(), user, |member| {
|
|
||||||
member.disable_communication_until_datetime(until)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.is_ok(),
|
|
||||||
};
|
|
||||||
if success {
|
|
||||||
count += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = users.len();
|
/// internal mod logging
|
||||||
if count == total {
|
pub async fn log_action(&self, ctx: &Context<'_>) -> Result<()> {
|
||||||
ctx.reply("✅ Done!").await?;
|
let channel_id = ctx
|
||||||
} else {
|
.data()
|
||||||
ctx.reply(format!("⚠️ {count}/{total} succeeded!"))
|
.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?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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)");
|
||||||
|
}
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ban a user
|
pub struct Ban {
|
||||||
#[poise::command(
|
pub purge_messages_days: u8,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kick a user
|
#[async_trait]
|
||||||
#[poise::command(
|
impl ModActionInfo for Ban {
|
||||||
slash_command,
|
fn to_fields(&self) -> Option<Fields> {
|
||||||
prefix_command,
|
let fields = vec![(
|
||||||
default_member_permissions = "KICK_MEMBERS",
|
"Purged messages:",
|
||||||
required_permissions = "KICK_MEMBERS"
|
format!("Last {} day(s)", self.purge_messages_days),
|
||||||
)]
|
false,
|
||||||
pub async fn kick(
|
)];
|
||||||
ctx: Context<'_>,
|
|
||||||
users: Vec<UserId>,
|
Some(fields)
|
||||||
reason: Option<String>,
|
}
|
||||||
quiet: Option<bool>,
|
|
||||||
) -> Result<()> {
|
fn description(&self) -> String {
|
||||||
moderate(
|
"Banned".to_string()
|
||||||
&ctx,
|
}
|
||||||
&users,
|
|
||||||
&Action {
|
async fn run_action(
|
||||||
reason: reason.unwrap_or_default(),
|
&self,
|
||||||
data: ActionData::Kick {},
|
http: impl CacheHttp + AsRef<Http>,
|
||||||
},
|
user: &Member,
|
||||||
quiet.unwrap_or(false),
|
reason: String,
|
||||||
)
|
) -> Result<()> {
|
||||||
.await
|
debug!("Banning user {user} with reason: \"{reason}\"");
|
||||||
|
|
||||||
|
user.ban_with_reason(http, 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<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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,166 @@
|
||||||
mod actions;
|
use crate::Context;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
pub use actions::*;
|
use color_eyre::eyre::{eyre, Result};
|
||||||
|
use poise::serenity_prelude::{ArgumentConvert, ChannelId, GuildId, Member};
|
||||||
|
|
||||||
|
mod actions;
|
||||||
|
use actions::{Ban, Kick, ModAction};
|
||||||
|
|
||||||
|
async fn split_argument<T>(
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
guild_id: Option<GuildId>,
|
||||||
|
channel_id: Option<ChannelId>,
|
||||||
|
list: String,
|
||||||
|
) -> Result<Vec<T>>
|
||||||
|
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<T> = 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: Member,
|
||||||
|
#[description = "Reason to ban"] reason: Option<String>,
|
||||||
|
#[description = "Number of days to purge their messages from (defaults to 0)"]
|
||||||
|
purge_messages_days: Option<u8>,
|
||||||
|
#[description = "If true, the reply from the bot will be ephemeral"] quiet: Option<bool>,
|
||||||
|
#[description = "If true, the affected user will be sent a DM"] dm_user: Option<bool>,
|
||||||
|
) -> 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<String>,
|
||||||
|
#[description = "Number of days to purge their messages from (defaults to 0)"]
|
||||||
|
purge_messages_days: Option<u8>,
|
||||||
|
#[description = "If true, the reply from the bot will be ephemeral"] quiet: Option<bool>,
|
||||||
|
#[description = "If true, the affected user will be sent a DM"] dm_user: Option<bool>,
|
||||||
|
) -> 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<Member> = 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: Member,
|
||||||
|
#[description = "Reason to kick"] reason: Option<String>,
|
||||||
|
#[description = "If true, the reply from the bot will be ephemeral"] quiet: Option<bool>,
|
||||||
|
#[description = "If true, the affected user will be sent a DM"] dm_user: Option<bool>,
|
||||||
|
) -> 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<String>,
|
||||||
|
#[description = "If true, the reply from the bot will be ephemeral"] quiet: Option<bool>,
|
||||||
|
#[description = "If true, the affected user will be sent a DM"] dm_user: Option<bool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let gid = ctx
|
||||||
|
.guild_id()
|
||||||
|
.ok_or_else(|| eyre!("Couldn't get GuildId!"))?;
|
||||||
|
let users: Vec<Member> = 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(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue