From f2cd19a0532428febde77990a914d7527fb9c904 Mon Sep 17 00:00:00 2001
From: Mow910
Date: Sun, 25 Jan 2026 17:28:38 +0100
Subject: [PATCH 1/3] =?UTF-8?q?Ajout=20d'un=20syst=C3=A8me=20de=20notifica?=
=?UTF-8?q?tions=20YouTube=20avec=20une=20nouvelle=20table=20`youtube=5Fno?=
=?UTF-8?q?tification`=20dans=20la=20base=20de=20donn=C3=A9es,=20int=C3=A9?=
=?UTF-8?q?gration=20de=20la=20v=C3=A9rification=20des=20vid=C3=A9os=20You?=
=?UTF-8?q?Tube,=20et=20cr=C3=A9ation=20d'une=20interface=20web=20pour=20g?=
=?UTF-8?q?=C3=A9rer=20les=20notifications.=20Le=20bot=20Discord=20enverra?=
=?UTF-8?q?=20des=20alertes=20pour=20les=20nouvelles=20vid=C3=A9os=20d?=
=?UTF-8?q?=C3=A9tect=C3=A9es.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
database/models.py | 10 ++
database/schema.sql | 10 ++
discordbot/__init__.py | 8 ++
discordbot/youtube.py | 180 +++++++++++++++++++++++++++++++++
webapp/__init__.py | 2 +-
webapp/templates/template.html | 1 +
webapp/templates/youtube.html | 113 +++++++++++++++++++++
webapp/youtube.py | 157 ++++++++++++++++++++++++++++
8 files changed, 480 insertions(+), 1 deletion(-)
create mode 100644 discordbot/youtube.py
create mode 100644 webapp/templates/youtube.html
create mode 100644 webapp/youtube.py
diff --git a/database/models.py b/database/models.py
index 9f103d1..e174fce 100644
--- a/database/models.py
+++ b/database/models.py
@@ -61,3 +61,13 @@ class AntiCheatCache(db.Model):
notes = db.Column(db.String(1024))
updated_at = db.Column(db.DateTime)
+class YouTubeNotification(db.Model):
+ __tablename__ = 'youtube_notification'
+ id = db.Column(db.Integer, primary_key=True)
+ enable = db.Column(db.Boolean, default=True)
+ channel_id = db.Column(db.String(128)) # ID de la chaîne YouTube
+ notify_channel = db.Column(db.Integer) # ID du canal Discord
+ message = db.Column(db.String(2000))
+ video_type = db.Column(db.String(16), default='all') # 'all', 'video', 'short'
+ last_video_id = db.Column(db.String(128)) # ID de la dernière vidéo notifiée
+
diff --git a/database/schema.sql b/database/schema.sql
index 04cef86..4f36e26 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -76,3 +76,13 @@ CREATE TABLE IF NOT EXISTS `member_invites` (
`inviter_name` VARCHAR(256),
`join_date` DATETIME NOT NULL
);
+
+CREATE TABLE IF NOT EXISTS `youtube_notification` (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ `enable` BOOLEAN NOT NULL DEFAULT TRUE,
+ `channel_id` VARCHAR(128) NOT NULL,
+ `notify_channel` INTEGER NOT NULL,
+ `message` VARCHAR(2000) NOT NULL,
+ `video_type` VARCHAR(16) NOT NULL DEFAULT 'all',
+ `last_video_id` VARCHAR(128)
+);
diff --git a/discordbot/__init__.py b/discordbot/__init__.py
index 2de81dd..9c48341 100644
--- a/discordbot/__init__.py
+++ b/discordbot/__init__.py
@@ -22,6 +22,7 @@ from discordbot.moderation import (
handle_say_command
)
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
+from discordbot.youtube import checkYouTubeVideos
from protondb import searhProtonDb
class DiscordBot(discord.Client):
@@ -35,6 +36,7 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateStatus())
self.loop.create_task(self.updateHumbleBundle())
+ self.loop.create_task(self.updateYouTube())
async def updateStatus(self):
while not self.is_closed():
@@ -50,6 +52,12 @@ class DiscordBot(discord.Client):
while not self.is_closed():
await checkHumbleBundleAndNotify(self)
await asyncio.sleep(30*60)
+
+ async def updateYouTube(self):
+ while not self.is_closed():
+ await checkYouTubeVideos()
+ # Vérification toutes les 5 minutes (comme pour Twitch)
+ await asyncio.sleep(5*60)
def getAllTextChannel(self) -> list[TextChannel]:
channels = []
diff --git a/discordbot/youtube.py b/discordbot/youtube.py
new file mode 100644
index 0000000..7211fca
--- /dev/null
+++ b/discordbot/youtube.py
@@ -0,0 +1,180 @@
+import logging
+import asyncio
+import xml.etree.ElementTree as ET
+import requests
+
+from database import db
+from database.models import YouTubeNotification
+from webapp import webapp
+
+logger = logging.getLogger('youtube-notification')
+logger.setLevel(logging.INFO)
+
+
+async def checkYouTubeVideos():
+ with webapp.app_context():
+ try:
+ notifications: list[YouTubeNotification] = YouTubeNotification.query.filter_by(enable=True).all()
+
+ for notification in notifications:
+ try:
+ await _checkChannelVideos(notification)
+ except Exception as e:
+ logger.error(f"Erreur lors de la vérification de la chaîne {notification.channel_id}: {e}")
+ continue
+ except Exception as e:
+ logger.error(f"Erreur lors de la vérification YouTube: {e}")
+
+
+async def _checkChannelVideos(notification: YouTubeNotification):
+ try:
+ channel_id = notification.channel_id
+
+ rss_url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
+
+ response = await asyncio.to_thread(requests.get, rss_url, timeout=10)
+
+ if response.status_code != 200:
+ logger.error(f"Erreur HTTP {response.status_code} lors de la récupération du RSS pour {channel_id}")
+ return
+
+ root = ET.fromstring(response.content)
+
+ ns = {'atom': 'http://www.w3.org/2005/Atom', 'yt': 'http://www.youtube.com/xml/schemas/2015', 'media': 'http://search.yahoo.com/mrss/'}
+
+ entries = root.findall('atom:entry', ns)
+
+ if not entries:
+ logger.warning(f"Aucune vidéo trouvée dans le RSS pour {channel_id}")
+ return
+
+ videos = []
+ for entry in entries:
+ video_id = entry.find('yt:videoId', ns)
+ if video_id is None:
+ continue
+ video_id = video_id.text
+
+ title_elem = entry.find('atom:title', ns)
+ video_title = title_elem.text if title_elem is not None else 'Sans titre'
+
+ link_elem = entry.find('atom:link', ns)
+ video_url = link_elem.get('href') if link_elem is not None else f"https://www.youtube.com/watch?v={video_id}"
+
+ published_elem = entry.find('atom:published', ns)
+ published_at = published_elem.text if published_elem is not None else ''
+
+ author_elem = entry.find('atom:author/atom:name', ns)
+ channel_name = author_elem.text if author_elem is not None else 'Inconnu'
+
+ thumbnail = None
+ media_thumbnail = entry.find('media:group/media:thumbnail', ns)
+ if media_thumbnail is not None:
+ thumbnail = media_thumbnail.get('url')
+
+ is_short = False
+ if video_title and ('#shorts' in video_title.lower() or '#short' in video_title.lower()):
+ is_short = True
+
+ if notification.video_type == 'all':
+ videos.append((video_id, {
+ 'title': video_title,
+ 'url': video_url,
+ 'published': published_at,
+ 'channel_name': channel_name,
+ 'thumbnail': thumbnail,
+ 'is_short': is_short
+ }))
+ elif notification.video_type == 'short' and is_short:
+ videos.append((video_id, {
+ 'title': video_title,
+ 'url': video_url,
+ 'published': published_at,
+ 'channel_name': channel_name,
+ 'thumbnail': thumbnail,
+ 'is_short': is_short
+ }))
+ elif notification.video_type == 'video' and not is_short:
+ videos.append((video_id, {
+ 'title': video_title,
+ 'url': video_url,
+ 'published': published_at,
+ 'channel_name': channel_name,
+ 'thumbnail': thumbnail,
+ 'is_short': is_short
+ }))
+
+ videos.sort(key=lambda x: x[1]['published'], reverse=True)
+
+ if videos:
+ latest_video_id, latest_video = videos[0]
+
+ if not notification.last_video_id:
+ notification.last_video_id = latest_video_id
+ db.session.commit()
+ return
+
+ if latest_video_id != notification.last_video_id:
+ logger.info(f"Nouvelle vidéo détectée: {latest_video_id} pour la chaîne {notification.channel_id}")
+ await _notifyVideo(notification, latest_video, latest_video_id)
+ notification.last_video_id = latest_video_id
+ db.session.commit()
+
+ except Exception as e:
+ logger.error(f"Erreur lors de la vérification des vidéos: {e}")
+
+
+async def _notifyVideo(notification: YouTubeNotification, video_data: dict, video_id: str):
+ from discordbot import bot
+ try:
+ channel_name = video_data.get('channel_name', 'Inconnu')
+ video_title = video_data.get('title', 'Sans titre')
+ video_url = video_data.get('url', f"https://www.youtube.com/watch?v={video_id}")
+ thumbnail = video_data.get('thumbnail', '')
+ published_at = video_data.get('published', '')
+ is_short = video_data.get('is_short', False)
+
+ try:
+ message = notification.message.format(
+ channel_name=channel_name or 'Inconnu',
+ video_title=video_title or 'Sans titre',
+ video_url=video_url,
+ video_id=video_id,
+ thumbnail=thumbnail or '',
+ published_at=published_at or '',
+ is_short=is_short
+ )
+ except KeyError as e:
+ logger.error(f"Variable manquante dans le message de notification: {e}")
+ message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})"
+
+ logger.info(f"Envoi de notification YouTube: {message}")
+ bot.loop.create_task(_sendMessage(notification.notify_channel, message, video_url, thumbnail, video_title, channel_name))
+
+ except Exception as e:
+ logger.error(f"Erreur lors de la notification: {e}")
+
+
+async def _sendMessage(channel_id: int, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str):
+ from discordbot import bot
+ try:
+ discord_channel = bot.get_channel(channel_id)
+ if not discord_channel:
+ logger.error(f"Canal Discord {channel_id} introuvable")
+ return
+
+ import discord
+ embed = discord.Embed(
+ title=video_title,
+ url=video_url,
+ color=0xFF0000
+ )
+ embed.set_author(name=channel_name, icon_url="https://www.youtube.com/img/desktop/yt_1200.png")
+ if thumbnail:
+ embed.set_image(url=thumbnail)
+
+ await discord_channel.send(message, embed=embed)
+ logger.info(f"Notification YouTube envoyée avec succès")
+
+ except Exception as e:
+ logger.error(f"Erreur lors de l'envoi du message Discord: {e}")
diff --git a/webapp/__init__.py b/webapp/__init__.py
index 28f6c3c..cdaaa26 100644
--- a/webapp/__init__.py
+++ b/webapp/__init__.py
@@ -2,4 +2,4 @@ from flask import Flask
webapp = Flask(__name__)
-from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation
+from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, youtube
diff --git a/webapp/templates/template.html b/webapp/templates/template.html
index e36f520..1539260 100644
--- a/webapp/templates/template.html
+++ b/webapp/templates/template.html
@@ -19,6 +19,7 @@
Alerte live
+ YouTube
Commandes
Humeurs
Modération
diff --git a/webapp/templates/youtube.html b/webapp/templates/youtube.html
new file mode 100644
index 0000000..66d2241
--- /dev/null
+++ b/webapp/templates/youtube.html
@@ -0,0 +1,113 @@
+{% extends "template.html" %}
+
+{% block content %}
+Notifications YouTube
+
+{% if msg %}
+
+ {{ msg }}
+
+
+{% endif %}
+
+
+ Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos.
+
+ Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous.
+ Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
+
+
+{% if not notification %}
+Notifications
+
+
+
+ Chaîne YouTube
+ Canal Discord
+ Type
+ Message
+ #
+
+
+
+ {% for notification in notifications %}
+
+ {{notification.channel_id}}
+ {{notification.notify_channel_name}}
+
+ {% if notification.video_type == 'all' %}
+ Toutes
+ {% elif notification.video_type == 'video' %}
+ Vidéos uniquement
+ {% elif notification.video_type == 'short' %}
+ Shorts uniquement
+ {% endif %}
+
+ {{notification.message}}
+
+ {{ '✅' if notification.enable else '❌' }}
+ ✐
+ 🗑
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{{ 'Editer une notification' if notification else 'Ajouter une notification YouTube' }}
+
+
+ Pour le message vous avez accès à ces variables :
+
+ {channel_name} : nom de la chaîne YouTube
+ {video_title} : titre de la vidéo
+ {video_url} : lien vers la vidéo
+ {video_id} : ID de la vidéo
+ {thumbnail} : URL de la miniature
+ {published_at} : date de publication
+ {is_short} : True si c'est un short, False sinon
+
+ Le message est au format common-mark dans la limite de ce que
+ support Discord.
+ Exemple : 🎥 Nouvelle vidéo de {channel_name} : [{video_title}]({video_url})
+
+
+
+{% endblock %}
diff --git a/webapp/youtube.py b/webapp/youtube.py
new file mode 100644
index 0000000..0e70118
--- /dev/null
+++ b/webapp/youtube.py
@@ -0,0 +1,157 @@
+import re
+import requests
+from urllib.parse import urlencode
+from flask import render_template, request, redirect, url_for
+
+from webapp import webapp
+from database import db
+from database.models import YouTubeNotification
+from discordbot import bot
+
+
+def extract_channel_id(channel_input: str) -> str:
+ """Extrait l'ID de la chaîne YouTube depuis différents formats"""
+ if not channel_input:
+ return None
+
+ channel_input = channel_input.strip()
+
+ if channel_input.startswith('UC') and len(channel_input) == 24:
+ return channel_input
+
+ if '/channel/' in channel_input:
+ match = re.search(r'/channel/([a-zA-Z0-9_-]{24})', channel_input)
+ if match:
+ return match.group(1)
+
+ if '/c/' in channel_input or '/user/' in channel_input:
+ parts = channel_input.split('/')
+ for i, part in enumerate(parts):
+ if part in ['c', 'user'] and i + 1 < len(parts):
+ handle = parts[i + 1].split('?')[0].split('&')[0]
+ channel_id = _get_channel_id_from_handle(handle)
+ if channel_id:
+ return channel_id
+
+ if '@' in channel_input:
+ handle = re.search(r'@([a-zA-Z0-9_-]+)', channel_input)
+ if handle:
+ channel_id = _get_channel_id_from_handle(handle.group(1))
+ if channel_id:
+ return channel_id
+
+ return None
+
+
+def _get_channel_id_from_handle(handle: str) -> str:
+ """Récupère l'ID de la chaîne depuis un handle en utilisant le flux RSS"""
+ try:
+ url = f"https://www.youtube.com/@{handle}"
+ response = requests.get(url, timeout=10, allow_redirects=True)
+
+ if response.status_code == 200:
+ channel_id_match = re.search(r'"channelId":"([^"]{24})"', response.text)
+ if channel_id_match:
+ return channel_id_match.group(1)
+
+ canonical_match = re.search(r' ")
+def toggleYouTube(id):
+ notification: YouTubeNotification = YouTubeNotification.query.get_or_404(id)
+ notification.enable = not notification.enable
+ db.session.commit()
+ return redirect(url_for("openYouTube"))
+
+
+@webapp.route("/youtube/edit/")
+def openEditYouTube(id):
+ notification = YouTubeNotification.query.get_or_404(id)
+ channels = bot.getAllTextChannel()
+ msg = request.args.get('msg')
+ msg_type = request.args.get('type', 'info')
+ return render_template("youtube.html", notification=notification, channels=channels, notifications=YouTubeNotification.query.all(), msg=msg, msg_type=msg_type)
+
+
+@webapp.route("/youtube/edit/", methods=['POST'])
+def submitEditYouTube(id):
+ notification: YouTubeNotification = YouTubeNotification.query.get_or_404(id)
+
+ channel_input = request.form.get('channel_id', '').strip()
+ channel_id = extract_channel_id(channel_input)
+
+ if not channel_id:
+ return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': f"Impossible d'extraire l'ID de la chaîne depuis : {channel_input}. Veuillez vérifier le lien.", 'type': 'error'}))
+
+ notify_channel_str = request.form.get('notify_channel')
+ if not notify_channel_str:
+ return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': "Veuillez sélectionner un canal Discord. Assurez-vous que le bot Discord est connecté.", 'type': 'error'}))
+
+ try:
+ notify_channel = int(notify_channel_str)
+ except ValueError:
+ return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': "Canal Discord invalide.", 'type': 'error'}))
+
+ notification.channel_id = channel_id
+ notification.notify_channel = notify_channel
+ notification.message = request.form.get('message')
+ notification.video_type = request.form.get('video_type', 'all')
+ db.session.commit()
+ return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Notification modifiée avec succès", 'type': 'success'}))
+
+
+@webapp.route("/youtube/del/")
+def delYouTube(id):
+ notification = YouTubeNotification.query.get_or_404(id)
+ db.session.delete(notification)
+ db.session.commit()
+ return redirect(url_for("openYouTube"))
From a8d2a0e0636e47f8db38c26d38965c4658c3c1cc Mon Sep 17 00:00:00 2001
From: Mow910
Date: Sun, 25 Jan 2026 17:45:59 +0100
Subject: [PATCH 2/3] =?UTF-8?q?Ajout=20de=20nouvelles=20colonnes=20pour=20?=
=?UTF-8?q?la=20personnalisation=20des=20notifications=20YouTube=20dans=20?=
=?UTF-8?q?la=20table=20`youtube=5Fnotification`,=20y=20compris=20le=20tit?=
=?UTF-8?q?re,=20la=20description,=20la=20couleur,=20le=20pied=20de=20page?=
=?UTF-8?q?,=20le=20nom=20et=20l'ic=C3=B4ne=20de=20l'auteur,=20ainsi=20que?=
=?UTF-8?q?=20des=20options=20pour=20afficher=20la=20miniature=20et=20l'im?=
=?UTF-8?q?age.=20Mise=20=C3=A0=20jour=20de=20l'interface=20web=20pour=20p?=
=?UTF-8?q?ermettre=20la=20configuration=20de=20ces=20options=20et=20ajout?=
=?UTF-8?q?=20d'une=20pr=C3=A9visualisation=20de=20l'embed=20Discord.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
database/__init__.py | 27 ++++
database/models.py | 16 ++-
database/schema.sql | 10 +-
discordbot/youtube.py | 63 ++++++++--
webapp/templates/youtube.html | 226 +++++++++++++++++++++++++++-------
webapp/youtube.py | 26 +++-
6 files changed, 306 insertions(+), 62 deletions(-)
diff --git a/database/__init__.py b/database/__init__.py
index 9d1d48e..444d20f 100644
--- a/database/__init__.py
+++ b/database/__init__.py
@@ -33,7 +33,13 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
except Exception:
pass
+def _tableExists(table_name:str, cursor:Cursor) -> bool:
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
+ return cursor.fetchone() is not None
+
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
+ if not _tableExists(table_name, cursor):
+ return False
cursor.execute(f'PRAGMA table_info({table_name})')
columns = cursor.fetchall()
return any(col[1] == column_name for col in columns)
@@ -64,6 +70,27 @@ def _doPostImportMigration(cursor:Cursor):
cursor.execute('INSERT INTO game_bundle(url, name, json) VALUES (?, ?, ?)', (url, name, json.dumps(json_data)))
logging.info("suppression de la table temporaire game_bundle_old")
_dropTable('game_bundle_old', cursor)
+
+ if _tableExists('youtube_notification', cursor):
+ logging.info("Migration de la table youtube_notification: ajout des colonnes d'embed")
+ embed_columns = [
+ ('embed_title', 'VARCHAR(256)'),
+ ('embed_description', 'VARCHAR(2000)'),
+ ('embed_color', 'VARCHAR(8) DEFAULT "FF0000"'),
+ ('embed_footer', 'VARCHAR(2048)'),
+ ('embed_author_name', 'VARCHAR(256)'),
+ ('embed_author_icon', 'VARCHAR(512)'),
+ ('embed_thumbnail', 'BOOLEAN DEFAULT 1'),
+ ('embed_image', 'BOOLEAN DEFAULT 1')
+ ]
+ for col_name, col_type in embed_columns:
+ if not _tableHaveColumn('youtube_notification', col_name, cursor):
+ try:
+ cursor.execute(f'ALTER TABLE youtube_notification ADD COLUMN {col_name} {col_type}')
+ logging.info(f"Colonne {col_name} ajoutée à youtube_notification")
+ except Exception as e:
+ logging.error(f"Impossible d'ajouter la colonne {col_name}: {e}")
+ raise
with webapp.app_context():
with open('database/schema.sql', 'r') as f:
diff --git a/database/models.py b/database/models.py
index e174fce..1562652 100644
--- a/database/models.py
+++ b/database/models.py
@@ -65,9 +65,17 @@ class YouTubeNotification(db.Model):
__tablename__ = 'youtube_notification'
id = db.Column(db.Integer, primary_key=True)
enable = db.Column(db.Boolean, default=True)
- channel_id = db.Column(db.String(128)) # ID de la chaîne YouTube
- notify_channel = db.Column(db.Integer) # ID du canal Discord
+ channel_id = db.Column(db.String(128))
+ notify_channel = db.Column(db.Integer)
message = db.Column(db.String(2000))
- video_type = db.Column(db.String(16), default='all') # 'all', 'video', 'short'
- last_video_id = db.Column(db.String(128)) # ID de la dernière vidéo notifiée
+ video_type = db.Column(db.String(16), default='all')
+ last_video_id = db.Column(db.String(128))
+ embed_title = db.Column(db.String(256))
+ embed_description = db.Column(db.String(2000))
+ embed_color = db.Column(db.String(8), default='FF0000')
+ embed_footer = db.Column(db.String(2048))
+ embed_author_name = db.Column(db.String(256))
+ embed_author_icon = db.Column(db.String(512))
+ embed_thumbnail = db.Column(db.Boolean, default=True)
+ embed_image = db.Column(db.Boolean, default=True)
diff --git a/database/schema.sql b/database/schema.sql
index 4f36e26..0af5439 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -84,5 +84,13 @@ CREATE TABLE IF NOT EXISTS `youtube_notification` (
`notify_channel` INTEGER NOT NULL,
`message` VARCHAR(2000) NOT NULL,
`video_type` VARCHAR(16) NOT NULL DEFAULT 'all',
- `last_video_id` VARCHAR(128)
+ `last_video_id` VARCHAR(128),
+ `embed_title` VARCHAR(256),
+ `embed_description` VARCHAR(2000),
+ `embed_color` VARCHAR(8) NOT NULL DEFAULT 'FF0000',
+ `embed_footer` VARCHAR(2048),
+ `embed_author_name` VARCHAR(256),
+ `embed_author_icon` VARCHAR(512),
+ `embed_thumbnail` BOOLEAN NOT NULL DEFAULT TRUE,
+ `embed_image` BOOLEAN NOT NULL DEFAULT TRUE
);
diff --git a/discordbot/youtube.py b/discordbot/youtube.py
index 7211fca..aa78962 100644
--- a/discordbot/youtube.py
+++ b/discordbot/youtube.py
@@ -149,31 +149,76 @@ async def _notifyVideo(notification: YouTubeNotification, video_data: dict, vide
message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})"
logger.info(f"Envoi de notification YouTube: {message}")
- bot.loop.create_task(_sendMessage(notification.notify_channel, message, video_url, thumbnail, video_title, channel_name))
+ bot.loop.create_task(_sendMessage(notification, message, video_url, thumbnail, video_title, channel_name, video_id, published_at, is_short))
except Exception as e:
logger.error(f"Erreur lors de la notification: {e}")
-async def _sendMessage(channel_id: int, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str):
+def _format_embed_text(text: str, channel_name: str, video_title: str, video_url: str, video_id: str, thumbnail: str, published_at: str, is_short: bool) -> str:
+ """Formate un texte d'embed avec les variables disponibles"""
+ if not text:
+ return None
+ try:
+ return text.format(
+ channel_name=channel_name or 'Inconnu',
+ video_title=video_title or 'Sans titre',
+ video_url=video_url,
+ video_id=video_id,
+ thumbnail=thumbnail or '',
+ published_at=published_at or '',
+ is_short=is_short
+ )
+ except KeyError:
+ return text
+
+
+async def _sendMessage(notification: YouTubeNotification, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str, video_id: str, published_at: str, is_short: bool):
from discordbot import bot
try:
- discord_channel = bot.get_channel(channel_id)
+ discord_channel = bot.get_channel(notification.notify_channel)
if not discord_channel:
- logger.error(f"Canal Discord {channel_id} introuvable")
+ logger.error(f"Canal Discord {notification.notify_channel} introuvable")
return
import discord
+
+ embed_title = _format_embed_text(notification.embed_title, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_title else video_title
+ embed_description = _format_embed_text(notification.embed_description, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_description else None
+
+ try:
+ embed_color = int(notification.embed_color or 'FF0000', 16)
+ except ValueError:
+ embed_color = 0xFF0000
+
embed = discord.Embed(
- title=video_title,
+ title=embed_title,
url=video_url,
- color=0xFF0000
+ color=embed_color
)
- embed.set_author(name=channel_name, icon_url="https://www.youtube.com/img/desktop/yt_1200.png")
- if thumbnail:
+
+ if embed_description:
+ embed.description = embed_description
+
+ author_name = _format_embed_text(notification.embed_author_name, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_author_name else channel_name
+ author_icon = notification.embed_author_icon if notification.embed_author_icon else "https://www.youtube.com/img/desktop/yt_1200.png"
+ embed.set_author(name=author_name, icon_url=author_icon)
+
+ if notification.embed_thumbnail and thumbnail:
+ embed.set_thumbnail(url=thumbnail)
+
+ if notification.embed_image and thumbnail:
embed.set_image(url=thumbnail)
- await discord_channel.send(message, embed=embed)
+ if notification.embed_footer:
+ footer_text = _format_embed_text(notification.embed_footer, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short)
+ if footer_text:
+ embed.set_footer(text=footer_text)
+
+ if message and message.strip():
+ await discord_channel.send(message, embed=embed)
+ else:
+ await discord_channel.send(embed=embed)
logger.info(f"Notification YouTube envoyée avec succès")
except Exception as e:
diff --git a/webapp/templates/youtube.html b/webapp/templates/youtube.html
index 66d2241..e36fdef 100644
--- a/webapp/templates/youtube.html
+++ b/webapp/templates/youtube.html
@@ -62,52 +62,184 @@
{% endif %}
{{ 'Editer une notification' if notification else 'Ajouter une notification YouTube' }}
-
- Lien ou ID de la chaîne YouTube
-
- Canal de Notification Discord
-
- {% for channel in channels %}
- {{channel.name}}
- {% endfor %}
-
- Type de vidéo à notifier
-
- Toutes (vidéos + shorts)
- Vidéos uniquement
- Shorts uniquement
-
- Message
- {{notification.message if notification}}
-
-
- Vous pouvez coller directement le lien de la chaîne YouTube dans n'importe quel format :
-
- Lien avec handle : https://www.youtube.com/@513v3
- Lien avec ID : https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxxxxxxxxx
- ID seul : UCxxxxxxxxxxxxxxxxxxxxxxxxxx
- Handle seul : @513v3
-
- Le système extraira automatiquement l'ID de la chaîne.
-
- Note : Les notifications utilisent le flux RSS YouTube, aucune clé API n'est nécessaire !
-
-
- Pour le message vous avez accès à ces variables :
-
- {channel_name} : nom de la chaîne YouTube
- {video_title} : titre de la vidéo
- {video_url} : lien vers la vidéo
- {video_id} : ID de la vidéo
- {thumbnail} : URL de la miniature
- {published_at} : date de publication
- {is_short} : True si c'est un short, False sinon
-
- Le message est au format common-mark dans la limite de ce que
- support Discord.
- Exemple : 🎥 Nouvelle vidéo de {channel_name} : [{video_title}]({video_url})
-
-
+
+
+
+
+
+
+ Variables disponibles pour l'embed :
+
+ {channel_name} : nom de la chaîne YouTube
+ {video_title} : titre de la vidéo
+ {video_url} : lien vers la vidéo
+ {video_id} : ID de la vidéo
+ {thumbnail} : URL de la miniature
+ {published_at} : date de publication
+ {is_short} : True si c'est un short, False sinon
+
+
{% endblock %}
diff --git a/webapp/youtube.py b/webapp/youtube.py
index 0e70118..6f8ae7b 100644
--- a/webapp/youtube.py
+++ b/webapp/youtube.py
@@ -93,12 +93,24 @@ def addYouTube():
except ValueError:
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Canal Discord invalide.", 'type': 'error'}))
+ embed_color = request.form.get('embed_color', 'FF0000').strip().lstrip('#')
+ if len(embed_color) != 6:
+ embed_color = 'FF0000'
+
notification = YouTubeNotification(
enable=True,
channel_id=channel_id,
notify_channel=notify_channel,
message=request.form.get('message'),
- video_type=request.form.get('video_type', 'all')
+ video_type=request.form.get('video_type', 'all'),
+ embed_title=request.form.get('embed_title') or None,
+ embed_description=request.form.get('embed_description') or None,
+ embed_color=embed_color,
+ embed_footer=request.form.get('embed_footer') or None,
+ embed_author_name=request.form.get('embed_author_name') or None,
+ embed_author_icon=request.form.get('embed_author_icon') or None,
+ embed_thumbnail=request.form.get('embed_thumbnail') == 'on',
+ embed_image=request.form.get('embed_image') == 'on'
)
db.session.add(notification)
db.session.commit()
@@ -141,10 +153,22 @@ def submitEditYouTube(id):
except ValueError:
return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': "Canal Discord invalide.", 'type': 'error'}))
+ embed_color = request.form.get('embed_color', 'FF0000').strip().lstrip('#')
+ if len(embed_color) != 6:
+ embed_color = 'FF0000'
+
notification.channel_id = channel_id
notification.notify_channel = notify_channel
notification.message = request.form.get('message')
notification.video_type = request.form.get('video_type', 'all')
+ notification.embed_title = request.form.get('embed_title') or None
+ notification.embed_description = request.form.get('embed_description') or None
+ notification.embed_color = embed_color
+ notification.embed_footer = request.form.get('embed_footer') or None
+ notification.embed_author_name = request.form.get('embed_author_name') or None
+ notification.embed_author_icon = request.form.get('embed_author_icon') or None
+ notification.embed_thumbnail = request.form.get('embed_thumbnail') == 'on'
+ notification.embed_image = request.form.get('embed_image') == 'on'
db.session.commit()
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Notification modifiée avec succès", 'type': 'success'}))
From 48531690fded395d703d4b33eb48d2e8879cc263 Mon Sep 17 00:00:00 2001
From: Mow910
Date: Sun, 1 Feb 2026 12:55:38 +0100
Subject: [PATCH 3/3] =?UTF-8?q?Accueil=20restructur=C3=A9=20(zones=20Disco?=
=?UTF-8?q?rd/Twitch,=20stats,=20indicateur=20connexion)=20+=20Top=203=20s?=
=?UTF-8?q?anctions/mod=C3=A9rateurs=20+=20BOT=5FSTATUS=20dans=20config=20?=
=?UTF-8?q?Flask?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
database/__init__.py | 12 +-
database/models.py | 1 +
discordbot/__init__.py | 9 +-
twitchbot/__init__.py | 5 +-
webapp/__init__.py | 8 +
webapp/index.py | 12 +-
webapp/moderation.py | 46 ++++-
webapp/templates/index.html | 98 +++++++++-
webapp/templates/moderation.html | 301 +++++++++++++++++++++----------
webapp/templates/template.html | 261 ++++++++++++++++++++++++---
10 files changed, 612 insertions(+), 141 deletions(-)
diff --git a/database/__init__.py b/database/__init__.py
index 444d20f..77700d1 100644
--- a/database/__init__.py
+++ b/database/__init__.py
@@ -33,11 +33,11 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
except Exception:
pass
-def _tableExists(table_name:str, cursor:Cursor) -> bool:
+def _tableExists(table_name: str, cursor: Cursor) -> bool:
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
return cursor.fetchone() is not None
-def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
+def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
if not _tableExists(table_name, cursor):
return False
cursor.execute(f'PRAGMA table_info({table_name})')
@@ -70,9 +70,8 @@ def _doPostImportMigration(cursor:Cursor):
cursor.execute('INSERT INTO game_bundle(url, name, json) VALUES (?, ?, ?)', (url, name, json.dumps(json_data)))
logging.info("suppression de la table temporaire game_bundle_old")
_dropTable('game_bundle_old', cursor)
-
+
if _tableExists('youtube_notification', cursor):
- logging.info("Migration de la table youtube_notification: ajout des colonnes d'embed")
embed_columns = [
('embed_title', 'VARCHAR(256)'),
('embed_description', 'VARCHAR(2000)'),
@@ -81,7 +80,7 @@ def _doPostImportMigration(cursor:Cursor):
('embed_author_name', 'VARCHAR(256)'),
('embed_author_icon', 'VARCHAR(512)'),
('embed_thumbnail', 'BOOLEAN DEFAULT 1'),
- ('embed_image', 'BOOLEAN DEFAULT 1')
+ ('embed_image', 'BOOLEAN DEFAULT 1'),
]
for col_name, col_type in embed_columns:
if not _tableHaveColumn('youtube_notification', col_name, cursor):
@@ -89,8 +88,7 @@ def _doPostImportMigration(cursor:Cursor):
cursor.execute(f'ALTER TABLE youtube_notification ADD COLUMN {col_name} {col_type}')
logging.info(f"Colonne {col_name} ajoutée à youtube_notification")
except Exception as e:
- logging.error(f"Impossible d'ajouter la colonne {col_name}: {e}")
- raise
+ logging.warning(f"Colonne youtube_notification.{col_name}: {e}")
with webapp.app_context():
with open('database/schema.sql', 'r') as f:
diff --git a/database/models.py b/database/models.py
index 1562652..d144c7e 100644
--- a/database/models.py
+++ b/database/models.py
@@ -61,6 +61,7 @@ class AntiCheatCache(db.Model):
notes = db.Column(db.String(1024))
updated_at = db.Column(db.DateTime)
+
class YouTubeNotification(db.Model):
__tablename__ = 'youtube_notification'
id = db.Column(db.Integer, primary_key=True)
diff --git a/discordbot/__init__.py b/discordbot/__init__.py
index 9c48341..cffafb6 100644
--- a/discordbot/__init__.py
+++ b/discordbot/__init__.py
@@ -3,6 +3,7 @@ import discord
import logging
import random
+from webapp import webapp
from database import db
from database.helpers import ConfigurationHelper
from database.models import Configuration, Humeur, Commande
@@ -28,6 +29,8 @@ from protondb import searhProtonDb
class DiscordBot(discord.Client):
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)
for c in self.get_all_channels() :
logging.info(f'{c.id} {c.name}')
@@ -37,6 +40,9 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateStatus())
self.loop.create_task(self.updateHumbleBundle())
self.loop.create_task(self.updateYouTube())
+
+ async def on_disconnect(self):
+ webapp.config["BOT_STATUS"]["discord_connected"] = False
async def updateStatus(self):
while not self.is_closed():
@@ -52,11 +58,10 @@ class DiscordBot(discord.Client):
while not self.is_closed():
await checkHumbleBundleAndNotify(self)
await asyncio.sleep(30*60)
-
+
async def updateYouTube(self):
while not self.is_closed():
await checkYouTubeVideos()
- # Vérification toutes les 5 minutes (comme pour Twitch)
await asyncio.sleep(5*60)
def getAllTextChannel(self) -> list[TextChannel]:
diff --git a/twitchbot/__init__.py b/twitchbot/__init__.py
index 529bf0d..45796f6 100644
--- a/twitchbot/__init__.py
+++ b/twitchbot/__init__.py
@@ -14,8 +14,11 @@ USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]
async def _onReady(ready_event: EventData):
logging.info('Bot Twitch prêt')
+ channel = ConfigurationHelper().getValue('twitch_channel')
+ webapp.config["BOT_STATUS"]["twitch_connected"] = True
+ webapp.config["BOT_STATUS"]["twitch_channel_name"] = channel
with webapp.app_context():
- await ready_event.chat.join_room(ConfigurationHelper().getValue('twitch_channel'))
+ await ready_event.chat.join_room(channel)
asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())
diff --git a/webapp/__init__.py b/webapp/__init__.py
index cdaaa26..0ff86c7 100644
--- a/webapp/__init__.py
+++ b/webapp/__init__.py
@@ -2,4 +2,12 @@ from flask import Flask
webapp = Flask(__name__)
+# État des bots (mis à jour par les bots, lu par le panneau)
+webapp.config["BOT_STATUS"] = {
+ "discord_connected": False,
+ "discord_guild_count": 0,
+ "twitch_connected": False,
+ "twitch_channel_name": None,
+}
+
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, youtube
diff --git a/webapp/index.py b/webapp/index.py
index 10f735b..ffe42b2 100644
--- a/webapp/index.py
+++ b/webapp/index.py
@@ -1,6 +1,16 @@
from flask import render_template
from webapp import webapp
+from database.models import ModerationEvent
@webapp.route("/")
def index():
- return render_template("index.html")
+ status = webapp.config["BOT_STATUS"]
+ sanctions_count = ModerationEvent.query.count()
+ return render_template(
+ "index.html",
+ discord_connected=status["discord_connected"],
+ discord_guild_count=status["discord_guild_count"],
+ sanctions_count=sanctions_count,
+ twitch_connected=status["twitch_connected"],
+ twitch_channel_name=status["twitch_channel_name"],
+ )
diff --git a/webapp/moderation.py b/webapp/moderation.py
index 5571487..e0d92f4 100644
--- a/webapp/moderation.py
+++ b/webapp/moderation.py
@@ -3,16 +3,58 @@ from webapp import webapp
from database import db
from database.models import ModerationEvent
+def _top_sanctioned():
+ return (
+ db.session.query(
+ ModerationEvent.discord_id,
+ db.func.max(ModerationEvent.username).label("username"),
+ db.func.count(ModerationEvent.id).label("count"),
+ )
+ .group_by(ModerationEvent.discord_id)
+ .order_by(db.func.count(ModerationEvent.id).desc())
+ .limit(3)
+ .all()
+ )
+
+def _top_moderators():
+ return (
+ db.session.query(
+ ModerationEvent.staff_id,
+ db.func.max(ModerationEvent.staff_name).label("staff_name"),
+ db.func.count(ModerationEvent.id).label("count"),
+ )
+ .group_by(ModerationEvent.staff_id)
+ .order_by(db.func.count(ModerationEvent.id).desc())
+ .limit(3)
+ .all()
+ )
+
@webapp.route("/moderation")
def moderation():
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
- return render_template("moderation.html", events=events, event=None)
+ top_sanctioned = _top_sanctioned()
+ top_moderators = _top_moderators()
+ return render_template(
+ "moderation.html",
+ events=events,
+ event=None,
+ top_sanctioned=top_sanctioned,
+ top_moderators=top_moderators,
+ )
@webapp.route("/moderation/edit/")
def open_edit_moderation_event(event_id):
event = ModerationEvent.query.get_or_404(event_id)
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
- return render_template("moderation.html", events=events, event=event)
+ top_sanctioned = _top_sanctioned()
+ top_moderators = _top_moderators()
+ return render_template(
+ "moderation.html",
+ events=events,
+ event=event,
+ top_sanctioned=top_sanctioned,
+ top_moderators=top_moderators,
+ )
@webapp.route("/moderation/update/", methods=['POST'])
def update_moderation_event(event_id):
diff --git a/webapp/templates/index.html b/webapp/templates/index.html
index eb248da..bb109a5 100644
--- a/webapp/templates/index.html
+++ b/webapp/templates/index.html
@@ -1,7 +1,97 @@
{% extends "template.html" %}
{% block content %}
-Bienvenue sur l'interface d'administration de Mamie.
-Nous devons définir ce que nous souhaitons afficher sur la page d'accueil. Peut-être l'historique des dernières
- modifications ? de la modération ?
-{% endblock %}
\ No newline at end of file
+
+
+ Panneau d'administration
+
+
+ Gérez les fonctionnalités de votre bot Discord et Twitch depuis cette interface.
+
+
+
+{# Zone Discord #}
+
+
+
+
+
Discord
+
+
+ {% if discord_connected %}Connecté{% else %}Déconnecté{% endif %}
+
+
+
+
+
Serveurs connectés
+
{{ discord_guild_count }}
+
+
+
Sanctions enregistrées
+
{{ sanctions_count }}
+
+
+
+
+
+{# Zone Twitch #}
+
+
+
+
+
Twitch
+
+
+ {% if twitch_connected %}Connecté{% else %}Déconnecté{% endif %}
+
+
+
+
+
Canal connecté
+
{% if twitch_channel_name %}{{ twitch_channel_name }}{% else %}—{% endif %}
+
+
+
Sanctions
+
—
+
À venir
+
+
+
Intégrations du bot Twitch à venir.
+
+
+
+
+
+
+
+
+
À propos
+
+ Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
+ Cette interface vous permet de configurer et gérer toutes les fonctionnalités.
+
+
+
+
+
+{% endblock %}
diff --git a/webapp/templates/moderation.html b/webapp/templates/moderation.html
index fc1e316..b35f643 100644
--- a/webapp/templates/moderation.html
+++ b/webapp/templates/moderation.html
@@ -1,110 +1,215 @@
{% extends "template.html" %}
{% block content %}
-Modération Discord
+
+
Modération
+
+ Historique des actions de modération sur le serveur Discord.
+
+
-
- Historique des actions de modération effectuées sur le serveur Discord.
+
+
+
+
Top 3 sanctions
+
Utilisateurs les plus sanctionnés
+
+
+ {% for row in top_sanctioned %}
+
+
+
{{ loop.index }}
+
+ {{ row.username or '—' }}
+ {{ row.discord_id }}
+
+
+
{{ row.count }} sanction{{ 's' if row.count > 1 else '' }}
+
+ {% else %}
+
Aucune sanction enregistrée
+ {% endfor %}
+
+
+
+
+
Top 3 modérateurs
+
Staff ayant effectué le plus d'actions
+
+
+ {% for row in top_moderators %}
+
+
+
{{ loop.index }}
+
+ {{ row.staff_name or '—' }}
+ {{ row.staff_id }}
+
+
+
{{ row.count }} action{{ 's' if row.count > 1 else '' }}
+
+ {% else %}
+
Aucune action enregistrée
+ {% endfor %}
+
+
+
- Le bot enregistre automatiquement les avertissements, exclusions et bannissements.
-
-
-
-
- Commande
- Description
-
-
-
-
- !averto @utilisateur raison Alias : !warn, !av, !avertissement
- Avertit un utilisateur et enregistre l'avertissement dans la base de données
-
-
- !delaverto id Alias : !removewarn, !delwarn
- Retire un avertissement en utilisant son numéro d'ID
-
-
- !warnings ou !warnings @utilisateur Alias : !listevent, !listwarn
- Affiche la liste des événements de modération (tous ou pour un utilisateur spécifique)
-
-
- !inspect @utilisateur ou !inspect id
- Affiche des informations détaillées sur un utilisateur : création du compte, date d'arrivée, historique de modération
-
-
- !kick @utilisateur raison
- Expulse un utilisateur du serveur
-
-
- !ban @utilisateur raison
- Bannit définitivement un utilisateur du serveur
-
-
- !unban discord_id ou !unban #sanction_id raison
- Révoque le bannissement d'un utilisateur et lui envoie une invitation
-
-
- !banlist
- Affiche la liste des utilisateurs actuellement bannis du serveur
-
-
- !aide Alias : !help
- Affiche l'aide avec toutes les commandes disponibles
-
-
-
-
+
+
+
+ Commandes de modération disponibles
+
+
+
+
+
+ !averto @user raison
+ Avertit un utilisateur
+
+
+ !delaverto id
+ Retire un avertissement
+
+
+ !warnings [@user]
+ Liste les événements de modération
+
+
+ !inspect @user
+ Informations sur un utilisateur
+
+
+ !kick @user raison
+ Expulse un utilisateur
+
+
+ !ban @user raison
+ Bannit un utilisateur
+
+
+ !unban id
+ Révoque un bannissement
+
+
+ !banlist
+ Liste des utilisateurs bannis
+
+
+
+
+
{% if not event %}
-Événements de modération
-
-
-
- Type
- Utilisateur
- Discord ID
- Date & Heure
- Raison
- Staff
- #
-
-
-
- {% for mod_event in events %}
-
- {{ mod_event.type }}
- {{ mod_event.username }}
- {{ mod_event.discord_id }}
- {{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}
- {{ mod_event.reason }}
- {{ mod_event.staff_name }}
-
- ✐
- 🗑
-
-
- {% endfor %}
-
-
+
+
+
Événements de modération
+
+
+
+
+
+ Type
+ Utilisateur
+ Date
+ Raison
+ Staff
+ Actions
+
+
+
+ {% for mod_event in events %}
+
+
+ {% if mod_event.type == 'ban' %}
+ Ban
+ {% elif mod_event.type == 'kick' %}
+ Kick
+ {% elif mod_event.type == 'warn' or mod_event.type == 'warning' %}
+ Warn
+ {% elif mod_event.type == 'unban' %}
+ Unban
+ {% else %}
+ {{ mod_event.type }}
+ {% endif %}
+
+
+
+ {{ mod_event.username }}
+ {{ mod_event.discord_id }}
+
+
+
+ {{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}
+
+
+ {{ mod_event.reason }}
+
+
+ {{ mod_event.staff_name }}
+
+
+
+
+
+ {% else %}
+
+
+ Aucun événement de modération
+
+
+ {% endfor %}
+
+
+
+
{% endif %}
{% if event %}
-Editer un événement
-
- Type
-
- Utilisateur
-
- Discord ID
-
- Raison
-
- Staff
-
-
- Annuler
-
+
+
Modifier l'événement
+
+
+
+
+
+ Raison
+
+
+
+
+
+
{% endif %}
-
{% endblock %}
diff --git a/webapp/templates/template.html b/webapp/templates/template.html
index 1539260..be5a9c8 100644
--- a/webapp/templates/template.html
+++ b/webapp/templates/template.html
@@ -1,40 +1,249 @@
-
+
-
-
+
+
Mamie Henriette
-
-
+
+
+
-
-
-
-
-
-
-
-
- {% block content %}{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
-