Add ban command
This commit is contained in:
Generated
+7
@@ -1123,6 +1123,12 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -2001,6 +2007,7 @@ dependencies = [
|
|||||||
"csv",
|
"csv",
|
||||||
"deadpool-redis",
|
"deadpool-redis",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"humantime",
|
||||||
"image",
|
"image",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ colored = "3.0.0"
|
|||||||
csv = "1.3.1"
|
csv = "1.3.1"
|
||||||
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
|
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
humantime = "2.2.0"
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ impl<S, E> FromArguments<S, E> for NeedyString {
|
|||||||
args: &'a str,
|
args: &'a str,
|
||||||
_ctx: &'a CommandContext<'_, S, E>,
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
if !args.is_empty() {
|
if !args.trim().is_empty() {
|
||||||
match args.split_once(" ") {
|
match args.split_once(" ") {
|
||||||
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
|
Some((arg, rest)) => Ok((Self(arg.to_string()), rest)),
|
||||||
None => Ok((Self(args.to_string()), "")),
|
None => Ok((Self(args.to_string()), "")),
|
||||||
@@ -144,8 +144,8 @@ impl<S, E> FromArguments<S, E> for NeedyGreedyString {
|
|||||||
args: &'a str,
|
args: &'a str,
|
||||||
_ctx: &'a CommandContext<'_, S, E>,
|
_ctx: &'a CommandContext<'_, S, E>,
|
||||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
if !args.is_empty() {
|
if !args.trim().is_empty() {
|
||||||
Ok((Self(args.to_string()), ""))
|
Ok((Self(args.trim().to_string()), ""))
|
||||||
} else {
|
} else {
|
||||||
Err(ArgumentParsingError::MissingArgument)
|
Err(ArgumentParsingError::MissingArgument)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use scylla::client::session::Session;
|
use scylla::client::session::Session;
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@ use crate::{
|
|||||||
context::CommandContext,
|
context::CommandContext,
|
||||||
},
|
},
|
||||||
state::State,
|
state::State,
|
||||||
|
utils::split_at_nth_from_end,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fetch a user by their ID from the database.
|
/// Fetch a user by their ID from the database.
|
||||||
@@ -81,12 +85,13 @@ impl FromArguments<State, Extras> for UserArgument {
|
|||||||
args: &'a str,
|
args: &'a str,
|
||||||
ctx: &'a CommandContext<'_, State, Extras>,
|
ctx: &'a CommandContext<'_, State, Extras>,
|
||||||
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
) -> Result<(Self, &'a str), ArgumentParsingError> {
|
||||||
let user: User;
|
|
||||||
|
|
||||||
if !args.is_empty() {
|
if !args.is_empty() {
|
||||||
// There's an argument, so that argument is used to find the user.
|
// 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, args) = NeedyString::from_args(args, ctx).await?;
|
||||||
|
let identifier = identifier.0.trim();
|
||||||
|
|
||||||
if identifier.starts_with("<@") && identifier.ends_with(">") {
|
if identifier.starts_with("<@") && identifier.ends_with(">") {
|
||||||
// The argument is a Discord mention.
|
// The argument is a Discord mention.
|
||||||
@@ -108,7 +113,9 @@ impl FromArguments<State, Extras> for UserArgument {
|
|||||||
} else {
|
} else {
|
||||||
// The argument is (probably) a username.
|
// 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.
|
// Check in the message's reply, and if not use the command executor.
|
||||||
user = user_from_reply_or_command_executor(ctx).await?;
|
user = user_from_reply_or_command_executor(ctx).await?;
|
||||||
|
|
||||||
@@ -117,12 +124,16 @@ impl FromArguments<State, Extras> for UserArgument {
|
|||||||
|
|
||||||
user = db_user;
|
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 {
|
} else {
|
||||||
// Check in the message's reply, and if not use the command executor.
|
// 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
|
&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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use time::UtcDateTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::{Call, Message, User},
|
database::models::{UserIncident, UserIncidentType},
|
||||||
extras::Extras,
|
extras::Extras,
|
||||||
framework::{
|
framework::{
|
||||||
commands::{
|
commands::{
|
||||||
Command, CommandError,
|
Command, CommandError,
|
||||||
arguments::{Arg, Argument, ArgumentParsingError, ParseArguments},
|
arguments::{Arg, Argument, ArgumentParsingError, NeedyGreedyString, ParseArguments},
|
||||||
context::CommandContext,
|
context::CommandContext,
|
||||||
permissions::Permission,
|
permissions::Permission,
|
||||||
},
|
},
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
},
|
},
|
||||||
groups::moderation::{
|
groups::moderation::{
|
||||||
arguments::UserArgument,
|
arguments::{DurationArgument, UserArgument},
|
||||||
get_incident_page,
|
get_incident_page,
|
||||||
permissions::{BotModeratorPermission, BotOwnerPermission},
|
permissions::{BotModeratorPermission, BotOwnerPermission},
|
||||||
},
|
},
|
||||||
state::State,
|
state::State,
|
||||||
|
utils::get_random_u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -149,34 +151,101 @@ impl Command<State, Extras> for IncidentsCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StatsCommand;
|
pub struct BanCommand;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Command<State, Extras> for StatsCommand {
|
impl Command<State, Extras> for BanCommand {
|
||||||
fn name(&self) -> &'static str {
|
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>> {
|
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 {
|
fn show_in_help(&self, _rt: &Runtime<State, Extras>, extras: &Extras) -> bool {
|
||||||
extras.user.id == rt.state.bot_owner_id
|
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(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
ctx: CommandContext<'_, State, Extras>,
|
ctx: CommandContext<'_, State, Extras>,
|
||||||
_args: &str,
|
args: &str,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let users = User::count(&ctx.rt.state.db).await?;
|
let (user, duration, reason) = match <(
|
||||||
let calls = Call::count(&ctx.rt.state.db).await?;
|
UserArgument,
|
||||||
let messages = Message::count(&ctx.rt.state.db).await?;
|
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()
|
ctx.send()
|
||||||
.content(format!(
|
.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?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -141,11 +141,12 @@ use crate::{
|
|||||||
commands::{
|
commands::{
|
||||||
CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand,
|
CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand,
|
||||||
},
|
},
|
||||||
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, tasks::EndInactiveCallsTask,
|
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler},
|
||||||
|
tasks::EndInactiveCallsTask,
|
||||||
},
|
},
|
||||||
gateway::events::{ReadyHandler, ReconnectHandler},
|
gateway::events::{ReadyHandler, ReconnectHandler},
|
||||||
leveling::{commands::LeaderboardCommand, events::MessageLevelingHandler},
|
leveling::{commands::LeaderboardCommand, events::MessageLevelingHandler},
|
||||||
moderation::commands::{IncidentsCommand, ModeratorCommand, StatsCommand},
|
moderation::commands::{BanCommand, IncidentsCommand, ModeratorCommand},
|
||||||
servers::{
|
servers::{
|
||||||
commands::PrefixCommand,
|
commands::PrefixCommand,
|
||||||
events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
|
events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
|
||||||
@@ -301,7 +302,7 @@ async fn start(
|
|||||||
.command(PrefixCommand)
|
.command(PrefixCommand)
|
||||||
.command(ModeratorCommand)
|
.command(ModeratorCommand)
|
||||||
.command(IncidentsCommand)
|
.command(IncidentsCommand)
|
||||||
.command(StatsCommand)
|
.command(BanCommand)
|
||||||
.component(HelpGoBackButton)
|
.component(HelpGoBackButton)
|
||||||
.cache_resource_type(ResourceType::GUILD)
|
.cache_resource_type(ResourceType::GUILD)
|
||||||
.cache_resource_type(ResourceType::CHANNEL)
|
.cache_resource_type(ResourceType::CHANNEL)
|
||||||
|
|||||||
@@ -100,3 +100,32 @@ pub fn i64_to_u64(i: i64) -> u64 {
|
|||||||
pub fn u64_to_i64(u: u64) -> i64 {
|
pub fn u64_to_i64(u: u64) -> i64 {
|
||||||
i64::from_ne_bytes(u.to_ne_bytes())
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user