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
Generated
+7
View File
@@ -1123,6 +1123,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "hyper"
version = "1.6.0"
@@ -2001,6 +2007,7 @@ dependencies = [
"csv",
"deadpool-redis",
"dotenvy",
"humantime",
"image",
"include_dir",
"itertools 0.14.0",
+1
View File
@@ -14,6 +14,7 @@ colored = "3.0.0"
csv = "1.3.1"
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
dotenvy = "0.15.7"
humantime = "2.2.0"
image = "0.25.6"
include_dir = "0.7.4"
itertools = "0.14.0"
+3 -3
View File
@@ -106,7 +106,7 @@ impl<S, E> FromArguments<S, E> for NeedyString {
args: &'a str,
_ctx: &'a CommandContext<'_, S, E>,
) -> Result<(Self, &'a str), ArgumentParsingError> {
if !args.is_empty() {
if !args.trim().is_empty() {
match args.split_once(" ") {
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
None => Ok((Self(args.to_string()), "")),
@@ -144,8 +144,8 @@ impl<S, E> FromArguments<S, E> for NeedyGreedyString {
args: &'a str,
_ctx: &'a CommandContext<'_, S, E>,
) -> Result<(Self, &'a str), ArgumentParsingError> {
if !args.is_empty() {
Ok((Self(args.to_string()), ""))
if !args.trim().is_empty() {
Ok((Self(args.trim().to_string()), ""))
} else {
Err(ArgumentParsingError::MissingArgument)
}
+61 -6
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,13 +124,17 @@ 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))
}
}
}
impl Deref for UserArgument {
@@ -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?;
+4 -3
View File
@@ -141,11 +141,12 @@ use crate::{
commands::{
CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand,
},
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, tasks::EndInactiveCallsTask,
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler},
tasks::EndInactiveCallsTask,
},
gateway::events::{ReadyHandler, ReconnectHandler},
leveling::{commands::LeaderboardCommand, events::MessageLevelingHandler},
moderation::commands::{IncidentsCommand, ModeratorCommand, StatsCommand},
moderation::commands::{BanCommand, IncidentsCommand, ModeratorCommand},
servers::{
commands::PrefixCommand,
events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
@@ -301,7 +302,7 @@ async fn start(
.command(PrefixCommand)
.command(ModeratorCommand)
.command(IncidentsCommand)
.command(StatsCommand)
.command(BanCommand)
.component(HelpGoBackButton)
.cache_resource_type(ResourceType::GUILD)
.cache_resource_type(ResourceType::CHANNEL)
+29
View File
@@ -100,3 +100,32 @@ pub fn i64_to_u64(i: i64) -> u64 {
pub fn u64_to_i64(u: u64) -> i64 {
i64::from_ne_bytes(u.to_ne_bytes())
}
/// Splits at the `n`th occurrence of `pattern` counting from the end of the string.
///
/// Arguments:
/// * `s` - The string to split.
/// * `pattern` - The pattern to split at.
/// * `n` - The occurrence to split at (counting from the end).
///
/// Returns:
/// [`Option<(&str, &str)>`] - The splitted parts of the string, or [`None`] if no occurrences of
/// `pattern` were found in `s`.
pub fn split_at_nth_from_end<'a, 'b>(s: &'a str, pattern: &'b str, n: usize) -> Option<(&'a str, &'a str)> {
let mut end = s.len();
let mut count = 0;
while let Some(pos) = s[..end].rfind(pattern) {
count += 1;
if count == n {
let before = &s[..pos];
let after = &s[pos + pattern.len()..];
return Some((before, after));
}
end = pos; // move left and keep searching
}
None
}