mirror of
https://github.com/skylanix/MamieHenriette.git
synced 2026-02-06 14:50:34 +01:00
Compare commits
12 Commits
6411b1e73c
...
welcome-wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9afd3b2588 | ||
|
|
54b014c4c8 | ||
|
|
559a780a4f | ||
|
|
9abd7b8101 | ||
|
|
a66c31ecf6 | ||
|
|
499fac9c12 | ||
|
|
3e12c2cf08 | ||
|
|
5c76b50797 | ||
|
|
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
|
- 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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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}')
|
||||||
|
|||||||
Reference in New Issue
Block a user