2026-05-06 17:07:09 +02:00
import disnake
from disnake . ext import commands
2026-05-01 16:44:50 +02:00
import sqlite3
import os
from datetime import datetime , timedelta
import re
2026-05-21 23:02:00 +02:00
from src . logger import kuby_logger
2026-05-01 16:44:50 +02:00
2026-05-06 17:07:09 +02:00
class ModReasonModal ( disnake . ui . Modal ) :
2026-05-16 18:05:04 +02:00
def __init__ ( self , cog , action_type , member ) :
self . cog = cog
2026-05-01 16:44:50 +02:00
self . action_type = action_type
2026-05-16 18:05:04 +02:00
self . member = member
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
self . reason_input = disnake . ui . TextInput (
2026-05-01 16:44:50 +02:00
label = " Raison " ,
placeholder = " Entrez la raison ici... " ,
required = True ,
2026-05-16 18:05:04 +02:00
max_length = 500 ,
custom_id = " reason "
2026-05-01 16:44:50 +02:00
)
2026-05-16 18:05:04 +02:00
components = [ self . reason_input ]
2026-05-01 16:44:50 +02:00
if action_type == " TIMEOUT " :
2026-05-16 18:05:04 +02:00
self . duration_input = disnake . ui . TextInput (
2026-05-01 16:44:50 +02:00
label = " Durée (ex: 1h, 30m, 1d) " ,
placeholder = " 1h " ,
required = True ,
2026-05-16 18:05:04 +02:00
max_length = 10 ,
custom_id = " duration "
2026-05-01 16:44:50 +02:00
)
2026-05-16 18:05:04 +02:00
components . append ( self . duration_input )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
super ( ) . __init__ ( title = f " Raison pour { action_type } " , components = components )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
reason = interaction . text_values [ " reason " ]
member = self . member
2026-05-01 16:44:50 +02:00
if self . action_type == " WARN " :
2026-05-16 18:05:04 +02:00
await self . cog . warn ( interaction , member , reason )
2026-05-01 16:44:50 +02:00
elif self . action_type == " TIMEOUT " :
2026-05-16 18:05:04 +02:00
duration = interaction . text_values [ " duration " ]
await self . cog . timeout ( interaction , member , duration , reason )
2026-05-01 16:44:50 +02:00
elif self . action_type == " KICK " :
2026-05-16 18:05:04 +02:00
await self . cog . kick ( interaction , member , reason )
2026-05-01 16:44:50 +02:00
elif self . action_type == " BAN " :
2026-05-16 18:05:04 +02:00
await self . cog . ban ( interaction , member , reason )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
await self . cog . send_modpanel_v2 ( interaction , member , edit = True )
2026-05-01 16:44:50 +02:00
2026-05-06 17:07:09 +02:00
class ModConfigModal ( disnake . ui . Modal ) :
2026-05-01 16:44:50 +02:00
def __init__ ( self , field_name , current_value ) :
self . field_name = field_name
2026-05-16 18:05:04 +02:00
self . input_field = disnake . ui . TextInput (
2026-05-01 16:44:50 +02:00
label = field_name ,
2026-05-09 18:42:42 +02:00
value = str ( current_value ) ,
2026-05-01 16:44:50 +02:00
placeholder = " Entrez une valeur numérique... " ,
2026-05-16 18:05:04 +02:00
required = True ,
custom_id = " value "
2026-05-01 16:44:50 +02:00
)
2026-05-16 18:05:04 +02:00
super ( ) . __init__ ( title = f " Modifier { field_name } " , components = [ self . input_field ] )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
value = interaction . text_values [ " value " ]
2026-05-06 17:07:09 +02:00
cog = interaction . bot . get_cog ( " Moderation " )
2026-05-01 16:44:50 +02:00
mapping = {
" Limite Timeout " : " warn_limit_timeout " ,
" Limite Kick " : " warn_limit_kick " ,
" Limite Ban " : " warn_limit_ban " ,
" Durée Timeout (s) " : " timeout_duration "
}
if not value . isdigit ( ) :
return await interaction . response . send_message ( " ❌ Veuillez entrer un nombre valide. " , ephemeral = True )
db_field = mapping . get ( self . field_name )
conn = sqlite3 . connect ( cog . db_path )
cursor = conn . cursor ( )
cursor . execute ( f " UPDATE mod_config SET { db_field } = ? WHERE guild_id = ? " , ( int ( value ) , interaction . guild_id ) )
conn . commit ( )
conn . close ( )
2026-05-16 18:05:04 +02:00
await interaction . response . send_message ( f " ✅ { self . field_name } mis à jour. " , ephemeral = True )
await cog . send_modconfig_v2 ( interaction , edit = True )
2026-05-01 16:44:50 +02:00
class Moderation ( commands . Cog ) :
def __init__ ( self , bot ) :
self . bot = bot
self . db_path = os . path . join ( os . path . dirname ( __file__ ) , " .. " , " config.db " )
self . _init_db ( )
def _init_db ( self ) :
conn = sqlite3 . connect ( self . db_path )
cursor = conn . cursor ( )
cursor . execute ( ''' CREATE TABLE IF NOT EXISTS sanctions (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, moderator_id INTEGER, type TEXT, reason TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, duration INTEGER, status TEXT DEFAULT ' ACTIVE ' ) ''' )
2026-05-16 18:05:04 +02:00
cursor . execute ( ''' CREATE TABLE IF NOT EXISTS mod_config (
guild_id INTEGER PRIMARY KEY ,
log_channel_id INTEGER ,
board_channel_id INTEGER ,
2026-05-21 23:02:00 +02:00
appeal_channel_id INTEGER ,
2026-05-16 18:05:04 +02:00
warn_limit_timeout INTEGER DEFAULT 3 ,
warn_limit_kick INTEGER DEFAULT 5 ,
warn_limit_ban INTEGER DEFAULT 10 ,
timeout_duration INTEGER DEFAULT 3600
) ''' )
try : cursor . execute ( " ALTER TABLE mod_config ADD COLUMN board_channel_id INTEGER " )
except : pass
2026-05-21 23:02:00 +02:00
try : cursor . execute ( " ALTER TABLE mod_config ADD COLUMN appeal_channel_id INTEGER " )
except : pass
2026-05-01 16:44:50 +02:00
conn . commit ( ) ; conn . close ( )
def parse_duration ( self , duration_str : str ) - > int :
if not duration_str : return 0
units = { ' s ' : 1 , ' m ' : 60 , ' h ' : 3600 , ' d ' : 86400 , ' w ' : 604800 }
total_seconds = 0
matches = re . findall ( r ' ( \ d+)([smhdw]) ' , duration_str . lower ( ) )
for value , unit in matches : total_seconds + = int ( value ) * units [ unit ]
return total_seconds
2026-05-16 18:05:04 +02:00
def _type_to_emoji ( self , s_type : str ) - > str :
return { " WARN " : " ⚠️ " , " TIMEOUT " : " ⏳ " , " KICK " : " 👢 " , " BAN " : " 🔨 " } . get ( s_type , " 🛡️ " )
2026-05-21 23:02:00 +02:00
def _type_to_action_name ( self , s_type : str ) - > str :
return { " WARN " : " Avertissement " , " TIMEOUT " : " Timeout " , " KICK " : " Expulsion " , " BAN " : " Ban " } . get ( s_type , " Sanction " )
def _format_duration ( self , seconds : int ) - > str :
if seconds > = 86400 :
return f " { seconds / / 86400 } jour(s) "
elif seconds > = 3600 :
return f " { seconds / / 3600 } heure(s) "
elif seconds > = 60 :
return f " { seconds / / 60 } minute(s) "
return f " { seconds } seconde(s) "
async def send_sanction_dm ( self , user , s_type : str , reason : str , moderator , guild , duration : int = None ) - > bool :
""" Envoie un DM détaillé à l ' utilisateur sanctionné. Retourne True si envoyé. """
from commandes . modules_security . rules import load_settings as load_rules_settings
emoji = self . _type_to_emoji ( s_type )
action_name = self . _type_to_action_name ( s_type )
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' SELECT appeal_channel_id FROM mod_config WHERE guild_id = ? ' , ( guild . id , ) )
res = cursor . fetchone ( )
conn . close ( )
rules_settings = load_rules_settings ( guild . id )
appeal_channel = guild . get_channel ( res [ 0 ] ) if res and res [ 0 ] else None
rules_channel = guild . get_channel ( rules_settings . get ( " rules_channel_id " ) ) if rules_settings . get ( " rules_channel_id " ) else None
embed = disnake . Embed (
title = f " { emoji } { action_name } sur ** { guild . name } ** " ,
color = disnake . Color . red ( ) if s_type in ( " BAN " , " KICK " ) else disnake . Color . orange ( )
)
embed . set_thumbnail ( url = guild . icon . url if guild . icon else None )
embed . add_field ( name = " 📋 Action " , value = action_name , inline = True )
embed . add_field ( name = " 👤 Modérateur " , value = f " ** { moderator } ** " , inline = True )
if duration :
embed . add_field ( name = " ⏱️ Durée " , value = self . _format_duration ( duration ) , inline = True )
embed . add_field ( name = " 📝 Raison " , value = f " * { reason if reason else ' Aucune raison spécifiée ' } * " , inline = False )
if appeal_channel :
embed . add_field ( name = " 🔗 Contester cette sanction " , value = f " Vous pouvez contester cette mesure dans { appeal_channel . mention } . " , inline = False )
if rules_channel :
embed . add_field ( name = " 📜 Règlement " , value = f " [Voir le règlement](https://discord.com/channels/ { guild . id } / { rules_channel . id } ) " , inline = False )
try :
await user . send ( embed = embed )
return True
except disnake . Forbidden :
kuby_logger . warning ( f " Impossible d ' envoyer un DM à { user } (DMs bloqués) " )
return False
2026-05-16 18:05:04 +02:00
def build_sanction_board_components ( self , * , s_type : str , user , moderator , reason , timestamp , s_id , duration = None ) - > list :
ts_int = int ( datetime . fromisoformat ( str ( timestamp ) ) . timestamp ( ) ) if not isinstance ( timestamp , datetime ) else int ( timestamp . timestamp ( ) )
emoji = self . _type_to_emoji ( s_type )
children = [
disnake . ui . Section ( f " { emoji } **SANCTION : { s_type } ** " , accessory = disnake . ui . Thumbnail ( user . display_avatar . url ) ) ,
disnake . ui . Separator ( divider = True ) ,
disnake . ui . TextDisplay ( f " - **Membre sanctionné** : { user . mention } " ) ,
disnake . ui . TextDisplay ( f " - **Modérateur** : { moderator . mention } " ) ,
disnake . ui . TextDisplay ( f " - **Raison** : “ * { reason if reason else ' Aucune raison spécifiée ' } * ” " ) ,
disnake . ui . TextDisplay ( f " - **Date** : <t: { ts_int } :F> " )
]
if s_type == " TIMEOUT " and duration :
children . append ( disnake . ui . TextDisplay ( f " - **Durée** : ` { duration } s` " ) )
children . extend ( [
disnake . ui . Separator ( divider = True ) ,
disnake . ui . TextDisplay ( f " 🆔 **ID** : ` { s_id } ` " )
] )
return [ disnake . ui . Container ( * children ) ]
async def send_mod_log ( self , guild , components ) :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
2026-05-01 16:44:50 +02:00
cursor . execute ( ' SELECT log_channel_id FROM mod_config WHERE guild_id = ? ' , ( guild . id , ) )
2026-05-16 18:05:04 +02:00
res = cursor . fetchone ( ) ; conn . close ( )
if res and res [ 0 ] :
chan = guild . get_channel ( res [ 0 ] )
if chan : await chan . send ( components = components )
async def send_sanction_board ( self , guild , components ) :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' SELECT board_channel_id FROM mod_config WHERE guild_id = ? ' , ( guild . id , ) )
res = cursor . fetchone ( ) ; conn . close ( )
if res and res [ 0 ] :
chan = guild . get_channel ( res [ 0 ] )
if chan : await chan . send ( components = components )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def send_modpanel_v2 ( self , interaction : disnake . Interaction , member : disnake . Member , edit = False ) :
2026-06-03 15:52:43 +02:00
self . _init_db ( )
2026-05-16 18:05:04 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " WARN " AND status = " ACTIVE " ' , ( interaction . guild_id , member . id ) )
warn_count = cursor . fetchone ( ) [ 0 ]
cursor . execute ( ' SELECT type, reason, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND status = " ACTIVE " ORDER BY timestamp DESC LIMIT 3 ' , ( interaction . guild_id , member . id ) )
rows = cursor . fetchall ( ) ; conn . close ( )
children = [
disnake . ui . Section ( f " 🛡️ Panel : { member . name } " , accessory = disnake . ui . Thumbnail ( member . display_avatar . url ) ) ,
disnake . ui . Separator ( divider = True ) ,
disnake . ui . TextDisplay ( f " 👤 **Membre :** { member . mention } (` { member . id } `) " ) ,
disnake . ui . TextDisplay ( f " ⚠️ **Avertissements actifs :** { warn_count } " ) ,
disnake . ui . Separator ( divider = True ) ,
disnake . ui . Section ( " ⚠️ Infliger un avertissement " , accessory = disnake . ui . Button ( label = " Warn " , style = disnake . ButtonStyle . secondary , custom_id = f " modpan_warn: { member . id } " ) ) ,
disnake . ui . Section ( " ⏳ Mettre en sourdine " , accessory = disnake . ui . Button ( label = " Timeout " , style = disnake . ButtonStyle . secondary , custom_id = f " modpan_timeout: { member . id } " ) ) ,
disnake . ui . Section ( " 👢 Expulser du serveur " , accessory = disnake . ui . Button ( label = " Kick " , style = disnake . ButtonStyle . secondary , custom_id = f " modpan_kick: { member . id } " ) ) ,
disnake . ui . Section ( " 🔨 Bannir définitivement " , accessory = disnake . ui . Button ( label = " Ban " , style = disnake . ButtonStyle . danger , custom_id = f " modpan_ban: { member . id } " ) ) ,
disnake . ui . Separator ( divider = True ) ,
disnake . ui . Section ( " 🧹 Réinitialiser les warns " , accessory = disnake . ui . Button ( label = " Clear " , style = disnake . ButtonStyle . danger , custom_id = f " modpan_clear: { member . id } " ) )
]
if rows :
children . append ( disnake . ui . Separator ( divider = True ) )
children . append ( disnake . ui . TextDisplay ( " 📌 **Dernières sanctions :** " ) )
for t , r , ts in rows :
ts_int = int ( datetime . fromisoformat ( str ( ts ) ) . timestamp ( ) ) if not isinstance ( ts , datetime ) else int ( ts . timestamp ( ) )
children . append ( disnake . ui . TextDisplay ( f " • { self . _type_to_emoji ( t ) } ** { t } ** — { r } (<t: { ts_int } :R>) " ) )
components = [ disnake . ui . Container ( * children ) ]
if edit :
if not interaction . response . is_done ( ) : await interaction . response . edit_message ( content = None , components = components )
else : await interaction . edit_original_response ( content = None , components = components )
else :
await interaction . response . send_message ( components = components , ephemeral = True )
@commands.slash_command ( name = " modpanel " , description = " Ouvre le panel de modération " )
2026-05-06 17:07:09 +02:00
@commands.has_permissions ( moderate_members = True )
async def modpanel ( self , interaction : disnake . ApplicationCommandInteraction , member : disnake . Member ) :
2026-05-01 16:44:50 +02:00
if member . top_role > = interaction . user . top_role and interaction . user . id != interaction . guild . owner_id :
2026-05-16 18:05:04 +02:00
return await interaction . response . send_message ( " ❌ Permissions insuffisantes. " , ephemeral = True )
await self . send_modpanel_v2 ( interaction , member )
async def send_modconfig_v2 ( self , interaction : disnake . Interaction , edit = False ) :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
2026-05-21 23:02:00 +02:00
cursor . execute ( ' SELECT log_channel_id, board_channel_id, appeal_channel_id, warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ? ' , ( interaction . guild_id , ) )
2026-05-16 18:05:04 +02:00
res = cursor . fetchone ( ) ; conn . close ( )
if not res : return
2026-05-21 23:02:00 +02:00
2026-05-16 18:05:04 +02:00
children = [
disnake . ui . Section ( " ⚙️ Configuration Modération " , accessory = disnake . ui . Thumbnail ( interaction . guild . icon . url if interaction . guild . icon else None ) ) ,
disnake . ui . Separator ( divider = True ) ,
disnake . ui . TextDisplay ( f " 📁 **Logs internes :** <# { res [ 0 ] } > " if res [ 0 ] else " 📁 **Logs :** Non défini " ) ,
disnake . ui . TextDisplay ( f " 📢 **Affichage public :** <# { res [ 1 ] } > " if res [ 1 ] else " 📢 **Affichage :** Non défini " ) ,
2026-05-21 23:02:00 +02:00
disnake . ui . TextDisplay ( f " 🔗 **Contestation :** <# { res [ 2 ] } > " if res [ 2 ] else " 🔗 **Contestation :** Non défini " ) ,
disnake . ui . TextDisplay ( f " ⚖️ **Paliers :** T: { res [ 3 ] } | K: { res [ 4 ] } | B: { res [ 5 ] } " ) ,
disnake . ui . TextDisplay ( f " ⏱️ **Timeout auto :** { res [ 6 ] } s " ) ,
2026-05-16 18:05:04 +02:00
disnake . ui . Separator ( divider = True ) ,
disnake . ui . Section ( " 📁 Salon des logs " , accessory = disnake . ui . Button ( label = " Logs " , style = disnake . ButtonStyle . primary , custom_id = " modcfg_logs " ) ) ,
disnake . ui . Section ( " 📢 Salon d ' affichage " , accessory = disnake . ui . Button ( label = " Public " , style = disnake . ButtonStyle . primary , custom_id = " modcfg_board " ) ) ,
2026-05-21 23:02:00 +02:00
disnake . ui . Section ( " 🔗 Salon de contestation " , accessory = disnake . ui . Button ( label = " Contester " , style = disnake . ButtonStyle . primary , custom_id = " modcfg_appeal " ) ) ,
2026-05-16 18:05:04 +02:00
disnake . ui . Section ( " ⚖️ Modifier paliers " , accessory = disnake . ui . Button ( label = " Paliers " , style = disnake . ButtonStyle . secondary , custom_id = " modcfg_limits " ) ) ,
disnake . ui . Section ( " ⏱️ Durée timeout " , accessory = disnake . ui . Button ( label = " Durée " , style = disnake . ButtonStyle . secondary , custom_id = " modcfg_duration " ) )
]
components = [ disnake . ui . Container ( * children ) ]
if edit :
if not interaction . response . is_done ( ) : await interaction . response . edit_message ( content = None , components = components )
else : await interaction . edit_original_response ( content = None , components = components )
else :
await interaction . response . send_message ( components = components , ephemeral = True )
2026-05-01 16:44:50 +02:00
2026-05-21 23:02:00 +02:00
@commands.slash_command ( name = " sanctions " , description = " Voir l ' historique des sanctions d ' un membre " )
@commands.has_permissions ( moderate_members = True )
async def sanctions ( self , interaction : disnake . ApplicationCommandInteraction , member : disnake . Member ) :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' SELECT type, reason, timestamp, moderator_id, duration FROM sanctions WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC LIMIT 10 ' , ( interaction . guild_id , member . id ) )
rows = cursor . fetchall ( ) ; conn . close ( )
if not rows :
return await interaction . response . send_message ( f " Aucune sanction trouvée pour { member . mention } . " , ephemeral = True )
children = [
disnake . ui . Section ( f " 📋 Historique : { member . name } " , accessory = disnake . ui . Thumbnail ( member . display_avatar . url ) ) ,
disnake . ui . Separator ( divider = True ) ,
]
for t , r , ts , mod_id , dur in rows :
ts_int = int ( datetime . fromisoformat ( str ( ts ) ) . timestamp ( ) ) if not isinstance ( ts , datetime ) else int ( ts . timestamp ( ) )
moderator = interaction . guild . get_member ( mod_id )
mod_name = moderator . name if moderator else f " Modérateur # { mod_id } "
dur_str = f " ( { self . _format_duration ( dur ) } ) " if dur else " "
children . append ( disnake . ui . TextDisplay ( f " { self . _type_to_emoji ( t ) } ** { t } { dur_str } ** par ** { mod_name } ** — { r or ' Sans raison ' } (<t: { ts_int } :R>) " ) )
components = [ disnake . ui . Container ( * children ) ]
await interaction . response . send_message ( components = components , ephemeral = True )
2026-05-06 17:07:09 +02:00
@commands.slash_command ( name = " modconfig " , description = " Configuration de la modération " )
@commands.has_permissions ( administrator = True )
async def modconfig ( self , interaction : disnake . ApplicationCommandInteraction ) :
2026-05-16 18:05:04 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
2026-05-01 16:44:50 +02:00
cursor . execute ( ' INSERT OR IGNORE INTO mod_config (guild_id) VALUES (?) ' , ( interaction . guild_id , ) )
2026-05-16 18:05:04 +02:00
conn . commit ( ) ; conn . close ( )
await self . send_modconfig_v2 ( interaction )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
@commands.Cog.listener ( " on_message_interaction " )
async def on_mod_interaction ( self , interaction : disnake . MessageInteraction ) :
cid = interaction . data . custom_id
if not cid : return
if cid . startswith ( " modpan_ " ) :
action , mid = cid . replace ( " modpan_ " , " " ) . split ( " : " )
member = interaction . guild . get_member ( int ( mid ) )
if not member : return await interaction . response . send_message ( " ❌ Membre introuvable. " , ephemeral = True )
2026-05-19 18:50:16 +02:00
# Vérifications des permissions et de la hiérarchie
if member . top_role > = interaction . user . top_role and interaction . user . id != interaction . guild . owner_id :
return await interaction . response . send_message ( " ❌ Permissions insuffisantes : ce membre a un rôle supérieur ou égal au vôtre. " , ephemeral = True )
perms = interaction . user . guild_permissions
if action == " ban " and not perms . ban_members :
return await interaction . response . send_message ( " ❌ Vous n ' avez pas la permission de bannir des membres. " , ephemeral = True )
elif action == " kick " and not perms . kick_members :
return await interaction . response . send_message ( " ❌ Vous n ' avez pas la permission d ' expulser des membres. " , ephemeral = True )
elif action in [ " timeout " , " warn " , " clear " ] and not perms . moderate_members :
return await interaction . response . send_message ( " ❌ Vous n ' avez pas la permission de gérer les sanctions (moderate_members). " , ephemeral = True )
2026-05-16 18:05:04 +02:00
if action == " clear " :
await self . clearwarns ( interaction , member , " Nettoyage Panel " )
await self . send_modpanel_v2 ( interaction , member , edit = True )
else :
await interaction . response . send_modal ( ModReasonModal ( self , action . upper ( ) , member ) )
elif cid . startswith ( " modcfg_ " ) :
2026-05-19 18:50:16 +02:00
if not interaction . user . guild_permissions . administrator :
return await interaction . response . send_message ( " ❌ Vous devez être administrateur pour modifier cette configuration. " , ephemeral = True )
2026-05-16 18:05:04 +02:00
act = cid . replace ( " modcfg_ " , " " )
if act in [ " logs " , " board " ] :
col = " log_channel_id " if act == " logs " else " board_channel_id "
view = disnake . ui . View ( )
sel = disnake . ui . ChannelSelect ( placeholder = " Choisir salon... " , channel_types = [ disnake . ChannelType . text ] )
async def sel_cb ( i ) :
c = sel . values [ 0 ]
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( f " UPDATE mod_config SET { col } = ? WHERE guild_id = ? " , ( c . id , i . guild_id ) )
conn . commit ( ) ; conn . close ( )
await i . response . send_message ( f " ✅ Salon mis à jour. " , ephemeral = True )
await self . send_modconfig_v2 ( interaction , edit = True )
sel . callback = sel_cb ; view . add_item ( sel )
await interaction . response . send_message ( f " Sélectionnez le salon pour { act } : " , view = view , ephemeral = True )
elif act == " limits " :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( " SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban FROM mod_config WHERE guild_id = ? " , ( interaction . guild_id , ) )
res = cursor . fetchone ( ) ; conn . close ( )
view = disnake . ui . View ( )
b1 = disnake . ui . Button ( label = f " Timeout ( { res [ 0 ] } ) " ) ; b1 . callback = lambda i : i . response . send_modal ( ModConfigModal ( " Limite Timeout " , res [ 0 ] ) )
b2 = disnake . ui . Button ( label = f " Kick ( { res [ 1 ] } ) " ) ; b2 . callback = lambda i : i . response . send_modal ( ModConfigModal ( " Limite Kick " , res [ 1 ] ) )
b3 = disnake . ui . Button ( label = f " Ban ( { res [ 2 ] } ) " ) ; b3 . callback = lambda i : i . response . send_modal ( ModConfigModal ( " Limite Ban " , res [ 2 ] ) )
view . add_item ( b1 ) ; view . add_item ( b2 ) ; view . add_item ( b3 )
await interaction . response . send_message ( " Modifier quel palier ? " , view = view , ephemeral = True )
elif act == " duration " :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( " SELECT timeout_duration FROM mod_config WHERE guild_id = ? " , ( interaction . guild_id , ) )
res = cursor . fetchone ( ) ; conn . close ( )
await interaction . response . send_modal ( ModConfigModal ( " Durée Timeout (s) " , res [ 0 ] ) )
2026-05-21 23:02:00 +02:00
elif act == " appeal " :
view = disnake . ui . View ( )
sel = disnake . ui . ChannelSelect ( placeholder = " Choisir salon... " , channel_types = [ disnake . ChannelType . text ] )
async def sel_cb ( i ) :
c = sel . values [ 0 ]
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( " UPDATE mod_config SET appeal_channel_id = ? WHERE guild_id = ? " , ( c . id , i . guild_id ) )
conn . commit ( ) ; conn . close ( )
await i . response . send_message ( f " ✅ Salon de contestation mis à jour. " , ephemeral = True )
await self . send_modconfig_v2 ( interaction , edit = True )
sel . callback = sel_cb ; view . add_item ( sel )
await interaction . response . send_message ( " Sélectionnez le salon pour la contestation : " , view = view , ephemeral = True )
2026-05-16 18:05:04 +02:00
async def warn ( self , interaction , member , reason ) :
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
2026-05-01 16:44:50 +02:00
cursor . execute ( ' INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?) ' , ( interaction . guild_id , member . id , interaction . user . id , ' WARN ' , reason ) )
cursor . execute ( ' SELECT COUNT(*) FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " WARN " AND status = " ACTIVE " ' , ( interaction . guild_id , member . id ) )
2026-05-16 18:05:04 +02:00
count = cursor . fetchone ( ) [ 0 ]
2026-05-01 16:44:50 +02:00
cursor . execute ( ' SELECT warn_limit_timeout, warn_limit_kick, warn_limit_ban, timeout_duration FROM mod_config WHERE guild_id = ? ' , ( interaction . guild_id , ) )
2026-05-16 18:05:04 +02:00
cfg = cursor . fetchone ( )
cursor . execute ( ' SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " WARN " ORDER BY id DESC LIMIT 1 ' , ( interaction . guild_id , member . id ) )
s_id , ts = cursor . fetchone ( ) ; conn . commit ( ) ; conn . close ( )
if not interaction . response . is_done ( ) : await interaction . response . send_message ( f " ✅ { member . mention } averti ( { count } ). " , ephemeral = True )
await self . send_mod_log ( interaction . guild , [ disnake . ui . Container ( disnake . ui . Section ( f " ⚠️ Warn : { member } " , accessory = disnake . ui . Thumbnail ( member . display_avatar . url ) ) , disnake . ui . Separator ( divider = True ) , disnake . ui . TextDisplay ( f " Raison: { reason } " ) , disnake . ui . TextDisplay ( f " Total: { count } " ) ) ] )
await self . send_sanction_board ( interaction . guild , self . build_sanction_board_components ( s_type = " WARN " , user = member , moderator = interaction . user , reason = reason , timestamp = ts , s_id = s_id ) )
2026-05-21 23:02:00 +02:00
dm_sent = await self . send_sanction_dm ( member , " WARN " , reason , interaction . user , interaction . guild )
if not dm_sent :
kuby_logger . warning ( f " DM non envoyé à { member } (DMs peut-être bloqués) " )
2026-05-16 18:05:04 +02:00
if cfg :
lt , lk , lb , dur = cfg
if count > = lb : await member . ban ( reason = f " Auto: { count } warns " )
elif count > = lk : await member . kick ( reason = f " Auto: { count } warns " )
elif count > = lt : await member . timeout ( duration = timedelta ( seconds = dur ) , reason = f " Auto: { count } warns " )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def timeout ( self , interaction , member , duration_str , reason ) :
2026-05-01 16:44:50 +02:00
secs = self . parse_duration ( duration_str )
if secs < = 0 : return await interaction . response . send_message ( " ❌ Durée invalide. " , ephemeral = True )
2026-05-06 17:07:09 +02:00
await member . timeout ( duration = timedelta ( seconds = secs ) , reason = reason )
2026-05-01 16:44:50 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason, duration) VALUES (?, ?, ?, ?, ?, ?) ' , ( interaction . guild_id , member . id , interaction . user . id , ' TIMEOUT ' , reason , secs ) )
2026-05-16 18:05:04 +02:00
cursor . execute ( ' SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " TIMEOUT " ORDER BY id DESC LIMIT 1 ' , ( interaction . guild_id , member . id ) )
s_id , ts = cursor . fetchone ( ) ; conn . commit ( ) ; conn . close ( )
if not interaction . response . is_done ( ) : await interaction . response . send_message ( f " ✅ { member . mention } timeout. " , ephemeral = True )
await self . send_sanction_board ( interaction . guild , self . build_sanction_board_components ( s_type = " TIMEOUT " , user = member , moderator = interaction . user , reason = reason , timestamp = ts , s_id = s_id , duration = secs ) )
2026-05-21 23:02:00 +02:00
await self . send_sanction_dm ( member , " TIMEOUT " , reason , interaction . user , interaction . guild , duration = secs )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def kick ( self , interaction , member , reason ) :
2026-05-01 16:44:50 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?) ' , ( interaction . guild_id , member . id , interaction . user . id , ' KICK ' , reason ) )
2026-05-16 18:05:04 +02:00
cursor . execute ( ' SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " KICK " ORDER BY id DESC LIMIT 1 ' , ( interaction . guild_id , member . id ) )
s_id , ts = cursor . fetchone ( ) ; conn . commit ( ) ; conn . close ( )
2026-05-21 23:02:00 +02:00
await self . send_sanction_dm ( member , " KICK " , reason , interaction . user , interaction . guild )
await member . kick ( reason = reason )
2026-05-01 16:44:50 +02:00
if not interaction . response . is_done ( ) : await interaction . response . send_message ( f " ✅ { member . name } expulsé. " , ephemeral = True )
2026-05-16 18:05:04 +02:00
await self . send_sanction_board ( interaction . guild , self . build_sanction_board_components ( s_type = " KICK " , user = member , moderator = interaction . user , reason = reason , timestamp = ts , s_id = s_id ) )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def ban ( self , interaction , user , reason ) :
2026-05-01 16:44:50 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' INSERT INTO sanctions (guild_id, user_id, moderator_id, type, reason) VALUES (?, ?, ?, ?, ?) ' , ( interaction . guild_id , user . id , interaction . user . id , ' BAN ' , reason ) )
2026-05-16 18:05:04 +02:00
cursor . execute ( ' SELECT id, timestamp FROM sanctions WHERE guild_id = ? AND user_id = ? AND type = " BAN " ORDER BY id DESC LIMIT 1 ' , ( interaction . guild_id , user . id ) )
s_id , ts = cursor . fetchone ( ) ; conn . commit ( ) ; conn . close ( )
2026-05-21 23:02:00 +02:00
await self . send_sanction_dm ( user , " BAN " , reason , interaction . user , interaction . guild )
await interaction . guild . ban ( user , reason = reason )
2026-05-01 16:44:50 +02:00
if not interaction . response . is_done ( ) : await interaction . response . send_message ( f " ✅ { user . name } banni. " , ephemeral = True )
2026-05-16 18:05:04 +02:00
await self . send_sanction_board ( interaction . guild , self . build_sanction_board_components ( s_type = " BAN " , user = user , moderator = interaction . user , reason = reason , timestamp = ts , s_id = s_id ) )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
async def clearwarns ( self , interaction , member , reason ) :
2026-05-01 16:44:50 +02:00
conn = sqlite3 . connect ( self . db_path ) ; cursor = conn . cursor ( )
cursor . execute ( ' UPDATE sanctions SET status = " REVOKED " WHERE guild_id = ? AND user_id = ? AND type = " WARN " ' , ( interaction . guild_id , member . id ) )
conn . commit ( ) ; conn . close ( )
2026-05-16 18:05:04 +02:00
if not interaction . response . is_done ( ) : await interaction . response . send_message ( f " ✅ Warns effacés. " , ephemeral = True )
2026-05-01 16:44:50 +02:00
2026-05-16 18:05:04 +02:00
def setup ( bot ) : bot . add_cog ( Moderation ( bot ) )