mirror of
https://github.com/skylanix/MamieHenriette.git
synced 2026-02-06 14:50:34 +01:00
Compare commits
4 Commits
6411b1e73c
...
d5d3e45a62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5d3e45a62 | ||
|
|
cb559c2863 | ||
|
|
a0a14abf57 | ||
|
|
a987ca311e |
93
README.md
93
README.md
@@ -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
|
||||
- Recherche intelligente avec support des alias de jeux
|
||||
- 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
|
||||
- Avertissements : `!averto`, `!warn`, `!av`, `!avertissement`
|
||||
- Gestion des avertissements : `!delaverto`, `!removewarn`, `!delwarn`
|
||||
- Liste des événements : `!warnings`, `!listevent`, `!listwarn`
|
||||
- Inspection utilisateur : `!inspect` (historique complet, date d'arrivée, compte)
|
||||
- Bannissement : `!ban`, `!unban` (avec invitation automatique), `!banlist`
|
||||
- Expulsion : `!kick`
|
||||
- Aide : `!aide`, `!help`
|
||||
- Messages de bienvenue et départ personnalisables
|
||||
- Panneau d'administration web pour consulter l'historique
|
||||
- **Avertissements** : `!averto`, `!warn`, `!av`, `!avertissement`
|
||||
- Envoi automatique de DM à l'utilisateur averti
|
||||
- Support des timeouts combinés : `!warn @user raison --to durée`
|
||||
- **Timeout** : `!timeout`, `!to` - Exclusion temporaire d'un utilisateur
|
||||
- Syntaxe : `!to @user durée raison` (ex: `!to @User 10m Spam`)
|
||||
- Durées supportées : secondes (s), minutes (m), heures (h), jours (j/days)
|
||||
- **Gestion des avertissements** : `!delaverto`, `!removewarn`, `!delwarn`
|
||||
- **Liste des événements** : `!warnings`, `!listevent`, `!listwarn`
|
||||
- **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
|
||||
- **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
|
||||
- **Dashboard** : Vue d'ensemble et statistiques
|
||||
- **Configuration** : Tokens, paramètres des plateformes, configuration ProtonDB
|
||||
- **Gestion des humeurs** : Création et modification des statuts
|
||||
- **Commandes** : Édition des commandes personnalisées
|
||||
- **Modération** : Outils de gestion communautaire
|
||||
- **Configuration** :
|
||||
- Tokens Discord/Twitch et paramètres des plateformes
|
||||
- Configuration ProtonDB (API Algolia)
|
||||
- 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
|
||||
@@ -134,7 +171,7 @@ cd MamieHenriette
|
||||
```
|
||||
|
||||
```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 up -d
|
||||
```
|
||||
@@ -163,12 +200,12 @@ cd MamieHenriette
|
||||
|
||||
```yaml
|
||||
services:
|
||||
MamieHenriette:
|
||||
mamiehenriette:
|
||||
container_name: MamieHenriette
|
||||
restart: unless-stopped
|
||||
build: . # ← Décommentez cette lignes
|
||||
image: mamiehenriette # ← Décommentez cette lignes
|
||||
# image: skylanix/mamiehenriette:latest # ← Commentez cette ligne
|
||||
build: . # ← Décommentez cette ligne
|
||||
image: mamiehenriette # ← Décommentez cette ligne
|
||||
# image: ghcr.io/skylanix/mamiehenriette:latest # ← Commentez cette ligne
|
||||
# ... reste de la configuration
|
||||
```
|
||||
|
||||
@@ -234,9 +271,9 @@ services:
|
||||
- Donnez un nom : `MamieHenriette`
|
||||
- Collez la configuration ci-dessus dans l'éditeur
|
||||
|
||||
3. **Adapter les chemins** :
|
||||
- Remplacez `/chemin/vers/instance` par le chemin absolu sur votre serveur (ex: `/opt/containers/MamieHenriette/instance`)
|
||||
- Remplacez `/chemin/vers/logs` par le chemin absolu sur votre serveur (ex: `/opt/containers/MamieHenriette/logs`)
|
||||
3. **Adapter les chemins des volumes** :
|
||||
- Modifiez `./instance` et `./logs` selon votre configuration
|
||||
- Exemple : `/opt/containers/MamieHenriette/instance` et `/opt/containers/MamieHenriette/logs`
|
||||
|
||||
4. **Déployer** :
|
||||
- Cliquez sur "Deploy the stack"
|
||||
@@ -281,10 +318,12 @@ git pull origin main
|
||||
# 3. Mettre à jour l'image Docker
|
||||
docker compose pull
|
||||
|
||||
# 4. Reconstruire et relancer
|
||||
docker compose up --build -d
|
||||
# 4. Relancer
|
||||
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)
|
||||
```bash
|
||||
# 1. Arrêter l'application
|
||||
@@ -352,14 +391,16 @@ python run-web.py
|
||||
## Spécifications techniques
|
||||
|
||||
### 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
|
||||
- **Commande** : Commandes personnalisées multi-plateformes (Discord/Twitch)
|
||||
- **LiveAlert** : Configuration surveillance streamers Twitch (nom, canal Discord, statut)
|
||||
- **GameAlias** : Alias pour améliorer les recherches ProtonDB
|
||||
- **GameBundle** : Historique et notifications Humble Bundle
|
||||
- **Message** : Messages automatiques périodiques (implémenté)
|
||||
- **Moderation** : Historique complet des actions de modération (avertissements, bans, kicks, unbans) avec raison, staff et timestamp
|
||||
- **AntiCheatCache** : Cache des informations anti-cheat pour ProtonDB (mise à jour automatique hebdomadaire)
|
||||
- **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
|
||||
- **Thread 1** : Interface web Flask (port 5000) avec logging rotatif
|
||||
|
||||
@@ -17,7 +17,9 @@ from discordbot.moderation import (
|
||||
handle_unban_command,
|
||||
handle_inspect_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 protondb import searhProtonDb
|
||||
@@ -101,6 +103,10 @@ async def on_message(message: Message):
|
||||
await handle_warning_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name in ['!to', '!timeout']:
|
||||
await handle_timeout_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name in ['!delaverto', '!removewarn', '!unwarn']:
|
||||
await handle_remove_warning_command(message, bot)
|
||||
return
|
||||
@@ -131,6 +137,10 @@ async def on_message(message: Message):
|
||||
await handle_inspect_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name == '!say':
|
||||
await handle_say_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name in ['!aide', '!help']:
|
||||
await handle_staff_help_command(message, bot)
|
||||
return
|
||||
|
||||
@@ -2,8 +2,9 @@ import asyncio
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import discord
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
@@ -96,27 +97,68 @@ async def send_user_not_found(channel):
|
||||
msg = await channel.send(embed=embed)
|
||||
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):
|
||||
full_text = message.content
|
||||
timeout_seconds = parse_timeout_duration(full_text)
|
||||
|
||||
if message.mentions:
|
||||
target_user = message.mentions[0]
|
||||
reason = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
return target_user, reason
|
||||
reason_text = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
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:
|
||||
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
|
||||
reason_text = parts[2] if len(parts) > 2 else "Sans raison"
|
||||
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):
|
||||
return None, None
|
||||
return None, 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]`",
|
||||
description="**Syntaxe :** `!averto @utilisateur raison` ou `!averto <id> raison`\n**Option :** Ajouter `--to durée` pour exclure temporairement l'utilisateur",
|
||||
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)
|
||||
msg = await channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
@@ -151,11 +193,41 @@ def _commit_with_retry(max_retries: int = 5, base_delay: float = 0.1):
|
||||
db.session.rollback()
|
||||
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))
|
||||
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(
|
||||
title="⚠️ Avertissement",
|
||||
description=f"**{target_user.name}** (`{target_user.name}`) a reçu un avertissement",
|
||||
title=title,
|
||||
description=description,
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -164,6 +236,12 @@ async def send_warning_confirmation(channel, target_user, reason: str, original_
|
||||
embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
|
||||
if reason != "Sans raison":
|
||||
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}")
|
||||
|
||||
await send_to_moderation_log_channel(bot, embed)
|
||||
@@ -176,15 +254,184 @@ async def handle_warning_command(message: Message, bot):
|
||||
elif len(parts) < 2:
|
||||
await send_warning_usage(message.channel)
|
||||
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:
|
||||
await send_user_not_found(message.channel)
|
||||
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)
|
||||
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.mention}\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="📅 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):
|
||||
embed = discord.Embed(
|
||||
@@ -751,18 +998,27 @@ async def handle_staff_help_command(message: Message, bot):
|
||||
|
||||
if ConfigurationHelper().getValue('moderation_enable'):
|
||||
value = (
|
||||
"• `!averto @utilisateur raison`\n"
|
||||
" *Alias: !warn, !av, !avertissement*\n"
|
||||
"• `!delaverto id`\n"
|
||||
" *Alias: !removewarn, !delwarn*\n"
|
||||
"• `!warnings` ou `!warnings @utilisateur`\n"
|
||||
" *Alias: !listevent, !listwarn*\n"
|
||||
"Exemples:\n"
|
||||
"`!averto @User Spam dans le chat`\n"
|
||||
"`!delaverto 12`\n"
|
||||
"**Avertissements:**\n"
|
||||
"• `!warn @utilisateur raison`\n"
|
||||
" *Alias: !averto, !av, !avertissement*\n"
|
||||
" Donne un avertissement\n"
|
||||
"• `!warn @utilisateur raison --to durée`\n"
|
||||
" Avertissement + time out temporaire\n\n"
|
||||
"**Time out uniquement:**\n"
|
||||
"• `!to @utilisateur durée raison`\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`"
|
||||
)
|
||||
embed.add_field(name="⚠️ Avertissements", value=value, inline=False)
|
||||
embed.add_field(name="⚠️ Avertissements & Time out", value=value, inline=False)
|
||||
embed.add_field(
|
||||
name="🔎 Inspection",
|
||||
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`"
|
||||
)
|
||||
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:
|
||||
sent = await message.channel.send(embed=embed)
|
||||
@@ -1079,3 +1345,48 @@ async def handle_inspect_command(message: Message, bot):
|
||||
await message.channel.send(embed=embed)
|
||||
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`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Exemple", value="`!say #general Bonjour à tous !`", inline=False)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
if not message.channel_mentions:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Vous devez mentionner un canal avec #",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
target_channel = message.channel_mentions[0]
|
||||
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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user