feat(moderation): add commands for anti-raid reset, mute role setting, spam overrides, strikes management, and timeout toggling

- Implemented `resetantiraide` command to reset anti-raid protections to default settings.
- Added `set_muterole` command to define the mute role when timeout mode is disabled.
- Created `spam` command to manage spam moderation channel overrides (allow, deny, reset).
- Developed `strikes` command to display and modify strike rules for various triggers.
- Introduced `timeout` command to toggle the use of Discord timeout for mutes.

feat(outils): add piconly command to manage photo-only channels

- Implemented `piconly` command to define or remove channels where only photos can be sent.
- Added functionality to enforce photo-only rules in designated channels.

feat(roles): add ancien and noderank commands for role management

- Created `ancien` command to set up a role for members after a specified delay.
- Implemented `noderank` command to manage protected roles that are not removed by derank actions.
This commit is contained in:
Puechberty Arthur
2026-04-10 15:04:10 +02:00
parent f945f3f378
commit e0f40e9190
35 changed files with 6353 additions and 227 deletions
+602
View File
@@ -0,0 +1,602 @@
use std::collections::{HashMap, VecDeque};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::commands::moderation_sanction_helpers::{add_sanction, handle_timeout};
use crate::db::{
self, DbPoolKey, ModerationSettings, PunishRule, count_member_strikes_in_window,
ensure_default_punish_rules, get_last_punish_triggered_at, upsert_last_punish_triggered_at,
};
use crate::permissions;
static SPAM_TRACKER: OnceLock<Mutex<HashMap<(u64, u64, u64), VecDeque<Instant>>>> = OnceLock::new();
pub async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub fn parse_on_off(input: &str) -> Option<bool> {
match input.trim().to_lowercase().as_str() {
"on" | "enable" | "enabled" | "true" | "1" => Some(true),
"off" | "disable" | "disabled" | "false" | "0" => Some(false),
_ => None,
}
}
pub fn parse_duration_to_seconds(input: &str) -> Option<i64> {
let raw = input.trim().to_lowercase();
if raw.is_empty() {
return None;
}
let mut digits = String::new();
let mut suffix = String::new();
for ch in raw.chars() {
if ch.is_ascii_digit() {
if !suffix.is_empty() {
return None;
}
digits.push(ch);
} else if !ch.is_whitespace() {
suffix.push(ch);
}
}
let value = digits.parse::<i64>().ok()?;
if value <= 0 {
return None;
}
let unit = if suffix.is_empty() { "s" } else { &suffix };
let seconds = match unit {
"s" | "sec" | "secs" | "seconde" | "secondes" => value,
"m" | "min" | "mins" | "minute" | "minutes" => value.checked_mul(60)?,
"h" | "heure" | "heures" => value.checked_mul(3_600)?,
"j" | "d" | "jour" | "jours" => value.checked_mul(86_400)?,
"w" | "sem" | "semaine" | "semaines" => value.checked_mul(604_800)?,
_ => return None,
};
Some(seconds.max(1))
}
pub fn format_duration(mut seconds: i64) -> String {
seconds = seconds.max(1);
let days = seconds / 86_400;
seconds %= 86_400;
let hours = seconds / 3_600;
seconds %= 3_600;
let minutes = seconds / 60;
seconds %= 60;
let mut out = Vec::new();
if days > 0 {
out.push(format!("{}j", days));
}
if hours > 0 {
out.push(format!("{}h", hours));
}
if minutes > 0 {
out.push(format!("{}m", minutes));
}
if seconds > 0 || out.is_empty() {
out.push(format!("{}s", seconds));
}
out.join(" ")
}
pub fn parse_rate_limit(input: &str) -> Option<(i32, i32)> {
let mut parts = input.splitn(2, '/');
let limit = parts.next()?.trim().parse::<i32>().ok()?.max(1);
let duration = parse_duration_to_seconds(parts.next()?.trim())?;
if duration > i32::MAX as i64 {
return None;
}
Some((limit, duration as i32))
}
pub fn parse_trigger(input: &str) -> Option<&'static str> {
match input.trim().to_lowercase().as_str() {
"spam" | "antispam" => Some("spam"),
"link" | "antilink" => Some("link"),
"massmention" | "antimassmention" | "mention" | "mentions" => Some("massmention"),
"badword" | "badwords" | "mauvaismot" | "motinterdit" => Some("badword"),
_ => None,
}
}
pub fn parse_profile(input: Option<&str>) -> Option<&'static str> {
let raw = input?.trim().to_lowercase();
match raw.as_str() {
"ancien" | "old" => Some("old"),
"nouveau" | "new" => Some("new"),
_ => None,
}
}
pub fn parse_sanction(input: &str) -> Option<&'static str> {
match input.trim().to_lowercase().as_str() {
"warn" | "avert" => Some("warn"),
"mute" | "timeout" => Some("mute"),
"kick" => Some("kick"),
"ban" => Some("ban"),
_ => None,
}
}
pub fn apply_channel_override(global_enabled: bool, override_mode: Option<&str>) -> bool {
match override_mode {
Some(mode) if mode.eq_ignore_ascii_case("allow") => false,
Some(mode) if mode.eq_ignore_ascii_case("deny") => true,
_ => global_enabled,
}
}
fn contains_invite_link(content: &str) -> bool {
let lower = content.to_lowercase();
lower.contains("discord.gg/")
|| lower.contains("discord.com/invite/")
|| lower.contains("discordapp.com/invite/")
}
fn contains_any_link(content: &str) -> bool {
let lower = content.to_lowercase();
lower.contains("http://")
|| lower.contains("https://")
|| lower.contains("www.")
|| lower.contains("discord.gg/")
}
fn spam_hit(bot_id: u64, guild_id: u64, user_id: u64, limit: i32, window_seconds: i32) -> bool {
let lock = SPAM_TRACKER.get_or_init(|| Mutex::new(HashMap::new()));
let mut tracker = lock.lock().expect("spam tracker lock poisoned");
let key = (bot_id, guild_id, user_id);
let now = Instant::now();
let window = Duration::from_secs(window_seconds.max(1) as u64);
let queue = tracker.entry(key).or_insert_with(VecDeque::new);
queue.push_back(now);
while let Some(oldest) = queue.front() {
if now.duration_since(*oldest) > window {
let _ = queue.pop_front();
} else {
break;
}
}
queue.len() > limit.max(1) as usize
}
async fn user_profile(
ctx: &Context,
pool: &sqlx::PgPool,
bot_id: i64,
guild_id: GuildId,
user_id: UserId,
) -> &'static str {
let Ok(old_settings) =
db::get_or_create_old_member_settings(pool, bot_id, guild_id.get() as i64).await
else {
return "new";
};
if !old_settings.enabled {
return "new";
}
let Some(role_id_raw) = old_settings.role_id else {
return "new";
};
let Ok(member) = guild_id.member(&ctx.http, user_id).await else {
return "new";
};
if member
.roles
.iter()
.any(|role_id| role_id.get() as i64 == role_id_raw)
{
"old"
} else {
"new"
}
}
async fn execute_rule(
ctx: &Context,
guild_id: GuildId,
user_id: UserId,
rule: &PunishRule,
settings: &ModerationSettings,
) -> String {
let sanction = rule.sanction.to_lowercase();
let bot_user_id = ctx.cache.current_user().id;
if sanction == "warn" {
add_sanction(
ctx,
guild_id,
user_id,
bot_user_id,
"warn",
"AutoMod: seuil de strikes atteint.",
None,
None,
)
.await;
return "warn".to_string();
}
if sanction == "mute" || sanction == "timeout" {
let duration = rule
.sanction_seconds
.unwrap_or(3_600)
.clamp(1, 28 * 24 * 3_600);
let expires = Some(Utc::now() + chrono::Duration::seconds(duration));
let _ = handle_timeout(ctx, guild_id, &[user_id], expires).await;
add_sanction(
ctx,
guild_id,
user_id,
bot_user_id,
"tempmute",
"AutoMod: seuil de strikes atteint.",
None,
expires,
)
.await;
if settings.use_timeout {
return format!("timeout {}", format_duration(duration));
}
return format!("mute role {}", format_duration(duration));
}
if sanction == "kick" {
let result = guild_id
.kick_with_reason(&ctx.http, user_id, "AutoMod: seuil de strikes atteint")
.await;
if result.is_ok() {
add_sanction(
ctx,
guild_id,
user_id,
bot_user_id,
"kick",
"AutoMod: seuil de strikes atteint.",
None,
None,
)
.await;
return "kick".to_string();
}
return "kick (echec)".to_string();
}
if sanction == "ban" {
let result = guild_id
.ban_with_reason(&ctx.http, user_id, 0, "AutoMod: seuil de strikes atteint")
.await;
if result.is_ok() {
add_sanction(
ctx,
guild_id,
user_id,
bot_user_id,
"ban",
"AutoMod: seuil de strikes atteint.",
None,
None,
)
.await;
return "ban".to_string();
}
return "ban (echec)".to_string();
}
"aucune".to_string()
}
async fn apply_violation(
ctx: &Context,
msg: &Message,
pool: &sqlx::PgPool,
settings: &ModerationSettings,
trigger: &str,
reason: &str,
) {
let Some(guild_id) = msg.guild_id else {
return;
};
let _ = msg.delete(&ctx.http).await;
let bot_id = settings.bot_id;
let guild_id_raw = settings.guild_id;
let user_id = msg.author.id;
let profile = user_profile(ctx, pool, bot_id, guild_id, user_id).await;
let strikes = db::get_strike_rule(pool, bot_id, guild_id_raw, trigger, profile)
.await
.ok()
.flatten()
.unwrap_or(1)
.max(0);
if strikes > 0 {
let _ = db::add_member_strike_event(
pool,
bot_id,
guild_id_raw,
user_id.get() as i64,
trigger,
strikes,
)
.await;
}
let _ = ensure_default_punish_rules(pool, bot_id, guild_id_raw).await;
let rules = db::list_punish_rules(pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
let mut action = String::from("aucune");
for rule in rules.iter().rev() {
let Ok(total) = count_member_strikes_in_window(
pool,
bot_id,
guild_id_raw,
user_id.get() as i64,
rule.window_seconds,
)
.await
else {
continue;
};
if total < rule.threshold as i64 {
continue;
}
let recent_trigger =
get_last_punish_triggered_at(pool, bot_id, guild_id_raw, user_id.get() as i64, rule.id)
.await
.ok()
.flatten()
.map(|at| Utc::now() - at < chrono::Duration::seconds(rule.window_seconds))
.unwrap_or(false);
if recent_trigger {
continue;
}
action = execute_rule(ctx, guild_id, user_id, rule, settings).await;
let _ = upsert_last_punish_triggered_at(
pool,
bot_id,
guild_id_raw,
user_id.get() as i64,
rule.id,
)
.await;
break;
}
let embed = CreateEmbed::new()
.title("AutoMod")
.description(format!(
"{}\nMembre: <@{}>\nTrigger: `{}` · Profil: `{}` · Strikes: `+{}`\nAction: `{}`",
reason,
user_id.get(),
trigger,
profile,
strikes,
action
))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
}
pub async fn enforce_automod_message(ctx: &Context, msg: &Message) -> bool {
let Some(guild_id) = msg.guild_id else {
return false;
};
let Some(pool) = pool(ctx).await else {
return false;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let settings =
match db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await {
Ok(settings) => settings,
Err(_) => return false,
};
let channel_id = msg.channel_id.get() as i64;
let spam_override = db::get_moderation_channel_override(
&pool,
bot_id,
guild_id.get() as i64,
channel_id,
"spam",
)
.await
.ok()
.flatten();
let link_override = db::get_moderation_channel_override(
&pool,
bot_id,
guild_id.get() as i64,
channel_id,
"link",
)
.await
.ok()
.flatten();
let antispam_enabled =
apply_channel_override(settings.antispam_enabled, spam_override.as_deref());
let antilink_enabled =
apply_channel_override(settings.antilink_enabled, link_override.as_deref());
if settings.badwords_enabled {
let content = msg.content.to_lowercase();
let badwords = db::list_badwords(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or_default();
if badwords
.iter()
.any(|word| !word.is_empty() && content.contains(word))
{
apply_violation(
ctx,
msg,
&pool,
&settings,
"badword",
"Message supprime: mot interdit detecte.",
)
.await;
return true;
}
}
if settings.antimassmention_enabled {
let mention_count = msg.mentions.len() + msg.mention_roles.len();
if mention_count >= settings.antimassmention_limit.max(1) as usize {
apply_violation(
ctx,
msg,
&pool,
&settings,
"massmention",
"Message supprime: spam de mentions detecte.",
)
.await;
return true;
}
}
if antilink_enabled {
let link_hit = if settings.antilink_mode.eq_ignore_ascii_case("all") {
contains_any_link(&msg.content)
} else {
contains_invite_link(&msg.content)
};
if link_hit {
apply_violation(
ctx,
msg,
&pool,
&settings,
"link",
"Message supprime: lien interdit detecte.",
)
.await;
return true;
}
}
if antispam_enabled {
let hit = spam_hit(
ctx.cache.current_user().id.get(),
guild_id.get(),
msg.author.id.get(),
settings.antispam_limit.max(1),
settings.antispam_window_seconds.max(1),
);
if hit {
apply_violation(
ctx,
msg,
&pool,
&settings,
"spam",
"Message supprime: spam detecte.",
)
.await;
return true;
}
}
false
}
pub async fn public_command_allowed(
ctx: &Context,
msg: &Message,
command_key: &str,
required_permission: u8,
) -> bool {
if permissions::is_owner_user(ctx, msg.author.id).await {
return true;
}
if required_permission > 0 {
return true;
}
let Some(guild_id) = msg.guild_id else {
return true;
};
let Some(pool) = pool(ctx).await else {
return true;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let settings =
match db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await {
Ok(settings) => settings,
Err(_) => return true,
};
let override_mode = db::get_moderation_channel_override(
&pool,
bot_id,
guild_id.get() as i64,
msg.channel_id.get() as i64,
"public",
)
.await
.ok()
.flatten();
let allowed = match override_mode.as_deref() {
Some(mode) if mode.eq_ignore_ascii_case("allow") => true,
Some(mode) if mode.eq_ignore_ascii_case("deny") => false,
_ => settings.public_commands_enabled,
};
if allowed {
return true;
}
let embed = CreateEmbed::new()
.title("Commandes publiques desactivees")
.description(format!(
"La commande `{}` est desactivee dans ce salon.",
command_key.replace('_', " ")
))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
false
}
+9 -1
View File
@@ -11,7 +11,15 @@ use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
use crate::commands::logs_command_helpers::{pool, set_log_channel};
const LOG_TYPES: &[&str] = &["moderation", "message", "voice", "boost", "role", "raid", "channel"];
const LOG_TYPES: &[&str] = &[
"moderation",
"message",
"voice",
"boost",
"role",
"raid",
"channel",
];
const LOG_CATEGORY_NAME: &str = "📁 ➜ Espace Logs";
const LOG_CHANNEL_PREFIX: &str = "📁・";
const AUTOCONFIGLOG_COMPONENT_PREFIX: &str = "autoconfiglog";
+4 -1
View File
@@ -125,7 +125,10 @@ pub async fn emit_log(
embed = embed.timestamp(timestamp);
record_audit_log(ctx, guild_id, log_type, user_id, channel_id, role_id, action).await;
record_audit_log(
ctx, guild_id, log_type, user_id, channel_id, role_id, action,
)
.await;
if let Some(log_channel_id) = get_log_channel(ctx, guild_id, log_type).await {
let _ = log_channel_id
+58
View File
@@ -13,16 +13,29 @@ pub mod alladmins;
pub mod allbots;
#[path = "permissions/allperms.rs"]
pub mod allperms;
#[path = "roles/ancien.rs"]
pub mod ancien;
#[path = "moderation/antilink.rs"]
pub mod antilink;
#[path = "moderation/antimassmention.rs"]
pub mod antimassmention;
#[path = "moderation/antiraideautoconfig.rs"]
pub mod antiraideautoconfig;
#[path = "moderation/antispam.rs"]
pub mod antispam;
#[path = "outils/autobackup.rs"]
pub mod autobackup;
#[path = "logs/autoconfiglog.rs"]
pub mod autoconfiglog;
pub mod automod_service;
#[path = "outils/autopublish.rs"]
pub mod autopublish;
#[path = "outils/autoreact.rs"]
pub mod autoreact;
#[path = "outils/backup.rs"]
pub mod backup;
#[path = "moderation/badwords.rs"]
pub mod badwords;
#[path = "moderation/ban.rs"]
pub mod ban;
#[path = "moderation/banlist.rs"]
@@ -63,8 +76,12 @@ pub mod claim;
pub mod cleanup;
#[path = "moderation/clear_all_sanctions.rs"]
pub mod clear_all_sanctions;
#[path = "moderation/clear_badwords.rs"]
pub mod clear_badwords;
#[path = "administration/clear_bl.rs"]
pub mod clear_bl;
#[path = "moderation/clear_limit.rs"]
pub mod clear_limit;
#[path = "moderation/clear_messages.rs"]
pub mod clear_messages;
#[path = "administration/clear_owners.rs"]
@@ -125,6 +142,8 @@ pub mod kick;
pub mod leave;
#[path = "logs/leave_settings.rs"]
pub mod leave_settings;
#[path = "moderation/link.rs"]
pub mod link;
#[path = "bot/listen.rs"]
pub mod listen;
#[path = "outils/loading.rs"]
@@ -154,8 +173,12 @@ pub mod mp;
pub mod mute;
#[path = "moderation/mutelist.rs"]
pub mod mutelist;
#[path = "moderation/muterole.rs"]
pub mod muterole;
#[path = "outils/newsticker.rs"]
pub mod newsticker;
#[path = "roles/noderank.rs"]
pub mod noderank;
#[path = "logs/nolog.rs"]
pub mod nolog;
#[path = "bot/online.rs"]
@@ -168,12 +191,18 @@ pub mod perms_helpers;
pub mod perms_service;
#[path = "infos/pic.rs"]
pub mod pic;
#[path = "outils/piconly.rs"]
pub mod piconly;
#[path = "infos/ping.rs"]
pub mod ping;
#[path = "bot/playto.rs"]
pub mod playto;
#[path = "administration/prefix.rs"]
pub mod prefix;
#[path = "moderation/public.rs"]
pub mod public;
#[path = "moderation/punish.rs"]
pub mod punish;
#[path = "logs/raidlog.rs"]
pub mod raidlog;
#[path = "bot/remove_activity.rs"]
@@ -184,6 +213,8 @@ pub mod rename;
pub mod renew;
#[path = "outils/reroll.rs"]
pub mod reroll;
#[path = "moderation/resetantiraide.rs"]
pub mod resetantiraide;
#[path = "infos/role.rs"]
pub mod role;
#[path = "logs/rolelog.rs"]
@@ -206,6 +237,8 @@ pub mod set;
pub mod set_boostembed;
#[path = "logs/set_modlogs.rs"]
pub mod set_modlogs;
#[path = "moderation/set_muterole.rs"]
pub mod set_muterole;
#[path = "bot/shadowbot.rs"]
pub mod shadowbot;
#[path = "infos/showpics.rs"]
@@ -214,8 +247,12 @@ pub mod showpics;
pub mod slowmode;
#[path = "outils/snipe.rs"]
pub mod snipe;
#[path = "moderation/spam.rs"]
pub mod spam;
#[path = "bot/stream.rs"]
pub mod stream;
#[path = "moderation/strikes.rs"]
pub mod strikes;
#[path = "outils/suggestion.rs"]
pub mod suggestion;
#[path = "roles/sync.rs"]
@@ -240,6 +277,8 @@ pub mod ticket;
pub mod ticket_member;
#[path = "outils/tickets.rs"]
pub mod tickets;
#[path = "moderation/timeout.rs"]
pub mod timeout;
#[path = "moderation/unban.rs"]
pub mod unban;
#[path = "moderation/unbanall.rs"]
@@ -286,9 +325,12 @@ pub mod watch;
pub fn all_command_metadata() -> Vec<CommandMetadata> {
vec![
ping::COMMAND_DESCRIPTOR.metadata(),
timeout::COMMAND_DESCRIPTOR.metadata(),
allbots::COMMAND_DESCRIPTOR.metadata(),
alladmins::COMMAND_DESCRIPTOR.metadata(),
botadmins::COMMAND_DESCRIPTOR.metadata(),
ancien::COMMAND_DESCRIPTOR.metadata(),
antiraideautoconfig::COMMAND_DESCRIPTOR.metadata(),
boosters::COMMAND_DESCRIPTOR.metadata(),
rolemembers::COMMAND_DESCRIPTOR.metadata(),
rolemenu::COMMAND_DESCRIPTOR.metadata(),
@@ -322,6 +364,20 @@ pub fn all_command_metadata() -> Vec<CommandMetadata> {
choose::COMMAND_DESCRIPTOR.metadata(),
embed::COMMAND_DESCRIPTOR.metadata(),
clear_messages::COMMAND_DESCRIPTOR.metadata(),
clear_limit::COMMAND_DESCRIPTOR.metadata(),
clear_badwords::COMMAND_DESCRIPTOR.metadata(),
muterole::COMMAND_DESCRIPTOR.metadata(),
set_muterole::COMMAND_DESCRIPTOR.metadata(),
antispam::COMMAND_DESCRIPTOR.metadata(),
antilink::COMMAND_DESCRIPTOR.metadata(),
antimassmention::COMMAND_DESCRIPTOR.metadata(),
badwords::COMMAND_DESCRIPTOR.metadata(),
spam::COMMAND_DESCRIPTOR.metadata(),
link::COMMAND_DESCRIPTOR.metadata(),
strikes::COMMAND_DESCRIPTOR.metadata(),
punish::COMMAND_DESCRIPTOR.metadata(),
public::COMMAND_DESCRIPTOR.metadata(),
resetantiraide::COMMAND_DESCRIPTOR.metadata(),
backup::COMMAND_DESCRIPTOR.metadata(),
ticket::COMMAND_DESCRIPTOR.metadata(),
claim::COMMAND_DESCRIPTOR.metadata(),
@@ -330,6 +386,7 @@ pub fn all_command_metadata() -> Vec<CommandMetadata> {
close::COMMAND_DESCRIPTOR.metadata(),
tickets::COMMAND_DESCRIPTOR.metadata(),
showpics::COMMAND_DESCRIPTOR.metadata(),
piconly::COMMAND_DESCRIPTOR.metadata(),
suggestion::COMMAND_DESCRIPTOR.metadata(),
autopublish::COMMAND_DESCRIPTOR.metadata(),
tempvoc::COMMAND_DESCRIPTOR.metadata(),
@@ -372,6 +429,7 @@ pub fn all_command_metadata() -> Vec<CommandMetadata> {
addrole::COMMAND_DESCRIPTOR.metadata(),
delrole::COMMAND_DESCRIPTOR.metadata(),
derank::COMMAND_DESCRIPTOR.metadata(),
noderank::COMMAND_DESCRIPTOR.metadata(),
del_sanction::COMMAND_DESCRIPTOR.metadata(),
clear_sanctions::COMMAND_DESCRIPTOR.metadata(),
clear_all_sanctions::COMMAND_DESCRIPTOR.metadata(),
+100
View File
@@ -0,0 +1,100 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_on_off, pool};
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_antilink(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(first) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiLink")
.description("Usage: +antilink <on/off> | +antilink <invite/all>")
.color(0xED4245),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(current) =
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await
else {
return;
};
let updated = if let Some(value) = parse_on_off(first) {
db::set_antilink_settings(
&pool,
bot_id,
guild_id.get() as i64,
value,
&current.antilink_mode,
)
.await
.ok()
} else if first.eq_ignore_ascii_case("invite") || first.eq_ignore_ascii_case("all") {
db::set_antilink_settings(
&pool,
bot_id,
guild_id.get() as i64,
current.antilink_enabled,
&first.to_lowercase(),
)
.await
.ok()
} else {
None
};
let Some(updated) = updated else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiLink")
.description(format!(
"Etat: **{}**\nMode: **{}**",
if updated.antilink_enabled {
"ON"
} else {
"OFF"
},
updated.antilink_mode
))
.color(0x57F287),
)
.await;
}
pub struct AntilinkCommand;
pub static COMMAND_DESCRIPTOR: AntilinkCommand = AntilinkCommand;
impl crate::commands::command_contract::CommandSpec for AntilinkCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "antilink",
category: "moderation",
params: "<on/off> | <invite/all>",
description: "Active ou configure la protection anti liens.",
examples: &["+antilink on", "+antilink invite", "+help antilink"],
default_aliases: &["alink"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_on_off, pool};
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_antimassmention(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(first) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiMassMention")
.description("Usage: +antimassmention <on/off> | +antimassmention <nombre>")
.color(0xED4245),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(current) =
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await
else {
return;
};
let updated = if let Some(value) = parse_on_off(first) {
db::set_antimassmention_settings(
&pool,
bot_id,
guild_id.get() as i64,
value,
current.antimassmention_limit,
)
.await
.ok()
} else if let Ok(limit) = first.parse::<i32>() {
db::set_antimassmention_settings(
&pool,
bot_id,
guild_id.get() as i64,
current.antimassmention_enabled,
limit.clamp(1, 50),
)
.await
.ok()
} else {
None
};
let Some(updated) = updated else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiMassMention")
.description(format!(
"Etat: **{}**\nSeuil: **{} mention(s)**",
if updated.antimassmention_enabled {
"ON"
} else {
"OFF"
},
updated.antimassmention_limit
))
.color(0x57F287),
)
.await;
}
pub struct AntimassmentionCommand;
pub static COMMAND_DESCRIPTOR: AntimassmentionCommand = AntimassmentionCommand;
impl crate::commands::command_contract::CommandSpec for AntimassmentionCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "antimassmention",
category: "moderation",
params: "<on/off> | <nombre>",
description: "Active ou configure la protection anti spam de mentions.",
examples: &[
"+antimassmention on",
"+antimassmention 6",
"+help antimassmention",
],
default_aliases: &["amm"],
allow_in_dm: false,
default_permission: 8,
}
}
}
@@ -0,0 +1,137 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
use crate::db;
const DEFAULT_STRIKE_RULES: &[(&str, &str, i32)] = &[
("spam", "new", 2),
("spam", "old", 1),
("link", "new", 2),
("link", "old", 1),
("massmention", "new", 3),
("massmention", "old", 2),
("badword", "new", 2),
("badword", "old", 1),
];
pub async fn handle_antiraideautoconfig(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
let mut failed = Vec::new();
if db::set_antispam_settings(&pool, bot_id, guild_id_raw, true, 6, 5)
.await
.is_err()
{
failed.push("antispam");
}
if db::set_antilink_settings(&pool, bot_id, guild_id_raw, true, "invite")
.await
.is_err()
{
failed.push("antilink");
}
if db::set_antimassmention_settings(&pool, bot_id, guild_id_raw, true, 5)
.await
.is_err()
{
failed.push("antimassmention");
}
if db::set_badwords_enabled(&pool, bot_id, guild_id_raw, true)
.await
.is_err()
{
failed.push("badwords");
}
if db::clear_moderation_channel_overrides_by_kind(&pool, bot_id, guild_id_raw, "spam")
.await
.is_err()
{
failed.push("spam overrides");
}
if db::clear_moderation_channel_overrides_by_kind(&pool, bot_id, guild_id_raw, "link")
.await
.is_err()
{
failed.push("link overrides");
}
for (trigger, profile, strike_count) in DEFAULT_STRIKE_RULES {
if db::upsert_strike_rule(&pool, bot_id, guild_id_raw, trigger, profile, *strike_count)
.await
.is_err()
{
failed.push("strikes");
break;
}
}
if db::setup_default_punish_rules(&pool, bot_id, guild_id_raw)
.await
.is_err()
{
failed.push("punish");
}
let mut description = String::from(
"Configuration anti raid appliquee.\n\n- Antispam: ON (6/5s)\n- AntiLink: ON (invite)\n- AntiMassMention: ON (5)\n- BadWords: ON\n- Strikes: profils par defaut\n- Punish: regles par defaut",
);
if !failed.is_empty() {
description.push_str("\n\nErreurs detectees: ");
description.push_str(&failed.join(", "));
}
let color = if failed.is_empty() {
theme_color(ctx).await
} else {
0xFEE75C
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiRaid AutoConfig")
.description(description)
.color(color),
)
.await;
}
pub struct AntiraideautoconfigCommand;
pub static COMMAND_DESCRIPTOR: AntiraideautoconfigCommand = AntiraideautoconfigCommand;
impl crate::commands::command_contract::CommandSpec for AntiraideautoconfigCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "antiraideautoconfig",
category: "moderation",
params: "aucun",
description: "Configure automatiquement les protections anti raid du serveur.",
examples: &["+antiraideautoconfig", "+help antiraideautoconfig"],
default_aliases: &["arcfg"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+104
View File
@@ -0,0 +1,104 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{format_duration, parse_on_off, parse_rate_limit, pool};
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_antispam(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(first) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiSpam")
.description("Usage: +antispam <on/off> | +antispam <nombre>/<duree>")
.color(0xED4245),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(current) =
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await
else {
return;
};
let updated = if let Some(value) = parse_on_off(first) {
db::set_antispam_settings(
&pool,
bot_id,
guild_id.get() as i64,
value,
current.antispam_limit,
current.antispam_window_seconds,
)
.await
.ok()
} else if let Some((limit, window)) = parse_rate_limit(first) {
db::set_antispam_settings(
&pool,
bot_id,
guild_id.get() as i64,
current.antispam_enabled,
limit,
window,
)
.await
.ok()
} else {
None
};
let Some(updated) = updated else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AntiSpam")
.description(format!(
"Etat: **{}**\nSensibilite: **{}/{}**",
if updated.antispam_enabled {
"ON"
} else {
"OFF"
},
updated.antispam_limit,
format_duration(updated.antispam_window_seconds as i64)
))
.color(0x57F287),
)
.await;
}
pub struct AntispamCommand;
pub static COMMAND_DESCRIPTOR: AntispamCommand = AntispamCommand;
impl crate::commands::command_contract::CommandSpec for AntispamCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "antispam",
category: "moderation",
params: "<on/off> | <nombre>/<duree>",
description: "Active ou configure la protection antispam du serveur.",
examples: &["+antispam on", "+antispam 6/5s", "+help antispam"],
default_aliases: &["aspam"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+142
View File
@@ -0,0 +1,142 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_on_off, pool};
use crate::commands::common::{add_list_fields, send_embed};
use crate::db;
pub async fn handle_badwords(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
if args.is_empty() {
let Ok(settings) =
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await
else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("BadWords")
.description(format!(
"Etat: **{}**\nUsage: +badwords <on/off|add/del/list>",
if settings.badwords_enabled {
"ON"
} else {
"OFF"
}
))
.color(0x5865F2),
)
.await;
return;
}
let action = args[0].to_lowercase();
if action == "list" {
let words = db::list_badwords(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or_default();
let lines = words
.into_iter()
.map(|word| format!("- {}", word))
.collect::<Vec<_>>();
let mut embed = CreateEmbed::new().title("BadWords list").color(0x5865F2);
embed = add_list_fields(embed, &lines, "Mots interdits");
send_embed(ctx, msg, embed).await;
return;
}
if let Some(value) = parse_on_off(&action) {
let _ = db::set_badwords_enabled(&pool, bot_id, guild_id.get() as i64, value).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("BadWords")
.description(format!(
"Protection badwords: **{}**",
if value { "ON" } else { "OFF" }
))
.color(0x57F287),
)
.await;
return;
}
if action == "add" {
let Some(word) = args.get(1) else {
return;
};
let _ = db::add_badword(&pool, bot_id, guild_id.get() as i64, word).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("BadWords")
.description(format!("Mot ajoute: **{}**", word))
.color(0x57F287),
)
.await;
return;
}
if action == "del" || action == "remove" {
let Some(word) = args.get(1) else {
return;
};
let removed = db::remove_badword(&pool, bot_id, guild_id.get() as i64, word)
.await
.unwrap_or(0);
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("BadWords")
.description(format!("Mot supprime: **{}** ({}).", word, removed))
.color(0x57F287),
)
.await;
return;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("BadWords")
.description("Usage: +badwords <on/off|add <mot>|del <mot>|list>")
.color(0xED4245),
)
.await;
}
pub struct BadwordsCommand;
pub static COMMAND_DESCRIPTOR: BadwordsCommand = BadwordsCommand;
impl crate::commands::command_contract::CommandSpec for BadwordsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "badwords",
category: "moderation",
params: "<on/off|add <mot>|del <mot>|list>",
description: "Active la protection badwords et gere la liste des mots interdits.",
examples: &["+badwords on", "+badwords add insulte", "+badwords list"],
default_aliases: &["bw"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+50
View File
@@ -0,0 +1,50 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_clear_badwords(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let cleared = db::clear_badwords(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or(0);
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Clear BadWords")
.description(format!("{} mot(s) interdit(s) supprime(s).", cleared))
.color(0x57F287),
)
.await;
}
pub struct ClearBadwordsCommand;
pub static COMMAND_DESCRIPTOR: ClearBadwordsCommand = ClearBadwordsCommand;
impl crate::commands::command_contract::CommandSpec for ClearBadwordsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "clear_badwords",
category: "moderation",
params: "badwords",
description: "Supprime l ensemble des mots interdits enregistres.",
examples: &["+clear badwords", "+help clear badwords"],
default_aliases: &["cbw"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+74
View File
@@ -0,0 +1,74 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_clear_limit(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(raw_value) = args.get(1) else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Clear Limit")
.description("Usage: +clear limit <nombre>")
.color(0xED4245),
)
.await;
return;
};
let Ok(value) = raw_value.parse::<i32>() else {
return;
};
let clamped = value.clamp(1, 1_000);
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
if db::set_clear_limit(&pool, bot_id, guild_id.get() as i64, clamped)
.await
.is_err()
{
return;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Clear Limit")
.description(format!(
"Limite de suppression definie a **{}** message(s) par commande clear.",
clamped
))
.color(0x57F287),
)
.await;
}
pub struct ClearLimitCommand;
pub static COMMAND_DESCRIPTOR: ClearLimitCommand = ClearLimitCommand;
impl crate::commands::command_contract::CommandSpec for ClearLimitCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "clear_limit",
category: "moderation",
params: "limit <nombre>",
description: "Definit la limite max de messages supprimables avec +clear.",
examples: &["+clear limit 100", "+help clear limit"],
default_aliases: &["climit"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+23 -1
View File
@@ -4,6 +4,7 @@ use serenity::prelude::*;
use crate::commands::admin_common::parse_user_id;
use crate::commands::common::{send_embed, theme_color};
use crate::db::{self, DbPoolKey};
pub async fn handle_clear_messages(ctx: &Context, msg: &Message, args: &[&str]) {
let Ok(mut amount) = args.first().unwrap_or(&"0").parse::<u64>() else {
@@ -12,7 +13,28 @@ pub async fn handle_clear_messages(ctx: &Context, msg: &Message, args: &[&str])
if amount == 0 {
return;
}
amount = amount.clamp(1, 100);
let max_limit = if let Some(guild_id) = msg.guild_id {
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let bot_id = ctx.cache.current_user().id.get() as i64;
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64)
.await
.ok()
.map(|settings| settings.clear_limit.max(1) as u64)
.unwrap_or(100)
} else {
100
}
} else {
100
};
amount = amount.clamp(1, max_limit);
let filter_user = args.get(1).and_then(|raw| parse_user_id(raw));
+103
View File
@@ -0,0 +1,103 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
pub async fn handle_link_override(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(action) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Link")
.description("Usage: +link <allow/deny/reset> [#salon]")
.color(0xED4245),
)
.await;
return;
};
let channel_id = args
.get(1)
.and_then(|raw| parse_channel_id(raw))
.unwrap_or(msg.channel_id);
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
let channel_id_raw = channel_id.get() as i64;
let description = if action.eq_ignore_ascii_case("allow") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"link",
"allow",
)
.await;
format!("AntiLink desactive dans <#{}>.", channel_id.get())
} else if action.eq_ignore_ascii_case("deny") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"link",
"deny",
)
.await;
format!("AntiLink force dans <#{}>.", channel_id.get())
} else if action.eq_ignore_ascii_case("reset") {
let _ = db::remove_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"link",
)
.await;
format!("Override antilink supprime dans <#{}>.", channel_id.get())
} else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Link Override")
.description(description)
.color(0x57F287),
)
.await;
}
pub struct LinkCommand;
pub static COMMAND_DESCRIPTOR: LinkCommand = LinkCommand;
impl crate::commands::command_contract::CommandSpec for LinkCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "link",
category: "moderation",
params: "<allow/deny/reset> [#salon]",
description: "Definit l override antilink pour un salon (allow, deny, reset).",
examples: &["+link allow #general", "+link deny #regles", "+link reset"],
default_aliases: &["linkch"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+141
View File
@@ -0,0 +1,141 @@
use serenity::builder::{CreateEmbed, EditRole};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::send_embed;
use crate::db;
fn mute_permissions() -> Permissions {
Permissions::SEND_MESSAGES | Permissions::ADD_REACTIONS | Permissions::SPEAK
}
pub async fn handle_muterole(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(settings) =
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64).await
else {
return;
};
let mut role_id = settings
.mute_role_id
.and_then(|raw| u64::try_from(raw).ok())
.map(RoleId::new);
let Ok(partial_guild) = guild_id.to_partial_guild(&ctx.http).await else {
return;
};
if role_id.is_none() {
role_id = partial_guild
.roles
.values()
.find(|role| role.name.eq_ignore_ascii_case("Muted"))
.map(|role| role.id);
}
if role_id.is_none() {
let created = guild_id
.create_role(
&ctx.http,
EditRole::new()
.name("Muted")
.permissions(Permissions::empty()),
)
.await
.ok();
role_id = created.map(|role| role.id);
}
let Some(role_id) = role_id else {
return;
};
let mut failed_channels = Vec::new();
if let Ok(channels) = guild_id.channels(&ctx.http).await {
for channel in channels.values() {
let result = channel
.create_permission(
&ctx.http,
PermissionOverwrite {
allow: Permissions::empty(),
deny: mute_permissions(),
kind: PermissionOverwriteType::Role(role_id),
},
)
.await;
if result.is_err() {
failed_channels.push(channel.id.get());
}
}
}
let _ = db::set_mute_role(
&pool,
bot_id,
guild_id.get() as i64,
Some(role_id.get() as i64),
)
.await;
let mut description = format!(
"Role muet configure: <@&{}>.\nPermissions appliquees sur les salons du serveur.",
role_id.get()
);
if !failed_channels.is_empty() {
let list = failed_channels
.iter()
.take(10)
.map(|id| format!("<#{}>", id))
.collect::<Vec<_>>()
.join(", ");
description.push_str(&format!(
"\nErreurs permissions: {} salon(s). {}",
failed_channels.len(),
list
));
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("MuteRole")
.description(description)
.color(if failed_channels.is_empty() {
0x57F287
} else {
0xFEE75C
}),
)
.await;
}
pub struct MuteRoleCommand;
pub static COMMAND_DESCRIPTOR: MuteRoleCommand = MuteRoleCommand;
impl crate::commands::command_contract::CommandSpec for MuteRoleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "muterole",
category: "moderation",
params: "aucun",
description: "Cree ou met a jour le role muet et tente de corriger les permissions des salons.",
examples: &["+muterole", "+help muterole"],
default_aliases: &["mr"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+126
View File
@@ -0,0 +1,126 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_on_off, pool};
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
pub async fn handle_public(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(first) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Public")
.description("Usage: +public <on/off> | +public <allow/deny/reset> [#salon]")
.color(0xED4245),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
if let Some(enabled) = parse_on_off(first) {
let _ = db::set_public_commands_enabled(&pool, bot_id, guild_id_raw, enabled).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Public")
.description(format!(
"Commandes publiques sur le serveur: **{}**",
if enabled { "ON" } else { "OFF" }
))
.color(0x57F287),
)
.await;
return;
}
let channel_id = args
.get(1)
.and_then(|raw| parse_channel_id(raw))
.unwrap_or(msg.channel_id);
let description = if first.eq_ignore_ascii_case("allow") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id.get() as i64,
"public",
"allow",
)
.await;
format!("Commandes publiques forcees dans <#{}>.", channel_id.get())
} else if first.eq_ignore_ascii_case("deny") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id.get() as i64,
"public",
"deny",
)
.await;
format!(
"Commandes publiques desactivees dans <#{}>.",
channel_id.get()
)
} else if first.eq_ignore_ascii_case("reset") {
let _ = db::remove_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id.get() as i64,
"public",
)
.await;
format!("Override public supprime dans <#{}>.", channel_id.get())
} else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Public")
.description(description)
.color(0x57F287),
)
.await;
}
pub struct PublicCommand;
pub static COMMAND_DESCRIPTOR: PublicCommand = PublicCommand;
impl crate::commands::command_contract::CommandSpec for PublicCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "public",
category: "moderation",
params: "<on/off> | <allow/deny/reset> [#salon]",
description: "Active/desactive les commandes publiques globalement ou par salon.",
examples: &[
"+public on",
"+public deny #annonces",
"+public reset #annonces",
],
default_aliases: &["pubc"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+176
View File
@@ -0,0 +1,176 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{
format_duration, parse_duration_to_seconds, parse_sanction, pool,
};
use crate::commands::common::send_embed;
use crate::db;
fn describe_rule(index: usize, rule: &db::PunishRule) -> String {
let sanction = if let Some(duration) = rule.sanction_seconds {
format!("{} {}", rule.sanction, format_duration(duration))
} else {
rule.sanction.clone()
};
format!(
"{}. {} strikes / {} -> {}",
index,
rule.threshold,
format_duration(rule.window_seconds),
sanction
)
}
pub async fn handle_punish(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
if args.is_empty() {
let rules = db::list_punish_rules(&pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
let description = if rules.is_empty() {
"Aucune regle.".to_string()
} else {
rules
.iter()
.enumerate()
.map(|(idx, rule)| describe_rule(idx + 1, rule))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Punish")
.description(description)
.color(0x5865F2),
)
.await;
return;
}
if args[0].eq_ignore_ascii_case("setup") {
let _ = db::setup_default_punish_rules(&pool, bot_id, guild_id_raw).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Punish")
.description("Regles par defaut restaurees.")
.color(0x57F287),
)
.await;
return;
}
if args[0].eq_ignore_ascii_case("add") {
if args.len() < 4 {
return;
}
let Ok(threshold) = args[1].parse::<i32>() else {
return;
};
let Some(window_seconds) = parse_duration_to_seconds(args[2]) else {
return;
};
let Some(sanction) = parse_sanction(args[3]) else {
return;
};
let sanction_seconds = args.get(4).and_then(|raw| parse_duration_to_seconds(raw));
let _ = db::upsert_punish_rule(
&pool,
bot_id,
guild_id_raw,
threshold.clamp(1, 200),
window_seconds,
sanction,
sanction_seconds,
)
.await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Punish")
.description("Regle ajoutee ou mise a jour.")
.color(0x57F287),
)
.await;
return;
}
if args[0].eq_ignore_ascii_case("del") {
let Some(raw_index) = args.get(1) else {
return;
};
let Ok(index) = raw_index.parse::<usize>() else {
return;
};
let rules = db::list_punish_rules(&pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
if index == 0 || index > rules.len() {
return;
}
let rule = &rules[index - 1];
let _ = db::delete_punish_rule_by_id(&pool, bot_id, guild_id_raw, rule.id).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Punish")
.description(format!("Regle {} supprimee.", index))
.color(0x57F287),
)
.await;
return;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Punish")
.description("Usage: +punish | +punish add <nombre> <duree> <sanction> [duree] | +punish del <numero> | +punish setup")
.color(0xED4245),
)
.await;
}
pub struct PunishCommand;
pub static COMMAND_DESCRIPTOR: PunishCommand = PunishCommand;
impl crate::commands::command_contract::CommandSpec for PunishCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "punish",
category: "moderation",
params: "[add <nombre> <duree> <sanction> [duree] | del <numero> | setup]",
description: "Affiche et gere les sanctions automatiques appliquees selon les strikes.",
examples: &["+punish", "+punish add 8 1h mute 30m", "+punish setup"],
default_aliases: &["pn"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+144
View File
@@ -0,0 +1,144 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
use crate::db;
const DEFAULT_STRIKE_RULES: &[(&str, &str, i32)] = &[
("spam", "new", 2),
("spam", "old", 1),
("link", "new", 2),
("link", "old", 1),
("massmention", "new", 3),
("massmention", "old", 2),
("badword", "new", 2),
("badword", "old", 1),
];
pub async fn handle_resetantiraide(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
let mut failed = Vec::new();
if db::set_antispam_settings(&pool, bot_id, guild_id_raw, false, 6, 5)
.await
.is_err()
{
failed.push("antispam");
}
if db::set_antilink_settings(&pool, bot_id, guild_id_raw, false, "invite")
.await
.is_err()
{
failed.push("antilink");
}
if db::set_antimassmention_settings(&pool, bot_id, guild_id_raw, false, 5)
.await
.is_err()
{
failed.push("antimassmention");
}
if db::set_badwords_enabled(&pool, bot_id, guild_id_raw, false)
.await
.is_err()
{
failed.push("badwords");
}
if db::clear_badwords(&pool, bot_id, guild_id_raw)
.await
.is_err()
{
failed.push("clear badwords");
}
if db::clear_moderation_channel_overrides_by_kind(&pool, bot_id, guild_id_raw, "spam")
.await
.is_err()
{
failed.push("spam overrides");
}
if db::clear_moderation_channel_overrides_by_kind(&pool, bot_id, guild_id_raw, "link")
.await
.is_err()
{
failed.push("link overrides");
}
for (trigger, profile, strike_count) in DEFAULT_STRIKE_RULES {
if db::upsert_strike_rule(&pool, bot_id, guild_id_raw, trigger, profile, *strike_count)
.await
.is_err()
{
failed.push("strikes");
break;
}
}
if db::setup_default_punish_rules(&pool, bot_id, guild_id_raw)
.await
.is_err()
{
failed.push("punish");
}
let mut description = String::from(
"Les protections anti raid ont ete remises a leur etat par defaut.\n\n- Antispam: OFF (6/5s)\n- AntiLink: OFF (invite)\n- AntiMassMention: OFF (5)\n- BadWords: OFF et liste vide\n- Overrides spam/link: supprimes\n- Strikes: profils par defaut\n- Punish: regles par defaut",
);
if !failed.is_empty() {
description.push_str("\n\nErreurs detectees: ");
description.push_str(&failed.join(", "));
}
let color = if failed.is_empty() {
theme_color(ctx).await
} else {
0xFEE75C
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Reset AntiRaid")
.description(description)
.color(color),
)
.await;
}
pub struct ResetantiraideCommand;
pub static COMMAND_DESCRIPTOR: ResetantiraideCommand = ResetantiraideCommand;
impl crate::commands::command_contract::CommandSpec for ResetantiraideCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "resetantiraide",
category: "moderation",
params: "aucun",
description: "Arrete et reinitialise les protections anti raid avec les valeurs par defaut.",
examples: &["+resetantiraide", "+help resetantiraide"],
default_aliases: &["rra"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+79
View File
@@ -0,0 +1,79 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::{parse_role, send_embed};
use crate::db;
pub async fn handle_set_muterole(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(raw_role) = args.get(1) else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Set MuteRole")
.description("Usage: +set muterole <@role/ID/nom>")
.color(0xED4245),
)
.await;
return;
};
let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else {
return;
};
let Some(role) = parse_role(&guild, raw_role) else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
if db::set_mute_role(
&pool,
bot_id,
guild_id.get() as i64,
Some(role.id.get() as i64),
)
.await
.is_err()
{
return;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("MuteRole")
.description(format!("Role muet defini sur <@&{}>.", role.id.get()))
.color(0x57F287),
)
.await;
}
pub struct SetMuteRoleCommand;
pub static COMMAND_DESCRIPTOR: SetMuteRoleCommand = SetMuteRoleCommand;
impl crate::commands::command_contract::CommandSpec for SetMuteRoleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "set_muterole",
category: "moderation",
params: "muterole <@role/ID/nom>",
description: "Definit le role utilise pour le mute lorsque le mode timeout est desactive.",
examples: &["+set muterole @Muted", "+help set muterole"],
default_aliases: &["smr"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+103
View File
@@ -0,0 +1,103 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
pub async fn handle_spam_override(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(action) = args.first() else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Spam")
.description("Usage: +spam <allow/deny/reset> [#salon]")
.color(0xED4245),
)
.await;
return;
};
let channel_id = args
.get(1)
.and_then(|raw| parse_channel_id(raw))
.unwrap_or(msg.channel_id);
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
let channel_id_raw = channel_id.get() as i64;
let description = if action.eq_ignore_ascii_case("allow") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"spam",
"allow",
)
.await;
format!("Antispam desactive dans <#{}>.", channel_id.get())
} else if action.eq_ignore_ascii_case("deny") {
let _ = db::set_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"spam",
"deny",
)
.await;
format!("Antispam force dans <#{}>.", channel_id.get())
} else if action.eq_ignore_ascii_case("reset") {
let _ = db::remove_moderation_channel_override(
&pool,
bot_id,
guild_id_raw,
channel_id_raw,
"spam",
)
.await;
format!("Override antispam supprime dans <#{}>.", channel_id.get())
} else {
return;
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Spam Override")
.description(description)
.color(0x57F287),
)
.await;
}
pub struct SpamCommand;
pub static COMMAND_DESCRIPTOR: SpamCommand = SpamCommand;
impl crate::commands::command_contract::CommandSpec for SpamCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "spam",
category: "moderation",
params: "<allow/deny/reset> [#salon]",
description: "Definit l override antispam pour un salon (allow, deny, reset).",
examples: &["+spam allow #general", "+spam deny #flood", "+spam reset"],
default_aliases: &["spamch"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+119
View File
@@ -0,0 +1,119 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_profile, parse_trigger, pool};
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_strikes(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
if args.is_empty() {
let rules = db::list_strike_rules(&pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
let mut lines = Vec::new();
for trigger in ["spam", "link", "massmention", "badword"] {
let new_count = rules
.iter()
.find(|r| r.trigger == trigger && r.profile == "new")
.map(|r| r.strike_count)
.unwrap_or(0);
let old_count = rules
.iter()
.find(|r| r.trigger == trigger && r.profile == "old")
.map(|r| r.strike_count)
.unwrap_or(0);
lines.push(format!(
"`{}` -> nouveau: `{}` | ancien: `{}`",
trigger, new_count, old_count
));
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Strikes")
.description(lines.join("\n"))
.color(0x5865F2),
)
.await;
return;
}
if args.len() < 2 {
return;
}
let Some(trigger) = parse_trigger(args[0]) else {
return;
};
let Ok(count) = args[1].parse::<i32>() else {
return;
};
let count = count.clamp(0, 20);
if let Some(profile) = parse_profile(args.get(2).copied()) {
let _ = db::upsert_strike_rule(&pool, bot_id, guild_id_raw, trigger, profile, count).await;
} else {
let _ = db::upsert_strike_rule(&pool, bot_id, guild_id_raw, trigger, "new", count).await;
let _ = db::upsert_strike_rule(&pool, bot_id, guild_id_raw, trigger, "old", count).await;
}
let rules = db::list_strike_rules(&pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
let new_count = rules
.iter()
.find(|r| r.trigger == trigger && r.profile == "new")
.map(|r| r.strike_count)
.unwrap_or(0);
let old_count = rules
.iter()
.find(|r| r.trigger == trigger && r.profile == "old")
.map(|r| r.strike_count)
.unwrap_or(0);
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Strikes")
.description(format!(
"Regle mise a jour pour `{}`\nNouveau: `{}`\nAncien: `{}`",
trigger, new_count, old_count
))
.color(0x57F287),
)
.await;
}
pub struct StrikesCommand;
pub static COMMAND_DESCRIPTOR: StrikesCommand = StrikesCommand;
impl crate::commands::command_contract::CommandSpec for StrikesCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "strikes",
category: "moderation",
params: "[<trigger> <nombre> [ancien/nouveau]]",
description: "Affiche ou modifie les strikes attribues pour chaque trigger automod.",
examples: &["+strikes", "+strikes spam 2", "+strikes link 1 ancien"],
default_aliases: &["stk"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+74
View File
@@ -0,0 +1,74 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::{parse_on_off, pool};
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_timeout_toggle(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(value) = args.first().and_then(|raw| parse_on_off(raw)) else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Timeout")
.description("Usage: +timeout <on/off>")
.color(0xED4245),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(settings) =
db::set_use_timeout_for_mute(&pool, bot_id, guild_id.get() as i64, value).await
else {
return;
};
let mode = if settings.use_timeout {
"Timeout Discord"
} else {
"Role mute"
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Timeout")
.description(format!(
"Mode mute mis a jour: **{}**.\nNote: les timeouts Discord sont limites a 28 jours.",
mode
))
.color(0x57F287),
)
.await;
}
pub struct TimeoutCommand;
pub static COMMAND_DESCRIPTOR: TimeoutCommand = TimeoutCommand;
impl crate::commands::command_contract::CommandSpec for TimeoutCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "timeout",
category: "moderation",
params: "<on/off>",
description: "Active ou desactive l utilisation du timeout Discord pour les mutes.",
examples: &["+timeout on", "+timeout off", "+help timeout"],
default_aliases: &["to"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+45 -9
View File
@@ -6,7 +6,7 @@ use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::parse_user_id;
use crate::db::DbPoolKey;
use crate::db::{self, DbPoolKey};
pub fn duration_from_input(input: &str) -> Option<Duration> {
let raw = input.trim().to_lowercase();
@@ -95,20 +95,56 @@ pub async fn handle_timeout(
users: &[UserId],
expires: Option<chrono::DateTime<Utc>>,
) -> usize {
let settings = if let Some(pool) = pool(ctx).await {
let bot_id = ctx.cache.current_user().id.get() as i64;
db::get_or_create_moderation_settings(&pool, bot_id, guild_id.get() as i64)
.await
.ok()
} else {
None
};
let mute_role_id = settings
.as_ref()
.and_then(|s| s.mute_role_id)
.and_then(|raw| u64::try_from(raw).ok())
.map(RoleId::new);
let use_timeout = settings
.as_ref()
.map(|s| s.use_timeout || s.mute_role_id.is_none())
.unwrap_or(true);
let mut done = 0usize;
for user_id in users {
if let Ok(mut member) = guild_id.member(&ctx.http, *user_id).await {
let mut builder = EditMember::new();
if let Some(ts) = expires {
if let Ok(discord_ts) = Timestamp::from_unix_timestamp(ts.timestamp()) {
builder = builder.disable_communication_until_datetime(discord_ts);
if use_timeout {
let mut builder = EditMember::new();
if let Some(ts) = expires {
if let Ok(discord_ts) = Timestamp::from_unix_timestamp(ts.timestamp()) {
builder = builder.disable_communication_until_datetime(discord_ts);
}
} else {
builder = builder.enable_communication();
}
if member.edit(&ctx.http, builder).await.is_ok() {
done += 1;
}
} else {
builder = builder.enable_communication();
}
let Some(role_id) = mute_role_id else {
continue;
};
if member.edit(&ctx.http, builder).await.is_ok() {
done += 1;
let result = if expires.is_some() {
member.add_role(&ctx.http, role_id).await
} else {
member.remove_role(&ctx.http, role_id).await
};
if result.is_ok() {
done += 1;
}
}
}
}
+1 -61
View File
@@ -2,10 +2,10 @@ use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use chrono::Utc;
use serenity::builder::EditMember;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::moderation_sanction_helpers::{channel_mute_users, handle_timeout};
use crate::db::DbPoolKey;
static MODERATION_TICK: OnceLock<Mutex<Instant>> = OnceLock::new();
@@ -15,66 +15,6 @@ async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
data.get::<DbPoolKey>().cloned()
}
async fn handle_timeout(
ctx: &Context,
guild_id: GuildId,
users: &[UserId],
expires: Option<chrono::DateTime<Utc>>,
) -> usize {
let mut done = 0usize;
for user_id in users {
if let Ok(mut member) = guild_id.member(&ctx.http, *user_id).await {
let mut builder = EditMember::new();
if let Some(ts) = expires {
if let Ok(discord_ts) = Timestamp::from_unix_timestamp(ts.timestamp()) {
builder = builder.disable_communication_until_datetime(discord_ts);
}
} else {
builder = builder.enable_communication();
}
if member.edit(&ctx.http, builder).await.is_ok() {
done += 1;
}
}
}
done
}
async fn channel_mute_users(
ctx: &Context,
channel_id: ChannelId,
users: &[UserId],
mute: bool,
) -> usize {
let mut done = 0usize;
for user_id in users {
let result = if mute {
channel_id
.create_permission(
&ctx.http,
PermissionOverwrite {
allow: Permissions::empty(),
deny: Permissions::SEND_MESSAGES
| Permissions::ADD_REACTIONS
| Permissions::SPEAK,
kind: PermissionOverwriteType::Member(*user_id),
},
)
.await
} else {
channel_id
.delete_permission(&ctx.http, PermissionOverwriteType::Member(*user_id))
.await
};
if result.is_ok() {
done += 1;
}
}
done
}
pub async fn maybe_run_maintenance(ctx: &Context, guild_id: Option<GuildId>) {
let Some(guild_id) = guild_id else {
return;
+209
View File
@@ -0,0 +1,209 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
fn is_image_filename(filename: &str) -> bool {
let extension = filename
.rsplit('.')
.next()
.unwrap_or("")
.to_ascii_lowercase();
matches!(
extension.as_str(),
"jpg" | "jpeg" | "png" | "gif" | "webp" | "bmp" | "heic" | "heif"
)
}
fn has_only_photo_attachments(msg: &Message) -> bool {
!msg.attachments.is_empty()
&& msg
.attachments
.iter()
.all(|attachment| is_image_filename(&attachment.filename))
}
fn is_piconly_command_message(content: &str, prefix: &str) -> bool {
if !content.starts_with(prefix) {
return false;
}
let without_prefix = content.trim_start_matches(prefix).trim();
without_prefix
.split_whitespace()
.next()
.map(|command| command.eq_ignore_ascii_case("piconly"))
.unwrap_or(false)
}
pub async fn enforce_piconly_message(
ctx: &Context,
msg: &Message,
content: &str,
prefix: &str,
) -> bool {
let Some(guild_id) = msg.guild_id else {
return false;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return false;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let is_selfie_channel = db::is_piconly_channel(
&pool,
bot_id,
guild_id.get() as i64,
msg.channel_id.get() as i64,
)
.await
.unwrap_or(false);
if !is_selfie_channel || is_piconly_command_message(content, prefix) {
return false;
}
if has_only_photo_attachments(msg) {
return false;
}
let _ = msg.delete(&ctx.http).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Salon selfie")
.description("Seules les photos sont autorisees dans ce salon.")
.color(0xED4245)
.timestamp(Utc::now()),
)
.await;
true
}
pub async fn handle_piconly(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
if args.is_empty() {
let channels = db::get_piconly_channels(&pool, bot_id, guild_id_i64)
.await
.unwrap_or_default();
let description = if channels.is_empty() {
"Aucun salon selfie configure.".to_string()
} else {
channels
.into_iter()
.map(|channel| format!("<#{}>", channel.channel_id))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description(description)
.timestamp(Utc::now()),
)
.await;
return;
}
let adding = args[0].eq_ignore_ascii_case("add");
let deleting = args[0].eq_ignore_ascii_case("del")
|| args[0].eq_ignore_ascii_case("remove")
|| args[0].eq_ignore_ascii_case("delete");
if !adding && !deleting {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description("Utilisation: +piconly <add/del> [#salon]")
.color(0xED4245),
)
.await;
return;
}
let channel_id = args
.get(1)
.and_then(|raw| parse_channel_id(raw))
.unwrap_or(msg.channel_id);
let result = if adding {
db::add_piconly_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
} else {
db::remove_piconly_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
};
if result.is_err() {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description("Impossible de mettre a jour le salon selfie.")
.color(0xED4245),
)
.await;
return;
}
let embed = if adding {
CreateEmbed::new()
.title("Salon selfie ajoute")
.description(format!("Salon: <#{}>", channel_id.get()))
.timestamp(Utc::now())
} else {
CreateEmbed::new()
.title("Salon selfie retire")
.description(format!("Salon: <#{}>", channel_id.get()))
.timestamp(Utc::now())
};
send_embed(ctx, msg, embed).await;
}
pub struct PiconlyCommand;
pub static COMMAND_DESCRIPTOR: PiconlyCommand = PiconlyCommand;
impl crate::commands::command_contract::CommandSpec for PiconlyCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "piconly",
category: "outils",
params: "<add/del> [salon]",
description: "Definit ou supprime un salon selfie, ou les membres ne peuvent envoyer que des photos.",
examples: &["+piconly", "+piconly add #selfie", "+piconly del #selfie"],
default_aliases: &["selfieonly"],
allow_in_dm: false,
default_permission: 8,
}
}
}
+14 -6
View File
@@ -448,7 +448,10 @@ fn help_lookup_to_key(input: &str) -> Option<&'static str> {
matched.or_else(|| help_metadata_lookup_key(input))
}
fn resolve_help_command_key(input: &str, alias_map: &BTreeMap<String, Vec<String>>) -> Option<String> {
fn resolve_help_command_key(
input: &str,
alias_map: &BTreeMap<String, Vec<String>>,
) -> Option<String> {
if let Some(key) = help_lookup_to_key(input) {
return Some(key.to_string());
}
@@ -628,7 +631,10 @@ fn build_help_view_pages(
let mut intro_lines = Vec::with_capacity(4 + HELP_PAGES.len());
intro_lines.push("Shadow Bot est un bot de gestion de serveur.".to_string());
intro_lines.push(String::new());
intro_lines.push(format!("**Nombre total de commandes :** {}", total_commands));
intro_lines.push(format!(
"**Nombre total de commandes :** {}",
total_commands
));
intro_lines.push("**Nombre de commandes par catégorie :**".to_string());
for (index, page) in HELP_PAGES.iter().enumerate() {
intro_lines.push(format!("{} : {}", page.title, counts[index]));
@@ -767,9 +773,8 @@ fn help_components(
HelpLayout::Select | HelpLayout::Hybrid => {
let mut options = Vec::with_capacity(HELP_PAGES.len() + 1);
options.push(
CreateSelectMenuOption::new("Présentation", "0").description(
"Shadow Bot, total des commandes et répartition par catégorie.",
),
CreateSelectMenuOption::new("Présentation", "0")
.description("Shadow Bot, total des commandes et répartition par catégorie."),
);
for (index, page) in HELP_PAGES.iter().enumerate() {
@@ -849,7 +854,10 @@ async fn build_command_help_embed(
.join("\n");
let mut embed = CreateEmbed::new()
.title(format!("Aide commande · +{}", doc.command.replace('_', " ")))
.title(format!(
"Aide commande · +{}",
doc.command.replace('_', " ")
))
.description(doc.description)
.field(
"Commande",
+507
View File
@@ -0,0 +1,507 @@
use chrono::Utc;
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage, CreateModal,
};
use serenity::model::application::{
ActionRowComponent, ButtonStyle, ComponentInteraction, InputTextStyle, ModalInteraction,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::theme_color;
use crate::db;
const ANCIEN_MENU: &str = "ancien:settings";
const ANCIEN_ROLE_INPUT_ID: &str = "role_id";
const ANCIEN_DELAY_INPUT_ID: &str = "delay";
fn parse_owner_id(custom_id: &str) -> Option<(String, u64)> {
let mut parts = custom_id.rsplitn(2, ':');
let owner = parts.next()?.parse::<u64>().ok()?;
let action = parts.next()?.to_string();
Some((action, owner))
}
fn modal_value(modal: &ModalInteraction, wanted_id: &str) -> Option<String> {
for row in &modal.data.components {
for component in &row.components {
if let ActionRowComponent::InputText(input) = component {
if input.custom_id == wanted_id {
return input.value.clone();
}
}
}
}
None
}
fn parse_role_id_input(raw: &str) -> Option<RoleId> {
let cleaned = raw.trim().trim_start_matches("<@&").trim_end_matches('>');
cleaned.parse::<u64>().ok().map(RoleId::new)
}
fn parse_delay_seconds(input: &str) -> Option<i64> {
let raw = input.trim().to_lowercase();
if raw.is_empty() {
return None;
}
let mut number = String::new();
let mut suffix = String::new();
for ch in raw.chars() {
if ch.is_ascii_digit() {
if !suffix.is_empty() {
return None;
}
number.push(ch);
} else if !ch.is_whitespace() {
suffix.push(ch);
}
}
let value = number.parse::<i64>().ok()?;
if value <= 0 {
return None;
}
let unit = if suffix.is_empty() { "j" } else { &suffix };
let seconds = match unit {
"s" | "sec" | "secs" | "seconde" | "secondes" => value,
"m" | "min" | "mins" | "minute" | "minutes" => value.checked_mul(60)?,
"h" | "heure" | "heures" => value.checked_mul(3_600)?,
"j" | "d" | "jour" | "jours" => value.checked_mul(86_400)?,
"w" | "sem" | "semaine" | "semaines" => value.checked_mul(604_800)?,
_ => return None,
};
Some(seconds.max(1))
}
fn format_delay(seconds: i64) -> String {
let mut remaining = seconds.max(1);
let days = remaining / 86_400;
remaining %= 86_400;
let hours = remaining / 3_600;
remaining %= 3_600;
let minutes = remaining / 60;
let mut parts = Vec::new();
if days > 0 {
parts.push(format!("{}j", days));
}
if hours > 0 {
parts.push(format!("{}h", hours));
}
if minutes > 0 {
parts.push(format!("{}m", minutes));
}
if parts.is_empty() {
parts.push(format!("{}s", seconds.max(1)));
}
parts.join(" ")
}
fn ancien_embed(settings: &db::OldMemberSettings) -> CreateEmbed {
let role_label = settings
.role_id
.and_then(|id| u64::try_from(id).ok())
.map(|id| format!("<@&{}>", id))
.unwrap_or_else(|| "Non configure".to_string());
CreateEmbed::new()
.title("Ancien")
.description("Definit au bout de combien de temps un membre devient ancien sur le serveur.")
.field(
"Statut",
if settings.enabled { "Actif" } else { "Inactif" },
true,
)
.field("Role ancien", role_label, true)
.field("Delai", format_delay(settings.delay_seconds), true)
.field(
"Configuration",
"Utilise le bouton Configurer pour definir l'ID du role et le delai.",
false,
)
.timestamp(Utc::now())
}
fn ancien_components(owner_id: UserId, settings: &db::OldMemberSettings) -> Vec<CreateActionRow> {
let toggle_label = if settings.enabled {
"Desactiver"
} else {
"Activer"
};
let toggle_style = if settings.enabled {
ButtonStyle::Danger
} else {
ButtonStyle::Success
};
vec![CreateActionRow::Buttons(vec![
CreateButton::new(format!("{}:toggle:{}", ANCIEN_MENU, owner_id.get()))
.label(toggle_label)
.style(toggle_style),
CreateButton::new(format!("{}:configure:{}", ANCIEN_MENU, owner_id.get()))
.label("Configurer")
.style(ButtonStyle::Primary),
CreateButton::new(format!("{}:refresh:{}", ANCIEN_MENU, owner_id.get()))
.label("Rafraichir")
.style(ButtonStyle::Secondary),
])]
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}
async fn show_menu(ctx: &Context, msg: &Message) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let settings = db::get_or_create_old_member_settings(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or(db::OldMemberSettings {
bot_id,
guild_id: guild_id.get() as i64,
role_id: None,
delay_seconds: 2_592_000,
enabled: false,
updated_at: Utc::now(),
});
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.embed(ancien_embed(&settings).color(theme_color(ctx).await))
.components(ancien_components(msg.author.id, &settings)),
)
.await;
}
async fn respond_ephemeral_component(
ctx: &Context,
component: &ComponentInteraction,
content: &str,
) {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content(content)
.ephemeral(true),
),
)
.await;
}
async fn respond_ephemeral_modal(ctx: &Context, modal: &ModalInteraction, content: &str) {
let _ = modal
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content(content)
.ephemeral(true),
),
)
.await;
}
pub async fn handle_ancien(ctx: &Context, msg: &Message, _args: &[&str]) {
show_menu(ctx, msg).await;
}
pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool {
if !component.data.custom_id.starts_with(ANCIEN_MENU) {
return false;
}
let Some((action, owner_id)) = parse_owner_id(&component.data.custom_id) else {
return false;
};
if component.user.id.get() != owner_id {
respond_ephemeral_component(
ctx,
component,
"Seul l'auteur du menu peut utiliser ces boutons.",
)
.await;
return true;
}
let Some(guild_id) = component.guild_id else {
return true;
};
let Some(pool) = pool(ctx).await else {
return true;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let settings = db::get_or_create_old_member_settings(&pool, bot_id, guild_id.get() as i64)
.await
.ok();
let Some(settings) = settings else {
return true;
};
if action.ends_with(":configure") {
let modal = CreateModal::new(component.data.custom_id.clone(), "Configurer Ancien")
.components(vec![
CreateActionRow::InputText(
CreateInputText::new(
InputTextStyle::Short,
"ID du role ancien (ou mention)",
ANCIEN_ROLE_INPUT_ID,
)
.required(true),
),
CreateActionRow::InputText(
CreateInputText::new(
InputTextStyle::Short,
"Delai (ex: 30j, 72h, 90m)",
ANCIEN_DELAY_INPUT_ID,
)
.required(true),
),
]);
let _ = component
.create_response(&ctx.http, CreateInteractionResponse::Modal(modal))
.await;
return true;
}
if action.ends_with(":toggle") {
if !settings.enabled && settings.role_id.is_none() {
respond_ephemeral_component(
ctx,
component,
"Configure d'abord le role et le delai avant d'activer.",
)
.await;
return true;
}
let updated = db::update_old_member_settings(
&pool,
bot_id,
guild_id.get() as i64,
settings.role_id,
settings.delay_seconds,
!settings.enabled,
)
.await
.ok();
if let Some(updated) = updated {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(ancien_embed(&updated).color(theme_color(ctx).await))
.components(ancien_components(component.user.id, &updated)),
),
)
.await;
}
return true;
}
if action.ends_with(":refresh") {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(ancien_embed(&settings).color(theme_color(ctx).await))
.components(ancien_components(component.user.id, &settings)),
),
)
.await;
return true;
}
false
}
pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) -> bool {
if !modal.data.custom_id.starts_with(ANCIEN_MENU) {
return false;
}
let Some((action, owner_id)) = parse_owner_id(&modal.data.custom_id) else {
return false;
};
if modal.user.id.get() != owner_id {
respond_ephemeral_modal(
ctx,
modal,
"Seul l'auteur du menu peut soumettre ce formulaire.",
)
.await;
return true;
}
if !action.contains(":configure") {
return false;
}
let Some(guild_id) = modal.guild_id else {
return true;
};
let Some(pool) = pool(ctx).await else {
return true;
};
let role_raw = modal_value(modal, ANCIEN_ROLE_INPUT_ID).unwrap_or_default();
let Some(role_id) = parse_role_id_input(&role_raw) else {
respond_ephemeral_modal(ctx, modal, "Role invalide. Fournis un ID ou une mention.").await;
return true;
};
let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else {
respond_ephemeral_modal(ctx, modal, "Impossible de verifier le role sur ce serveur.").await;
return true;
};
if !guild.roles.contains_key(&role_id) {
respond_ephemeral_modal(ctx, modal, "Le role indique n'existe pas sur ce serveur.").await;
return true;
}
let delay_raw = modal_value(modal, ANCIEN_DELAY_INPUT_ID).unwrap_or_default();
let Some(delay_seconds) = parse_delay_seconds(&delay_raw) else {
respond_ephemeral_modal(
ctx,
modal,
"Delai invalide. Exemples valides: 30j, 72h, 90m.",
)
.await;
return true;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let current = db::get_or_create_old_member_settings(&pool, bot_id, guild_id.get() as i64)
.await
.ok();
let Some(current) = current else {
return true;
};
let updated = db::update_old_member_settings(
&pool,
bot_id,
guild_id.get() as i64,
Some(role_id.get() as i64),
delay_seconds,
current.enabled,
)
.await
.ok();
if let Some(updated) = updated {
let _ = modal
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.embed(ancien_embed(&updated).color(theme_color(ctx).await))
.components(ancien_components(modal.user.id, &updated))
.ephemeral(true),
),
)
.await;
}
true
}
pub async fn maybe_assign_ancien_role(ctx: &Context, guild_id: GuildId, user_id: UserId) {
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let settings = db::get_or_create_old_member_settings(&pool, bot_id, guild_id.get() as i64)
.await
.ok();
let Some(settings) = settings else {
return;
};
if !settings.enabled {
return;
}
let Some(role_raw) = settings.role_id else {
return;
};
let Ok(role_id_u64) = u64::try_from(role_raw) else {
return;
};
let role_id = RoleId::new(role_id_u64);
let Ok(member) = guild_id.member(&ctx.http, user_id).await else {
return;
};
if member.user.bot || member.roles.contains(&role_id) {
return;
}
let joined_at = member.joined_at.unwrap_or_else(|| member.user.created_at());
let elapsed = Utc::now()
.timestamp()
.saturating_sub(joined_at.unix_timestamp());
if elapsed < settings.delay_seconds.max(1) {
return;
}
let _ = member.add_role(&ctx.http, role_id).await;
}
pub struct AncienCommand;
pub static COMMAND_DESCRIPTOR: AncienCommand = AncienCommand;
impl crate::commands::command_contract::CommandSpec for AncienCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "ancien",
category: "roles",
params: "aucun",
description: "Definit au bout de combien de temps un membre est considere comme ancien et recoit le role configure.",
examples: &["+ancien", "+help ancien"],
default_aliases: &[],
allow_in_dm: false,
default_permission: 8,
}
}
}
+19
View File
@@ -1,9 +1,13 @@
use std::collections::HashSet;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::{send_embed, theme_color};
use crate::commands::moderation_sanction_helpers::parse_targets;
use crate::db;
pub async fn handle_derank(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
@@ -18,12 +22,27 @@ pub async fn handle_derank(ctx: &Context, msg: &Message, args: &[&str]) {
return;
}
let protected_roles: HashSet<u64> = if let Some(pool) = pool(ctx).await {
let bot_id = ctx.cache.current_user().id.get() as i64;
db::list_noderank_roles(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or_default()
.into_iter()
.filter_map(|id| u64::try_from(id).ok())
.collect()
} else {
HashSet::new()
};
let mut done = 0usize;
for uid in &targets {
if let Ok(member) = guild_id.member(&ctx.http, *uid).await {
let roles = member.roles.clone();
let mut ok = true;
for role_id in roles {
if protected_roles.contains(&role_id.get()) {
continue;
}
if member.remove_role(&ctx.http, role_id).await.is_err() {
ok = false;
}
+101
View File
@@ -0,0 +1,101 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::automod_service::pool;
use crate::commands::common::{parse_role, send_embed};
use crate::db;
pub async fn handle_noderank(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_raw = guild_id.get() as i64;
if args.is_empty() {
let roles = db::list_noderank_roles(&pool, bot_id, guild_id_raw)
.await
.unwrap_or_default();
let description = if roles.is_empty() {
"Aucun role protege.".to_string()
} else {
roles
.iter()
.map(|role_id| format!("<@&{}>", role_id))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("NoDeRank")
.description(description)
.color(0x5865F2),
)
.await;
return;
}
if args.len() < 2 {
return;
}
let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else {
return;
};
let Some(role) = parse_role(&guild, args[1]) else {
return;
};
if args[0].eq_ignore_ascii_case("add") {
let _ = db::add_noderank_role(&pool, bot_id, guild_id_raw, role.id.get() as i64).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("NoDeRank")
.description(format!("Role protege ajoute: <@&{}>", role.id.get()))
.color(0x57F287),
)
.await;
return;
}
if args[0].eq_ignore_ascii_case("del") || args[0].eq_ignore_ascii_case("remove") {
let _ = db::remove_noderank_role(&pool, bot_id, guild_id_raw, role.id.get() as i64).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("NoDeRank")
.description(format!("Role protege retire: <@&{}>", role.id.get()))
.color(0x57F287),
)
.await;
}
}
pub struct NoderankCommand;
pub static COMMAND_DESCRIPTOR: NoderankCommand = NoderankCommand;
impl crate::commands::command_contract::CommandSpec for NoderankCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "noderank",
category: "roles",
params: "[add/del <@role/ID/nom>]",
description: "Definit des roles proteges qui ne sont pas retires par +derank.",
examples: &["+noderank", "+noderank add @VIP", "+noderank del @VIP"],
default_aliases: &["ndr"],
allow_in_dm: false,
default_permission: 8,
}
}
}
File diff suppressed because it is too large Load Diff
+1653 -2
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,10 +1,11 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::logs_service;
use crate::commands::{ancien, logs_service};
pub async fn handle_member_addition(ctx: &Context, new_member: &Member) {
logs_service::on_member_join(ctx, new_member.guild_id, &new_member.user).await;
ancien::maybe_assign_ancien_role(ctx, new_member.guild_id, new_member.user.id).await;
}
pub async fn handle_member_removal(ctx: &Context, guild_id: GuildId, user: &User) {
@@ -35,6 +36,8 @@ pub async fn handle_member_update(
new_member.premium_since,
)
.await;
ancien::maybe_assign_ancien_role(ctx, new_member.guild_id, new_member.user.id).await;
return;
}
+10 -2
View File
@@ -2,8 +2,8 @@ use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::{
advanced_tools, autoconfiglog, boostembed, help, helpsetting, mp, perms_service, rolemenu,
suggestion, tempvoc, ticket, viewlogs,
advanced_tools, ancien, autoconfiglog, boostembed, help, helpsetting, mp, perms_service,
rolemenu, suggestion, tempvoc, ticket, viewlogs,
};
pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) {
@@ -14,6 +14,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction)
}
if let Interaction::Component(component) = interaction {
if ancien::handle_component_interaction(ctx, component).await {
return;
}
if autoconfiglog::handle_component_interaction(ctx, component).await {
return;
}
@@ -63,6 +67,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction)
}
if let Interaction::Modal(modal) = interaction {
if ancien::handle_modal_interaction(ctx, modal).await {
return;
}
if ticket::handle_modal_interaction(ctx, modal).await {
return;
}
+78 -15
View File
@@ -6,19 +6,22 @@ use std::sync::{Mutex, OnceLock};
use crate::commands::moderation_tools;
use crate::commands::remove_activity;
use crate::commands::{
addrole, alias, autobackup, autoconfiglog, autopublish, autoreact, backup, ban, banlist,
banner, bl, blinfo, boostembed, boosters, boostlog, bringall, button, calc, change, changeall,
channel, choose, claim, cleanup, clear_all_sanctions, clear_bl, clear_messages, clear_owners,
clear_perms, clear_sanctions, close, cmute, compet, create, del, del_sanction, delrole, derank,
discussion, dnd, embed, emoji, end, giveaway, help, helpsetting, hide, hideall, idle,
invisible, invite, join, kick, leave, leave_settings, listen, loading, lock, lockall,
mainprefix, massiverole, member, messagelog, modlog, mp, mute, mutelist, newsticker, nolog,
online, owner, perms, pic, ping, playto, prefix, raidlog, rename, renew, reroll, role, rolelog,
addrole, alias, ancien, antilink, antimassmention, antiraideautoconfig, antispam, autobackup,
autoconfiglog, autopublish, autoreact, backup, badwords, ban, banlist, banner, bl, blinfo,
boostembed, boosters, boostlog, bringall, button, calc, change, changeall, channel, choose,
claim, cleanup, clear_all_sanctions, clear_badwords, clear_bl, clear_limit, clear_messages,
clear_owners, clear_perms, clear_sanctions, close, cmute, compet, create, del, del_sanction,
delrole, derank, discussion, dnd, embed, emoji, end, giveaway, help, helpsetting, hide,
hideall, idle, invisible, invite, join, kick, leave, leave_settings, link, listen, loading,
lock, lockall, mainprefix, massiverole, member, messagelog, modlog, mp, mute, mutelist,
muterole, newsticker, noderank, nolog, online, owner, perms, pic, piconly, ping, playto,
prefix, public, punish, raidlog, rename, renew, reroll, resetantiraide, role, rolelog,
rolemembers, rolemenu, sanctions, say, server, serverinfo, set, set_boostembed, set_modlogs,
shadowbot, showpics, slowmode, snipe, stream, suggestion, sync, tempban, tempcmute, tempmute,
temprole, tempvoc, tempvoc_cmd, theme, ticket, ticket_member, tickets, unban, unbanall, unbl,
uncmute, unhide, unhideall, unlock, unlockall, unmassiverole, unmute, unmuteall, unowner,
untemprole, user, viewlogs, vocinfo, voicekick, voicelog, voicemove, warn, watch,
set_muterole, shadowbot, showpics, slowmode, snipe, spam, stream, strikes, suggestion, sync,
tempban, tempcmute, tempmute, temprole, tempvoc, tempvoc_cmd, theme, ticket, ticket_member,
tickets, timeout, unban, unbanall, unbl, uncmute, unhide, unhideall, unlock, unlockall,
unmassiverole, unmute, unmuteall, unowner, untemprole, user, viewlogs, vocinfo, voicekick,
voicelog, voicemove, warn, watch,
};
use crate::commands::{alladmins, allbots, allperms, botadmins};
use crate::db::{DbPoolKey, upsert_message_observed};
@@ -83,12 +86,24 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
return;
}
if let Some(guild_id) = msg.guild_id {
ancien::maybe_assign_ancien_role(ctx, guild_id, msg.author.id).await;
}
let content = msg.content.trim();
let prefix_value = permissions::resolve_prefix(ctx, msg.guild_id).await;
if piconly::enforce_piconly_message(ctx, msg, content, &prefix_value).await {
return;
}
crate::commands::advanced_tools::apply_autoreacts(ctx, msg).await;
crate::commands::advanced_tools::maybe_run_maintenance(ctx, msg.guild_id).await;
moderation_tools::maybe_run_maintenance(ctx, msg.guild_id).await;
let content = msg.content.trim();
let prefix_value = permissions::resolve_prefix(ctx, msg.guild_id).await;
if crate::commands::automod_service::enforce_automod_message(ctx, msg).await {
return;
}
if !content.starts_with(&prefix_value) {
return;
}
@@ -151,9 +166,15 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
}
}
let required = permissions::command_required_permission(ctx, &command_key).await;
if !crate::commands::automod_service::public_command_allowed(ctx, msg, &command_key, required)
.await
{
return;
}
let can_use = permissions::can_use_command(ctx, msg, &command_key).await;
if !can_use {
let required = permissions::command_required_permission(ctx, &command_key).await;
permissions::deny_permission(ctx, msg, &command_key, required).await;
return;
}
@@ -190,6 +211,7 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
{
showpics::handle_show_pics(ctx, msg, &args[1..]).await
}
"piconly" => piconly::handle_piconly(ctx, msg, &args).await,
"suggestion" => suggestion::handle_suggestion(ctx, msg, &args).await,
"autopublish" => autopublish::handle_autopublish(ctx, msg, &args).await,
"tempvoc"
@@ -202,6 +224,21 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
}
"tempvoc" => tempvoc::handle_tempvoc(ctx, msg, &args).await,
"ping" => ping::handle_ping(ctx, msg, &args).await,
"timeout" => timeout::handle_timeout_toggle(ctx, msg, &args).await,
"muterole" => muterole::handle_muterole(ctx, msg, &args).await,
"antispam" => antispam::handle_antispam(ctx, msg, &args).await,
"antiraideautoconfig" => {
antiraideautoconfig::handle_antiraideautoconfig(ctx, msg, &args).await
}
"antilink" => antilink::handle_antilink(ctx, msg, &args).await,
"antimassmention" => antimassmention::handle_antimassmention(ctx, msg, &args).await,
"badwords" => badwords::handle_badwords(ctx, msg, &args).await,
"spam" => spam::handle_spam_override(ctx, msg, &args).await,
"link" => link::handle_link_override(ctx, msg, &args).await,
"strikes" => strikes::handle_strikes(ctx, msg, &args).await,
"punish" => punish::handle_punish(ctx, msg, &args).await,
"public" => public::handle_public(ctx, msg, &args).await,
"resetantiraide" => resetantiraide::handle_resetantiraide(ctx, msg, &args).await,
"allbots" => allbots::handle_allbots(ctx, msg, &args).await,
"alladmins" => alladmins::handle_alladmins(ctx, msg, &args).await,
"botadmins" => botadmins::handle_botadmins(ctx, msg, &args).await,
@@ -211,6 +248,7 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
"vocinfo" => vocinfo::handle_vocinfo(ctx, msg, &args).await,
"role" => role::handle_role(ctx, msg, &args).await,
"rolemenu" => rolemenu::handle_rolemenu(ctx, msg, &args).await,
"ancien" => ancien::handle_ancien(ctx, msg, &args).await,
"channel" => channel::handle_channel(ctx, msg, &args).await,
"user" => user::handle_user(ctx, msg, &args).await,
"member" => member::handle_member(ctx, msg, &args).await,
@@ -359,6 +397,7 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
derank::handle_derank(ctx, msg, &args).await;
crate::commands::logs_service::log_moderation_command(ctx, msg, "derank", &args).await;
}
"noderank" => noderank::handle_noderank(ctx, msg, &args).await,
"temprole" => temprole::handle_temprole(ctx, msg, &args).await,
"untemprole" => untemprole::handle_untemprole(ctx, msg, &args).await,
"sync" => sync::handle_sync(ctx, msg, &args).await,
@@ -366,6 +405,14 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
"autoreact" => autoreact::handle_autoreact(ctx, msg, &args).await,
"calc" => calc::handle_calc(ctx, msg, &args).await,
"shadowbot" => shadowbot::handle_shadowbot(ctx, msg, &args).await,
"set"
if args
.first()
.map(|s| s.eq_ignore_ascii_case("muterole"))
.unwrap_or(false) =>
{
set_muterole::handle_set_muterole(ctx, msg, &args).await
}
"set"
if args
.first()
@@ -453,6 +500,22 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
{
clear_perms::handle_clear_perms(ctx, msg).await
}
"clear"
if args
.first()
.map(|s| s.eq_ignore_ascii_case("limit"))
.unwrap_or(false) =>
{
clear_limit::handle_clear_limit(ctx, msg, &args).await
}
"clear"
if args
.first()
.map(|s| s.eq_ignore_ascii_case("badwords"))
.unwrap_or(false) =>
{
clear_badwords::handle_clear_badwords(ctx, msg, &args).await
}
"clear"
if args
.first()
+2 -1
View File
@@ -1,11 +1,12 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::{admin_service, botconfig_service, help};
use crate::commands::{admin_service, botconfig_service, help, tempvoc};
pub async fn handle_ready(ctx: &Context, ready: &Ready) {
botconfig_service::restore_presence_from_db(ctx).await;
help::register_slash_help(ctx).await;
tempvoc::cleanup_stale_rooms_on_ready(ctx).await;
for guild_id in ctx.cache.guilds() {
admin_service::enforce_blacklist_on_guild(ctx, guild_id).await;
+56 -2
View File
@@ -82,6 +82,18 @@ pub fn command_key(command: &str, args: &[&str]) -> String {
.unwrap_or(false)
{
"clear_bl".to_string()
} else if args
.first()
.map(|s| s.eq_ignore_ascii_case("limit"))
.unwrap_or(false)
{
"clear_limit".to_string()
} else if args
.first()
.map(|s| s.eq_ignore_ascii_case("badwords"))
.unwrap_or(false)
{
"clear_badwords".to_string()
} else if args
.first()
.map(|s| s.eq_ignore_ascii_case("perms"))
@@ -132,6 +144,12 @@ pub fn command_key(command: &str, args: &[&str]) -> String {
.unwrap_or(false)
{
"set_perm".to_string()
} else if args
.first()
.map(|s| s.eq_ignore_ascii_case("muterole"))
.unwrap_or(false)
{
"set_muterole".to_string()
} else if args
.first()
.map(|s| s.eq_ignore_ascii_case("modlogs"))
@@ -233,6 +251,7 @@ pub fn all_command_keys() -> Vec<String> {
"boosters",
"rolemembers",
"rolemenu",
"ancien",
"serverinfo",
"vocinfo",
"role",
@@ -252,6 +271,23 @@ pub fn all_command_keys() -> Vec<String> {
"clear_sanctions",
"clear_all_sanctions",
"clear_messages",
"clear_limit",
"clear_badwords",
"timeout",
"muterole",
"set_muterole",
"antispam",
"antiraideautoconfig",
"antilink",
"antimassmention",
"badwords",
"spam",
"link",
"strikes",
"punish",
"noderank",
"public",
"resetantiraide",
"warn",
"mute",
"tempmute",
@@ -290,6 +326,7 @@ pub fn all_command_keys() -> Vec<String> {
"ticket_close",
"tickets",
"show_pics",
"piconly",
"suggestion_create",
"suggestion_settings",
"autopublish",
@@ -374,14 +411,14 @@ pub fn all_command_keys() -> Vec<String> {
pub fn default_permission(command_key: &str) -> u8 {
match command_key {
"ticket_settings" | "suggestion_settings" | "autopublish" | "tempvoc" => 8,
"ticket_settings" | "suggestion_settings" | "autopublish" | "piconly" | "tempvoc" => 8,
"claim" | "rename" | "ticket_add" | "ticket_remove" | "ticket_close" | "tickets" => 2,
"show_pics" | "suggestion_create" | "tempvoc_cmd" => 0,
"owner" | "unowner" | "clear_owners" => 9,
"bl" | "unbl" | "blinfo" | "clear_bl" => 9,
"change" | "changeall" | "change_reset" | "mainprefix" | "set_perm" | "del_perm"
| "clear_perms" => 9,
"set_modlogs" | "set_boostembed" => 8,
"set_modlogs" | "set_boostembed" | "set_muterole" => 8,
"prefix" | "perms" | "allperms" => 8,
"help" | "server_list" => 0,
"helpsetting" | "alias" | "leave" => 9,
@@ -428,6 +465,22 @@ pub fn default_permission(command_key: &str) -> u8 {
| "clear_sanctions"
| "clear_all_sanctions"
| "clear_messages"
| "clear_limit"
| "clear_badwords"
| "timeout"
| "muterole"
| "antispam"
| "antiraideautoconfig"
| "antilink"
| "antimassmention"
| "badwords"
| "spam"
| "link"
| "strikes"
| "punish"
| "noderank"
| "public"
| "resetantiraide"
| "warn"
| "mute"
| "tempmute"
@@ -455,6 +508,7 @@ pub fn default_permission(command_key: &str) -> u8 {
| "delrole"
| "derank"
| "rolemenu"
| "ancien"
| "modlog"
| "messagelog"
| "voicelog"