Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942b23c956 | ||
|
|
651773e63d | ||
|
|
3450cd031c | ||
|
|
60b90edcb3 | ||
|
|
252e169af5 | ||
|
|
179876d2ce | ||
|
|
830ce61796 | ||
|
|
5709b1c0a3 | ||
|
|
11348bda39 | ||
|
|
f7e85bac69 | ||
|
|
d1d4e3b5a5 | ||
|
|
5aa52c8137 | ||
|
|
9bbfa1fade | ||
|
|
920ddfa172 |
+37
-2
@@ -7,7 +7,7 @@ from webapp import webapp
|
|||||||
from database import db
|
from database import db
|
||||||
from database.helpers import ConfigurationHelper
|
from database.helpers import ConfigurationHelper
|
||||||
from database.models import Configuration, Humeur, Commande
|
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.humblebundle import checkHumbleBundleAndNotify
|
||||||
from discordbot.freeloot import checkFreeLootAndNotify
|
from discordbot.freeloot import checkFreeLootAndNotify
|
||||||
from discordbot.moderation import (
|
from discordbot.moderation import (
|
||||||
@@ -21,7 +21,9 @@ from discordbot.moderation import (
|
|||||||
handle_ban_list_command,
|
handle_ban_list_command,
|
||||||
handle_staff_help_command,
|
handle_staff_help_command,
|
||||||
handle_timeout_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.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
||||||
from discordbot.youtube import checkYouTubeVideos
|
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
|
from protondb import searhProtonDb
|
||||||
|
|
||||||
class DiscordBot(discord.Client):
|
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):
|
async def on_ready(self):
|
||||||
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
|
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_connected"] = True
|
||||||
webapp.config["BOT_STATUS"]["discord_guild_count"] = len(self.guilds)
|
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() :
|
for c in self.get_all_channels() :
|
||||||
logging.info(f'{c.id} {c.name}')
|
logging.info(f'{c.id} {c.name}')
|
||||||
|
|
||||||
@@ -167,6 +198,10 @@ async def on_message(message: Message):
|
|||||||
await handle_say_command(message, bot)
|
await handle_say_command(message, bot)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if command_name in ['!transfert', '!transfer', '!move']:
|
||||||
|
await handle_transfer_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
|
||||||
|
|||||||
+542
-2
@@ -4,12 +4,14 @@ import time
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import discord
|
import discord
|
||||||
|
import io
|
||||||
from datetime import datetime, timezone, timedelta
|
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
|
||||||
from database.models import ModerationEvent
|
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():
|
def _get_local_tz():
|
||||||
tz_name = os.environ.get('APP_TZ') or os.environ.get('TZ') or 'Europe/Paris'
|
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=(
|
value=(
|
||||||
"• `!say #channel message`\n"
|
"• `!say #channel message`\n"
|
||||||
" Envoie un message en tant que bot\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
|
inline=False
|
||||||
)
|
)
|
||||||
@@ -1405,3 +1415,533 @@ async def handle_say_command(message: Message, bot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Erreur lors de l'envoi du message: {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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,603 +0,0 @@
|
|||||||
/* MVP.css v1.17.2 - https://github.com/andybrewer/mvp */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--active-brightness: 0.85;
|
|
||||||
--border-radius: 5px;
|
|
||||||
--box-shadow: 2px 2px 10px;
|
|
||||||
--color-accent: #118bee15;
|
|
||||||
--color-bg: #fff;
|
|
||||||
--color-bg-secondary: #e9e9e9;
|
|
||||||
--color-link: #118bee;
|
|
||||||
--color-secondary: #920de9;
|
|
||||||
--color-secondary-accent: #920de90b;
|
|
||||||
--color-shadow: #f4f4f4;
|
|
||||||
--color-table: #118bee;
|
|
||||||
--color-text: #000;
|
|
||||||
--color-text-secondary: #999;
|
|
||||||
--color-scrollbar: #cacae8;
|
|
||||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
|
||||||
--hover-brightness: 1.2;
|
|
||||||
--justify-important: center;
|
|
||||||
--justify-normal: left;
|
|
||||||
--line-height: 1.5;
|
|
||||||
--width-card: 285px;
|
|
||||||
--width-card-medium: 460px;
|
|
||||||
--width-card-wide: 800px;
|
|
||||||
--width-content: 1080px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root[color-mode="user"] {
|
|
||||||
--color-accent: #0097fc4f;
|
|
||||||
--color-bg: #333;
|
|
||||||
--color-bg-secondary: #555;
|
|
||||||
--color-link: #0097fc;
|
|
||||||
--color-secondary: #e20de9;
|
|
||||||
--color-secondary-accent: #e20de94f;
|
|
||||||
--color-shadow: #bbbbbb20;
|
|
||||||
--color-table: #0097fc;
|
|
||||||
--color-text: #f7f7f7;
|
|
||||||
--color-text-secondary: #aaa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
html {
|
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
article aside {
|
|
||||||
background: var(--color-secondary-accent);
|
|
||||||
border-left: 4px solid var(--color-secondary);
|
|
||||||
padding: 0.01rem 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-family: var(--font-family);
|
|
||||||
line-height: var(--line-height);
|
|
||||||
margin: 0;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
main {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: var(--width-content);
|
|
||||||
/* padding: 3rem 1rem; */
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
border: none;
|
|
||||||
height: 1px;
|
|
||||||
margin: 4rem 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: var(--justify-important);
|
|
||||||
}
|
|
||||||
|
|
||||||
section img,
|
|
||||||
article img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section pre {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
section aside {
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
width: var(--width-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
section aside:hover {
|
|
||||||
box-shadow: var(--box-shadow) var(--color-bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headers */
|
|
||||||
article header,
|
|
||||||
div header,
|
|
||||||
main header {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
text-align: var(--justify-important);
|
|
||||||
}
|
|
||||||
|
|
||||||
header a b,
|
|
||||||
header a em,
|
|
||||||
header a i,
|
|
||||||
header a strong {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* header nav img {
|
|
||||||
margin: 1rem 0;
|
|
||||||
} */
|
|
||||||
|
|
||||||
section header {
|
|
||||||
padding-top: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav */
|
|
||||||
nav {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* margin-bottom: 7rem; */
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav Dropdown */
|
|
||||||
nav ul li:hover ul {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li ul {
|
|
||||||
background: var(--color-bg);
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
|
||||||
display: none;
|
|
||||||
height: auto;
|
|
||||||
left: -2px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 1.7rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: auto;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li ul::before {
|
|
||||||
/* fill gap above to make mousing over them easier */
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: -0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li ul li,
|
|
||||||
nav ul li ul li a {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav for Mobile */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
nav {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
width: calc(100% - 1em);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li ul {
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
display: block;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
code,
|
|
||||||
samp {
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-text);
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 0.1rem;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
margin: 1.3rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
details summary {
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
line-height: var(--line-height);
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark {
|
|
||||||
padding: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol li,
|
|
||||||
ul li {
|
|
||||||
padding: 0.2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
margin: 1rem 0;
|
|
||||||
max-width: var(--width-card-wide);
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre code,
|
|
||||||
pre samp {
|
|
||||||
display: block;
|
|
||||||
max-width: var(--width-card-wide);
|
|
||||||
padding: 0.5rem 2rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-bg);
|
|
||||||
font-size: xx-small;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0.2rem;
|
|
||||||
padding: 0.2rem 0.3rem;
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
a {
|
|
||||||
color: var(--color-link);
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
filter: brightness(var(--hover-brightness));
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
filter: brightness(var(--active-brightness));
|
|
||||||
}
|
|
||||||
|
|
||||||
a b,
|
|
||||||
a em,
|
|
||||||
a i,
|
|
||||||
a strong,
|
|
||||||
button,
|
|
||||||
input[type="submit"] {
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
display: inline-block;
|
|
||||||
font-size: medium;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: var(--line-height);
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input[type="submit"] {
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover,
|
|
||||||
input[type="submit"]:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
filter: brightness(var(--hover-brightness));
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active,
|
|
||||||
input[type="submit"]:active {
|
|
||||||
filter: brightness(var(--active-brightness));
|
|
||||||
}
|
|
||||||
|
|
||||||
a b,
|
|
||||||
a strong,
|
|
||||||
button,
|
|
||||||
input[type="submit"] {
|
|
||||||
background-color: var(--color-link);
|
|
||||||
border: 2px solid var(--color-link);
|
|
||||||
color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
a em,
|
|
||||||
a i {
|
|
||||||
border: 2px solid var(--color-link);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-link);
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article aside a {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
figure {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
figure img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
figure figcaption {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
button:disabled,
|
|
||||||
input:disabled {
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
border-color: var(--color-bg-secondary);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[disabled]:hover,
|
|
||||||
input[type="submit"][disabled]:hover {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
|
||||||
display: block;
|
|
||||||
max-width: var(--width-card-wide);
|
|
||||||
min-width: var(--width-card);
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: var(--justify-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
form header {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
label,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
display: block;
|
|
||||||
font-size: inherit;
|
|
||||||
max-width: var(--width-card-wide);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]+label,
|
|
||||||
input[type="radio"]+label {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: normal;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"] {
|
|
||||||
padding: 0.4rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="email"],
|
|
||||||
textarea {
|
|
||||||
width: calc(100% - 1.6rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[readonly],
|
|
||||||
textarea[readonly] {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Popups */
|
|
||||||
dialog {
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 85dvh;
|
|
||||||
margin: auto;
|
|
||||||
padding-block: 0;
|
|
||||||
padding-inline: 20px;
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
scrollbar-width: none;
|
|
||||||
/* Hide scrollbar for Firefox */
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
/* Hide scrollbar for IE and Edge */
|
|
||||||
scrollbar-color: transparent transparent;
|
|
||||||
animation: bottom-to-top 0.25s ease-in-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog::-webkit-scrollbar-thumb {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 650px) {
|
|
||||||
dialog {
|
|
||||||
max-width: 39rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog::backdrop {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bottom-to-top {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog hr {
|
|
||||||
margin-block: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
table {
|
|
||||||
border: 1px solid var(--color-bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border-spacing: 0;
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td,
|
|
||||||
table th,
|
|
||||||
table tr {
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
text-align: var(--justify-important);
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead {
|
|
||||||
background-color: var(--color-table);
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-bg);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead tr:first-child th:first-child {
|
|
||||||
border-top-left-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead tr:first-child th:last-child {
|
|
||||||
border-top-right-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child,
|
|
||||||
table tr td:first-child {
|
|
||||||
text-align: var(--justify-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
table tr:nth-child(even) {
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quotes */
|
|
||||||
blockquote {
|
|
||||||
display: block;
|
|
||||||
font-size: x-large;
|
|
||||||
line-height: var(--line-height);
|
|
||||||
margin: 1rem auto;
|
|
||||||
max-width: var(--width-card-medium);
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
text-align: var(--justify-important);
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote footer {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
display: block;
|
|
||||||
font-size: small;
|
|
||||||
line-height: var(--line-height);
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbars */
|
|
||||||
* {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--color-scrollbar) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--color-scrollbar);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
header nav img {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table th,
|
|
||||||
table td {
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.live-alert tr td:last-child {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.icon {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
@@ -95,6 +95,10 @@
|
|||||||
<code class="text-slate-700 dark:text-slate-300 font-mono">!banlist</code>
|
<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>
|
<span class="text-slate-500 dark:text-slate-400">Liste des utilisateurs bannis</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -129,6 +133,10 @@
|
|||||||
<span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
|
<span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
|
||||||
{% elif mod_event.type == 'unban' %}
|
{% elif mod_event.type == 'unban' %}
|
||||||
<span class="text-xs font-medium text-green-600 dark:text-green-400">Unban</span>
|
<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 %}
|
{% else %}
|
||||||
<span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
|
<span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,35 +1,92 @@
|
|||||||
{% extends "template.html" %}
|
{% extends "template.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Procédure de configuration de Twitch</h1>
|
<div class="mb-6">
|
||||||
<p>
|
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Configuration Twitch</h1>
|
||||||
<strong>Avant toute chose, activez l'authentification à deux facteurs (2FA) :</strong>
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
<a href="https://help.twitch.tv/s/article/two-factor-authentication?language=en_US" target="_blank">Guide officiel
|
Guide étape par étape pour configurer l'API Twitch.
|
||||||
Twitch pour la 2FA</a>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
|
||||||
Rendez-vous sur <a href="https://dev.twitch.tv/console" target="_blank">la console d'applications Twitch</a> et
|
|
||||||
ajoutez une application. Renseignez :
|
|
||||||
<ul>
|
|
||||||
<li>URL de redirection : {{token_redirect_url}}</li>
|
|
||||||
<li>Catégorie : Chat Bot</li>
|
|
||||||
</ul>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<img src="/static/img/twitch-api-01.jpg">
|
<div class="space-y-4">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">1</span>
|
||||||
|
<h2 class="font-medium text-slate-800 dark:text-white">Activer l'authentification à deux facteurs (2FA)</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5">
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
|
||||||
|
Avant de créer une application Twitch, vous devez activer la 2FA sur votre compte.
|
||||||
|
</p>
|
||||||
|
<a href="https://help.twitch.tv/s/article/two-factor-authentication?language=en_US" target="_blank" class="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-white">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||||
|
Guide officiel Twitch
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
Créez le bot. Puis, de retour à la liste, éditez-le en cliquant sur Gérer. Puis cliquez sur <strong>Nouveau
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
Secret</strong>. Vous trouverez ici le <strong>Client ID</strong> et le <strong>Client Secret</strong>.
|
<div class="flex items-center gap-3">
|
||||||
</p>
|
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">2</span>
|
||||||
|
<h2 class="font-medium text-slate-800 dark:text-white">Créer une application Twitch</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Rendez-vous sur la console Twitch et créez une nouvelle application :
|
||||||
|
</p>
|
||||||
|
<a href="https://dev.twitch.tv/console" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white text-sm rounded-lg hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
|
||||||
|
Console Twitch
|
||||||
|
</a>
|
||||||
|
|
||||||
<img src="/static/img/twitch-api-02.jpg">
|
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 space-y-2 text-sm">
|
||||||
|
<div><span class="text-slate-500 dark:text-slate-400">URL de redirection :</span> <code class="ml-1 px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs">{{ token_redirect_url }}</code></div>
|
||||||
|
<div><span class="text-slate-500 dark:text-slate-400">Catégorie :</span> <span class="ml-1 text-slate-800 dark:text-white">Chat Bot</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||||
Ensuite, retournez sur la page de <a href="{{url_for('openConfigurations')}}">Configuration</a>, après avoir
|
<img src="/static/img/twitch-api-01.jpg" alt="Création d'application Twitch" class="w-full">
|
||||||
enregistré le <strong>Client ID</strong> et le <strong>Client Secret</strong>, cliquez sur le lien <strong>Obtenir
|
</div>
|
||||||
token et refresh token</strong>. Si tout se passe bien les champs <strong>Access Token</strong> et
|
</div>
|
||||||
<strong>Refresh Token</strong> sont remplis.
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">3</span>
|
||||||
|
<h2 class="font-medium text-slate-800 dark:text-white">Récupérer les identifiants</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Cliquez sur <strong class="text-slate-800 dark:text-white">Gérer</strong> puis <strong class="text-slate-800 dark:text-white">Nouveau Secret</strong> pour obtenir le Client ID et Client Secret.
|
||||||
|
</p>
|
||||||
|
<div class="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||||
|
<img src="/static/img/twitch-api-02.jpg" alt="Récupération des identifiants" class="w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">4</span>
|
||||||
|
<h2 class="font-medium text-slate-800 dark:text-white">Configurer Mamie Henriette</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<ol class="list-decimal list-inside space-y-1 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<li>Entrez le Client ID et Client Secret</li>
|
||||||
|
<li>Cliquez sur Enregistrer</li>
|
||||||
|
<li>Cliquez sur "Obtenir token et refresh token"</li>
|
||||||
|
</ol>
|
||||||
|
<a href="{{ url_for('openConfigurations') }}" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white text-sm rounded-lg hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
|
||||||
|
Aller à la Configuration
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def twitchConfigurationHelp():
|
|||||||
def twitchRequestToken():
|
def twitchRequestToken():
|
||||||
global auth
|
global auth
|
||||||
helper = ConfigurationHelper()
|
helper = ConfigurationHelper()
|
||||||
twitch = asyncio.run(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')))
|
twitch = Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
|
||||||
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
|
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
|
||||||
return redirect(auth.return_auth_url())
|
return redirect(auth.return_auth_url())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user