mirror of
https://github.com/skylanix/MamieHenriette.git
synced 2026-02-06 06:40:35 +01:00
Ajout de la gestion des rôles de modération dans le panneau d'administration. Mise à jour de la logique de configuration pour permettre la sélection de plusieurs rôles. Amélioration de l'interface utilisateur pour la gestion des rôles et des commandes de modération. Ajout de la mise à jour automatique du cache anti-cheat et de nouvelles fonctionnalités pour récupérer et stocker les données anti-cheat.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlite3 import Cursor, Connection
|
||||
@@ -9,8 +11,28 @@ from webapp import webapp
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
webapp.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(basedir, "instance", "database.db")}'
|
||||
# Options moteur pour améliorer la concurrence SQLite
|
||||
webapp.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
'connect_args': {
|
||||
'check_same_thread': False,
|
||||
'timeout': 30
|
||||
},
|
||||
}
|
||||
webapp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db = SQLAlchemy(webapp)
|
||||
|
||||
# PRAGMA pour SQLite (WAL, busy timeout)
|
||||
@event.listens_for(Engine, "connect")
|
||||
def _set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
try:
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||
cursor.execute("PRAGMA synchronous=NORMAL;")
|
||||
cursor.execute("PRAGMA busy_timeout=30000;")
|
||||
cursor.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
|
||||
cursor.execute(f'PRAGMA table_info({table_name})')
|
||||
columns = cursor.fetchall()
|
||||
|
||||
@@ -51,3 +51,13 @@ class ModerationEvent(db.Model):
|
||||
staff_name = db.Column(db.String(256))
|
||||
duration = db.Column(db.Integer)
|
||||
|
||||
class AntiCheatCache(db.Model):
|
||||
__tablename__ = 'anticheat_cache'
|
||||
steam_id = db.Column(db.String(32), primary_key=True)
|
||||
game_name = db.Column(db.String(256))
|
||||
status = db.Column(db.String(32))
|
||||
anticheats = db.Column(db.String(512))
|
||||
reference = db.Column(db.String(512))
|
||||
notes = db.Column(db.String(1024))
|
||||
updated_at = db.Column(db.DateTime)
|
||||
|
||||
|
||||
@@ -57,3 +57,13 @@ CREATE TABLE IF NOT EXISTS `moderation_event` (
|
||||
`staff_name` VARCHAR(256) NOT NULL,
|
||||
`duration` INTEGER NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `anticheat_cache` (
|
||||
steam_id VARCHAR(32) PRIMARY KEY,
|
||||
game_name VARCHAR(256) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
anticheats VARCHAR(512),
|
||||
reference VARCHAR(512),
|
||||
notes VARCHAR(1024),
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
@@ -8,7 +8,17 @@ from database.helpers import ConfigurationHelper
|
||||
from database.models import Configuration, Humeur, Commande
|
||||
from discord import Message, TextChannel, Member
|
||||
from discordbot.humblebundle import checkHumbleBundleAndNotify
|
||||
from discordbot.command import handle_warning_command, handle_remove_warning_command, handle_list_warnings_command, handle_ban_command, handle_kick_command, handle_unban_command
|
||||
from discordbot.moderation import (
|
||||
handle_warning_command,
|
||||
handle_remove_warning_command,
|
||||
handle_list_warnings_command,
|
||||
handle_ban_command,
|
||||
handle_kick_command,
|
||||
handle_unban_command,
|
||||
handle_inspect_command,
|
||||
handle_ban_list_command,
|
||||
handle_staff_help_command
|
||||
)
|
||||
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
||||
from protondb import searhProtonDb
|
||||
|
||||
@@ -48,10 +58,25 @@ class DiscordBot(discord.Client):
|
||||
channels.append(channel)
|
||||
return channels
|
||||
|
||||
def getAllRoles(self):
|
||||
guilds_roles = []
|
||||
for guild in self.guilds:
|
||||
roles = []
|
||||
for role in guild.roles:
|
||||
if role.name != "@everyone":
|
||||
roles.append(role)
|
||||
if roles:
|
||||
guilds_roles.append({
|
||||
'guild_name': guild.name,
|
||||
'guild_id': guild.id,
|
||||
'roles': roles
|
||||
})
|
||||
return guilds_roles
|
||||
|
||||
|
||||
def begin(self) :
|
||||
token = Configuration.query.filter_by(key='discord_token').first()
|
||||
if token :
|
||||
if token and token.value and token.value.strip():
|
||||
self.run(token.value)
|
||||
else :
|
||||
logging.error('Aucun token Discord configuré. Le bot ne peut pas être démarré')
|
||||
@@ -92,12 +117,23 @@ async def on_message(message: Message):
|
||||
if command_name == '!unban':
|
||||
await handle_unban_command(message, bot)
|
||||
return
|
||||
if command_name == '!banlist':
|
||||
await handle_ban_list_command(message, bot)
|
||||
return
|
||||
|
||||
if ConfigurationHelper().getValue('moderation_kick_enable'):
|
||||
if command_name == '!kick':
|
||||
await handle_kick_command(message, bot)
|
||||
return
|
||||
|
||||
if ConfigurationHelper().getValue('moderation_enable'):
|
||||
if command_name == '!inspect':
|
||||
await handle_inspect_command(message, bot)
|
||||
return
|
||||
if command_name in ['!aide', '!help']:
|
||||
await handle_staff_help_command(message, bot)
|
||||
return
|
||||
|
||||
commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first()
|
||||
if commande:
|
||||
try:
|
||||
@@ -106,27 +142,80 @@ async def on_message(message: Message):
|
||||
except Exception as e:
|
||||
logging.error(f'Échec de l\'exécution de la commande Discord : {e}')
|
||||
|
||||
if(ConfigurationHelper().getValue('proton_db_enable_enable') and message.content.find('!protondb')==0) :
|
||||
# Commande !protondb ou !pdb avec embed
|
||||
if (ConfigurationHelper().getValue('proton_db_enable_enable') and (message.content.startswith('!protondb') or message.content.startswith('!pdb'))):
|
||||
if (message.content.find('<@')>0) :
|
||||
mention = message.content[message.content.find('<@'):]
|
||||
else :
|
||||
mention = message.author.mention
|
||||
name = message.content.replace('!protondb', '').replace(f'{mention}', '').strip();
|
||||
# Nettoyer le nom en enlevant la commande (!protondb ou !pdb)
|
||||
name = message.content
|
||||
if name.startswith('!protondb'):
|
||||
name = name.replace('!protondb', '', 1)
|
||||
elif name.startswith('!pdb'):
|
||||
name = name.replace('!pdb', '', 1)
|
||||
name = name.replace(f'{mention}', '').strip();
|
||||
games = searhProtonDb(name)
|
||||
if (len(games)==0) :
|
||||
msg = f'{mention} Je n\'ai pas trouvé de jeux correspondant à **{name}**. Es-tu sûr que le jeu est disponible sur Steam ?'
|
||||
else :
|
||||
msg = f'{mention} J\'ai trouvé {len(games)} jeux :\n'
|
||||
ite = iter(games)
|
||||
while (game := next(ite, None)) is not None and len(msg) < 1850 :
|
||||
msg += f'- [{game.get('name')}](https://www.protondb.com/app/{game.get('id')}) classé **{game.get('tier')}**\n'
|
||||
rest = sum(1 for _ in ite)
|
||||
if (rest > 0):
|
||||
msg += f'- et encore {rest} autres jeux'
|
||||
try :
|
||||
try:
|
||||
await message.channel.send(msg, suppress_embeds=True)
|
||||
except Exception as e:
|
||||
logging.error(f'Échec de l\'envoi du message ProtonDB : {e}')
|
||||
logging.error(f"Échec de l'envoi du message ProtonDB : {e}")
|
||||
return
|
||||
|
||||
# Construire un bel embed
|
||||
embed = discord.Embed(
|
||||
title=f"🔎 Résultats ProtonDB pour {name}",
|
||||
color=discord.Color.blurple()
|
||||
)
|
||||
embed.set_footer(text=f"Demandé par {message.author.name}")
|
||||
|
||||
max_fields = 10
|
||||
count = 0
|
||||
for game in games:
|
||||
if count >= max_fields:
|
||||
break
|
||||
g_name = str(game.get('name'))
|
||||
g_id = str(game.get('id'))
|
||||
tier = str(game.get('tier') or 'N/A')
|
||||
# Anti-cheat info si disponible
|
||||
ac_status = game.get('anticheat_status')
|
||||
ac_emoji = ''
|
||||
ac_text = ''
|
||||
if ac_status:
|
||||
status_lower = str(ac_status).lower()
|
||||
if status_lower == 'supported':
|
||||
ac_emoji, ac_text = '✅', 'Supporté'
|
||||
elif status_lower == 'running':
|
||||
ac_emoji, ac_text = '⚠️', 'Fonctionne'
|
||||
elif status_lower == 'broken':
|
||||
ac_emoji, ac_text = '❌', 'Cassé'
|
||||
elif status_lower == 'denied':
|
||||
ac_emoji, ac_text = '🚫', 'Refusé'
|
||||
elif status_lower == 'planned':
|
||||
ac_emoji, ac_text = '📅', 'Planifié'
|
||||
else:
|
||||
ac_emoji, ac_text = '❔', str(ac_status)
|
||||
acs = game.get('anticheats') or []
|
||||
ac_list = ', '.join([str(ac) for ac in acs if ac])
|
||||
ac_line = f" | Anti-cheat: {ac_emoji} **{ac_text}**"
|
||||
if ac_list:
|
||||
ac_line += f" ({ac_list})"
|
||||
else:
|
||||
ac_line = ''
|
||||
value = f"Tier: **{tier}**{ac_line}\nLien: https://www.protondb.com/app/{g_id}"
|
||||
embed.add_field(name=g_name, value=value[:1024], inline=False)
|
||||
count += 1
|
||||
|
||||
rest = max(0, len(games) - count)
|
||||
if rest > 0:
|
||||
embed.add_field(name="…", value=f"et encore {rest} autres jeux", inline=False)
|
||||
|
||||
try :
|
||||
await message.channel.send(content=mention, embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Échec de l'envoi de l'embed ProtonDB : {e}")
|
||||
|
||||
@bot.event
|
||||
async def on_member_join(member: Member):
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
import discord
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
from database.models import ModerationEvent
|
||||
from discord import Message
|
||||
|
||||
def get_staff_role_id():
|
||||
staff_role = ConfigurationHelper().getValue('moderation_staff_role_id')
|
||||
return int(staff_role) if staff_role else 581990740431732738
|
||||
|
||||
def get_embed_delete_delay():
|
||||
delay = ConfigurationHelper().getValue('moderation_embed_delete_delay')
|
||||
return int(delay) if delay else 0
|
||||
|
||||
async def delete_after_delay(message):
|
||||
delay = get_embed_delete_delay()
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def handle_warning_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!averto @utilisateur [raison]` ou `!averto <id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!averto @User Spam dans le chat`\n• `!warn 123456789012345678 Comportement inapproprié`\n• `!av @User`", inline=False)
|
||||
embed.add_field(name="Aliases", value="`!averto`, `!av`, `!avertissement`, `!warn`", inline=False)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
target_user = None
|
||||
if message.mentions:
|
||||
target_user = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
else:
|
||||
try:
|
||||
user_id = int(parts[1])
|
||||
target_user = await bot.fetch_user(user_id)
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
except (ValueError, discord.NotFound):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
if not target_user:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Impossible de trouver l'utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
event = ModerationEvent(
|
||||
type='warning',
|
||||
username=target_user.name,
|
||||
discord_id=str(target_user.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **averti**.",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await message.delete()
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
async def handle_remove_warning_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!delaverto <id>`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!delaverto 5`\n• `!removewarn 12`", inline=False)
|
||||
embed.add_field(name="Aliases", value="`!delaverto`, `!removewarn`, `!delwarn`", inline=False)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
try:
|
||||
event_id = int(parts[1])
|
||||
except ValueError:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="L'ID doit être un nombre entier.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
event = ModerationEvent.query.filter_by(id=event_id).first()
|
||||
|
||||
if not event:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Aucun événement de modération trouvé avec l'ID `{event_id}`.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
username = event.username
|
||||
event_type = event.type
|
||||
|
||||
db.session.delete(event)
|
||||
db.session.commit()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="✅ Événement supprimé",
|
||||
description=f"L'événement de type **{event_type}** pour **{username}** (ID: {event_id}) a été supprimé.",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.add_field(name="🛡️ Modérateur", value=f"{message.author.name}\n`{message.author.id}`", inline=True)
|
||||
embed.set_footer(text="Mamie Henriette")
|
||||
|
||||
await message.channel.send(embed=embed)
|
||||
await message.delete()
|
||||
|
||||
async def handle_list_warnings_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=1)
|
||||
user_filter = None
|
||||
|
||||
if len(parts) > 1 and message.mentions:
|
||||
user_filter = str(message.mentions[0].id)
|
||||
|
||||
if user_filter:
|
||||
events = ModerationEvent.query.filter_by(discord_id=user_filter).order_by(ModerationEvent.created_at.desc()).all()
|
||||
else:
|
||||
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||
|
||||
if not events:
|
||||
embed = discord.Embed(
|
||||
title="📋 Liste des événements",
|
||||
description="Aucun événement de modération trouvé.",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
page = 0
|
||||
per_page = 5
|
||||
max_page = (len(events) - 1) // per_page
|
||||
|
||||
def create_embed(page_num):
|
||||
start = page_num * per_page
|
||||
end = start + per_page
|
||||
page_events = events[start:end]
|
||||
|
||||
embed = discord.Embed(
|
||||
title="📋 Liste des événements de modération",
|
||||
description=f"Total : {len(events)} événement(s)",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
for event in page_events:
|
||||
date_str = event.created_at.strftime('%d/%m/%Y %H:%M') if event.created_at else 'N/A'
|
||||
embed.add_field(
|
||||
name=f"ID {event.id} - {event.type.upper()} - {event.username}",
|
||||
value=f"**Discord ID:** `{event.discord_id}`\n**Date:** {date_str}\n**Raison:** {event.reason}\n**Staff:** {event.staff_name}",
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.set_footer(text=f"Page {page_num + 1}/{max_page + 1}")
|
||||
return embed
|
||||
|
||||
msg = await message.channel.send(embed=create_embed(page))
|
||||
|
||||
if max_page > 0:
|
||||
await msg.add_reaction('⬅️')
|
||||
await msg.add_reaction('➡️')
|
||||
await msg.add_reaction('❌')
|
||||
|
||||
def check(reaction, user):
|
||||
return user == message.author and str(reaction.emoji) in ['⬅️', '➡️', '❌'] and reaction.message.id == msg.id
|
||||
|
||||
while True:
|
||||
try:
|
||||
reaction, user = await bot.wait_for('reaction_add', timeout=60.0, check=check)
|
||||
|
||||
if str(reaction.emoji) == '❌':
|
||||
await msg.delete()
|
||||
break
|
||||
elif str(reaction.emoji) == '➡️' and page < max_page:
|
||||
page += 1
|
||||
await msg.edit(embed=create_embed(page))
|
||||
elif str(reaction.emoji) == '⬅️' and page > 0:
|
||||
page -= 1
|
||||
await msg.edit(embed=create_embed(page))
|
||||
|
||||
await msg.remove_reaction(reaction, user)
|
||||
except:
|
||||
break
|
||||
|
||||
if msg:
|
||||
try:
|
||||
await msg.clear_reactions()
|
||||
except:
|
||||
pass
|
||||
|
||||
await message.delete()
|
||||
|
||||
async def handle_ban_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!ban @utilisateur [raison]` ou `!ban <id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!ban @User Spam répété`\n• `!ban 123456789012345678 Comportement toxique`", inline=False)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
target_user = None
|
||||
if message.mentions:
|
||||
target_user = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
else:
|
||||
try:
|
||||
user_id = int(parts[1])
|
||||
target_user = await bot.fetch_user(user_id)
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
except (ValueError, discord.NotFound):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
if not target_user:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Impossible de trouver l'utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
member = message.guild.get_member(target_user.id)
|
||||
joined_days = None
|
||||
if member and member.joined_at:
|
||||
delta = datetime.utcnow() - member.joined_at.replace(tzinfo=None)
|
||||
joined_days = delta.days
|
||||
|
||||
try:
|
||||
await message.guild.ban(target_user, reason=reason)
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour bannir cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
event = ModerationEvent(
|
||||
type='ban',
|
||||
username=target_user.name,
|
||||
discord_id=str(target_user.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **banni**.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
embed.add_field(name="ID Discord", value=f"`{target_user.id}`", inline=False)
|
||||
if joined_days is not None:
|
||||
embed.add_field(name="Membre depuis", value=f"{joined_days} jour{'s' if joined_days > 1 else ''}", inline=False)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await message.delete()
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
async def handle_kick_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2 or not message.mentions:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!kick @utilisateur [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!kick @User Spam dans le chat`\n• `!kick @User Comportement inapproprié`", inline=False)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
target_member = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
|
||||
member_obj = message.guild.get_member(target_member.id)
|
||||
if not member_obj:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="L'utilisateur n'est pas membre du serveur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
joined_days = None
|
||||
if member_obj.joined_at:
|
||||
delta = datetime.utcnow() - member_obj.joined_at.replace(tzinfo=None)
|
||||
joined_days = delta.days
|
||||
|
||||
try:
|
||||
await message.guild.kick(member_obj, reason=reason)
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour expulser cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
event = ModerationEvent(
|
||||
type='kick',
|
||||
username=target_member.name,
|
||||
discord_id=str(target_member.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_member.name}** (`@{target_member.name}`) a été **expulsé**.",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
if joined_days is not None:
|
||||
embed.add_field(name="Membre depuis", value=f"{joined_days} jour{'s' if joined_days > 1 else ''}", inline=False)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await message.delete()
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
async def handle_unban_command(message: Message, bot):
|
||||
if not any(role.id == get_staff_role_id() for role in message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!unban <discord_id>` ou `!unban #<sanction_id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!unban 123456789012345678`\n• `!unban #5 Appel accepté`", inline=False)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
target_user = None
|
||||
discord_id = None
|
||||
|
||||
if parts[1].startswith('#'):
|
||||
try:
|
||||
sanction_id = int(parts[1][1:])
|
||||
event = ModerationEvent.query.filter_by(id=sanction_id, type='ban').first()
|
||||
if not event:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Aucune sanction de ban trouvée avec l'ID #{sanction_id}.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
discord_id = event.discord_id
|
||||
try:
|
||||
target_user = await bot.fetch_user(int(discord_id))
|
||||
except discord.NotFound:
|
||||
pass
|
||||
except ValueError:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="ID de sanction invalide.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
discord_id = parts[1]
|
||||
target_user = await bot.fetch_user(int(discord_id))
|
||||
except (ValueError, discord.NotFound):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="ID Discord invalide ou utilisateur introuvable.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
try:
|
||||
await message.guild.unban(discord.Object(id=int(discord_id)), reason=reason)
|
||||
except discord.NotFound:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Cet utilisateur n'est pas banni.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour débannir cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
username = target_user.name if target_user else f"ID: {discord_id}"
|
||||
|
||||
event = ModerationEvent(
|
||||
type='unban',
|
||||
username=username,
|
||||
discord_id=discord_id,
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{username}** (`@{username}`) a été **débanni**.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await message.delete()
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
925
discordbot/moderation.py
Normal file
925
discordbot/moderation.py
Normal file
@@ -0,0 +1,925 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import discord
|
||||
from datetime import datetime, timezone
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
from database.models import ModerationEvent
|
||||
from discord import Message
|
||||
|
||||
def get_staff_role_ids():
|
||||
staff_roles = ConfigurationHelper().getValue('moderation_staff_role_ids')
|
||||
if staff_roles:
|
||||
return [int(role_id.strip()) for role_id in staff_roles.split(',') if role_id.strip()]
|
||||
staff_role_old = ConfigurationHelper().getValue('moderation_staff_role_id')
|
||||
if staff_role_old:
|
||||
return [int(staff_role_old)]
|
||||
return []
|
||||
|
||||
def has_staff_role(user_roles):
|
||||
staff_role_ids = get_staff_role_ids()
|
||||
if not staff_role_ids:
|
||||
return False
|
||||
return any(role.id in staff_role_ids for role in user_roles)
|
||||
|
||||
def get_embed_delete_delay():
|
||||
delay = ConfigurationHelper().getValue('moderation_embed_delete_delay')
|
||||
return int(delay) if delay else 0
|
||||
|
||||
async def delete_after_delay(message):
|
||||
delay = get_embed_delete_delay()
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def safe_delete_message(message: Message):
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def send_access_denied(channel):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def send_user_not_found(channel):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def parse_target_user_and_reason(message, bot, parts: list):
|
||||
if message.mentions:
|
||||
target_user = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
return target_user, reason
|
||||
|
||||
try:
|
||||
user_id = int(parts[1])
|
||||
target_user = await bot.fetch_user(user_id)
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
return target_user, reason
|
||||
except (ValueError, discord.NotFound):
|
||||
return None, None
|
||||
|
||||
async def send_warning_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!averto @utilisateur [raison]` ou `!averto <id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!averto @User Spam dans le chat`\n• `!warn 123456789012345678 Comportement inapproprié`\n• `!av @User`", inline=False)
|
||||
embed.add_field(name="Aliases", value="`!averto`, `!av`, `!avertissement`, `!warn`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
def create_warning_event(target_user, reason: str, staff_member):
|
||||
event = ModerationEvent(
|
||||
type='warning',
|
||||
username=target_user.name,
|
||||
discord_id=str(target_user.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(staff_member.id),
|
||||
staff_name=staff_member.name
|
||||
)
|
||||
db.session.add(event)
|
||||
_commit_with_retry()
|
||||
|
||||
def _commit_with_retry(max_retries: int = 5, base_delay: float = 0.1):
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
db.session.commit()
|
||||
return
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if 'database is locked' in msg.lower() and attempt < max_retries:
|
||||
db.session.rollback()
|
||||
delay = base_delay * (2 ** attempt)
|
||||
time.sleep(delay)
|
||||
attempt += 1
|
||||
continue
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
async def send_warning_confirmation(channel, target_user, reason: str, original_message: Message):
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **averti**.",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await channel.send(embed=embed)
|
||||
await safe_delete_message(original_message)
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
async def handle_warning_command(message: Message, bot):
|
||||
parts = message.content.split(maxsplit=2)
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
elif len(parts) < 2:
|
||||
await send_warning_usage(message.channel)
|
||||
else:
|
||||
target_user, reason = await parse_target_user_and_reason(message, bot, parts)
|
||||
if not target_user:
|
||||
await send_user_not_found(message.channel)
|
||||
else:
|
||||
await _process_warning_success(message, target_user, reason)
|
||||
|
||||
async def _process_warning_success(message: Message, target_user, reason: str):
|
||||
create_warning_event(target_user, reason, message.author)
|
||||
await send_warning_confirmation(message.channel, target_user, reason, message)
|
||||
|
||||
async def send_remove_warning_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!delaverto <id>`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!delaverto 5`\n• `!removewarn 12`", inline=False)
|
||||
embed.add_field(name="Aliases", value="`!delaverto`, `!removewarn`, `!delwarn`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def send_invalid_event_id(channel):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="L'ID doit être un nombre entier.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def send_event_not_found(channel, event_id: int):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Aucun événement de modération trouvé avec l'ID `{event_id}`.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
def delete_moderation_event(event: ModerationEvent):
|
||||
db.session.delete(event)
|
||||
db.session.commit()
|
||||
|
||||
async def send_event_deleted_confirmation(channel, event: ModerationEvent, moderator, original_message: Message):
|
||||
embed = discord.Embed(
|
||||
title="✅ Événement supprimé",
|
||||
description=f"L'événement de type **{event.type}** pour **{event.username}** (ID: {event.id}) a été supprimé.",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.add_field(name="🛡️ Modérateur", value=f"{moderator.name}\n`{moderator.id}`", inline=True)
|
||||
embed.set_footer(text="Mamie Henriette")
|
||||
|
||||
await channel.send(embed=embed)
|
||||
await safe_delete_message(original_message)
|
||||
|
||||
async def handle_remove_warning_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
await send_remove_warning_usage(message.channel)
|
||||
return
|
||||
|
||||
try:
|
||||
event_id = int(parts[1])
|
||||
except ValueError:
|
||||
await send_invalid_event_id(message.channel)
|
||||
return
|
||||
|
||||
event = ModerationEvent.query.filter_by(id=event_id).first()
|
||||
|
||||
if not event:
|
||||
await send_event_not_found(message.channel, event_id)
|
||||
return
|
||||
|
||||
delete_moderation_event(event)
|
||||
await send_event_deleted_confirmation(message.channel, event, message.author, message)
|
||||
|
||||
def get_moderation_events(user_filter: str = None):
|
||||
if user_filter:
|
||||
return ModerationEvent.query.filter_by(discord_id=user_filter).order_by(ModerationEvent.created_at.desc()).all()
|
||||
return ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||
|
||||
async def send_no_events_found(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Liste des événements",
|
||||
description="Aucun événement de modération trouvé.",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
def create_events_list_embed(events: list, page_num: int, per_page: int):
|
||||
start = page_num * per_page
|
||||
end = start + per_page
|
||||
page_events = events[start:end]
|
||||
max_page = (len(events) - 1) // per_page
|
||||
|
||||
embed = discord.Embed(
|
||||
title="📋 Liste des événements de modération",
|
||||
description=f"Total : {len(events)} événement(s)",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
for event in page_events:
|
||||
date_str = event.created_at.strftime('%d/%m/%Y %H:%M') if event.created_at else 'N/A'
|
||||
embed.add_field(
|
||||
name=f"ID {event.id} - {event.type.upper()} - {event.username}",
|
||||
value=f"**Discord ID:** `{event.discord_id}`\n**Date:** {date_str}\n**Raison:** {event.reason}\n**Staff:** {event.staff_name}",
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.set_footer(text=f"Page {page_num + 1}/{max_page + 1}")
|
||||
return embed
|
||||
|
||||
async def add_pagination_reactions(msg, max_page: int):
|
||||
if max_page > 0:
|
||||
await msg.add_reaction('⬅️')
|
||||
await msg.add_reaction('➡️')
|
||||
await msg.add_reaction('❌')
|
||||
|
||||
async def handle_pagination_loop(msg, bot, message_author, events: list, per_page: int):
|
||||
page = 0
|
||||
max_page = (len(events) - 1) // per_page
|
||||
|
||||
def check(reaction, user):
|
||||
return user == message_author and str(reaction.emoji) in ['⬅️', '➡️', '❌'] and reaction.message.id == msg.id
|
||||
|
||||
while True:
|
||||
try:
|
||||
reaction, user = await bot.wait_for('reaction_add', timeout=60.0, check=check)
|
||||
|
||||
if str(reaction.emoji) == '❌':
|
||||
await msg.delete()
|
||||
break
|
||||
elif str(reaction.emoji) == '➡️' and page < max_page:
|
||||
page += 1
|
||||
await msg.edit(embed=create_events_list_embed(events, page, per_page))
|
||||
elif str(reaction.emoji) == '⬅️' and page > 0:
|
||||
page -= 1
|
||||
await msg.edit(embed=create_events_list_embed(events, page, per_page))
|
||||
|
||||
await msg.remove_reaction(reaction, user)
|
||||
except:
|
||||
break
|
||||
|
||||
try:
|
||||
await msg.clear_reactions()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def handle_list_warnings_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=1)
|
||||
user_filter = str(message.mentions[0].id) if len(parts) > 1 and message.mentions else None
|
||||
|
||||
events = get_moderation_events(user_filter)
|
||||
|
||||
if not events:
|
||||
await send_no_events_found(message.channel)
|
||||
return
|
||||
|
||||
per_page = 5
|
||||
max_page = (len(events) - 1) // per_page
|
||||
|
||||
msg = await message.channel.send(embed=create_events_list_embed(events, 0, per_page))
|
||||
await add_pagination_reactions(msg, max_page)
|
||||
await handle_pagination_loop(msg, bot, message.author, events, per_page)
|
||||
await safe_delete_message(message)
|
||||
|
||||
async def handle_ban_command(message: Message, bot):
|
||||
parts = message.content.split(maxsplit=2)
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
elif len(parts) < 2:
|
||||
await _send_ban_usage(message.channel)
|
||||
else:
|
||||
target_user, reason = await _parse_ban_target_and_reason(message, bot, parts)
|
||||
if not target_user:
|
||||
await _send_user_not_found_for_ban(message.channel)
|
||||
else:
|
||||
await _process_ban_success(message, target_user, reason)
|
||||
|
||||
async def _send_ban_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!ban @utilisateur [raison]` ou `!ban <id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!ban @User Spam répété`\n• `!ban 123456789012345678 Comportement toxique`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def _parse_ban_target_and_reason(message: Message, bot, parts: list):
|
||||
if message.mentions:
|
||||
return message.mentions[0], (parts[2] if len(parts) > 2 else "Sans raison")
|
||||
try:
|
||||
user_id = int(parts[1])
|
||||
user = await bot.fetch_user(user_id)
|
||||
return user, (parts[2] if len(parts) > 2 else "Sans raison")
|
||||
except (ValueError, discord.NotFound):
|
||||
return None, None
|
||||
|
||||
async def _send_user_not_found_for_ban(channel):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Utilisateur introuvable. Vérifiez la mention ou l'ID Discord.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
def _create_ban_event(target_user, reason: str, staff_member):
|
||||
event = ModerationEvent(
|
||||
type='ban',
|
||||
username=target_user.name,
|
||||
discord_id=str(target_user.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(staff_member.id),
|
||||
staff_name=staff_member.name
|
||||
)
|
||||
db.session.add(event)
|
||||
_commit_with_retry()
|
||||
return event
|
||||
|
||||
async def _process_ban_success(message: Message, target_user, reason: str):
|
||||
member = message.guild.get_member(target_user.id)
|
||||
joined_days = None
|
||||
if member and member.joined_at:
|
||||
delta = datetime.utcnow() - member.joined_at.replace(tzinfo=None)
|
||||
joined_days = delta.days
|
||||
try:
|
||||
await message.guild.ban(target_user, reason=reason)
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour bannir cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
event = _create_ban_event(target_user, reason, message.author)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_user.name}** (`@{target_user.name}`) a été **banni**.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
embed.add_field(name="ID Discord", value=f"`{target_user.id}`", inline=False)
|
||||
if joined_days is not None:
|
||||
embed.add_field(name="Membre depuis", value=format_days_to_age(joined_days), inline=False)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await safe_delete_message(message)
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
async def handle_unban_command(message: Message, bot):
|
||||
parts = message.content.split(maxsplit=2)
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
elif len(parts) < 2:
|
||||
await _send_unban_usage(message.channel)
|
||||
else:
|
||||
target_user, discord_id, reason = await _parse_unban_target_and_reason(message, bot, parts)
|
||||
if not discord_id:
|
||||
await _send_unban_invalid_id(message.channel)
|
||||
else:
|
||||
await _process_unban_success(message, bot, target_user, discord_id, reason)
|
||||
|
||||
async def _send_unban_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!unban <discord_id>` ou `!unban #<sanction_id> [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!unban 123456789012345678`\n• `!unban #5 Appel accepté`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def _parse_unban_target_and_reason(message: Message, bot, parts: list):
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
target_user = None
|
||||
discord_id = None
|
||||
if parts[1].startswith('#'):
|
||||
try:
|
||||
sanction_id = int(parts[1][1:])
|
||||
evt = ModerationEvent.query.filter_by(id=sanction_id, type='ban').first()
|
||||
if not evt:
|
||||
return None, None, reason
|
||||
discord_id = evt.discord_id
|
||||
try:
|
||||
target_user = await bot.fetch_user(int(discord_id))
|
||||
except discord.NotFound:
|
||||
pass
|
||||
except ValueError:
|
||||
return None, None, reason
|
||||
else:
|
||||
try:
|
||||
discord_id = parts[1]
|
||||
target_user = await bot.fetch_user(int(discord_id))
|
||||
except (ValueError, discord.NotFound):
|
||||
return None, None, reason
|
||||
return target_user, discord_id, reason
|
||||
|
||||
async def _send_unban_invalid_id(channel):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="ID Discord invalide ou utilisateur introuvable.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def _process_unban_success(message: Message, bot, target_user, discord_id: str, reason: str):
|
||||
try:
|
||||
await message.guild.unban(discord.Object(id=int(discord_id)), reason=reason)
|
||||
except discord.NotFound:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Cet utilisateur n'est pas banni.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour débannir cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
username = target_user.name if target_user else f"ID: {discord_id}"
|
||||
create = ModerationEvent(
|
||||
type='unban',
|
||||
username=username,
|
||||
discord_id=discord_id,
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(create)
|
||||
_commit_with_retry()
|
||||
|
||||
try:
|
||||
asyncio.create_task(_send_unban_invite(message, bot, target_user, discord_id))
|
||||
except:
|
||||
pass
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{username}** (`@{username}`) a été **débanni**.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await safe_delete_message(message)
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
async def _send_unban_invite(message: Message, bot, target_user, discord_id: str):
|
||||
try:
|
||||
user_obj = target_user or await bot.fetch_user(int(discord_id))
|
||||
channel = None
|
||||
try:
|
||||
channel_id = ConfigurationHelper().getIntValue('welcome_channel_id')
|
||||
if channel_id:
|
||||
channel = bot.get_channel(channel_id) or message.guild.get_channel(channel_id)
|
||||
except:
|
||||
pass
|
||||
if not channel:
|
||||
me = message.guild.me or message.guild.get_member(bot.user.id)
|
||||
for ch in message.guild.text_channels:
|
||||
try:
|
||||
perms = ch.permissions_for(me) if me else None
|
||||
if not perms or not perms.create_instant_invite:
|
||||
continue
|
||||
channel = ch
|
||||
break
|
||||
except:
|
||||
continue
|
||||
if not channel:
|
||||
channel = message.guild.system_channel or message.channel
|
||||
invite = None
|
||||
try:
|
||||
invite = await channel.create_invite(max_age=86400, max_uses=1, unique=True, reason='Invitation automatique après débannissement')
|
||||
except Exception as e:
|
||||
logging.warning(f"[UNBAN] Échec création d'invitation sur #{channel and channel.name}: {e}")
|
||||
return
|
||||
if user_obj and invite:
|
||||
try:
|
||||
msg = f"Tu as été débanni de {message.guild.name}. Voici une invitation pour revenir : {invite.url}"
|
||||
await user_obj.send(msg)
|
||||
except Exception as e:
|
||||
logging.warning(f"[UNBAN] Impossible d'envoyer un MP à {user_obj} ({user_obj.id}): {e}")
|
||||
try:
|
||||
await message.author.send(f"Impossible d'envoyer un MP à {user_obj} pour l'unban. Voici l'invitation à lui transmettre : {invite.url}")
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
async def handle_ban_list_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Vous n'avez pas les permissions nécessaires pour utiliser cette commande.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
# Récupérer la liste des bannis
|
||||
bans = []
|
||||
try:
|
||||
async for entry in message.guild.bans(limit=None):
|
||||
bans.append(entry)
|
||||
except TypeError:
|
||||
try:
|
||||
bans = await message.guild.bans()
|
||||
except Exception:
|
||||
bans = []
|
||||
except Exception:
|
||||
bans = []
|
||||
|
||||
if not bans:
|
||||
embed = discord.Embed(
|
||||
title="🔨 Utilisateurs bannis",
|
||||
description="Aucun utilisateur banni sur ce serveur.",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
await safe_delete_message(message)
|
||||
return
|
||||
|
||||
page = 0
|
||||
per_page = 10
|
||||
max_page = (len(bans) - 1) // per_page
|
||||
|
||||
def create_banlist_embed(page_num: int):
|
||||
start = page_num * per_page
|
||||
end = start + per_page
|
||||
page_bans = bans[start:end]
|
||||
embed = discord.Embed(
|
||||
title="🔨 Utilisateurs bannis",
|
||||
description=f"Total : {len(bans)} utilisateur(s) banni(s)",
|
||||
color=discord.Color.red(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
for entry in page_bans:
|
||||
user = entry.user
|
||||
reason = entry.reason or 'Sans raison'
|
||||
embed.add_field(
|
||||
name=f"{user.name} ({user.id})",
|
||||
value=f"Raison: {reason}",
|
||||
inline=False
|
||||
)
|
||||
embed.set_footer(text=f"Page {page_num + 1}/{max_page + 1}")
|
||||
return embed
|
||||
|
||||
msg = await message.channel.send(embed=create_banlist_embed(page))
|
||||
if max_page > 0:
|
||||
await msg.add_reaction('⬅️')
|
||||
await msg.add_reaction('➡️')
|
||||
await msg.add_reaction('❌')
|
||||
|
||||
def check(reaction, user):
|
||||
return user == message.author and str(reaction.emoji) in ['⬅️', '➡️', '❌'] and reaction.message.id == msg.id
|
||||
|
||||
while True:
|
||||
try:
|
||||
reaction, user = await bot.wait_for('reaction_add', timeout=60.0, check=check)
|
||||
if str(reaction.emoji) == '❌':
|
||||
await msg.delete()
|
||||
break
|
||||
elif str(reaction.emoji) == '➡️' and page < max_page:
|
||||
page += 1
|
||||
await msg.edit(embed=create_banlist_embed(page))
|
||||
elif str(reaction.emoji) == '⬅️' and page > 0:
|
||||
page -= 1
|
||||
await msg.edit(embed=create_banlist_embed(page))
|
||||
await msg.remove_reaction(reaction, user)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
try:
|
||||
await msg.clear_reactions()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await safe_delete_message(message)
|
||||
|
||||
async def handle_staff_help_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
embed = discord.Embed(
|
||||
title="❌ Accès refusé",
|
||||
description="Cette commande est réservée au staff.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
|
||||
embed = discord.Embed(
|
||||
title="🛠️ Aide staff",
|
||||
description="Commandes de modération disponibles",
|
||||
color=discord.Color.blurple(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.set_footer(text=f"Demandé par {message.author.name}")
|
||||
|
||||
# Avertissements
|
||||
if ConfigurationHelper().getValue('moderation_enable'):
|
||||
value = (
|
||||
"• `!averto @utilisateur [raison]`\n"
|
||||
"• `!delaverto <id>`\n"
|
||||
"• `!warnings` ou `!warnings @utilisateur`\n"
|
||||
"Exemples:\n"
|
||||
"`!averto @User Spam`\n"
|
||||
"`!delaverto 12`\n"
|
||||
"`!warnings @User`"
|
||||
)
|
||||
embed.add_field(name="⚠️ Avertissements", value=value, inline=False)
|
||||
# Inspect
|
||||
embed.add_field(
|
||||
name="🔎 Inspection",
|
||||
value=("• `!inspect @utilisateur` ou `!inspect <id>`\n"
|
||||
"Ex: `!inspect @User`"),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Bans / Unban
|
||||
if ConfigurationHelper().getValue('moderation_ban_enable'):
|
||||
value = (
|
||||
"• `!ban @utilisateur [raison]`\n"
|
||||
"• `!unban <discord_id>` ou `!unban #<sanction_id> [raison]`\n"
|
||||
"• `!banlist`\n"
|
||||
"Exemples:\n"
|
||||
"`!ban @User Toxicité`\n"
|
||||
"`!unban 123456789012345678 Erreur`\n"
|
||||
"`!unban #5 Appel accepté`"
|
||||
)
|
||||
embed.add_field(name="🔨 Ban / Unban", value=value, inline=False)
|
||||
|
||||
# Kick
|
||||
if ConfigurationHelper().getValue('moderation_kick_enable'):
|
||||
value = (
|
||||
"• `!kick @utilisateur [raison]`\n"
|
||||
"Exemple: `!kick @User Spam`"
|
||||
)
|
||||
embed.add_field(name="👢 Kick", value=value, inline=False)
|
||||
|
||||
try:
|
||||
sent = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(sent))
|
||||
except Exception:
|
||||
pass
|
||||
await safe_delete_message(message)
|
||||
|
||||
async def handle_kick_command(message: Message, bot):
|
||||
parts = message.content.split(maxsplit=2)
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
elif len(parts) < 2 or not message.mentions:
|
||||
await _send_kick_usage(message.channel)
|
||||
else:
|
||||
target_member = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
await _process_kick_success(message, target_member, reason)
|
||||
|
||||
async def _send_kick_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!kick @utilisateur [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!kick @User Spam dans le chat`\n• `!kick @User Comportement inapproprié`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def _process_kick_success(message: Message, target_member, reason: str):
|
||||
member_obj = message.guild.get_member(target_member.id)
|
||||
if not member_obj:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="L'utilisateur n'est pas membre du serveur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
joined_days = None
|
||||
if member_obj.joined_at:
|
||||
delta = datetime.utcnow() - member_obj.joined_at.replace(tzinfo=None)
|
||||
joined_days = delta.days
|
||||
try:
|
||||
await message.guild.kick(member_obj, reason=reason)
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas les permissions nécessaires pour expulser cet utilisateur.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
return
|
||||
create = ModerationEvent(
|
||||
type='kick',
|
||||
username=target_member.name,
|
||||
discord_id=str(target_member.id),
|
||||
created_at=datetime.utcnow(),
|
||||
reason=reason,
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(create)
|
||||
_commit_with_retry()
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Sanction",
|
||||
description=f"L'utilisateur **{target_member.name}** (`@{target_member.name}`) a été **expulsé**.",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
if joined_days is not None:
|
||||
embed.add_field(name="Membre depuis", value=format_days_to_age(joined_days), inline=False)
|
||||
if reason != "Sans raison":
|
||||
embed.add_field(name="Raison", value=reason, inline=False)
|
||||
sent_message = await message.channel.send(embed=embed)
|
||||
await safe_delete_message(message)
|
||||
asyncio.create_task(delete_after_delay(sent_message))
|
||||
|
||||
def format_days_to_age(days: int) -> str:
|
||||
if days >= 365:
|
||||
years = days // 365
|
||||
remaining_days = days % 365
|
||||
if remaining_days > 0:
|
||||
return f"{years} an{'s' if years > 1 else ''} et {remaining_days} jour{'s' if remaining_days > 1 else ''}"
|
||||
return f"{years} an{'s' if years > 1 else ''}"
|
||||
return f"{days} jour{'s' if days > 1 else ''}"
|
||||
|
||||
async def get_member_join_info(guild, member_id: int):
|
||||
member = guild.get_member(member_id)
|
||||
if not member or not member.joined_at:
|
||||
return None, None
|
||||
|
||||
join_date = member.joined_at
|
||||
days_on_server = (datetime.now(timezone.utc) - join_date).days
|
||||
return join_date, days_on_server
|
||||
|
||||
def get_account_age(user):
|
||||
if not user.created_at:
|
||||
return None
|
||||
account_age = (datetime.now(timezone.utc) - user.created_at).days
|
||||
return account_age
|
||||
|
||||
def get_user_moderation_history(discord_id: str):
|
||||
events = ModerationEvent.query.filter_by(discord_id=discord_id).order_by(ModerationEvent.created_at.desc()).all()
|
||||
|
||||
warnings = [e for e in events if e.type == 'warning']
|
||||
kicks = [e for e in events if e.type == 'kick']
|
||||
bans = [e for e in events if e.type == 'ban']
|
||||
|
||||
return warnings, kicks, bans
|
||||
|
||||
async def send_inspect_usage(channel):
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!inspect @utilisateur` ou `!inspect <id>`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemples", value="• `!inspect @User`\n• `!inspect 123456789012345678`", inline=False)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def parse_target_user(message: Message, bot, parts: list):
|
||||
if message.mentions:
|
||||
return message.mentions[0]
|
||||
|
||||
try:
|
||||
user_id = int(parts[1])
|
||||
return await bot.fetch_user(user_id)
|
||||
except (ValueError, discord.NotFound):
|
||||
return None
|
||||
|
||||
def create_inspect_embed(user, member, join_date, days_on_server, account_age, warnings, kicks, bans, invite_info):
|
||||
embed = discord.Embed(
|
||||
title=f"🔍 Inspection de {user.name}",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
embed.set_thumbnail(url=user.display_avatar.url)
|
||||
embed.add_field(name="👤 Utilisateur", value=f"{user.mention}\n`{user.id}`", inline=True)
|
||||
|
||||
if account_age is not None:
|
||||
embed.add_field(
|
||||
name="📅 Compte créé",
|
||||
value=f"{user.created_at.strftime('%d/%m/%Y')}\n({format_days_to_age(account_age)})",
|
||||
inline=True
|
||||
)
|
||||
|
||||
if member and join_date:
|
||||
embed.add_field(
|
||||
name="📥 Rejoint le serveur",
|
||||
value=f"{join_date.strftime('%d/%m/%Y à %H:%M')}\n({format_days_to_age(days_on_server)})",
|
||||
inline=True
|
||||
)
|
||||
|
||||
if invite_info:
|
||||
embed.add_field(name="🎫 Invitation", value=invite_info, inline=True)
|
||||
else:
|
||||
embed.add_field(name="🎫 Invitation", value="Inconnue", inline=True)
|
||||
|
||||
warning_text = f"⚠️ **{len(warnings)}** avertissement{'s' if len(warnings) > 1 else ''}"
|
||||
kick_text = f"👢 **{len(kicks)}** expulsion{'s' if len(kicks) > 1 else ''}"
|
||||
ban_text = f"🔨 **{len(bans)}** ban{'s' if len(bans) > 1 else ''}"
|
||||
|
||||
mod_history = f"{warning_text}\n{kick_text}\n{ban_text}"
|
||||
|
||||
if warnings or kicks or bans:
|
||||
embed.add_field(name="📋 Historique de modération", value=mod_history, inline=False)
|
||||
|
||||
if warnings:
|
||||
recent_warnings = warnings[:3]
|
||||
warnings_detail = "\n".join([
|
||||
f"• ID {w.id} - {w.created_at.strftime('%d/%m/%Y')} - {w.reason[:50]}{'...' if len(w.reason) > 50 else ''}"
|
||||
for w in recent_warnings
|
||||
])
|
||||
if len(warnings) > 3:
|
||||
warnings_detail += f"\n*... et {len(warnings) - 3} autre(s)*"
|
||||
embed.add_field(name="⚠️ Derniers avertissements", value=warnings_detail, inline=False)
|
||||
else:
|
||||
embed.add_field(name="✅ Historique de modération", value="Aucun incident", inline=False)
|
||||
|
||||
embed.set_footer(text="Mamie Henriette")
|
||||
return embed
|
||||
|
||||
async def get_invite_info_for_user(bot, guild, user_id: int):
|
||||
try:
|
||||
from discordbot.welcome import invite_cache
|
||||
|
||||
audit_logs = [entry async for entry in guild.audit_logs(limit=100, action=discord.AuditLogAction.member_join)]
|
||||
|
||||
for entry in audit_logs:
|
||||
if entry.target and entry.target.id == user_id:
|
||||
if hasattr(entry, 'extra') and entry.extra:
|
||||
return f"Code: `{entry.extra}`"
|
||||
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
async def handle_inspect_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
await send_inspect_usage(message.channel)
|
||||
return
|
||||
|
||||
target_user = await parse_target_user(message, bot, parts)
|
||||
|
||||
if not target_user:
|
||||
await send_user_not_found(message.channel)
|
||||
return
|
||||
|
||||
member = message.guild.get_member(target_user.id)
|
||||
join_date, days_on_server = await get_member_join_info(message.guild, target_user.id)
|
||||
account_age = get_account_age(target_user)
|
||||
warnings, kicks, bans = get_user_moderation_history(str(target_user.id))
|
||||
invite_info = await get_invite_info_for_user(bot, message.guild, target_user.id)
|
||||
|
||||
embed = create_inspect_embed(
|
||||
target_user,
|
||||
member,
|
||||
join_date,
|
||||
days_on_server,
|
||||
account_age,
|
||||
warnings,
|
||||
kicks,
|
||||
bans,
|
||||
invite_info
|
||||
)
|
||||
|
||||
await message.channel.send(embed=embed)
|
||||
await safe_delete_message(message)
|
||||
|
||||
@@ -112,7 +112,9 @@ async def sendLeaveMessage(bot: discord.Client, member: Member):
|
||||
reason = 'Départ volontaire'
|
||||
try:
|
||||
async for entry in member.guild.audit_logs(limit=5):
|
||||
if entry.target and entry.target.id == member.id:
|
||||
if not (entry.target and entry.target.id == member.id):
|
||||
continue
|
||||
|
||||
if entry.action == discord.AuditLogAction.kick:
|
||||
reason = f'Expulsé par {entry.user.mention}'
|
||||
if entry.reason:
|
||||
|
||||
0
logs/app.log
Normal file
0
logs/app.log
Normal file
5373
logs/runner.out
Normal file
5373
logs/runner.out
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from algoliasearch.search.client import SearchClientSync, SearchConfig
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
from database.models import GameAlias
|
||||
from sqlalchemy import desc,func
|
||||
from database.models import GameAlias, AntiCheatCache, Configuration
|
||||
from sqlalchemy import desc, func
|
||||
|
||||
def _call_algoliasearch(search_name:str):
|
||||
config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'),
|
||||
@@ -37,9 +39,130 @@ def _apply_game_aliases(search_name:str) -> str:
|
||||
search_name = re.sub(re.escape(alias.alias), alias.name, search_name, flags=re.IGNORECASE)
|
||||
return search_name
|
||||
|
||||
def _should_update_anticheat_cache() -> bool:
|
||||
try:
|
||||
last_update_conf = Configuration.query.filter_by(key='anticheat_last_update').first()
|
||||
if not last_update_conf:
|
||||
return True
|
||||
try:
|
||||
last_update = datetime.fromisoformat(last_update_conf.value)
|
||||
return datetime.now() - last_update > timedelta(days=7)
|
||||
except:
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la vérification du cache anti-cheat: {e}')
|
||||
return False
|
||||
|
||||
def _fetch_anticheat_data():
|
||||
try:
|
||||
url = 'https://raw.githubusercontent.com/AreWeAntiCheatYet/AreWeAntiCheatYet/master/games.json'
|
||||
response = requests.get(url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logging.error(f'Échec de la récupération des données anti-cheat. Code HTTP: {response.status_code}')
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la récupération des données anti-cheat: {e}')
|
||||
return None
|
||||
|
||||
def _update_anticheat_cache_if_needed():
|
||||
try:
|
||||
if not _should_update_anticheat_cache():
|
||||
return
|
||||
|
||||
logging.info('Mise à jour du cache anti-cheat...')
|
||||
anticheat_data = _fetch_anticheat_data()
|
||||
if not anticheat_data:
|
||||
return
|
||||
|
||||
for game in anticheat_data:
|
||||
try:
|
||||
steam_id = str(game.get('storeIds', {}).get('steam', ''))
|
||||
if not steam_id or steam_id == '0':
|
||||
continue
|
||||
|
||||
cache_entry = AntiCheatCache.query.filter_by(steam_id=steam_id).first()
|
||||
|
||||
status = game.get('status', 'Unknown')
|
||||
anticheats_list = game.get('anticheats', [])
|
||||
anticheats_str = json.dumps(anticheats_list) if anticheats_list else None
|
||||
reference = game.get('reference', '')
|
||||
notes_data = game.get('notes', '')
|
||||
if isinstance(notes_data, list):
|
||||
notes = json.dumps(notes_data)
|
||||
else:
|
||||
notes = str(notes_data) if notes_data else ''
|
||||
game_name = game.get('name', '')
|
||||
|
||||
if cache_entry:
|
||||
cache_entry.game_name = game_name
|
||||
cache_entry.status = status
|
||||
cache_entry.anticheats = anticheats_str
|
||||
cache_entry.reference = reference
|
||||
cache_entry.notes = notes
|
||||
cache_entry.updated_at = datetime.now()
|
||||
else:
|
||||
cache_entry = AntiCheatCache(
|
||||
steam_id=steam_id,
|
||||
game_name=game_name,
|
||||
status=status,
|
||||
anticheats=anticheats_str,
|
||||
reference=reference,
|
||||
notes=notes,
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
db.session.add(cache_entry)
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la mise à jour du jeu {game.get("name")}: {e}')
|
||||
continue
|
||||
|
||||
last_update_conf = Configuration.query.filter_by(key='anticheat_last_update').first()
|
||||
if last_update_conf:
|
||||
last_update_conf.value = datetime.now().isoformat()
|
||||
else:
|
||||
last_update_conf = Configuration(key='anticheat_last_update', value=datetime.now().isoformat())
|
||||
db.session.add(last_update_conf)
|
||||
|
||||
db.session.commit()
|
||||
logging.info('Cache anti-cheat mis à jour avec succès')
|
||||
except Exception as e:
|
||||
try:
|
||||
db.session.rollback()
|
||||
except:
|
||||
pass
|
||||
logging.error(f'Erreur lors de la mise à jour du cache anti-cheat: {e}')
|
||||
|
||||
def _get_anticheat_info(steam_id: str) -> dict:
|
||||
try:
|
||||
cache_entry = AntiCheatCache.query.filter_by(steam_id=steam_id).first()
|
||||
if not cache_entry:
|
||||
return None
|
||||
|
||||
try:
|
||||
anticheats = json.loads(cache_entry.anticheats) if cache_entry.anticheats else []
|
||||
except:
|
||||
anticheats = []
|
||||
|
||||
return {
|
||||
'status': cache_entry.status,
|
||||
'anticheats': anticheats,
|
||||
'reference': cache_entry.reference,
|
||||
'notes': cache_entry.notes
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la récupération des infos anti-cheat pour {steam_id}: {e}')
|
||||
return None
|
||||
|
||||
def searhProtonDb(search_name:str):
|
||||
results = []
|
||||
search_name = _apply_game_aliases(search_name)
|
||||
|
||||
try:
|
||||
_update_anticheat_cache_if_needed()
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la mise à jour du cache anti-cheat: {e}')
|
||||
|
||||
responses = _call_algoliasearch(search_name)
|
||||
for hit in responses.model_dump().get('hits'):
|
||||
id = hit.get('object_id')
|
||||
@@ -49,12 +172,27 @@ def searhProtonDb(search_name:str):
|
||||
summmary = _call_summary(id)
|
||||
if (summmary != None) :
|
||||
tier = summmary.get('tier')
|
||||
results.append({
|
||||
|
||||
anticheat_info = None
|
||||
try:
|
||||
anticheat_info = _get_anticheat_info(str(id))
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors de la récupération anti-cheat pour {name}: {e}')
|
||||
|
||||
result = {
|
||||
'id':id,
|
||||
'name' : name,
|
||||
'tier' : tier
|
||||
})
|
||||
logging.info(f'Trouvé {name}({id}) : {tier}')
|
||||
}
|
||||
|
||||
if anticheat_info:
|
||||
result['anticheat_status'] = anticheat_info.get('status')
|
||||
result['anticheats'] = anticheat_info.get('anticheats', [])
|
||||
result['anticheat_reference'] = anticheat_info.get('reference')
|
||||
result['anticheat_notes'] = anticheat_info.get('notes')
|
||||
|
||||
results.append(result)
|
||||
logging.info(f'Trouvé {name}({id}) : {tier}' + (f' [Anti-cheat: {anticheat_info.get("status")}]' if anticheat_info else ''))
|
||||
except Exception as e:
|
||||
logging.error(f'Erreur lors du traitement du jeu {name} (ID: {id}) : {e}')
|
||||
else:
|
||||
|
||||
40
run-web.py
40
run-web.py
@@ -1,6 +1,8 @@
|
||||
import locale
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from webapp import webapp
|
||||
from discordbot import bot
|
||||
@@ -23,12 +25,40 @@ def start_twitch_bot():
|
||||
twitchBot.begin()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Config logs (console + fichier avec rotation)
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
log_formatter = logging.Formatter('%(asctime)s %(levelname)s [%(threadName)s] %(name)s: %(message)s')
|
||||
handlers = []
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(log_formatter)
|
||||
handlers.append(stream_handler)
|
||||
file_handler = RotatingFileHandler('logs/app.log', maxBytes=5*1024*1024, backupCount=5, encoding='utf-8')
|
||||
file_handler.setFormatter(log_formatter)
|
||||
handlers.append(file_handler)
|
||||
logging.basicConfig(level=logging.INFO, handlers=handlers)
|
||||
|
||||
# Calmer les logs verbeux de certaines libs si besoin
|
||||
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||
logging.getLogger('discord').setLevel(logging.WARNING)
|
||||
|
||||
# Hook exceptions non-capturées (threads inclus)
|
||||
def _log_uncaught(exc_type, exc, tb):
|
||||
logging.exception('Exception non capturée', exc_info=(exc_type, exc, tb))
|
||||
import sys
|
||||
sys.excepthook = _log_uncaught
|
||||
if hasattr(threading, 'excepthook'):
|
||||
def _thread_excepthook(args):
|
||||
logging.exception(f"Exception dans le thread {args.thread.name}", exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
|
||||
threading.excepthook = _thread_excepthook
|
||||
|
||||
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
||||
|
||||
jobs = []
|
||||
jobs.append(threading.Thread(target=start_discord_bot))
|
||||
jobs.append(threading.Thread(target=start_server))
|
||||
jobs.append(threading.Thread(target=start_twitch_bot))
|
||||
jobs.append(threading.Thread(target=start_discord_bot, name='discord-bot'))
|
||||
jobs.append(threading.Thread(target=start_server, name='web-server'))
|
||||
jobs.append(threading.Thread(target=start_twitch_bot, name='twitch-bot'))
|
||||
|
||||
for job in jobs: job.start()
|
||||
for job in jobs: job.join()
|
||||
for job in jobs:
|
||||
job.start()
|
||||
for job in jobs:
|
||||
job.join()
|
||||
|
||||
@@ -6,22 +6,32 @@ from discordbot import bot
|
||||
|
||||
@webapp.route("/configurations")
|
||||
def openConfigurations():
|
||||
return render_template("configurations.html", configuration = ConfigurationHelper(), channels = bot.getAllTextChannel())
|
||||
return render_template("configurations.html", configuration = ConfigurationHelper(), channels = bot.getAllTextChannel(), roles = bot.getAllRoles())
|
||||
|
||||
@webapp.route("/configurations/update", methods=['POST'])
|
||||
def updateConfiguration():
|
||||
checkboxes = {
|
||||
'humble_bundle_enable': 'humble_bundle_channel',
|
||||
'proton_db_enable_enable': 'proton_db_api_id',
|
||||
'moderation_enable': 'moderation_staff_role_id',
|
||||
'moderation_ban_enable': 'moderation_staff_role_id',
|
||||
'moderation_kick_enable': 'moderation_staff_role_id',
|
||||
'moderation_enable': 'moderation_staff_role_ids',
|
||||
'moderation_ban_enable': 'moderation_staff_role_ids',
|
||||
'moderation_kick_enable': 'moderation_staff_role_ids',
|
||||
'welcome_enable': 'welcome_channel_id',
|
||||
'leave_enable': 'leave_channel_id'
|
||||
}
|
||||
|
||||
staff_roles = request.form.getlist('moderation_staff_role_ids')
|
||||
if staff_roles:
|
||||
ConfigurationHelper().createOrUpdate('moderation_staff_role_ids', ','.join(staff_roles))
|
||||
else:
|
||||
ConfigurationHelper().createOrUpdate('moderation_staff_role_ids', '')
|
||||
|
||||
for key in request.form:
|
||||
ConfigurationHelper().createOrUpdate(key, request.form.get(key))
|
||||
if key == 'moderation_staff_role_ids':
|
||||
continue
|
||||
value = request.form.get(key)
|
||||
if value and value.strip():
|
||||
ConfigurationHelper().createOrUpdate(key, value)
|
||||
|
||||
for checkbox, reference_field in checkboxes.items():
|
||||
if request.form.get(reference_field) is not None and request.form.get(checkbox) is None:
|
||||
|
||||
@@ -70,8 +70,87 @@
|
||||
Activer la commande d'expulsion (!kick)
|
||||
</label>
|
||||
|
||||
<label for="moderation_staff_role_id">ID du rôle Staff</label>
|
||||
<input name="moderation_staff_role_id" type="text" value="{{ configuration.getValue('moderation_staff_role_id') }}" placeholder="581990740431732738" />
|
||||
<label>Rôles Staff autorisés</label>
|
||||
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
|
||||
|
||||
{% if roles|length > 1 %}
|
||||
<div class="tabs">
|
||||
{% for guild_data in roles %}
|
||||
<button type="button" class="tab-button" onclick="openTab(event, 'guild-{{guild_data.guild_id}}')" {% if loop.first %}id="defaultOpen"{% endif %}>
|
||||
{{ guild_data.guild_name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for guild_data in roles %}
|
||||
<div id="guild-{{guild_data.guild_id}}" class="tab-content" {% if not loop.first %}style="display: none;"{% endif %}>
|
||||
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; border-radius: 5px;">
|
||||
{% for role in guild_data.roles %}
|
||||
<label style="display: block; margin: 5px 0;">
|
||||
<input type="checkbox" name="moderation_staff_role_ids" value="{{role.id}}" {% if role.id|string in selected_roles %}checked="checked"{% endif %}>
|
||||
{% if role.color.value != 0 %}
|
||||
<span style="color:#{{ '%06x' % role.color.value }}">●</span>
|
||||
{% else %}
|
||||
<span>○</span>
|
||||
{% endif %}
|
||||
{{role.name}}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<small>Sélectionnez un ou plusieurs rôles qui peuvent utiliser les commandes de modération</small>
|
||||
|
||||
<script>
|
||||
function openTab(evt, tabName) {
|
||||
var i, tabcontent, tabbuttons;
|
||||
tabcontent = document.getElementsByClassName("tab-content");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tabbuttons = document.getElementsByClassName("tab-button");
|
||||
for (i = 0; i < tabbuttons.length; i++) {
|
||||
tabbuttons[i].className = tabbuttons[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
document.getElementById("defaultOpen")?.click();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tabs {
|
||||
overflow: hidden;
|
||||
border-bottom: 2px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tab-button {
|
||||
background-color: #f1f1f1;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 20px;
|
||||
transition: 0.3s;
|
||||
font-size: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.tab-button.active {
|
||||
background-color: #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-content {
|
||||
animation: fadeEffect 0.3s;
|
||||
}
|
||||
@keyframes fadeEffect {
|
||||
from {opacity: 0;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<label for="moderation_embed_delete_delay">Délai de suppression des embeds (en secondes)</label>
|
||||
<input name="moderation_embed_delete_delay" type="number" value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" placeholder="0" min="0" />
|
||||
@@ -114,6 +193,8 @@
|
||||
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}
|
||||
checked="checked" {% endif %}>
|
||||
<label>Activer les notifications Humble Bundle</label>
|
||||
<p>Humble Bundle est un service qui propose des bundles de jeux vidéo à des prix réduits. Il est utile pour trouver des jeux vidéo à des prix intéressants.</p>
|
||||
<p>Il est possible de configurer un canal de notification pour recevoir les notifications des bundles Humble Bundle.</p>
|
||||
<label for="humble_bundle_channel">Canal de notification des packs Humble Bundle</label>
|
||||
<select name="humble_bundle_channel">
|
||||
{% for channel in channels %}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>Modération Discord</h1>
|
||||
<p>Les commandes de modération sont :</p>
|
||||
<ul>
|
||||
<li>!kick @utilisateur [raison]</li>
|
||||
<li>!ban @utilisateur [raison]</li>
|
||||
<li>!warn @utilisateur [raison]</li>
|
||||
<li>!unwarn @utilisateur</li>
|
||||
</ul>
|
||||
<p>Historique des actions de modération sur le serveur Discord.</p>
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user