12 Commits

Author SHA1 Message Date
Mow
9afd3b2588 Ajout d'une réaction d'avertissement pour les nouveaux membres ayant moins de 7 jours de compte lors de l'envoi du message de bienvenue. 2025-12-03 20:40:39 +01:00
skylanix
54b014c4c8 Merge pull request #39 from skylanix/humblebundlev3
Humblebundlev3
2025-11-16 22:25:12 +01:00
Mow
559a780a4f Correctif !pdb avec ajout du message de recherche, correction des bugs mention sur les notifications, et correction !say pour mettre un ID de channel 2025-11-16 21:55:05 +01:00
Mow
9abd7b8101 Ajout de la limitation de carractere 4096 MAx et 15 resultat a afficher 2025-11-12 23:47:46 +01:00
Mow
a66c31ecf6 Modification de l'embed affiche jusqu'a 35 jeux + notifi quand il cherche 2025-11-12 23:31:23 +01:00
skylanix
499fac9c12 Merge pull request #38 from skylanix/humblebundle+anticheat
Refonte de l'embed + controle !pdb si vide affiche un message d'aide
2025-11-12 22:21:18 +01:00
Mow
3e12c2cf08 Refonte de l'embed + controle !pdb si vide affiche un message d'aide 2025-11-12 22:13:08 +01:00
skylanix
5c76b50797 Merge pull request #36 from skylanix Fonctionnalités de modération
Fonctionnalités de modération
2025-11-12 01:26:15 +00:00
skylanix
d5d3e45a62 Ajouts des fonctionnalitées 2025-11-12 02:09:21 +01:00
Mow
cb559c2863 Ajout de la commande !say pour permettre l'envoi de messages en tant que bot dans un canal spécifié, avec des instructions d'utilisation . 2025-11-11 22:38:22 +01:00
Mow
a0a14abf57 correction de la exclusion temporaire et ajout de la commande de time out !to 2025-11-11 22:23:30 +01:00
Mow
a987ca311e Ajout de la commande !timeout pour exclure temporairement un utilisateur avec une durée spécifiée, ainsi que des améliorations dans la gestion des avertissements, y compris l'envoi de messages privés de confirmation et la mise à jour des messages d'utilisation. 2025-11-11 22:11:21 +01:00
4 changed files with 512 additions and 109 deletions

View File

@@ -56,16 +56,40 @@ Mamie Henriette est un bot intelligent open-source développé spécifiquement p
- Commande `!protondb nom_du_jeu` ou `!pdb nom_du_jeu` pour vérifier la compatibilité Linux/Steam Deck - Commande `!protondb nom_du_jeu` ou `!pdb nom_du_jeu` pour vérifier la compatibilité Linux/Steam Deck
- Recherche intelligente avec support des alias de jeux - Recherche intelligente avec support des alias de jeux
- Affichage du score de compatibilité, nombre de rapports et lien direct - Affichage du score de compatibilité, nombre de rapports et lien direct
- **Intégration anti-cheat** : Affiche automatiquement les systèmes anti-cheat et leur statut (supporté, cassé, refusé)
- Cache mis à jour automatiquement depuis AreWeAntiCheatYet
- **Modération** : Système complet de modération avec historique - **Modération** : Système complet de modération avec historique
- Avertissements : `!averto`, `!warn`, `!av`, `!avertissement` - **Avertissements** : `!averto`, `!warn`, `!av`, `!avertissement`
- Gestion des avertissements : `!delaverto`, `!removewarn`, `!delwarn` - Envoi automatique de DM à l'utilisateur averti
- Liste des événements : `!warnings`, `!listevent`, `!listwarn` - Support des timeouts combinés : `!warn @user raison --to durée`
- Inspection utilisateur : `!inspect` (historique complet, date d'arrivée, compte) - **Timeout** : `!timeout`, `!to` - Exclusion temporaire d'un utilisateur
- Bannissement : `!ban`, `!unban` (avec invitation automatique), `!banlist` - Syntaxe : `!to @user durée raison` (ex: `!to @User 10m Spam`)
- Expulsion : `!kick` - Durées supportées : secondes (s), minutes (m), heures (h), jours (j/days)
- Aide : `!aide`, `!help` - **Gestion des avertissements** : `!delaverto`, `!removewarn`, `!delwarn`
- Messages de bienvenue et départ personnalisables - **Liste des événements** : `!warnings`, `!listevent`, `!listwarn`
- Panneau d'administration web pour consulter l'historique - **Inspection utilisateur** : `!inspect @user`
- Historique complet des sanctions
- Date d'arrivée et durée sur le serveur
- Détection des comptes suspects (< 7 jours)
- Affichage du code d'invitation utilisé et de l'inviteur
- **Bannissement** : `!ban @user raison`, `!banlist`
- `!unban @user raison` ou `!unban #ID raison` (débannir par ID de sanction)
- Invitation automatique par DM lors du débannissement
- **Expulsion** : `!kick @user raison`
- **Annonces** : `!say #canal message` - Envoi de messages en tant que bot (staff uniquement)
- **Aide** : `!aide`, `!help` - Liste complète des commandes disponibles
- **Configuration avancée** :
- Support de multiples rôles staff
- Canal de logs dédié pour toutes les actions
- Suppression automatique des messages de modération (délai configurable)
- Activation/désactivation individuelle des fonctionnalités
- Panneau d'administration web pour consulter, éditer et supprimer l'historique
- **Messages de bienvenue et départ** :
- Messages personnalisables avec variables : `{member.mention}`, `{member.name}`, `{server.name}`, `{server.member_count}`
- **Système de tracking d'invitations** : Affiche qui a invité le nouveau membre
- **Messages de départ intelligents** : Détection automatique de la raison (volontaire, kick, ban)
- Affichage de la durée passée sur le serveur
- Embeds enrichis avec avatar et informations détaillées
### Twitch ### Twitch
- **Chat bot** : Commandes et interactions automatiques - **Chat bot** : Commandes et interactions automatiques
@@ -82,10 +106,23 @@ Mamie Henriette est un bot intelligent open-source développé spécifiquement p
### Interface d'administration ### Interface d'administration
- **Dashboard** : Vue d'ensemble et statistiques - **Dashboard** : Vue d'ensemble et statistiques
- **Configuration** : Tokens, paramètres des plateformes, configuration ProtonDB - **Configuration** :
- **Gestion des humeurs** : Création et modification des statuts - Tokens Discord/Twitch et paramètres des plateformes
- **Commandes** : Édition des commandes personnalisées - Configuration ProtonDB (API Algolia)
- **Modération** : Outils de gestion communautaire - Gestion des rôles staff (support de multiples rôles)
- Activation/désactivation individuelle des fonctionnalités (modération, ban, kick, welcome, leave)
- Configuration du délai de suppression automatique des messages de modération
- **Gestion des humeurs** : Création et modification des statuts Discord rotatifs
- **Commandes** : Édition des commandes personnalisées multi-plateformes
- **Modération** :
- Consultation de l'historique complet des sanctions
- Édition des raisons des événements de modération
- Suppression d'événements de modération
- Filtrage et recherche dans l'historique
- **Messages de bienvenue/départ** :
- Personnalisation des messages avec variables dynamiques
- Configuration des canaux de bienvenue et départ
- Activation/désactivation indépendante
## Installation ## Installation
@@ -134,7 +171,7 @@ cd MamieHenriette
``` ```
```bash ```bash
# 2. Récupérer l'image depuis Docker Hub et lancer # 2. Récupérer l'image depuis GitHub Container Registry et lancer
docker compose pull docker compose pull
docker compose up -d docker compose up -d
``` ```
@@ -163,12 +200,12 @@ cd MamieHenriette
```yaml ```yaml
services: services:
MamieHenriette: mamiehenriette:
container_name: MamieHenriette container_name: MamieHenriette
restart: unless-stopped restart: unless-stopped
build: . # ← Décommentez cette lignes build: . # ← Décommentez cette ligne
image: mamiehenriette # ← Décommentez cette lignes image: mamiehenriette # ← Décommentez cette ligne
# image: skylanix/mamiehenriette:latest # ← Commentez cette ligne # image: ghcr.io/skylanix/mamiehenriette:latest # ← Commentez cette ligne
# ... reste de la configuration # ... reste de la configuration
``` ```
@@ -234,9 +271,9 @@ services:
- Donnez un nom : `MamieHenriette` - Donnez un nom : `MamieHenriette`
- Collez la configuration ci-dessus dans l'éditeur - Collez la configuration ci-dessus dans l'éditeur
3. **Adapter les chemins** : 3. **Adapter les chemins des volumes** :
- Remplacez `/chemin/vers/instance` par le chemin absolu sur votre serveur (ex: `/opt/containers/MamieHenriette/instance`) - Modifiez `./instance` et `./logs` selon votre configuration
- Remplacez `/chemin/vers/logs` par le chemin absolu sur votre serveur (ex: `/opt/containers/MamieHenriette/logs`) - Exemple : `/opt/containers/MamieHenriette/instance` et `/opt/containers/MamieHenriette/logs`
4. **Déployer** : 4. **Déployer** :
- Cliquez sur "Deploy the stack" - Cliquez sur "Deploy the stack"
@@ -281,10 +318,12 @@ git pull origin main
# 3. Mettre à jour l'image Docker # 3. Mettre à jour l'image Docker
docker compose pull docker compose pull
# 4. Reconstruire et relancer # 4. Relancer
docker compose up --build -d docker compose up -d
``` ```
> 💡 **Note** : Si vous utilisez Watchtower, les mises à jour de l'image sont automatiques (vérification toutes les 30 minutes).
#### Sans Docker (installation locale) #### Sans Docker (installation locale)
```bash ```bash
# 1. Arrêter l'application # 1. Arrêter l'application
@@ -352,14 +391,16 @@ python run-web.py
## Spécifications techniques ## Spécifications techniques
### Base de données (SQLite) ### Base de données (SQLite)
- **Configuration** : Paramètres et tokens des plateformes - **Configuration** : Paramètres et tokens des plateformes, configuration des fonctionnalités
- **Humeur** : Statuts Discord rotatifs avec gestion automatique - **Humeur** : Statuts Discord rotatifs avec gestion automatique
- **Commande** : Commandes personnalisées multi-plateformes (Discord/Twitch) - **Commande** : Commandes personnalisées multi-plateformes (Discord/Twitch)
- **LiveAlert** : Configuration surveillance streamers Twitch (nom, canal Discord, statut) - **LiveAlert** : Configuration surveillance streamers Twitch (nom, canal Discord, statut)
- **GameAlias** : Alias pour améliorer les recherches ProtonDB - **GameAlias** : Alias pour améliorer les recherches ProtonDB
- **GameBundle** : Historique et notifications Humble Bundle - **GameBundle** : Historique et notifications Humble Bundle
- **Message** : Messages automatiques périodiques (implémenté) - **AntiCheatCache** : Cache des informations anti-cheat pour ProtonDB (mise à jour automatique hebdomadaire)
- **Moderation** : Historique complet des actions de modération (avertissements, bans, kicks, unbans) avec raison, staff et timestamp - **Message** : Messages automatiques périodiques
- **Moderation** : Historique complet des actions de modération (avertissements, timeouts, bans, kicks, unbans) avec raison, staff, timestamp et durée
- **MemberInvites** : Tracking des invitations (code d'invitation, inviteur, date de join)
### Architecture multi-thread ### Architecture multi-thread
- **Thread 1** : Interface web Flask (port 5000) avec logging rotatif - **Thread 1** : Interface web Flask (port 5000) avec logging rotatif

View File

@@ -17,7 +17,9 @@ from discordbot.moderation import (
handle_unban_command, handle_unban_command,
handle_inspect_command, handle_inspect_command,
handle_ban_list_command, handle_ban_list_command,
handle_staff_help_command handle_staff_help_command,
handle_timeout_command,
handle_say_command
) )
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from protondb import searhProtonDb from protondb import searhProtonDb
@@ -42,13 +44,11 @@ class DiscordBot(discord.Client):
if humeur != None: if humeur != None:
logging.info(f'Changement de statut : {humeur.text}') logging.info(f'Changement de statut : {humeur.text}')
await self.change_presence(status = discord.Status.online, activity = discord.CustomActivity(humeur.text)) await self.change_presence(status = discord.Status.online, activity = discord.CustomActivity(humeur.text))
# 10 minutes TODO à rendre configurable
await asyncio.sleep(10*60) await asyncio.sleep(10*60)
async def updateHumbleBundle(self): async def updateHumbleBundle(self):
while not self.is_closed(): while not self.is_closed():
await checkHumbleBundleAndNotify(self) await checkHumbleBundleAndNotify(self)
# toutes les 30 minutes
await asyncio.sleep(30*60) await asyncio.sleep(30*60)
def getAllTextChannel(self) -> list[TextChannel]: def getAllTextChannel(self) -> list[TextChannel]:
@@ -101,6 +101,10 @@ async def on_message(message: Message):
await handle_warning_command(message, bot) await handle_warning_command(message, bot)
return return
if command_name in ['!to', '!timeout']:
await handle_timeout_command(message, bot)
return
if command_name in ['!delaverto', '!removewarn', '!unwarn']: if command_name in ['!delaverto', '!removewarn', '!unwarn']:
await handle_remove_warning_command(message, bot) await handle_remove_warning_command(message, bot)
return return
@@ -131,6 +135,10 @@ async def on_message(message: Message):
await handle_inspect_command(message, bot) await handle_inspect_command(message, bot)
return return
if command_name == '!say':
await handle_say_command(message, bot)
return
if command_name in ['!aide', '!help']: if command_name in ['!aide', '!help']:
await handle_staff_help_command(message, bot) await handle_staff_help_command(message, bot)
return return
@@ -143,20 +151,40 @@ async def on_message(message: Message):
except Exception as e: except Exception as e:
logging.error(f'Échec de l\'exécution de la commande Discord : {e}') logging.error(f'Échec de l\'exécution de la commande Discord : {e}')
# Commande !protondb ou !pdb avec embed
if (ConfigurationHelper().getValue('proton_db_enable_enable') and (message.content.startswith('!protondb') or message.content.startswith('!pdb'))): if (ConfigurationHelper().getValue('proton_db_enable_enable') and (message.content.startswith('!protondb') or message.content.startswith('!pdb'))):
if (message.content.find('<@')>0) : if (message.content.find('<@')>0) :
mention = message.content[message.content.find('<@'):] mention = message.content[message.content.find('<@'):]
else : else :
mention = message.author.mention mention = message.author.mention
# Nettoyer le nom en enlevant la commande (!protondb ou !pdb)
name = message.content name = message.content
if name.startswith('!protondb'): if name.startswith('!protondb'):
name = name.replace('!protondb', '', 1) name = name.replace('!protondb', '', 1)
elif name.startswith('!pdb'): elif name.startswith('!pdb'):
name = name.replace('!pdb', '', 1) name = name.replace('!pdb', '', 1)
name = name.replace(f'{mention}', '').strip(); name = name.replace(f'{mention}', '').strip();
games = searhProtonDb(name)
if not name or len(name) == 0:
try:
await message.delete()
delete_time = ConfigurationHelper().getIntValue('proton_db_delete_time') or 10
help_msg = await message.channel.send(
f"{mention} ⚠️ Utilisation: `!pdb nom du jeu` ou `!protondb nom du jeu`\n"
f"Exemple: `!pdb Elden Ring`",
suppress_embeds=True
)
await asyncio.sleep(delete_time)
await help_msg.delete()
except Exception as e:
logging.error(f"Échec de la gestion du message d'aide ProtonDB : {e}")
return
try:
searching_msg = await message.channel.send(f"🔍 Recherche en cours pour **{name}**...")
games = searhProtonDb(name)
await searching_msg.delete()
except:
games = searhProtonDb(name)
if (len(games)==0) : 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 ?' msg = f'{mention} Je n\'ai pas trouvé de jeux correspondant à **{name}**. Es-tu sûr que le jeu est disponible sur Steam ?'
try: try:
@@ -164,57 +192,59 @@ async def on_message(message: Message):
except Exception as e: 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 return
total_games = len(games)
tier_colors = {'platinum': '🟣', 'gold': '🟡', 'silver': '', 'bronze': '🟤', 'borked': '🔴'}
content = ""
max_games = 15
# Construire un bel embed for count, game in enumerate(games[:max_games]):
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_name = str(game.get('name'))
g_id = str(game.get('id')) g_id = str(game.get('id'))
tier = str(game.get('tier') or 'N/A') tier = str(game.get('tier') or 'N/A').lower()
# Anti-cheat info si disponible tier_icon = tier_colors.get(tier, '')
new_entry = f"**[{g_name}](<https://www.protondb.com/app/{g_id}>)**\n{tier_icon} Classé **{tier.capitalize()}**"
ac_status = game.get('anticheat_status') ac_status = game.get('anticheat_status')
ac_emoji = ''
ac_text = ''
if ac_status: if ac_status:
status_lower = str(ac_status).lower() status_lower = str(ac_status).lower()
if status_lower == 'supported': ac_map = {
ac_emoji, ac_text = '', 'Supporté' 'supported': ('', 'Supporté'),
elif status_lower == 'running': 'running': ('⚠️', 'Fonctionne'),
ac_emoji, ac_text = '⚠️', 'Fonctionne' 'broken': ('', 'Cassé'),
elif status_lower == 'broken': 'denied': ('🚫', 'Refusé'),
ac_emoji, ac_text = '', 'Cassé' 'planned': ('📅', 'Planifié')
elif status_lower == 'denied': }
ac_emoji, ac_text = '🚫', 'Refusé' ac_emoji, ac_label = ac_map.get(status_lower, ('', str(ac_status)))
elif status_lower == 'planned':
ac_emoji, ac_text = '📅', 'Planifié'
else:
ac_emoji, ac_text = '', str(ac_status)
acs = game.get('anticheats') or [] acs = game.get('anticheats') or []
ac_list = ', '.join([str(ac) for ac in acs if ac]) ac_list = ', '.join([str(ac) for ac in acs if ac])
ac_line = f" | Anti-cheat: {ac_emoji} **{ac_text}**" new_entry += f" • [Anti-cheat {ac_emoji} {ac_label}"
if ac_list: if ac_list:
ac_line += f" ({ac_list})" new_entry += f" ({ac_list})"
else: new_entry += f"](<https://areweanticheatyet.com/game/{g_id}>)"
ac_line = ''
value = f"Tier: **{tier}**{ac_line}\nLien: https://www.protondb.com/app/{g_id}" new_entry += "\n\n"
embed.add_field(name=g_name, value=value[:1024], inline=False)
count += 1 # Vérifier la limite avant d'ajouter
if len(content) + len(new_entry) > 3900:
rest = len(games) - count
content += f"*... et {rest} autre{'s' if rest > 1 else ''} jeu{'x' if rest > 1 else ''}*"
break
content += new_entry
else:
rest = max(0, len(games) - max_games)
if rest > 0:
content += f"*... et {rest} autre{'s' if rest > 1 else ''} jeu{'x' if rest > 1 else ''}*"
rest = max(0, len(games) - count) embed = discord.Embed(
if rest > 0: title=f"🎮 Résultats ProtonDB - **{total_games} jeu{'x' if total_games > 1 else ''} trouvé{'s' if total_games > 1 else ''}**",
embed.add_field(name="", value=f"et encore {rest} autres jeux", inline=False) description=content,
color=0x5865F2
)
try : try :
await message.channel.send(content=mention, embed=embed) await message.channel.send(embed=embed)
except Exception as e: except Exception as e:
logging.error(f"Échec de l'envoi de l'embed ProtonDB : {e}") logging.error(f"Échec de l'envoi de l'embed ProtonDB : {e}")

View File

@@ -2,8 +2,9 @@ import asyncio
import logging import logging
import time import time
import os import os
import re
import discord import discord
from datetime import datetime, timezone from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from database import db from database import db
from database.helpers import ConfigurationHelper from database.helpers import ConfigurationHelper
@@ -96,27 +97,68 @@ async def send_user_not_found(channel):
msg = await channel.send(embed=embed) msg = await channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg)) asyncio.create_task(delete_after_delay(msg))
def parse_timeout_duration(text: str):
match = re.search(r'--to(?:meout)?[= ]?(\d+)([smhj])?', text.lower())
if not match:
return None
value = int(match.group(1))
unit = match.group(2) or 'm'
if unit == 's':
return value
elif unit == 'm':
return value * 60
elif unit == 'h':
return value * 3600
elif unit == 'j':
return value * 86400
return None
def format_timeout_duration(seconds: int) -> str:
if seconds < 60:
return f"{seconds} seconde{'s' if seconds > 1 else ''}"
elif seconds < 3600:
minutes = seconds // 60
return f"{minutes} minute{'s' if minutes > 1 else ''}"
elif seconds < 86400:
hours = seconds // 3600
return f"{hours} heure{'s' if hours > 1 else ''}"
else:
days = seconds // 86400
return f"{days} jour{'s' if days > 1 else ''}"
async def parse_target_user_and_reason(message, bot, parts: list): async def parse_target_user_and_reason(message, bot, parts: list):
full_text = message.content
timeout_seconds = parse_timeout_duration(full_text)
if message.mentions: if message.mentions:
target_user = message.mentions[0] target_user = message.mentions[0]
reason = parts[2] if len(parts) > 2 else "Sans raison" reason_text = parts[2] if len(parts) > 2 else "Sans raison"
return target_user, reason reason_text = re.sub(r'--to(?:meout)?[= ]?\d+[smhj]?', '', reason_text, flags=re.IGNORECASE).strip()
if not reason_text:
reason_text = "Sans raison"
return target_user, reason_text, timeout_seconds
try: try:
user_id = int(parts[1]) user_id = int(parts[1])
target_user = await bot.fetch_user(user_id) target_user = await bot.fetch_user(user_id)
reason = parts[2] if len(parts) > 2 else "Sans raison" reason_text = parts[2] if len(parts) > 2 else "Sans raison"
return target_user, reason reason_text = re.sub(r'--to(?:meout)?[= ]?\d+[smhj]?', '', reason_text, flags=re.IGNORECASE).strip()
if not reason_text:
reason_text = "Sans raison"
return target_user, reason_text, timeout_seconds
except (ValueError, discord.NotFound): except (ValueError, discord.NotFound):
return None, None return None, None, None
async def send_warning_usage(channel): async def send_warning_usage(channel):
embed = discord.Embed( embed = discord.Embed(
title="📋 Utilisation de la commande", title="📋 Utilisation de la commande",
description="**Syntaxe :** `!averto @utilisateur [raison]` ou `!averto <id> [raison]`", description="**Syntaxe :** `!averto @utilisateur raison` ou `!averto <id> raison`\n**Option :** Ajouter `--to durée` pour exclure temporairement l'utilisateur",
color=discord.Color.blue() 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="Exemples", value="• `!averto @User Spam dans le chat`\n• `!warn @User Comportement inapproprié --to 10m`\n• `!av @User --to 1h`\n• `!warn @User Spam --to 1j`", inline=False)
embed.add_field(name="Durées", value="`s` = secondes, `m` = minutes (défaut), `h` = heures, `j` = jours\nExemple: `--to 10m` ou `--to 60s`", inline=False)
embed.add_field(name="Aliases", value="`!averto`, `!av`, `!avertissement`, `!warn`", inline=False) embed.add_field(name="Aliases", value="`!averto`, `!av`, `!avertissement`, `!warn`", inline=False)
msg = await channel.send(embed=embed) msg = await channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg)) asyncio.create_task(delete_after_delay(msg))
@@ -151,19 +193,55 @@ def _commit_with_retry(max_retries: int = 5, base_delay: float = 0.1):
db.session.rollback() db.session.rollback()
raise raise
async def send_warning_confirmation(channel, target_user, reason: str, original_message: Message, bot): async def send_dm_to_warned_user(target_user, reason: str, guild_name: str):
try:
dm_embed = discord.Embed(
title="⚠️ Avertissement",
description=f"Vous avez reçu un avertissement sur le serveur **{guild_name}**",
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc)
)
if reason != "Sans raison":
dm_embed.add_field(name="📝 Raison", value=reason, inline=False)
dm_embed.add_field(name=" Information", value="Si vous avez des questions concernant cet avertissement, vous pouvez contacter l'équipe de modération.", inline=False)
await target_user.send(embed=dm_embed)
return True
except discord.Forbidden:
logging.warning(f"Impossible d'envoyer un MP à {target_user.name} ({target_user.id}) - MPs désactivés")
return False
except Exception as e:
logging.error(f"Erreur lors de l'envoi du MP à {target_user.name} ({target_user.id}): {e}")
return False
async def send_warning_confirmation(channel, target_user, reason: str, original_message: Message, bot, timeout_info: tuple = None):
local_now = _to_local(datetime.now(timezone.utc)) local_now = _to_local(datetime.now(timezone.utc))
dm_sent = await send_dm_to_warned_user(target_user, reason, original_message.guild.name)
was_timed_out = timeout_info is not None and timeout_info[0]
timeout_duration = timeout_info[1] if timeout_info else None
title = "⚠️ Avertissement + ⏱️ Time out" if was_timed_out else "⚠️ Avertissement"
description = f"**{target_user.name}** (`{target_user.name}`) a reçu un avertissement"
if was_timed_out:
description += f" et a été time out ({format_timeout_duration(timeout_duration)})"
embed = discord.Embed( embed = discord.Embed(
title="⚠️ Avertissement", title=title,
description=f"**{target_user.name}** (`{target_user.name}`) a reçu un avertissement", description=description,
color=discord.Color.orange(), color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc) timestamp=datetime.now(timezone.utc)
) )
embed.add_field(name="👤 Utilisateur", value=f"{target_user.mention}\n`{target_user.id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True)
embed.add_field(name="🛡️ Modérateur", value=f"{original_message.author.mention}\n`{original_message.author.name}`", inline=True) embed.add_field(name="🛡️ Modérateur", value=f"**{original_message.author.name}**", inline=True)
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
if reason != "Sans raison": if reason != "Sans raison":
embed.add_field(name="📝 Raison", value=reason, inline=False) embed.add_field(name="📝 Raison", value=reason, inline=False)
if dm_sent:
embed.add_field(name="✅ Message privé", value="L'utilisateur a été notifié par MP", inline=False)
else:
embed.add_field(name="⚠️ Message privé", value=f"Il faut contacter {target_user.mention} pour l'informer de cet avertissement (MPs désactivés). {original_message.author.mention}", inline=False)
embed.set_footer(text=f"ID: {target_user.id} • Serveur: {original_message.guild.name}") embed.set_footer(text=f"ID: {target_user.id} • Serveur: {original_message.guild.name}")
await send_to_moderation_log_channel(bot, embed) await send_to_moderation_log_channel(bot, embed)
@@ -176,15 +254,184 @@ async def handle_warning_command(message: Message, bot):
elif len(parts) < 2: elif len(parts) < 2:
await send_warning_usage(message.channel) await send_warning_usage(message.channel)
else: else:
target_user, reason = await parse_target_user_and_reason(message, bot, parts) target_user, reason, timeout_seconds = await parse_target_user_and_reason(message, bot, parts)
if not target_user: if not target_user:
await send_user_not_found(message.channel) await send_user_not_found(message.channel)
else: else:
await _process_warning_success(message, target_user, reason, bot) await _process_warning_success(message, target_user, reason, bot, timeout_seconds)
async def _process_warning_success(message: Message, target_user, reason: str, bot): async def _process_warning_success(message: Message, target_user, reason: str, bot, timeout_seconds: int = None):
create_warning_event(target_user, reason, message.author) create_warning_event(target_user, reason, message.author)
await send_warning_confirmation(message.channel, target_user, reason, message, bot)
timeout_info = None
if timeout_seconds:
member_obj = message.guild.get_member(target_user.id)
if member_obj:
try:
until = discord.utils.utcnow() + timedelta(seconds=timeout_seconds)
await member_obj.timeout(until, reason=reason)
timeout_info = (True, timeout_seconds)
timeout_event = ModerationEvent(
type='timeout',
username=target_user.name,
discord_id=str(target_user.id),
created_at=datetime.now(timezone.utc),
reason=reason,
staff_id=str(message.author.id),
staff_name=message.author.name,
duration=timeout_seconds
)
db.session.add(timeout_event)
_commit_with_retry()
except discord.Forbidden:
logging.error(f"Permissions insuffisantes pour timeout {target_user.name}")
except Exception as e:
logging.error(f"Erreur lors du timeout de {target_user.name}: {e}")
await send_warning_confirmation(message.channel, target_user, reason, message, bot, timeout_info)
async def send_timeout_usage(channel):
embed = discord.Embed(
title="📋 Utilisation de la commande",
description="**Syntaxe :** `!to @utilisateur durée raison` ou `!timeout @utilisateur durée raison`",
color=discord.Color.blue()
)
embed.add_field(name="Exemples", value="• `!to @User 10m Spam`\n• `!timeout @User 1h Comportement inapproprié`\n• `!to @User 30s Flood`\n• `!timeout @User 1j Toxicité`", inline=False)
embed.add_field(name="Durées", value="`s` = secondes, `m` = minutes (défaut), `h` = heures, `j` = jours\nExemple: `10m`, `1h`, `60s`", inline=False)
embed.add_field(name="Aliases", value="`!to`, `!timeout`", inline=False)
msg = await channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
def parse_timeout_from_args(duration_str: str):
match = re.match(r'^(\d+)([smhj])?$', duration_str.lower())
if not match:
return None
value = int(match.group(1))
unit = match.group(2) or 'm'
if unit == 's':
return value
elif unit == 'm':
return value * 60
elif unit == 'h':
return value * 3600
elif unit == 'j':
return value * 86400
return None
async def parse_timeout_target_and_params(message, bot, parts: list):
if len(parts) < 3:
return None, None, None
if message.mentions:
target_user = message.mentions[0]
timeout_seconds = parse_timeout_from_args(parts[2])
reason = " ".join(parts[3:]) if len(parts) > 3 else "Sans raison"
return target_user, timeout_seconds, reason
try:
user_id = int(parts[1])
target_user = await bot.fetch_user(user_id)
timeout_seconds = parse_timeout_from_args(parts[2])
reason = " ".join(parts[3:]) if len(parts) > 3 else "Sans raison"
return target_user, timeout_seconds, reason
except (ValueError, discord.NotFound):
return None, None, None
async def send_timeout_confirmation(channel, target_user, reason: str, timeout_seconds: int, original_message: Message, bot):
local_now = _to_local(datetime.now(timezone.utc))
embed = discord.Embed(
title="⏱️ Time out",
description=f"**{target_user.name}** (`{target_user.name}`) a été exclu temporairement ({format_timeout_duration(timeout_seconds)})",
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc)
)
embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True)
embed.add_field(name="🛡️ Modérateur", value=f"**{original_message.author.name}**", inline=True)
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
embed.add_field(name="⏱️ Durée", value=format_timeout_duration(timeout_seconds), inline=True)
if reason != "Sans raison":
embed.add_field(name="📝 Raison", value=reason, inline=False)
embed.set_footer(text=f"ID: {target_user.id} • Serveur: {original_message.guild.name}")
await send_to_moderation_log_channel(bot, embed)
await safe_delete_message(original_message)
async def send_invalid_timeout_duration(channel):
embed = discord.Embed(
title="❌ Erreur",
description="Durée invalide. Utilisez un format valide comme `10m`, `1h`, `60s`, etc.",
color=discord.Color.red()
)
msg = await channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
async def handle_timeout_command(message: Message, bot):
parts = message.content.split()
if not has_staff_role(message.author.roles):
await send_access_denied(message.channel)
elif len(parts) < 3:
await send_timeout_usage(message.channel)
else:
target_user, timeout_seconds, reason = await parse_timeout_target_and_params(message, bot, parts)
if not target_user:
await send_user_not_found(message.channel)
elif not timeout_seconds:
await send_invalid_timeout_duration(message.channel)
else:
await _process_timeout_success(message, target_user, reason, timeout_seconds, bot)
async def _process_timeout_success(message: Message, target_user, reason: str, timeout_seconds: int, bot):
member_obj = message.guild.get_member(target_user.id)
if not member_obj:
embed = discord.Embed(
title="❌ Erreur",
description="L'utilisateur n'est pas membre du serveur.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
try:
until = discord.utils.utcnow() + timedelta(seconds=timeout_seconds)
await member_obj.timeout(until, reason=reason)
timeout_event = ModerationEvent(
type='timeout',
username=target_user.name,
discord_id=str(target_user.id),
created_at=datetime.now(timezone.utc),
reason=reason,
staff_id=str(message.author.id),
staff_name=message.author.name,
duration=timeout_seconds
)
db.session.add(timeout_event)
_commit_with_retry()
await send_timeout_confirmation(message.channel, target_user, reason, timeout_seconds, message, bot)
except discord.Forbidden:
embed = discord.Embed(
title="❌ Erreur",
description="Je n'ai pas les permissions nécessaires pour exclure cet utilisateur.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
except Exception as e:
logging.error(f"Erreur lors du timeout de {target_user.name}: {e}")
embed = discord.Embed(
title="❌ Erreur",
description=f"Une erreur est survenue lors de l'exclusion : {str(e)}",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
async def send_remove_warning_usage(channel): async def send_remove_warning_usage(channel):
embed = discord.Embed( embed = discord.Embed(
@@ -439,8 +686,8 @@ async def _process_ban_success(message: Message, target_user, reason: str, bot):
color=discord.Color.red(), color=discord.Color.red(),
timestamp=datetime.now(timezone.utc) timestamp=datetime.now(timezone.utc)
) )
embed.add_field(name="👤 Utilisateur", value=f"{target_user.mention}\n`{target_user.id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"**{target_user.name}**\n`{target_user.id}`", inline=True)
embed.add_field(name="🛡️ Modérateur", value=f"{message.author.mention}\n`{message.author.name}`", inline=True) embed.add_field(name="🛡️ Modérateur", value=f"{message.author.name}", inline=True)
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
if joined_days is not None: if joined_days is not None:
embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True)
@@ -554,9 +801,9 @@ async def _process_unban_success(message: Message, bot, target_user, discord_id:
color=discord.Color.green(), color=discord.Color.green(),
timestamp=datetime.now(timezone.utc) timestamp=datetime.now(timezone.utc)
) )
user_mention = target_user.mention if target_user else username user_display = f"**{target_user.name}**" if target_user else f"**{username}**"
embed.add_field(name="👤 Utilisateur", value=f"{user_mention}\n`{discord_id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"{user_display}\n`{discord_id}`", inline=True)
embed.add_field(name="🛡️ Modérateur", value=f"{message.author.mention}\n`{message.author.name}`", inline=True) embed.add_field(name="🛡️ Modérateur", value=f"**{message.author.name}**", inline=True)
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
if reason != "Sans raison": if reason != "Sans raison":
embed.add_field(name="📝 Raison", value=reason, inline=False) embed.add_field(name="📝 Raison", value=reason, inline=False)
@@ -751,18 +998,27 @@ async def handle_staff_help_command(message: Message, bot):
if ConfigurationHelper().getValue('moderation_enable'): if ConfigurationHelper().getValue('moderation_enable'):
value = ( value = (
"• `!averto @utilisateur raison`\n" "**Avertissements:**\n"
" *Alias: !warn, !av, !avertissement*\n" "• `!warn @utilisateur raison`\n"
"• `!delaverto id`\n" " *Alias: !averto, !av, !avertissement*\n"
" *Alias: !removewarn, !delwarn*\n" " Donne un avertissement\n"
"• `!warnings` ou `!warnings @utilisateur`\n" "• `!warn @utilisateur raison --to durée`\n"
" *Alias: !listevent, !listwarn*\n" " Avertissement + time out temporaire\n\n"
"Exemples:\n" "**Time out uniquement:**\n"
"`!averto @User Spam dans le chat`\n" "• `!to @utilisateur durée raison`\n"
"`!delaverto 12`\n" " *Alias: !timeout*\n"
" Time out (sans avertissement)\n"
" *Durées: 10s, 5m, 1h, 2j*\n\n"
"**Gestion:**\n"
"• `!delaverto id` - Supprime un événement\n"
"• `!warnings [@utilisateur]` - Liste les événements\n\n"
"**Exemples:**\n"
"`!warn @User Spam`\n"
"`!warn @User Flood --to 10m` (averto + timeout)\n"
"`!to @User 5m Spam` (timeout seul)\n"
"`!warnings @User`" "`!warnings @User`"
) )
embed.add_field(name="⚠️ Avertissements", value=value, inline=False) embed.add_field(name="⚠️ Avertissements & Time out", value=value, inline=False)
embed.add_field( embed.add_field(
name="🔎 Inspection", name="🔎 Inspection",
value=("• `!inspect @utilisateur` ou `!inspect id`\n" value=("• `!inspect @utilisateur` ou `!inspect id`\n"
@@ -793,6 +1049,16 @@ async def handle_staff_help_command(message: Message, bot):
"Exemples: `!kick @User Spam de liens` ou `!kick 123456789012345678 Spam`" "Exemples: `!kick @User Spam de liens` ou `!kick 123456789012345678 Spam`"
) )
embed.add_field(name="👢 Expulsion", value=value, inline=False) embed.add_field(name="👢 Expulsion", value=value, inline=False)
embed.add_field(
name="💬 Autres",
value=(
"• `!say #channel message`\n"
" Envoie un message en tant que bot\n"
" Ex: `!say #annonces Nouvelle fonctionnalité !`"
),
inline=False
)
try: try:
sent = await message.channel.send(embed=embed) sent = await message.channel.send(embed=embed)
@@ -889,8 +1155,8 @@ async def _process_kick_success(message: Message, target_member, reason: str, bo
color=discord.Color.orange(), color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc) timestamp=datetime.now(timezone.utc)
) )
embed.add_field(name="👤 Utilisateur", value=f"{target_member.mention}\n`{target_member.id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"**{target_member.name}**\n`{target_member.id}`", inline=True)
embed.add_field(name="🛡️ Modérateur", value=f"{message.author.mention}\n`{message.author.name}`", inline=True) embed.add_field(name="🛡️ Modérateur", value=f"**{message.author.name}**", inline=True)
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True) embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
if joined_days is not None: if joined_days is not None:
embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True) embed.add_field(name="⏱️ Membre depuis", value=format_days_to_age(joined_days), inline=True)
@@ -962,7 +1228,7 @@ def create_inspect_embed(user, member, join_date, days_on_server, account_age, w
) )
embed.set_thumbnail(url=user.display_avatar.url) embed.set_thumbnail(url=user.display_avatar.url)
embed.add_field(name="👤 Utilisateur", value=f"{user.mention}\n`{user.id}`", inline=True) embed.add_field(name="👤 Utilisateur", value=f"**{user.name}**\n`{user.id}`", inline=True)
if account_age is not None: if account_age is not None:
embed.add_field( embed.add_field(
@@ -1079,3 +1345,63 @@ async def handle_inspect_command(message: Message, bot):
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
await safe_delete_message(message) await safe_delete_message(message)
async def handle_say_command(message: Message, bot):
if not has_staff_role(message.author.roles):
await send_access_denied(message.channel)
return
parts = message.content.split(maxsplit=2)
if len(parts) < 3:
embed = discord.Embed(
title="📋 Utilisation de la commande",
description="**Syntaxe :** `!say #channel message` ou `!say <id_salon> message`",
color=discord.Color.blue()
)
embed.add_field(name="Exemples", value="`!say #general Bonjour à tous !`\n`!say 123456789 Annonce importante`", inline=False)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
target_channel = None
if message.channel_mentions:
target_channel = message.channel_mentions[0]
else:
try:
channel_id = int(parts[1])
target_channel = bot.get_channel(channel_id)
if not target_channel:
try:
target_channel = await bot.fetch_channel(channel_id)
except discord.NotFound:
pass
except ValueError:
pass
if not target_channel:
embed = discord.Embed(
title="❌ Erreur",
description="Vous devez mentionner un canal avec # ou fournir un ID de salon valide.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
text_to_send = parts[2]
try:
await target_channel.send(text_to_send)
await safe_delete_message(message)
except discord.Forbidden:
embed = discord.Embed(
title="❌ Erreur",
description="Je n'ai pas les permissions pour écrire dans ce canal.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
except Exception as e:
logging.error(f"Erreur lors de l'envoi du message: {e}")

View File

@@ -94,14 +94,20 @@ async def sendWelcomeMessage(bot: discord.Client, member: Member):
) )
embed.set_thumbnail(url=member.display_avatar.url) embed.set_thumbnail(url=member.display_avatar.url)
embed.add_field(name='Membre', value=member.mention, inline=True) embed.add_field(name='Membre', value=member.name, inline=True)
embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True) embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True)
embed.add_field(name='Invitation utilisée', value=invite_display, inline=False) embed.add_field(name='Invitation utilisée', value=invite_display, inline=False)
embed.set_footer(text=f'ID: {member.id}') embed.set_footer(text=f'ID: {member.id}')
try: try:
await channel.send(embed=embed) message = await channel.send(embed=embed)
logging.info(f'Message de bienvenue envoyé pour {member.name}') logging.info(f'Message de bienvenue envoyé pour {member.name}')
now = datetime.now(timezone.utc)
account_age = (now - member.created_at).days
if account_age < 7:
await message.add_reaction('⚠️')
logging.info(f'Réaction warning ajoutée pour {member.name} (compte créé il y a {account_age} jours)')
except Exception as e: except Exception as e:
logging.error(f'Échec de l\'envoi du message de bienvenue : {e}') logging.error(f'Échec de l\'envoi du message de bienvenue : {e}')
@@ -179,7 +185,7 @@ async def sendLeaveMessage(bot: discord.Client, member: Member):
) )
embed.set_thumbnail(url=member.display_avatar.url) embed.set_thumbnail(url=member.display_avatar.url)
embed.add_field(name='Membre', value=f'{member.mention} ({member.name})', inline=True) embed.add_field(name='Membre', value=f'**{member.name}**', inline=True)
embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True) embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True)
embed.add_field(name='Temps sur le serveur', value=duration_text, inline=False) embed.add_field(name='Temps sur le serveur', value=duration_text, inline=False)
embed.set_footer(text=f'ID: {member.id}') embed.set_footer(text=f'ID: {member.id}')