From 9ea4914e42e34f86401bdb08f8a911712d59efb2 Mon Sep 17 00:00:00 2001 From: Puechberty Arthur Date: Fri, 10 Apr 2026 19:48:21 +0200 Subject: [PATCH] add invite commands --- src/commands/invitation/addinvite.rs | 108 +++++++++++++++++ src/commands/invitation/invite.rs | 85 +++++++++++++ src/commands/invitation/inviteboard.rs | 73 ++++++++++++ src/commands/invitation/invitereset.rs | 105 ++++++++++++++++ src/commands/invitation/removeinvite.rs | 108 +++++++++++++++++ src/commands/mod.rs | 16 ++- src/commands/perms/help.rs | 20 +++- src/db.rs | 151 ++++++++++++++++++++++++ src/events/message.rs | 42 ++++--- 9 files changed, 686 insertions(+), 22 deletions(-) create mode 100644 src/commands/invitation/addinvite.rs create mode 100644 src/commands/invitation/invite.rs create mode 100644 src/commands/invitation/inviteboard.rs create mode 100644 src/commands/invitation/invitereset.rs create mode 100644 src/commands/invitation/removeinvite.rs diff --git a/src/commands/invitation/addinvite.rs b/src/commands/invitation/addinvite.rs new file mode 100644 index 0000000..bc84a07 --- /dev/null +++ b/src/commands/invitation/addinvite.rs @@ -0,0 +1,108 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, add_invite_count}; + +pub async fn handle_addinvite(ctx: &Context, msg: &Message, args: &[&str]) { + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() || args.len() > 2 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+addinvite [@user/id] `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let (target_user, amount_arg) = if args.len() == 1 { + (msg.author.id, args[0]) + } else { + let Some(user_id) = parse_user_id(args[0]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Utilisateur invalide. Utilise une mention ou un ID.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + (user_id, args[1]) + }; + + let Ok(amount) = amount_arg.parse::() else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Le nombre doit etre un entier positif.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if amount <= 0 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Le nombre doit etre superieur a 0.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(pool) = ({ + let data = ctx.data.read().await; + data.get::().cloned() + }) else { + return; + }; + + let bot_id = ctx.cache.current_user().id.get() as i64; + let guild_id_raw = guild_id.get() as i64; + let user_id_raw = target_user.get() as i64; + + let Ok(new_total) = add_invite_count(&pool, bot_id, guild_id_raw, user_id_raw, amount).await + else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de mettre a jour les invitations.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let embed = CreateEmbed::new() + .title("AddInvite") + .description(format!( + "{} invitation(s) ajoutee(s) a <@{}>.\nNouveau total: `{}`.", + amount, + target_user.get(), + new_total + )) + .color(0x57F287); + send_embed(ctx, msg, embed).await; +} + +pub struct AddInviteCommand; +pub static COMMAND_DESCRIPTOR: AddInviteCommand = AddInviteCommand; + +impl crate::commands::command_contract::CommandSpec for AddInviteCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "addinvite", + category: "invitation", + params: "[@membre/ID] ", + description: "Ajoute un nombre specifique d invitations a un utilisateur.", + examples: &[ + "+addinvite @User 3", + "+addinvite 123456789012345678 2", + "+help addinvite", + ], + default_aliases: &["ainv"], + allow_in_dm: false, + default_permission: 5, + } + } +} diff --git a/src/commands/invitation/invite.rs b/src/commands/invitation/invite.rs new file mode 100644 index 0000000..c936426 --- /dev/null +++ b/src/commands/invitation/invite.rs @@ -0,0 +1,85 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, get_invite_count}; + +pub async fn handle_invite(ctx: &Context, msg: &Message, args: &[&str]) { + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.len() > 1 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+invite [@user/id]`") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let target_user = if args.is_empty() { + msg.author.id + } else { + let Some(user_id) = parse_user_id(args[0]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Utilisateur invalide. Utilise une mention ou un ID.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + user_id + }; + + let Some(pool) = ({ + let data = ctx.data.read().await; + data.get::().cloned() + }) else { + return; + }; + + let bot_id = ctx.cache.current_user().id.get() as i64; + let guild_id_raw = guild_id.get() as i64; + let user_id_raw = target_user.get() as i64; + + let Ok(invite_count) = get_invite_count(&pool, bot_id, guild_id_raw, user_id_raw).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de lire le compteur d invitations.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let embed = CreateEmbed::new() + .title("Invite") + .description(format!( + "<@{}> a actuellement `{}` invitation(s).", + target_user.get(), + invite_count + )) + .color(0x5865F2); + send_embed(ctx, msg, embed).await; +} + +pub struct InviteCommand; +pub static COMMAND_DESCRIPTOR: InviteCommand = InviteCommand; + +impl crate::commands::command_contract::CommandSpec for InviteCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "invite", + category: "invitation", + params: "[@membre/ID]", + description: "Affiche le nombre d invitations d un utilisateur.", + examples: &["+invite", "+invite @User", "+help invite"], + default_aliases: &["inv"], + allow_in_dm: false, + default_permission: 0, + } + } +} diff --git a/src/commands/invitation/inviteboard.rs b/src/commands/invitation/inviteboard.rs new file mode 100644 index 0000000..b8ba8fe --- /dev/null +++ b/src/commands/invitation/inviteboard.rs @@ -0,0 +1,73 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, list_invite_board}; + +pub async fn handle_inviteboard(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::().cloned() + }) else { + return; + }; + + let bot_id = ctx.cache.current_user().id.get() as i64; + let guild_id_raw = guild_id.get() as i64; + + let Ok(entries) = list_invite_board(&pool, bot_id, guild_id_raw, 10).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de lire le classement des invitations.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if entries.is_empty() { + let embed = CreateEmbed::new() + .title("InviteBoard") + .description("Aucune invitation enregistree pour le moment.") + .color(0x5865F2); + send_embed(ctx, msg, embed).await; + return; + } + + let lines = entries + .iter() + .enumerate() + .map(|(idx, (user_id, count))| { + format!("`#{}` <@{}> - `{}` invitation(s)", idx + 1, user_id, count) + }) + .collect::>() + .join("\n"); + + let embed = CreateEmbed::new() + .title("InviteBoard") + .description(lines) + .color(0x5865F2); + send_embed(ctx, msg, embed).await; +} + +pub struct InviteBoardCommand; +pub static COMMAND_DESCRIPTOR: InviteBoardCommand = InviteBoardCommand; + +impl crate::commands::command_contract::CommandSpec for InviteBoardCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "inviteboard", + category: "invitation", + params: "aucun", + description: "Affiche les 10 membres du serveur avec le plus d invitations.", + examples: &["+inviteboard", "+iboard", "+help inviteboard"], + default_aliases: &["iboard"], + allow_in_dm: false, + default_permission: 0, + } + } +} diff --git a/src/commands/invitation/invitereset.rs b/src/commands/invitation/invitereset.rs new file mode 100644 index 0000000..e83f5d0 --- /dev/null +++ b/src/commands/invitation/invitereset.rs @@ -0,0 +1,105 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, reset_invite_count_for_user, reset_invite_counts_for_guild}; + +pub async fn handle_invitereset(ctx: &Context, msg: &Message, args: &[&str]) { + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.len() != 1 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+invitereset <@user|guild>`") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(pool) = ({ + let data = ctx.data.read().await; + data.get::().cloned() + }) else { + return; + }; + + let bot_id = ctx.cache.current_user().id.get() as i64; + let guild_id_raw = guild_id.get() as i64; + let target = args[0].to_lowercase(); + + if matches!(target.as_str(), "guild" | "serveur" | "server") { + let Ok(affected) = reset_invite_counts_for_guild(&pool, bot_id, guild_id_raw).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de reinitialiser les invitations du serveur.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let embed = CreateEmbed::new() + .title("InviteReset") + .description(format!( + "Compteurs d invitations du serveur reinitialises. Entrees supprimees: `{}`.", + affected + )) + .color(0x57F287); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(user_id) = parse_user_id(args[0]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Cible invalide. Utilise une mention utilisateur, un ID, ou `guild`.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let Ok(_) = + reset_invite_count_for_user(&pool, bot_id, guild_id_raw, user_id.get() as i64).await + else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de reinitialiser les invitations de cet utilisateur.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let embed = CreateEmbed::new() + .title("InviteReset") + .description(format!( + "Compteur d invitations reinitialise pour <@{}>.", + user_id.get() + )) + .color(0x57F287); + send_embed(ctx, msg, embed).await; +} + +pub struct InviteResetCommand; +pub static COMMAND_DESCRIPTOR: InviteResetCommand = InviteResetCommand; + +impl crate::commands::command_contract::CommandSpec for InviteResetCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "invitereset", + category: "invitation", + params: "<@membre/ID|guild>", + description: "Reinitialise le compteur d invitations pour un utilisateur ou le serveur.", + examples: &[ + "+invitereset @User", + "+invitereset guild", + "+help invitereset", + ], + default_aliases: &["invreset"], + allow_in_dm: false, + default_permission: 5, + } + } +} diff --git a/src/commands/invitation/removeinvite.rs b/src/commands/invitation/removeinvite.rs new file mode 100644 index 0000000..c8b9490 --- /dev/null +++ b/src/commands/invitation/removeinvite.rs @@ -0,0 +1,108 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, add_invite_count}; + +pub async fn handle_removeinvite(ctx: &Context, msg: &Message, args: &[&str]) { + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() || args.len() > 2 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+removeinvite [@user/id] `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let (target_user, amount_arg) = if args.len() == 1 { + (msg.author.id, args[0]) + } else { + let Some(user_id) = parse_user_id(args[0]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Utilisateur invalide. Utilise une mention ou un ID.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + (user_id, args[1]) + }; + + let Ok(amount) = amount_arg.parse::() else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Le nombre doit etre un entier positif.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if amount <= 0 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Le nombre doit etre superieur a 0.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(pool) = ({ + let data = ctx.data.read().await; + data.get::().cloned() + }) else { + return; + }; + + let bot_id = ctx.cache.current_user().id.get() as i64; + let guild_id_raw = guild_id.get() as i64; + let user_id_raw = target_user.get() as i64; + + let Ok(new_total) = add_invite_count(&pool, bot_id, guild_id_raw, user_id_raw, -amount).await + else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Impossible de mettre a jour les invitations.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let embed = CreateEmbed::new() + .title("RemoveInvite") + .description(format!( + "{} invitation(s) retiree(s) a <@{}>.\nNouveau total: `{}`.", + amount, + target_user.get(), + new_total + )) + .color(0x57F287); + send_embed(ctx, msg, embed).await; +} + +pub struct RemoveInviteCommand; +pub static COMMAND_DESCRIPTOR: RemoveInviteCommand = RemoveInviteCommand; + +impl crate::commands::command_contract::CommandSpec for RemoveInviteCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "removeinvite", + category: "invitation", + params: "[@membre/ID] ", + description: "Retire un nombre specifique d invitations a un utilisateur.", + examples: &[ + "+removeinvite @User 1", + "+removeinvite 123456789012345678 2", + "+help removeinvite", + ], + default_aliases: &["rinv"], + allow_in_dm: false, + default_permission: 5, + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f5cb2ec..a15bc00 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,7 @@ use crate::commands::command_contract::{CommandMetadata, CommandSpec}; +#[path = "invitation/addinvite.rs"] +pub mod addinvite; #[path = "roles/addrole.rs"] pub mod addrole; #[path = "../utils/admin_common.rs"] @@ -150,8 +152,12 @@ pub mod hideall; pub mod idle; #[path = "botconfig/invisible.rs"] pub mod invisible; -#[path = "owner/invite.rs"] +#[path = "invitation/invite.rs"] pub mod invite; +#[path = "invitation/inviteboard.rs"] +pub mod inviteboard; +#[path = "invitation/invitereset.rs"] +pub mod invitereset; #[path = "config/join.rs"] pub mod join; #[path = "mod/kick.rs"] @@ -252,6 +258,8 @@ pub mod punishsetup; pub mod raidlog; #[path = "botconfig/removeactivity.rs"] pub mod remove_activity; +#[path = "invitation/removeinvite.rs"] +pub mod removeinvite; #[path = "ticket/rename.rs"] pub mod rename; #[path = "channel/renew.rs"] @@ -390,6 +398,11 @@ pub mod watch; pub fn all_command_metadata() -> Vec { vec![ ping::COMMAND_DESCRIPTOR.metadata(), + addinvite::COMMAND_DESCRIPTOR.metadata(), + invite::COMMAND_DESCRIPTOR.metadata(), + inviteboard::COMMAND_DESCRIPTOR.metadata(), + invitereset::COMMAND_DESCRIPTOR.metadata(), + removeinvite::COMMAND_DESCRIPTOR.metadata(), timeout::COMMAND_DESCRIPTOR.metadata(), allbots::COMMAND_DESCRIPTOR.metadata(), alladmins::COMMAND_DESCRIPTOR.metadata(), @@ -559,7 +572,6 @@ pub fn all_command_metadata() -> Vec { mpsettings::COMMAND_DESCRIPTOR.metadata(), mpsent::COMMAND_DESCRIPTOR.metadata(), mpdelete::COMMAND_DESCRIPTOR.metadata(), - invite::COMMAND_DESCRIPTOR.metadata(), leave::COMMAND_DESCRIPTOR.metadata(), leave_settings::COMMAND_DESCRIPTOR.metadata(), viewlogs::COMMAND_DESCRIPTOR.metadata(), diff --git a/src/commands/perms/help.rs b/src/commands/perms/help.rs index 5388242..cba264f 100644 --- a/src/commands/perms/help.rs +++ b/src/commands/perms/help.rs @@ -86,6 +86,11 @@ const HELP_PAGES: &[HelpPage] = &[ title: "Infos", description: "Informations sur le serveur, les membres et les profils.", }, + HelpPage { + key: "invitation", + title: "Invitation", + description: "Compteurs d invitations et classement des invitations du serveur.", + }, HelpPage { key: "logs", title: "Logs", @@ -132,6 +137,7 @@ fn help_page_for_command( meta: &crate::commands::command_contract::CommandMetadata, ) -> &'static str { match meta.name { + "addinvite" | "removeinvite" | "invite" | "invitereset" | "inviteboard" => "invitation", "modlog" | "messagelog" | "voicelog" | "boostlog" | "rolelog" | "raidlog" | "autoconfiglog" | "nolog" | "joinsettings" | "boostembed" | "setmodlogs" | "setboostembed" | "leavesettings" | "viewlogs" => "logs", @@ -151,12 +157,13 @@ fn help_page_for_command( | "idle" | "dnd" | "invisible" | "change" | "changereset" | "changeall" => "bot", "owner" | "unowner" | "clearowners" | "bl" | "unbl" | "blinfo" | "clearbl" | "allbots" | "alladmins" | "botadmins" | "mainprefix" | "prefix" | "mp" | "mpsettings" | "mpsent" - | "mpdelete" | "invite" | "leave" | "discussion" => "administration", + | "mpdelete" | "leave" | "discussion" => "administration", "perms" | "delperm" | "clearperms" | "allperms" | "alias" | "help" | "helpsetting" => { "permissions" } _ => match meta.category { "info" => "infos", + "invitation" => "invitation", "mod" => "moderation", "config" => "logs", "botconfig" => "bot", @@ -219,6 +226,13 @@ fn help_page_matches_input(page: &HelpPage, input: &str) -> bool { let normalized = help_lookup_key(input); let aliases = match page.key { "infos" => &["general", "info", "informations"][..], + "invitation" => &[ + "invite", + "invites", + "invitation", + "invitations", + "inviteboard", + ][..], "logs" => &["log", "journal"][..], "moderation" => &["mod", "sanction"][..], "roles" => &["role", "roles"][..], @@ -424,6 +438,10 @@ fn help_lookup_to_key(input: &str) -> Option<&'static str> { "mpsettings" => Some("mpsettings"), "mpsent" => Some("mpsent"), "mpdelete" | "mpdel" => Some("mpdelete"), + "add invite" | "addinvite" => Some("addinvite"), + "remove invite" | "removeinvite" => Some("removeinvite"), + "invite reset" | "invitereset" => Some("invitereset"), + "invite board" | "inviteboard" => Some("inviteboard"), "discussion" => Some("discussion"), "owner" => Some("owner"), "unowner" => Some("unowner"), diff --git a/src/db.rs b/src/db.rs index 5b5fd9c..aed0c57 100644 --- a/src/db.rs +++ b/src/db.rs @@ -184,6 +184,16 @@ pub struct TempvocProfile { pub updated_at: DateTime, } +#[derive(Debug, Clone, sqlx::FromRow)] +#[allow(dead_code)] +pub struct InviteCounter { + pub bot_id: i64, + pub guild_id: i64, + pub user_id: i64, + pub invite_count: i64, + pub updated_at: DateTime, +} + #[derive(Debug, Clone, sqlx::FromRow)] #[allow(dead_code)] pub struct ModerationSettings { @@ -906,6 +916,30 @@ pub async fn init_schema(pool: &PgPool) -> Result<(), sqlx::Error> { .execute(pool) .await?; + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS bot_invite_counts ( + bot_id BIGINT NOT NULL, + guild_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + invite_count BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (bot_id, guild_id, user_id) + ); + "#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#" + CREATE INDEX IF NOT EXISTS idx_bot_invite_counts_lookup + ON bot_invite_counts (bot_id, guild_id, invite_count DESC, updated_at ASC); + "#, + ) + .execute(pool) + .await?; + sqlx::query( r#" CREATE TABLE IF NOT EXISTS bot_tempvoc_settings ( @@ -3240,6 +3274,123 @@ pub async fn is_piconly_channel( Ok(row.0) } +// ========== INVITE COUNTERS FUNCTIONS ========== + +pub async fn get_invite_count( + pool: &PgPool, + bot_id: i64, + guild_id: i64, + user_id: i64, +) -> Result { + let row = sqlx::query_as::<_, (i64,)>( + r#" + SELECT invite_count + FROM bot_invite_counts + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3; + "#, + ) + .bind(bot_id) + .bind(guild_id) + .bind(user_id) + .fetch_optional(pool) + .await?; + + Ok(row.map(|value| value.0).unwrap_or(0)) +} + +pub async fn add_invite_count( + pool: &PgPool, + bot_id: i64, + guild_id: i64, + user_id: i64, + delta: i64, +) -> Result { + let row = sqlx::query_as::<_, (i64,)>( + r#" + INSERT INTO bot_invite_counts (bot_id, guild_id, user_id, invite_count) + VALUES ($1, $2, $3, GREATEST($4, 0)) + ON CONFLICT (bot_id, guild_id, user_id) + DO UPDATE SET + invite_count = GREATEST(bot_invite_counts.invite_count + $4, 0), + updated_at = NOW() + RETURNING invite_count; + "#, + ) + .bind(bot_id) + .bind(guild_id) + .bind(user_id) + .bind(delta) + .fetch_one(pool) + .await?; + + Ok(row.0) +} + +pub async fn reset_invite_count_for_user( + pool: &PgPool, + bot_id: i64, + guild_id: i64, + user_id: i64, +) -> Result { + let result = sqlx::query( + r#" + DELETE FROM bot_invite_counts + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3; + "#, + ) + .bind(bot_id) + .bind(guild_id) + .bind(user_id) + .execute(pool) + .await?; + + Ok(result.rows_affected()) +} + +pub async fn reset_invite_counts_for_guild( + pool: &PgPool, + bot_id: i64, + guild_id: i64, +) -> Result { + let result = sqlx::query( + r#" + DELETE FROM bot_invite_counts + WHERE bot_id = $1 AND guild_id = $2; + "#, + ) + .bind(bot_id) + .bind(guild_id) + .execute(pool) + .await?; + + Ok(result.rows_affected()) +} + +pub async fn list_invite_board( + pool: &PgPool, + bot_id: i64, + guild_id: i64, + limit: i64, +) -> Result, sqlx::Error> { + let capped_limit = limit.clamp(1, 100); + let rows = sqlx::query_as::<_, (i64, i64)>( + r#" + SELECT user_id, invite_count + FROM bot_invite_counts + WHERE bot_id = $1 AND guild_id = $2 AND invite_count > 0 + ORDER BY invite_count DESC, updated_at ASC + LIMIT $3; + "#, + ) + .bind(bot_id) + .bind(guild_id) + .bind(capped_limit) + .fetch_all(pool) + .await?; + + Ok(rows) +} + // ========== ANCIEN SETTINGS FUNCTIONS ========== pub async fn get_or_create_old_member_settings( diff --git a/src/events/message.rs b/src/events/message.rs index 81bc4f3..1ce0047 100644 --- a/src/events/message.rs +++ b/src/events/message.rs @@ -6,25 +6,25 @@ use std::sync::{Mutex, OnceLock}; use crate::commands::moderation_tools; use crate::commands::remove_activity; use crate::commands::{ - addrole, alias, ancien, antilink, antimassmention, antiraideautoconfig, antispam, autobackup, - autoconfiglog, autopublish, autopublishoff, autopublishon, autoreact, backup, badwords, ban, - banlist, banner, bl, blinfo, boostembed, boosters, boostlog, bringall, button, calc, change, - changeall, changereset, 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_sanction, delperm, delrole, derank, discussion, dnd, embed, emoji, - end, endgiveaway, giveaway, help, helpsetting, hide, hideall, idle, invisible, invite, join, - kick, leave, leave_settings, link, listen, loading, lock, lockall, mainprefix, massiverole, - member, messagelog, modlog, mp, mpdelete, mpsent, mpsettings, mute, mutelist, muterole, - newsticker, noderank, noderankadd, noderankdel, nolog, online, owner, perms, pic, piconly, - piconlyadd, piconlydel, ping, playto, prefix, public, punish, punishadd, punishdel, - punishsetup, raidlog, rename, renew, reroll, resetantiraide, role, rolelog, rolemembers, - rolemenu, sanctions, say, serverbanner, serverinfo, serverlist, serverpic, set_boostembed, - set_modlogs, set_muterole, setbanner, setname, setperm, setpic, setprofil, shadowbot, showpics, - slowmode, snipe, spam, stream, strikes, suggestion, suggestionsettings, sync, tempban, - tempcmute, tempmute, temprole, tempvoc, tempvoc_cmd, theme, ticket, ticket_member, tickets, - timeout, unalias, unban, unbanall, unbl, uncmute, unhide, unhideall, unlock, unlockall, - unmassiverole, unmute, unmuteall, unowner, untemprole, user, viewlogs, vocinfo, voicekick, - voicelog, voicemove, warn, watch, + addinvite, addrole, alias, ancien, antilink, antimassmention, antiraideautoconfig, antispam, + autobackup, autoconfiglog, autopublish, autopublishoff, autopublishon, autoreact, backup, + badwords, ban, banlist, banner, bl, blinfo, boostembed, boosters, boostlog, bringall, button, + calc, change, changeall, changereset, 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_sanction, delperm, delrole, derank, + discussion, dnd, embed, emoji, end, endgiveaway, giveaway, help, helpsetting, hide, hideall, + idle, invisible, invite, inviteboard, invitereset, join, kick, leave, leave_settings, link, + listen, loading, lock, lockall, mainprefix, massiverole, member, messagelog, modlog, mp, + mpdelete, mpsent, mpsettings, mute, mutelist, muterole, newsticker, noderank, noderankadd, + noderankdel, nolog, online, owner, perms, pic, piconly, piconlyadd, piconlydel, ping, playto, + prefix, public, punish, punishadd, punishdel, punishsetup, raidlog, removeinvite, rename, + renew, reroll, resetantiraide, role, rolelog, rolemembers, rolemenu, sanctions, say, + serverbanner, serverinfo, serverlist, serverpic, set_boostembed, set_modlogs, set_muterole, + setbanner, setname, setperm, setpic, setprofil, shadowbot, showpics, slowmode, snipe, spam, + stream, strikes, suggestion, suggestionsettings, sync, tempban, tempcmute, tempmute, temprole, + tempvoc, tempvoc_cmd, theme, ticket, ticket_member, tickets, timeout, unalias, 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}; @@ -418,7 +418,11 @@ pub async fn handle_message(ctx: &Context, msg: &Message) { "mpsent" => mpsent::handle_mpsent_command(ctx, msg, &args).await, "mpdelete" | "mpdel" => mpdelete::handle_mpdelete_command(ctx, msg, &args).await, "mp" => mp::handle_mp(ctx, msg, &args).await, + "addinvite" => addinvite::handle_addinvite(ctx, msg, &args).await, "invite" => invite::handle_invite(ctx, msg, &args).await, + "removeinvite" => removeinvite::handle_removeinvite(ctx, msg, &args).await, + "invitereset" => invitereset::handle_invitereset(ctx, msg, &args).await, + "inviteboard" => inviteboard::handle_inviteboard(ctx, msg, &args).await, "leavesettings" => leave_settings::handle_leave_settings(ctx, msg, &args).await, "leave" => leave::handle_leave(ctx, msg, &args).await, "viewlogs" => viewlogs::handle_viewlogs(ctx, msg, &args).await,