Author SHA1 Message Date
Mow910 942b23c956 Merge pull request #6 from Mow910/discord-transfert-msg
Ajout de la sélection de tags pour les posts dans les forums lors du …
2026-02-14 20:33:44 +01:00
Mow910 651773e63d Ajout de la sélection de tags pour les posts dans les forums lors du transfert de messages. Mise à jour de la classe TransferReasonModal pour inclure les tags sélectionnés et ajout d'une nouvelle classe ForumTagSelect pour gérer la sélection des tags. Amélioration de l'interaction utilisateur avec des messages contextuels pour la sélection des tags. 2026-02-14 20:15:08 +01:00
Mow910 3450cd031c Merge pull request #5 from Mow910/discord-transfert-msg
Ajout de la commande contextuelle "Déplacer le message" pour transfér…
2026-02-14 19:37:32 +01:00
Mow910 60b90edcb3 Ajout de la commande contextuelle "Déplacer le message" pour transférer des messages entre canaux, avec gestion des forums et des threads. Intégration d'un modal pour spécifier la raison du transfert et mise à jour de la synchronisation des commandes d'application. 2026-02-14 19:36:39 +01:00
Mow910 252e169af5 Merge pull request #4 from Mow910/discord-transfert-msg
Discord transfert msg
2026-02-14 18:34:03 +01:00
Mow910 179876d2ce Raison pour le titre du post dans le forum 2026-02-14 18:32:32 +01:00
Mow910 830ce61796 sans com 2026-02-14 18:28:03 +01:00
Mow910 5709b1c0a3 Amélioration de la commande !transfert pour supporter les canaux textuels, threads et forums. Ajout de vérifications de compatibilité pour le canal de destination et mise à jour de la documentation des commandes. Gestion des transferts de messages avec création de posts dans les forums et envoi via webhooks pour les autres types de canaux. 2026-02-14 18:01:31 +01:00
Mow910 11348bda39 Ajout de la commande !transfert pour transférer des messages entre canaux tout en préservant l'identité de l'auteur. Mise à jour de la documentation des commandes pour inclure cette nouvelle fonctionnalité et ses alias (!transfer, !move). 2026-02-14 16:55:52 +01:00
Mow910 f7e85bac69 Merge pull request #3 from Mow910/fix/twitch-auth-asyncio
Fix: Retrait de asyncio.run() pour le constructeur Twitch
2026-02-12 23:49:21 +01:00
3 changed files with 587 additions and 4 deletions
+37 -2
View File
@@ -7,7 +7,7 @@ from webapp import webapp
from database import db
from database.helpers import ConfigurationHelper
from database.models import Configuration, Humeur, Commande
from discord import Message, TextChannel, Member, VoiceChannel
from discord import Message, TextChannel, Member, VoiceChannel, app_commands
from discordbot.humblebundle import checkHumbleBundleAndNotify
from discordbot.freeloot import checkFreeLootAndNotify
from discordbot.moderation import (
@@ -21,7 +21,9 @@ from discordbot.moderation import (
handle_ban_list_command,
handle_staff_help_command,
handle_timeout_command,
handle_say_command
handle_say_command,
handle_transfer_command,
transfer_message_context_menu
)
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from discordbot.youtube import checkYouTubeVideos
@@ -29,10 +31,39 @@ from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_react
from protondb import searhProtonDb
class DiscordBot(discord.Client):
def __init__(self, *, intents: discord.Intents):
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
self.synced = False
async def setup_hook(self):
self.tree.add_command(transfer_message_context_menu)
logging.info("Commande contextuelle 'Déplacer le message' ajoutée au CommandTree")
async def on_ready(self):
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
webapp.config["BOT_STATUS"]["discord_connected"] = True
webapp.config["BOT_STATUS"]["discord_guild_count"] = len(self.guilds)
if not self.synced:
try:
logging.info("Synchronisation des commandes d'application en cours...")
for guild in self.guilds:
try:
synced = await self.tree.sync(guild=guild)
logging.info(f"{len(synced)} commande(s) synchronisée(s) pour le serveur '{guild.name}' (ID: {guild.id})")
except Exception as e:
logging.error(f"❌ Erreur lors de la synchronisation pour {guild.name}: {e}")
synced_global = await self.tree.sync()
logging.info(f"{len(synced_global)} commande(s) synchronisée(s) globalement")
self.synced = True
logging.info("🎉 Synchronisation complète terminée - Les commandes sont maintenant disponibles !")
except Exception as e:
logging.error(f"❌ Erreur lors de la synchronisation des commandes: {e}")
for c in self.get_all_channels() :
logging.info(f'{c.id} {c.name}')
@@ -167,6 +198,10 @@ async def on_message(message: Message):
await handle_say_command(message, bot)
return
if command_name in ['!transfert', '!transfer', '!move']:
await handle_transfer_command(message, bot)
return
if command_name in ['!aide', '!help']:
await handle_staff_help_command(message, bot)
return
+542 -2
View File
@@ -4,12 +4,14 @@ import time
import os
import re
import discord
import io
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
from database import db
from database.helpers import ConfigurationHelper
from database.models import ModerationEvent
from discord import Message
from discord import Message, TextChannel, ForumChannel, Thread, app_commands
from discord.ui import Modal, TextInput, View, Select, ChannelSelect
def _get_local_tz():
tz_name = os.environ.get('APP_TZ') or os.environ.get('TZ') or 'Europe/Paris'
@@ -1055,7 +1057,15 @@ async def handle_staff_help_command(message: Message, bot):
value=(
"• `!say #channel message`\n"
" Envoie un message en tant que bot\n"
" Ex: `!say #annonces Nouvelle fonctionnalité !`"
" Ex: `!say #annonces Nouvelle fonctionnalité !`\n\n"
"• `!transfert #canal message_id [raison]`\n"
" Transfère un message vers un autre canal\n"
" *Alias: !transfer, !move*\n"
" Ex: `!transfert #entraide 123456789012345678`\n"
" Ex: `!transfert #général https://discord.com/channels/.../...`\n"
" Le message sera envoyé comme si c'était l'auteur original\n"
" Supporte les canaux textuels, threads et forums (crée un post)\n"
" Pour les forums, la raison devient le titre du post"
),
inline=False
)
@@ -1405,3 +1415,533 @@ async def handle_say_command(message: Message, bot):
except Exception as e:
logging.error(f"Erreur lors de l'envoi du message: {e}")
async def handle_transfer_command(message: Message, bot):
if not has_staff_role(message.author.roles):
await send_access_denied(message.channel)
return
parts = message.content.split(maxsplit=3)
if len(parts) < 3:
embed = discord.Embed(
title="📋 Utilisation de la commande",
description="**Syntaxe :** `!transfert #canal message_id [raison]` ou `!transfert #canal lien_message [raison]`",
color=discord.Color.blue()
)
embed.add_field(
name="Exemples",
value=(
"• `!transfert #entraide 123456789012345678`\n"
"• `!transfert #general https://discord.com/channels/.../...`\n"
"• `!transfert #entraide 123456789012345678 Message posté dans le mauvais canal`"
),
inline=False
)
embed.add_field(
name="Aliases",
value="`!transfert`, `!transfer`, `!move`",
inline=False
)
embed.add_field(
name="💡 Astuce",
value="Faites un clic droit sur un message → Copier l'identifiant du message, ou copiez le lien du message",
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].strip('<#>'))
target_channel = bot.get_channel(channel_id)
except ValueError:
pass
if not target_channel:
embed = discord.Embed(
title="❌ Erreur",
description="Canal de destination invalide. Mentionnez un canal avec #canal.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
if not isinstance(target_channel, (TextChannel, ForumChannel, Thread)):
embed = discord.Embed(
title="❌ Erreur",
description=f"Le transfert n'est pas supporté vers ce type de canal. Utilisez un canal textuel, un forum ou un thread.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
message_id = None
source_channel = message.channel
if 'discord.com/channels/' in parts[2]:
try:
link_parts = parts[2].split('/')
source_channel_id = int(link_parts[-2])
message_id = int(link_parts[-1])
source_channel = bot.get_channel(source_channel_id)
if not source_channel:
embed = discord.Embed(
title="❌ Erreur",
description="Canal source introuvable.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
except (ValueError, IndexError):
embed = discord.Embed(
title="❌ Erreur",
description="Lien de message invalide.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
else:
try:
message_id = int(parts[2])
except ValueError:
embed = discord.Embed(
title="❌ Erreur",
description="ID de message invalide. Utilisez un ID numérique ou un lien Discord.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
try:
original_message = await source_channel.fetch_message(message_id)
except discord.NotFound:
embed = discord.Embed(
title="❌ Erreur",
description="Message introuvable. Vérifiez l'ID ou le lien.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
except discord.Forbidden:
embed = discord.Embed(
title="❌ Erreur",
description="Je n'ai pas la permission d'accéder à ce message.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
reason = parts[3] if len(parts) > 3 else "Message posté dans le mauvais canal"
content = original_message.content
embeds = original_message.embeds
files_to_send = []
for attachment in original_message.attachments:
try:
file_data = await attachment.read()
files_to_send.append(discord.File(
fp=io.BytesIO(file_data),
filename=attachment.filename
))
except Exception as e:
logging.error(f"Erreur lors du téléchargement de la pièce jointe: {e}")
transferred_message = None
if isinstance(target_channel, ForumChannel):
try:
post_title = f"{original_message.author.display_name} - "
if reason and reason != "Message posté dans le mauvais canal":
remaining_length = 100 - len(post_title)
post_title += reason[:remaining_length]
elif content and len(content) > 0:
remaining_length = 100 - len(post_title)
post_title += content[:remaining_length]
else:
post_title += "Message transféré"
if len(post_title) > 100:
post_title = post_title[:97] + "..."
transfer_notice = f"**Message original de {original_message.author.mention}**\n"
transfer_notice += f"*Ce message a été transféré par un membre du staff depuis {source_channel.mention}*\n"
transfer_notice += "" * 50 + "\n\n"
full_content = transfer_notice + (content or "")
thread = await target_channel.create_thread(
name=post_title,
content=full_content,
embeds=embeds[:10] if embeds else [],
files=files_to_send,
reason=f"Transfert depuis {source_channel.name} par {message.author.name}"
)
transferred_message = thread.message
except discord.HTTPException as e:
logging.error(f"Erreur lors de la création du post dans le forum: {e}")
embed = discord.Embed(
title="❌ Erreur",
description=f"Une erreur est survenue lors de la création du post dans le forum: {str(e)}",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
else:
webhooks = await target_channel.webhooks()
webhook = None
for wh in webhooks:
if wh.user == bot.user:
webhook = wh
break
if not webhook:
try:
webhook = await target_channel.create_webhook(name="Mamie Henriette - Transfert")
except discord.Forbidden:
embed = discord.Embed(
title="❌ Erreur",
description="Je n'ai pas la permission de créer un webhook dans le canal de destination.",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
try:
transferred_message = await webhook.send(
content=content,
username=original_message.author.display_name,
avatar_url=original_message.author.display_avatar.url,
embeds=embeds[:10] if embeds else [],
files=files_to_send,
allowed_mentions=discord.AllowedMentions.none(),
wait=True
)
except discord.HTTPException as e:
logging.error(f"Erreur lors du transfert du message: {e}")
embed = discord.Embed(
title="❌ Erreur",
description=f"Une erreur est survenue lors du transfert du message: {str(e)}",
color=discord.Color.red()
)
msg = await message.channel.send(embed=embed)
asyncio.create_task(delete_after_delay(msg))
return
try:
await original_message.delete()
except discord.Forbidden:
logging.warning(f"Impossible de supprimer le message original (ID: {message_id})")
transfer_details = f"De {source_channel.name} vers {target_channel.name}"
if isinstance(target_channel, ForumChannel):
transfer_details += " (forum)"
transfer_event = ModerationEvent(
type='transfer',
username=original_message.author.name,
discord_id=str(original_message.author.id),
created_at=datetime.now(timezone.utc),
reason=f"{reason} | {transfer_details}",
staff_id=str(message.author.id),
staff_name=message.author.name
)
db.session.add(transfer_event)
_commit_with_retry()
local_now = _to_local(datetime.now(timezone.utc))
destination_info = target_channel.mention if isinstance(target_channel, (TextChannel, Thread)) else f"le forum {target_channel.name}"
embed = discord.Embed(
title="✅ Message transféré",
description=f"Le message de **{original_message.author.name}** a été transféré vers {destination_info}",
color=discord.Color.green(),
timestamp=datetime.now(timezone.utc)
)
embed.add_field(name="👤 Auteur original", value=f"{original_message.author.mention}", inline=True)
embed.add_field(name="📤 Canal source", value=source_channel.mention, inline=True)
embed.add_field(name="📥 Canal destination", value=destination_info, inline=True)
embed.add_field(name="🛡️ Modérateur", value=message.author.mention, inline=True)
embed.add_field(name="📝 Raison", value=reason, inline=False)
if isinstance(target_channel, ForumChannel):
embed.add_field(name="️ Type", value="Nouveau post créé dans le forum", inline=False)
confirmation_msg = await source_channel.send(embed=embed)
asyncio.create_task(delete_after_delay(confirmation_msg))
log_embed = discord.Embed(
title="📨 Transfert de message",
description=f"Un message de **{original_message.author.name}** a été transféré",
color=discord.Color.blue(),
timestamp=datetime.now(timezone.utc)
)
log_embed.add_field(name="👤 Auteur original", value=f"{original_message.author.name}\n`{original_message.author.id}`", inline=True)
log_embed.add_field(name="🛡️ Modérateur", value=f"**{message.author.name}**", inline=True)
log_embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
log_embed.add_field(name="📤 De", value=source_channel.mention, inline=True)
log_embed.add_field(name="📥 Vers", value=f"{target_channel.name} ({type(target_channel).__name__})", inline=True)
log_embed.add_field(name="📝 Raison", value=reason, inline=False)
preview = content[:100] + "..." if content and len(content) > 100 else content
if preview:
log_embed.add_field(name="💬 Aperçu du message", value=preview, inline=False)
log_embed.set_footer(text=f"ID Auteur: {original_message.author.id} • Serveur: {message.guild.name}")
await send_to_moderation_log_channel(bot, log_embed)
await safe_delete_message(message)
class TransferReasonModal(Modal, title="Raison du transfert"):
reason = TextInput(
label="Raison / Titre du post (forum)",
placeholder="Pour les forums, ceci devient le titre du post",
required=False,
max_length=200,
style=discord.TextStyle.paragraph
)
def __init__(self, message: discord.Message, bot, target_channel, selected_tags=None):
super().__init__()
self.message = message
self.bot = bot
self.target_channel = target_channel
self.selected_tags = selected_tags or []
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
reason = self.reason.value.strip() if self.reason.value else "Message posté dans le mauvais canal"
target_channel = self.target_channel
source_channel = self.message.channel
content = self.message.content
embeds = self.message.embeds
files_to_send = []
for attachment in self.message.attachments:
try:
file_data = await attachment.read()
files_to_send.append(discord.File(
fp=io.BytesIO(file_data),
filename=attachment.filename
))
except Exception as e:
logging.error(f"Erreur lors du téléchargement de la pièce jointe: {e}")
transferred_message = None
if isinstance(target_channel, ForumChannel):
try:
post_title = f"{self.message.author.display_name} - "
if reason and reason != "Message posté dans le mauvais canal":
remaining_length = 100 - len(post_title)
post_title += reason[:remaining_length]
elif content and len(content) > 0:
remaining_length = 100 - len(post_title)
post_title += content[:remaining_length]
else:
post_title += "Message transféré"
if len(post_title) > 100:
post_title = post_title[:97] + "..."
transfer_notice = f"**Message original de {self.message.author.mention}**\n"
transfer_notice += f"*Ce message a été transféré par un membre du staff depuis {source_channel.mention}*\n"
transfer_notice += "" * 50 + "\n\n"
full_content = transfer_notice + (content or "")
thread = await target_channel.create_thread(
name=post_title,
content=full_content,
embeds=embeds[:10] if embeds else [],
files=files_to_send,
applied_tags=self.selected_tags,
reason=f"Transfert depuis {source_channel.name} par {interaction.user.name}"
)
transferred_message = thread.message
if self.selected_tags:
tag_names = ", ".join([tag.name for tag in self.selected_tags])
logging.info(f"Post créé avec les tags: {tag_names}")
except discord.HTTPException as e:
logging.error(f"Erreur lors de la création du post dans le forum: {e}")
await interaction.followup.send(f"❌ Erreur lors de la création du post: {str(e)}", ephemeral=True)
return
else:
webhooks = await target_channel.webhooks()
webhook = None
for wh in webhooks:
if wh.user == self.bot.user:
webhook = wh
break
if not webhook:
try:
webhook = await target_channel.create_webhook(name="Mamie Henriette - Transfert")
except discord.Forbidden:
await interaction.followup.send("❌ Je n'ai pas la permission de créer un webhook dans le canal de destination.", ephemeral=True)
return
try:
transferred_message = await webhook.send(
content=content,
username=self.message.author.display_name,
avatar_url=self.message.author.display_avatar.url,
embeds=embeds[:10] if embeds else [],
files=files_to_send,
allowed_mentions=discord.AllowedMentions.none(),
wait=True
)
except discord.HTTPException as e:
logging.error(f"Erreur lors du transfert du message: {e}")
await interaction.followup.send(f"❌ Erreur lors du transfert: {str(e)}", ephemeral=True)
return
try:
await self.message.delete()
except discord.Forbidden:
logging.warning(f"Impossible de supprimer le message original (ID: {self.message.id})")
transfer_details = f"De {source_channel.name} vers {target_channel.name}"
if isinstance(target_channel, ForumChannel):
transfer_details += " (forum)"
transfer_event = ModerationEvent(
type='transfer',
username=self.message.author.name,
discord_id=str(self.message.author.id),
created_at=datetime.now(timezone.utc),
reason=f"{reason} | {transfer_details}",
staff_id=str(interaction.user.id),
staff_name=interaction.user.name
)
db.session.add(transfer_event)
_commit_with_retry()
destination_info = target_channel.mention if isinstance(target_channel, (TextChannel, Thread)) else f"le forum {target_channel.name}"
await interaction.followup.send(
f"✅ Message de **{self.message.author.name}** transféré vers {destination_info}",
ephemeral=True
)
local_now = _to_local(datetime.now(timezone.utc))
log_embed = discord.Embed(
title="📨 Transfert de message",
description=f"Un message de **{self.message.author.name}** a été transféré",
color=discord.Color.blue(),
timestamp=datetime.now(timezone.utc)
)
log_embed.add_field(name="👤 Auteur original", value=f"{self.message.author.name}\n`{self.message.author.id}`", inline=True)
log_embed.add_field(name="🛡️ Modérateur", value=f"**{interaction.user.name}**", inline=True)
log_embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
log_embed.add_field(name="📤 De", value=source_channel.mention, inline=True)
log_embed.add_field(name="📥 Vers", value=f"{target_channel.name} ({type(target_channel).__name__})", inline=True)
log_embed.add_field(name="📝 Raison", value=reason, inline=False)
preview = content[:100] + "..." if content and len(content) > 100 else content
if preview:
log_embed.add_field(name="💬 Aperçu du message", value=preview, inline=False)
log_embed.set_footer(text=f"ID Auteur: {self.message.author.id} • Serveur: {interaction.guild.name}")
await send_to_moderation_log_channel(self.bot, log_embed)
class ForumTagSelect(Select):
def __init__(self, message: discord.Message, bot, target_channel: ForumChannel):
options = []
for tag in target_channel.available_tags[:25]:
options.append(discord.SelectOption(
label=tag.name,
value=str(tag.id),
emoji=tag.emoji if tag.emoji else None
))
super().__init__(
placeholder="Sélectionnez un ou plusieurs tags...",
options=options,
min_values=1,
max_values=min(5, len(options))
)
self.message = message
self.bot = bot
self.target_channel = target_channel
async def callback(self, interaction: discord.Interaction):
selected_tags = []
for tag_id_str in self.values:
tag_id = int(tag_id_str)
tag = discord.utils.get(self.target_channel.available_tags, id=tag_id)
if tag:
selected_tags.append(tag)
modal = TransferReasonModal(self.message, self.bot, self.target_channel, selected_tags)
await interaction.response.send_modal(modal)
class ForumTagView(View):
def __init__(self, message: discord.Message, bot, target_channel: ForumChannel):
super().__init__(timeout=180)
self.add_item(ForumTagSelect(message, bot, target_channel))
class TransferChannelSelect(ChannelSelect):
def __init__(self, message: discord.Message, bot):
super().__init__(
placeholder="Sélectionnez le canal de destination...",
channel_types=[discord.ChannelType.text, discord.ChannelType.forum, discord.ChannelType.public_thread, discord.ChannelType.private_thread],
min_values=1,
max_values=1
)
self.message = message
self.bot = bot
async def callback(self, interaction: discord.Interaction):
selected_channel = self.values[0]
target_channel = self.bot.get_channel(selected_channel.id)
if not target_channel:
await interaction.response.send_message("❌ Impossible de récupérer le canal sélectionné.", ephemeral=True)
return
if isinstance(target_channel, ForumChannel) and target_channel.available_tags:
view = ForumTagView(self.message, self.bot, target_channel)
await interaction.response.send_message("🏷️ Sélectionnez un ou plusieurs tags pour ce post :", view=view, ephemeral=True)
else:
modal = TransferReasonModal(self.message, self.bot, target_channel)
await interaction.response.send_modal(modal)
class TransferView(View):
def __init__(self, message: discord.Message, bot):
super().__init__(timeout=180)
self.add_item(TransferChannelSelect(message, bot))
@app_commands.context_menu(name="Déplacer le message")
@app_commands.default_permissions(manage_messages=True)
async def transfer_message_context_menu(interaction: discord.Interaction, message: discord.Message):
if not has_staff_role(interaction.user.roles):
await interaction.response.send_message("❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", ephemeral=True)
return
view = TransferView(message, interaction.client)
await interaction.response.send_message("📨 Sélectionnez le canal de destination :", view=view, ephemeral=True)
+8
View File
@@ -95,6 +95,10 @@
<code class="text-slate-700 dark:text-slate-300 font-mono">!banlist</code>
<span class="text-slate-500 dark:text-slate-400">Liste des utilisateurs bannis</span>
</div>
<div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
<code class="text-slate-700 dark:text-slate-300 font-mono">!transfert #canal message_id</code>
<span class="text-slate-500 dark:text-slate-400">Transfère un message vers un autre canal (textuel, thread ou forum)</span>
</div>
</div>
</div>
</details>
@@ -129,6 +133,10 @@
<span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
{% elif mod_event.type == 'unban' %}
<span class="text-xs font-medium text-green-600 dark:text-green-400">Unban</span>
{% elif mod_event.type == 'transfer' %}
<span class="text-xs font-medium text-blue-600 dark:text-blue-400">Transfert</span>
{% elif mod_event.type == 'timeout' %}
<span class="text-xs font-medium text-purple-600 dark:text-purple-400">Timeout</span>
{% else %}
<span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
{% endif %}