Add ban command

This commit is contained in:
2025-08-25 19:36:18 -03:00
parent 6d8217ed81
commit e44ab51adf
7 changed files with 189 additions and 27 deletions
+62 -7
View File
@@ -1,4 +1,7 @@
use std::ops::{Deref, DerefMut};
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
use scylla::client::session::Session;
@@ -10,6 +13,7 @@ use crate::{
context::CommandContext,
},
state::State,
utils::split_at_nth_from_end,
};
/// Fetch a user by their ID from the database.
@@ -81,12 +85,13 @@ impl FromArguments<State, Extras> for UserArgument {
args: &'a str,
ctx: &'a CommandContext<'_, State, Extras>,
) -> Result<(Self, &'a str), ArgumentParsingError> {
let user: User;
if !args.is_empty() {
// There's an argument, so that argument is used to find the user.
let user: User;
let (identifier, args) = NeedyString::from_args(args, ctx).await?;
let identifier = identifier.0.trim();
if identifier.starts_with("<@") && identifier.ends_with(">") {
// The argument is a Discord mention.
@@ -108,7 +113,9 @@ impl FromArguments<State, Extras> for UserArgument {
} else {
// The argument is (probably) a username.
let Some(db_user) = User::get_by_name(identifier.0, &ctx.rt.state.db).await? else {
let Some(db_user) =
User::get_by_name(identifier.to_string(), &ctx.rt.state.db).await?
else {
// Check in the message's reply, and if not use the command executor.
user = user_from_reply_or_command_executor(ctx).await?;
@@ -117,12 +124,16 @@ impl FromArguments<State, Extras> for UserArgument {
user = db_user;
}
// Notice that `args` is shadowed above, so this line can't be removed (or else the
// full args will be passed and not just the remaining ones as it should be).
Ok((UserArgument(user), args))
} else {
// Check in the message's reply, and if not use the command executor.
user = user_from_reply_or_command_executor(ctx).await?;
}
let user = user_from_reply_or_command_executor(ctx).await?;
Ok((UserArgument(user), args))
Ok((UserArgument(user), args))
}
}
}
@@ -139,3 +150,47 @@ impl DerefMut for UserArgument {
&mut self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct DurationArgument(pub Duration);
impl FromArguments<State, Extras> for DurationArgument {
async fn from_args<'a>(
mut args: &'a str,
_ctx: &'a CommandContext<'_, State, Extras>,
) -> Result<(Self, &'a str), ArgumentParsingError> {
let mut i = 1;
let mut finding = None;
while let Some((substring, remaining_args)) = split_at_nth_from_end(args, " ", i) {
if let Ok(duration) = humantime::parse_duration(substring) {
finding = Some(DurationArgument(duration));
args = remaining_args;
break;
}
i += 1;
}
if let Some(duration) = finding {
Ok((duration, args))
} else {
Err(ArgumentParsingError::InvalidArgument)
}
}
}
impl Deref for DurationArgument {
type Target = Duration;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DurationArgument {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
+83 -14
View File
@@ -1,23 +1,25 @@
use async_trait::async_trait;
use time::UtcDateTime;
use crate::{
database::models::{Call, Message, User},
database::models::{UserIncident, UserIncidentType},
extras::Extras,
framework::{
commands::{
Command, CommandError,
arguments::{Arg, Argument, ArgumentParsingError, ParseArguments},
arguments::{Arg, Argument, ArgumentParsingError, NeedyGreedyString, ParseArguments},
context::CommandContext,
permissions::Permission,
},
runtime::Runtime,
},
groups::moderation::{
arguments::UserArgument,
arguments::{DurationArgument, UserArgument},
get_incident_page,
permissions::{BotModeratorPermission, BotOwnerPermission},
},
state::State,
utils::get_random_u64,
};
#[derive(Debug)]
@@ -149,34 +151,101 @@ impl Command<State, Extras> for IncidentsCommand {
}
#[derive(Debug)]
pub struct StatsCommand;
pub struct BanCommand;
#[async_trait]
impl Command<State, Extras> for StatsCommand {
impl Command<State, Extras> for BanCommand {
fn name(&self) -> &'static str {
"stats"
"ban"
}
fn aliases(&self) -> &'static [&'static str] {
&["b"]
}
fn description(&self) -> &'static str {
"Bans a user from Payphone calls.\n\nThe user can be specified via either replying to them (works on call too), mentioning them, specifying their username, specifying their ID, or not setting it to default to yourself.\n\nThe duration can be specified as defined in the [`humantime::parse_duration` documentation](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html)."
}
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
vec![Box::new(BotOwnerPermission)]
vec![Box::new(BotModeratorPermission)]
}
fn show_in_help(&self, rt: &Runtime<State, Extras>, extras: &Extras) -> bool {
extras.user.id == rt.state.bot_owner_id
fn show_in_help(&self, _rt: &Runtime<State, Extras>, extras: &Extras) -> bool {
extras.user.is_moderator
}
fn arguments(&self) -> &'static [Argument] {
&[
Argument {
name: "user",
is_optional: false,
is_greedy: false,
},
Argument {
name: "duration",
is_optional: true,
is_greedy: false,
},
Argument {
name: "reason",
is_optional: false,
is_greedy: true,
},
]
}
async fn execute(
&self,
ctx: CommandContext<'_, State, Extras>,
_args: &str,
args: &str,
) -> Result<(), CommandError> {
let users = User::count(&ctx.rt.state.db).await?;
let calls = Call::count(&ctx.rt.state.db).await?;
let messages = Message::count(&ctx.rt.state.db).await?;
let (user, duration, reason) = match <(
UserArgument,
Option<DurationArgument>,
NeedyGreedyString,
)>::parse(args, &ctx)
.await
{
Ok(arg) => arg,
Err(e) => match e {
ArgumentParsingError::InvalidArgument => {
ctx.send()
.content("❌ User not found in DB or no reason was provided.")
.await?;
return Ok(());
}
ArgumentParsingError::DatabaseFailed(e) => {
return Err(CommandError::DatabaseFailed(e));
}
ArgumentParsingError::MissingArgument => {
ctx.send().content(format!("❌ No reason was provided. Use `{}help ban` to learn how to use this command.", ctx.prefix)).await?;
return Ok(());
}
},
};
let incident = UserIncident {
created_at: UtcDateTime::now(),
user_id: user.id,
incident_id: get_random_u64(),
report_id: None,
r#type: UserIncidentType::Ban,
message: None,
created_by_id: Some(ctx.event.author.id.get()),
reason: (*reason).clone(),
expires_at: duration.map(|d| UtcDateTime::now() + *d),
duration: duration.map(|d| humantime::format_duration(*d).to_string()),
};
incident.insert(&ctx.rt.state.db).await?;
ctx.send()
.content(format!(
"- Users: {users}\n- Calls: {calls}\n- Messages: {messages}"
"✅ User `{}` has been banned from Payphone{}.",
user.name,
duration
.map(|d| format!(" for {}", humantime::format_duration(*d)))
.unwrap_or_default()
))
.await?;