6 Commits

Author SHA1 Message Date
Mow910
10906bdae3 Merge branch 'main' into youtube-notif 2026-02-01 13:03:29 +01:00
Mow910
48531690fd Accueil restructuré (zones Discord/Twitch, stats, indicateur connexion) + Top 3 sanctions/modérateurs + BOT_STATUS dans config Flask 2026-02-01 12:55:38 +01:00
Mow910
9bbfa1fade Merge pull request #42 from skylanix/front-tailwind
Supprime les fichiers CSS obsolètes et met à jour les templates HTML …
2026-02-01 08:59:03 +01:00
Mow910
920ddfa172 Supprime les fichiers CSS obsolètes et met à jour les templates HTML pour une interface utilisateur améliorée. Les modifications incluent la refonte des pages de configuration, d'alerte live, de modération, et d'aide Twitch, avec une mise en page responsive et des styles modernisés. Les anciennes feuilles de style MVP.css et style.css ont été supprimées au profit d'une intégration de Tailwind CSS. 2026-01-25 19:07:13 +01:00
Mow910
a8d2a0e063 Ajout de nouvelles colonnes pour la personnalisation des notifications YouTube dans la table youtube_notification, y compris le titre, la description, la couleur, le pied de page, le nom et l'icône de l'auteur, ainsi que des options pour afficher la miniature et l'image. Mise à jour de l'interface web pour permettre la configuration de ces options et ajout d'une prévisualisation de l'embed Discord. 2026-01-25 17:45:59 +01:00
Mow910
f2cd19a053 Ajout d'un système de notifications YouTube avec une nouvelle table youtube_notification dans la base de données, intégration de la vérification des vidéos YouTube, et création d'une interface web pour gérer les notifications. Le bot Discord enverra des alertes pour les nouvelles vidéos détectées. 2026-01-25 17:28:38 +01:00
22 changed files with 2023 additions and 1200 deletions

View File

@@ -33,7 +33,13 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
except Exception: except Exception:
pass pass
def _tableHaveColumn(table_name:str, column_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:
if not _tableExists(table_name, cursor):
return False
cursor.execute(f'PRAGMA table_info({table_name})') cursor.execute(f'PRAGMA table_info({table_name})')
columns = cursor.fetchall() columns = cursor.fetchall()
return any(col[1] == column_name for col in columns) return any(col[1] == column_name for col in columns)
@@ -65,6 +71,25 @@ def _doPostImportMigration(cursor:Cursor):
logging.info("suppression de la table temporaire game_bundle_old") logging.info("suppression de la table temporaire game_bundle_old")
_dropTable('game_bundle_old', cursor) _dropTable('game_bundle_old', cursor)
if _tableExists('youtube_notification', cursor):
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.warning(f"Colonne youtube_notification.{col_name}: {e}")
with webapp.app_context(): with webapp.app_context():
with open('database/schema.sql', 'r') as f: with open('database/schema.sql', 'r') as f:
sql = f.read() sql = f.read()

View File

@@ -61,3 +61,22 @@ class AntiCheatCache(db.Model):
notes = db.Column(db.String(1024)) notes = db.Column(db.String(1024))
updated_at = db.Column(db.DateTime) 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))
notify_channel = db.Column(db.Integer)
message = db.Column(db.String(2000))
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)

View File

@@ -76,3 +76,21 @@ CREATE TABLE IF NOT EXISTS `member_invites` (
`inviter_name` VARCHAR(256), `inviter_name` VARCHAR(256),
`join_date` DATETIME NOT NULL `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),
`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
);

View File

@@ -3,6 +3,7 @@ import discord
import logging import logging
import random import random
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
@@ -22,11 +23,14 @@ from discordbot.moderation import (
handle_say_command handle_say_command
) )
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from discordbot.youtube import checkYouTubeVideos
from protondb import searhProtonDb from protondb import searhProtonDb
class DiscordBot(discord.Client): class DiscordBot(discord.Client):
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_guild_count"] = len(self.guilds)
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}')
@@ -35,6 +39,10 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateStatus()) self.loop.create_task(self.updateStatus())
self.loop.create_task(self.updateHumbleBundle()) 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): async def updateStatus(self):
while not self.is_closed(): while not self.is_closed():
@@ -51,6 +59,11 @@ class DiscordBot(discord.Client):
await checkHumbleBundleAndNotify(self) await checkHumbleBundleAndNotify(self)
await asyncio.sleep(30*60) await asyncio.sleep(30*60)
async def updateYouTube(self):
while not self.is_closed():
await checkYouTubeVideos()
await asyncio.sleep(5*60)
def getAllTextChannel(self) -> list[TextChannel]: def getAllTextChannel(self) -> list[TextChannel]:
channels = [] channels = []
for channel in self.get_all_channels(): for channel in self.get_all_channels():

225
discordbot/youtube.py Normal file
View File

@@ -0,0 +1,225 @@
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, 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}")
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(notification.notify_channel)
if not discord_channel:
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=embed_title,
url=video_url,
color=embed_color
)
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)
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:
logger.error(f"Erreur lors de l'envoi du message Discord: {e}")

View File

@@ -14,8 +14,11 @@ USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]
async def _onReady(ready_event: EventData): async def _onReady(ready_event: EventData):
logging.info('Bot Twitch prêt') 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(): 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()) asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())

View File

@@ -2,4 +2,12 @@ from flask import Flask
webapp = Flask(__name__) webapp = Flask(__name__)
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation # É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

View File

@@ -1,6 +1,16 @@
from flask import render_template from flask import render_template
from webapp import webapp from webapp import webapp
from database.models import ModerationEvent
@webapp.route("/") @webapp.route("/")
def index(): 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"],
)

View File

@@ -3,16 +3,58 @@ from webapp import webapp
from database import db from database import db
from database.models import ModerationEvent 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") @webapp.route("/moderation")
def moderation(): def moderation():
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() 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/<int:event_id>") @webapp.route("/moderation/edit/<int:event_id>")
def open_edit_moderation_event(event_id): def open_edit_moderation_event(event_id):
event = ModerationEvent.query.get_or_404(event_id) event = ModerationEvent.query.get_or_404(event_id)
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() 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/<int:event_id>", methods=['POST']) @webapp.route("/moderation/update/<int:event_id>", methods=['POST'])
def update_moderation_event(event_id): def update_moderation_event(event_id):

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,55 +1,101 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Commandes de Mamie</h1> <div class="mb-6">
<p>Gérez les commandes personnalisées du bot. Ces commandes peuvent être activées sur Discord et/ou Twitch selon vos besoins.</p> <h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Commandes</h1>
<table> <p class="text-sm text-slate-600 dark:text-slate-400">
<thead> Gérez les commandes personnalisées du bot. Ces commandes peuvent être activées sur Discord et/ou Twitch.
<tr> </p>
<th>Commande</th> </div>
<th>Réponse</th>
<th>Discord</th>
<th>Twitch</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for commande in commandes %}
<tr>
<td>{{ commande.trigger }}</td>
<td>{{ commande.response }}</td>
<td>
<a href="{{ url_for('toggle_discord_commande', commande_id = commande.id) }}" class="icon">
{{ '✅' if commande.discord_enable else '❌' }}
</a>
</td>
<td>
<a href="{{ url_for('toggle_twitch_commande', commande_id = commande.id) }}" class="icon">
{{ '✅' if commande.twitch_enable else '❌' }}
</a>
</td>
<td>
<a href="{{ url_for('delete_commande', commande_id = commande.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette commande ?')">Supprimer</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>Ajouter une commande</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<form action="{{ url_for('add_commande') }}" method="POST"> <div class="overflow-x-auto">
<label for="trigger">Commande</label> <table class="w-full">
<input name="trigger" type="text" /> <thead>
<label for="response">Réponse</label> <tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
<textarea name="response" rows="5" cols="50"></textarea> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Commande</th>
<div> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Réponse</th>
<label for="discord_enable">Discord</label> <th class="px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Discord</th>
<input name="discord_enable" type="checkbox" checked /> <th class="px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Twitch</th>
<th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
{% for commande in commandes %}
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<td class="px-4 py-3">
<code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded text-xs font-mono">{{ commande.trigger }}</code>
</td>
<td class="px-4 py-3 text-slate-600 dark:text-slate-400 text-sm max-w-xs">
<div class="line-clamp-2">{{ commande.response }}</div>
</td>
<td class="px-4 py-3 text-center">
<a href="{{ url_for('toggle_discord_commande', commande_id = commande.id) }}" class="inline-flex" title="{{ 'Désactiver' if commande.discord_enable else 'Activer' }}">
{% if commande.discord_enable %}
<span class="w-5 h-5 text-green-600 dark:text-green-500"></span>
{% else %}
<span class="w-5 h-5 text-slate-400"></span>
{% endif %}
</a>
</td>
<td class="px-4 py-3 text-center">
<a href="{{ url_for('toggle_twitch_commande', commande_id = commande.id) }}" class="inline-flex" title="{{ 'Désactiver' if commande.twitch_enable else 'Activer' }}">
{% if commande.twitch_enable %}
<span class="w-5 h-5 text-green-600 dark:text-green-500"></span>
{% else %}
<span class="w-5 h-5 text-slate-400"></span>
{% endif %}
</a>
</td>
<td class="px-4 py-3 text-right">
<a href="{{ url_for('delete_commande', commande_id = commande.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette commande ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
Supprimer
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucune commande configurée
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div> </div>
<label for="twitch_enable">Twitch</label>
<input name="twitch_enable" type="checkbox" unchecked /> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
</div> <h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Ajouter une commande</h2>
<input type="Submit" value="Ajouter">
</form> <form action="{{ url_for('add_commande') }}" method="POST" class="space-y-6">
{% endblock %} <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="trigger" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Commande</label>
<input type="text" name="trigger" id="trigger" placeholder="!macommande" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<div class="flex items-end gap-6">
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="discord_enable" checked class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500 focus:ring-2">
<span class="text-sm text-slate-700 dark:text-slate-300">Discord</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="twitch_enable" class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500 focus:ring-2">
<span class="text-sm text-slate-700 dark:text-slate-300">Twitch</span>
</label>
</div>
</div>
<div>
<label for="response" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Réponse</label>
<textarea name="response" id="response" rows="4" placeholder="Le message que le bot enverra..." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none"></textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Ajouter
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,240 +1,251 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Configuration de Mamie</h1> <div class="mb-6">
<p>Configurez les tokens Discord, les notifications Humble Bundle et l'API Twitch.</p> <h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Configurations</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">
Paramètres Discord, Twitch et Humble Bundle.
</p>
</div>
<h2>Discord</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 mb-6 overflow-hidden">
<form action="{{ url_for('updateConfiguration') }}" method="POST"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<fieldset> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Discord</h2>
<legend>API Discord</legend> </div>
<label for="discord_token">Token Discord (caché)</label>
<input name="discord_token" type="password" placeholder="Votre token Discord" /> <form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
<small>Nécessite un redémarrage après modification</small> <div>
</fieldset> <label for="discord_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Token Discord</label>
<input type="password" name="discord_token" id="discord_token" placeholder="Votre token Discord (caché)" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
<fieldset> <p class="mt-1 text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après modification</p>
<legend>Messages de bienvenue</legend>
<label for="welcome_enable">
<input type="checkbox" name="welcome_enable" {% if configuration.getValue('welcome_enable') %}checked="checked"{% endif %}>
Activer le message de bienvenue pour les nouveaux membres
</label>
<label for="welcome_channel_id">Canal de bienvenue</label>
<select name="welcome_channel_id">
{% for channel in channels %}
<option value="{{channel.id}}" {% if configuration.getIntValue('welcome_channel_id')==channel.id %}selected="selected"{% endif %}>
{{channel.name}}
</option>
{% endfor %}
</select>
<label for="welcome_message">Message personnalisé de bienvenue</label>
<textarea name="welcome_message" rows="3" placeholder="Bienvenue {member.mention} sur le serveur !">{{ configuration.getValue('welcome_message') }}</textarea>
<small>
<strong>Syntaxes disponibles :</strong><br>
<code>{member.mention}</code> - Mentionne l'utilisateur (@NomUtilisateur)<br>
<code>{member.name}</code> - Nom d'utilisateur (sans mention)<br>
<code>{member.display_name}</code> - Surnom sur le serveur<br>
<code>{member.id}</code> - ID de l'utilisateur<br>
<code>{server.name}</code> - Nom du serveur<br>
<code>{server.member_count}</code> - Nombre total de membres<br>
<code>&lt;#ID_DU_CHANNEL&gt;</code> - Mentionne un salon (ex: &lt;#123456789012345678&gt;)
</small>
</fieldset>
<fieldset>
<legend>Messages de départ</legend>
<label for="leave_enable">
<input type="checkbox" name="leave_enable" {% if configuration.getValue('leave_enable') %}checked="checked"{% endif %}>
Activer le message de départ quand un membre quitte le serveur
</label>
<label for="leave_channel_id">Canal de départ</label>
<select name="leave_channel_id">
{% for channel in channels %}
<option value="{{channel.id}}" {% if configuration.getIntValue('leave_channel_id')==channel.id %}selected="selected"{% endif %}>
{{channel.name}}
</option>
{% endfor %}
</select>
<label for="leave_message">Message personnalisé de départ</label>
<textarea name="leave_message" rows="3" placeholder="{member.mention} a quitté le serveur.">{{ configuration.getValue('leave_message') }}</textarea>
<small>
<strong>Syntaxes disponibles :</strong><br>
<code>{member.mention}</code> - Mentionne l'utilisateur (@NomUtilisateur)<br>
<code>{member.name}</code> - Nom d'utilisateur (sans mention)<br>
<code>{member.display_name}</code> - Surnom sur le serveur<br>
<code>{member.id}</code> - ID de l'utilisateur<br>
<code>{server.name}</code> - Nom du serveur<br>
<code>{server.member_count}</code> - Nombre total de membres<br>
<code>&lt;#ID_DU_CHANNEL&gt;</code> - Mentionne un salon (ex: &lt;#123456789012345678&gt;)
</small>
</fieldset>
<fieldset>
<legend>Modération</legend>
<label for="moderation_enable">
<input type="checkbox" name="moderation_enable" {% if configuration.getValue('moderation_enable') %}checked="checked"{% endif %}>
Activer les commandes d'avertissement (!warn, !unwarn, !inspect)
</label>
<label for="moderation_ban_enable">
<input type="checkbox" name="moderation_ban_enable" {% if configuration.getValue('moderation_ban_enable') %}checked="checked"{% endif %}>
Activer les commandes de bannissement (!ban, !unban)
</label>
<label for="moderation_kick_enable">
<input type="checkbox" name="moderation_kick_enable" {% if configuration.getValue('moderation_kick_enable') %}checked="checked"{% endif %}>
Activer la commande d'expulsion (!kick)
</label>
<label for="moderation_log_channel_id">Canal de logs de modération</label>
<select name="moderation_log_channel_id">
{% for channel in channels %}
<option value="{{channel.id}}" {% if configuration.getIntValue('moderation_log_channel_id')==channel.id %}selected="selected"{% endif %}>
{{channel.name}}
</option>
{% endfor %}
</select>
<small>Toutes les actions de modération seront notifiées dans ce canal</small>
<label>Rôles Staff autorisés</label>
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
{% if roles|length > 1 %}
<div class="tabs">
{% for guild_data in roles %}
<button type="button" class="tab-button" onclick="openTab(event, 'guild-{{guild_data.guild_id}}')" {% if loop.first %}id="defaultOpen"{% endif %}>
{{ guild_data.guild_name }}
</button>
{% endfor %}
</div> </div>
{% endif %}
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
{% for guild_data in roles %} <h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Messages de bienvenue</h3>
<div id="guild-{{guild_data.guild_id}}" class="tab-content" {% if not loop.first %}style="display: none;"{% endif %}>
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; border-radius: 5px;"> <label class="flex items-center gap-3 cursor-pointer mb-4">
{% for role in guild_data.roles %} <input type="checkbox" name="welcome_enable" {% if configuration.getValue('welcome_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<label style="display: block; margin: 5px 0;"> <span class="text-sm text-slate-700 dark:text-slate-300">Activer le message de bienvenue</span>
<input type="checkbox" name="moderation_staff_role_ids" value="{{role.id}}" {% if role.id|string in selected_roles %}checked="checked"{% endif %}> </label>
{% if role.color.value != 0 %}
<span style="color:#{{ '%06x' % role.color.value }}"></span> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{% else %} <div>
<span></span> <label for="welcome_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de bienvenue</label>
{% endif %} <select name="welcome_channel_id" id="welcome_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
{{role.name}} {% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('welcome_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<label for="welcome_message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message personnalisé</label>
<textarea name="welcome_message" id="welcome_message" rows="2" placeholder="Bienvenue {member.mention} sur le serveur !" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ configuration.getValue('welcome_message') }}</textarea>
</div>
<div class="mt-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg p-3">
<p class="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">Variables :</p>
<div class="flex flex-wrap gap-2 text-xs">
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{member.mention}</code>
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{member.name}</code>
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{server.name}</code>
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{server.member_count}</code>
</div>
</div>
</div>
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
<h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Messages de départ</h3>
<label class="flex items-center gap-3 cursor-pointer mb-4">
<input type="checkbox" name="leave_enable" {% if configuration.getValue('leave_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<span class="text-sm text-slate-700 dark:text-slate-300">Activer le message de départ</span>
</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="leave_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de départ</label>
<select name="leave_channel_id" id="leave_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
{% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('leave_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<label for="leave_message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message personnalisé</label>
<textarea name="leave_message" id="leave_message" rows="2" placeholder="{member.mention} a quitté le serveur." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ configuration.getValue('leave_message') }}</textarea>
</div>
</div>
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
<h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Modération</h3>
<div class="space-y-3 mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="moderation_enable" {% if configuration.getValue('moderation_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<span class="text-sm text-slate-700 dark:text-slate-300">Activer les commandes d'avertissement</span>
</label> </label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="moderation_ban_enable" {% if configuration.getValue('moderation_ban_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<span class="text-sm text-slate-700 dark:text-slate-300">Activer les commandes de bannissement</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="moderation_kick_enable" {% if configuration.getValue('moderation_kick_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<span class="text-sm text-slate-700 dark:text-slate-300">Activer la commande d'expulsion</span>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="moderation_log_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de logs</label>
<select name="moderation_log_channel_id" id="moderation_log_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
{% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('moderation_log_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="moderation_embed_delete_delay" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Délai suppression (sec)</label>
<input type="number" name="moderation_embed_delete_delay" id="moderation_embed_delete_delay" value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" min="0" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Rôles Staff autorisés</label>
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
{% if roles|length > 1 %}
<div class="flex flex-wrap gap-2 mb-3">
{% for guild_data in roles %}
<button type="button" onclick="openTab(event, 'guild-{{ guild_data.guild_id }}')" class="tab-button px-3 py-1.5 text-sm rounded-lg bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors {% if loop.first %}active bg-slate-200 dark:bg-slate-600{% endif %}">
{{ guild_data.guild_name }}
</button>
{% endfor %}
</div>
{% endif %}
{% for guild_data in roles %}
<div id="guild-{{ guild_data.guild_id }}" class="tab-content {% if not loop.first %}hidden{% endif %}">
<div class="max-h-48 overflow-y-auto bg-slate-50 dark:bg-slate-700/50 rounded-lg p-3 space-y-1">
{% for role in guild_data.roles %}
<label class="flex items-center gap-3 cursor-pointer p-2 rounded hover:bg-slate-100 dark:hover:bg-slate-600/50 transition-colors">
<input type="checkbox" name="moderation_staff_role_ids" value="{{ role.id }}" {% if role.id|string in selected_roles %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
{% if role.color.value != 0 %}
<span style="color:#{{ '%06x' % role.color.value }}"></span>
{% else %}
<span class="text-slate-400"></span>
{% endif %}
<span class="text-sm text-slate-700 dark:text-slate-300">{{ role.name }}</span>
</label>
{% endfor %}
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endfor %}
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Enregistrer
</button>
</div>
</form>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 mb-6 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-lg font-medium text-slate-800 dark:text-white">API Twitch</h2>
</div>
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="twitch_client_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Client ID</label>
<input type="text" name="twitch_client_id" id="twitch_client_id" value="{{ configuration.getValue('twitch_client_id') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<div>
<label for="twitch_client_secret" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Client Secret</label>
<input type="text" name="twitch_client_secret" id="twitch_client_secret" value="{{ configuration.getValue('twitch_client_secret') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
</div>
<small>Sélectionnez un ou plusieurs rôles qui peuvent utiliser les commandes de modération</small> <div>
<label for="twitch_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Chaîne à rejoindre</label>
<input type="text" name="twitch_channel" id="twitch_channel" value="{{ configuration.getValue('twitch_channel') }}" placeholder="#machinTruc" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
{% if configuration.getValue('twitch_client_secret') and configuration.getValue('twitch_client_id') %}
<div>
<a href="{{ url_for('twitchRequestToken') }}" class="text-sm text-slate-600 dark:text-slate-400 hover:underline">
Obtenir token et refresh token
</a>
</div>
<script> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
function openTab(evt, tabName) { <div>
var i, tabcontent, tabbuttons; <label for="twitch_access_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Access Token</label>
tabcontent = document.getElementsByClassName("tab-content"); <input type="text" name="twitch_access_token" id="twitch_access_token" value="{{ configuration.getValue('twitch_access_token') }}" readonly class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 font-mono">
for (i = 0; i < tabcontent.length; i++) { </div>
tabcontent[i].style.display = "none"; <div>
} <label for="twitch_refresh_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Refresh Token</label>
tabbuttons = document.getElementsByClassName("tab-button"); <input type="text" name="twitch_refresh_token" id="twitch_refresh_token" value="{{ configuration.getValue('twitch_refresh_token') }}" readonly class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 font-mono">
for (i = 0; i < tabbuttons.length; i++) { </div>
tabbuttons[i].className = tabbuttons[i].className.replace(" active", ""); </div>
} <p class="text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après l'obtention des tokens</p>
document.getElementById(tabName).style.display = "block"; {% endif %}
evt.currentTarget.className += " active";
}
document.getElementById("defaultOpen")?.click();
</script>
<style>
.tabs {
overflow: hidden;
border-bottom: 2px solid #ccc;
margin-bottom: 10px;
}
.tab-button {
background-color: #f1f1f1;
border: none;
outline: none;
cursor: pointer;
padding: 10px 20px;
transition: 0.3s;
font-size: 14px;
margin-right: 2px;
}
.tab-button:hover {
background-color: #ddd;
}
.tab-button.active {
background-color: #ccc;
font-weight: bold;
}
.tab-content {
animation: fadeEffect 0.3s;
}
@keyframes fadeEffect {
from {opacity: 0;}
to {opacity: 1;}
}
</style>
<label for="moderation_embed_delete_delay">Délai de suppression des embeds (en secondes)</label>
<input name="moderation_embed_delete_delay" type="number" value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" placeholder="0" min="0" />
<small>Mettre 0 pour ne pas supprimer automatiquement</small>
</fieldset>
<input type="Submit" value="Enregistrer la configuration Discord"> <div class="flex items-center gap-4">
</form> <button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Enregistrer
</button>
<a href="{{ url_for('twitchConfigurationHelp') }}" class="text-sm text-slate-600 dark:text-slate-400 hover:underline">Besoin d'aide ?</a>
</div>
</form>
</div>
<h2>API Twitch</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<form action="{{ url_for('updateConfiguration') }}" method="POST"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<label for="twitch_client_id">Client ID</label> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Humble Bundle</h2>
<input name="twitch_client_id" type="text" value="{{ configuration.getValue('twitch_client_id') }}" /> </div>
<label for="twitch_client_secret">Client Secret</label>
<input name="twitch_client_secret" type="text" value="{{ configuration.getValue('twitch_client_secret') }}" /> <form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
<label for="twitch_channel">Chaîne à rejoindre</label> <p class="text-sm text-slate-600 dark:text-slate-400">
<input name="twitch_channel" type="text" value="{{ configuration.getValue('twitch_channel') }}" Activez les notifications pour recevoir automatiquement les nouveaux packs sur Discord.
placeholder="#machinTruc" /> </p>
<input type="Submit" value="Enregistrer la configuration Twitch">
<p>
<a href="{{ url_for('twitchConfigurationHelp') }}">Aide</a>
</p>
{% if configuration.getValue('twitch_client_secret') and configuration.getValue('twitch_client_id') %}
<p>
<a href="{{ url_for('twitchRequestToken') }}">Obtenir token et refresh token</a>
</p>
<label for="twitch_access_token">Access Token</label>
<input name="twitch_access_token" type="text" value="{{ configuration.getValue('twitch_access_token') }}"
readonly="readonly" />
<label for="twitch_refresh_token">Refresh Token</label>
<input name="twitch_refresh_token" type="text" value="{{ configuration.getValue('twitch_refresh_token') }}"
readonly="readonly" />
<p>Nécessite un redémarrage après l'obtention des Tokens.</p>
{% endif %}
</form>
<h2>Humble Bundle</h2> <label class="flex items-center gap-3 cursor-pointer">
<form action="{{ url_for('updateConfiguration') }}" method="POST"> <input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<p>Humble Bundle propose régulièrement des bundles de jeux vidéo à des prix réduits. Activez les notifications pour recevoir automatiquement les nouveaux packs disponibles sur votre serveur Discord.</p> <span class="text-sm text-slate-700 dark:text-slate-300">Activer les notifications Humble Bundle</span>
</label>
<label for="humble_bundle_enable"> <div>
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}checked="checked"{% endif %}> <label for="humble_bundle_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de notification</label>
Activer les notifications Humble Bundle <select name="humble_bundle_channel" id="humble_bundle_channel" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</label> {% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('humble_bundle_channel') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
</div>
<label for="humble_bundle_channel">Canal de notification</label> <button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
<select name="humble_bundle_channel"> Enregistrer
{% for channel in channels %} </button>
<option value="{{channel.id}}" {% if configuration.getIntValue('humble_bundle_channel')==channel.id %}selected="selected"{% endif %}> </form>
{{channel.name}} </div>
</option>
{% endfor %}
</select>
<input type="Submit" value="Enregistrer la configuration Humble Bundle"> <script>
</form> function openTab(evt, tabName) {
{% endblock %} document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-button').forEach(el => {
el.classList.remove('active', 'bg-slate-200', 'dark:bg-slate-600');
el.classList.add('bg-slate-100', 'dark:bg-slate-700');
});
document.getElementById(tabName).classList.remove('hidden');
evt.currentTarget.classList.add('active', 'bg-slate-200', 'dark:bg-slate-600');
evt.currentTarget.classList.remove('bg-slate-100', 'dark:bg-slate-700');
}
</script>
{% endblock %}

View File

@@ -1,29 +1,44 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Humeurs de Mamie</h1> <div class="mb-6">
<p>Définissez les statuts Discord qui changeront automatiquement toutes les 10 minutes pour donner de la personnalité à votre bot.</p> <h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Humeurs</h1>
<table> <p class="text-sm text-slate-600 dark:text-slate-400">
<thead> Statuts Discord qui changeront automatiquement toutes les 10 minutes.
<tr> </p>
<th>Texte</th> </div>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for humeur in humeurs %}
<tr>
<td>{{humeur.text}}</td>
<td><a href="{{ url_for('delHumeur', id = humeur.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette humeur ?')">Supprimer</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>Ajouter une humeur</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<form action="{{ url_for('addHumeur') }}" method="POST"> {% if humeurs %}
<label for="text">Texte</label> <ul class="divide-y divide-slate-200 dark:divide-slate-700">
<input name="text" type="text" /> {% for humeur in humeurs %}
<input type="Submit" value="Ajouter"> <li class="flex items-center justify-between px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group">
</form> <span class="text-sm text-slate-700 dark:text-slate-300">{{ humeur.text }}</span>
{% endblock %} <a href="{{ url_for('delHumeur', id = humeur.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette humeur ?')" class="p-1.5 text-slate-400 hover:text-red-500 dark:hover:text-red-400 rounded opacity-0 group-hover:opacity-100 transition-all" title="Supprimer">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="px-4 py-8 text-center">
<p class="text-sm text-slate-500 dark:text-slate-400">Aucune humeur configurée</p>
</div>
{% endif %}
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
<h2 class="text-lg font-medium text-slate-800 dark:text-white mb-4">Ajouter une humeur</h2>
<form action="{{ url_for('addHumeur') }}" method="POST">
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<input type="text" name="text" id="text" placeholder="Joue à un super jeu..." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap">
Ajouter
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,7 +1,97 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Bienvenue sur l'interface d'administration de Mamie.</h1> <div class="text-center py-10">
<p>Nous devons définir ce que nous souhaitons afficher sur la page d'accueil. Peut-être l'historique des dernières <h1 class="text-3xl sm:text-4xl font-bold text-slate-800 dark:text-white mb-3">
modifications ? de la modération ?</p> Panneau d'administration
{% endblock %} </h1>
<p class="text-base text-slate-600 dark:text-slate-400 max-w-xl mx-auto">
Gérez les fonctionnalités de votre bot Discord et Twitch depuis cette interface.
</p>
</div>
{# Zone Discord #}
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if discord_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if discord_connected %}Bot Discord connecté{% else %}Bot Discord déconnecté{% endif %}"></span>
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Discord</h2>
</div>
<span class="text-sm text-slate-500 dark:text-slate-400">
{% if discord_connected %}Connecté{% else %}Déconnecté{% endif %}
</span>
</div>
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Serveurs connectés</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ discord_guild_count }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions enregistrées</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ sanctions_count }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
<div class="flex flex-wrap gap-3 justify-center">
<a href="/live-alert" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Alertes Live</a>
<a href="/youtube" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Notification YouTube</a>
<a href="/humeurs" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Humeurs</a>
<a href="/protondb" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">ProtonDB</a>
<a href="/commandes" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Commandes</a>
<a href="/moderation" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Modération</a>
</div>
</div>
</div>
</div>
{# Zone Twitch #}
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if twitch_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if twitch_connected %}Bot Twitch connecté{% else %}Bot Twitch déconnecté{% endif %}"></span>
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Twitch</h2>
</div>
<span class="text-sm text-slate-500 dark:text-slate-400">
{% if twitch_connected %}Connecté{% else %}Déconnecté{% endif %}
</span>
</div>
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Canal connecté</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{% if twitch_channel_name %}{{ twitch_channel_name }}{% else %}—{% endif %}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1"></p>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">À venir</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
<p class="text-sm text-slate-500 dark:text-slate-400">Intégrations du bot Twitch à venir.</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div>
<h3 class="font-medium text-slate-800 dark:text-white mb-2">À propos</h3>
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
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.
</p>
<div class="flex flex-wrap gap-3">
<a href="https://github.com/skylanix/MamieHenriette" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white rounded text-sm hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
GitHub
</a>
<a href="https://discord.com/invite/UwAPqMJnx3" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-600 rounded text-sm hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"></path></svg>
Discord
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,76 +1,135 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Alerte Live</h1> <div class="mb-6">
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Alertes Live</h1>
<p> <p class="text-sm text-slate-600 dark:text-slate-400">
Liste des chaines surveillées pour les alertes de live twitch. Chaînes Twitch surveillées. Vérification toutes les 5 minutes, maximum 100 chaînes.
</p>
Le bot vérifie toutes les 5 minutes qui est en live dans la liste en dessous. </div>
Le bot enregistre le status de stream toutes les 5 minutes, quand le status pass de "hors-ligne" à "en ligne" alors
le bot le notifiera sur discord.
Ne peu surveiller qu'au maximum 100 chaines.
</p>
{% if not alert %} {% if not alert %}
<h2>Alertes</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<table class="live-alert"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<thead> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Alertes configurées</h2>
<tr> </div>
<th>Chaine</th> <div class="overflow-x-auto">
<th>Canal</th> <table class="w-full">
<th>Message</th> <thead>
<th>#</th> <tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
</tr> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Chaîne</th>
</thead> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Canal Discord</th>
<tbody> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Message</th>
{% for alert in alerts %} <th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
<tr> </tr>
<td>{{alert.login}}</td> </thead>
<td>{{alert.notify_channel_name}}</td> <tbody class="divide-y divide-slate-200 dark:divide-slate-700">
<td>{{alert.message}}</td> {% for alert in alerts %}
<td> <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<a href="{{ url_for('toggleLiveAlert', id = alert.id) }}" class="icon">{{ '✅' if alert.enable else '❌' }}</a> <td class="px-4 py-3">
<a href="{{ url_for('openEditLiveAlert', id = alert.id) }}" class="icon"></a> <a href="https://www.twitch.tv/{{ alert.login }}" target="_blank" class="text-sm font-medium text-slate-700 dark:text-slate-300 hover:underline">{{ alert.login }}</a>
<a href="{{ url_for('delLiveAlert', id = alert.id) }}" </td>
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette alerte ?')" class="icon">🗑</a> <td class="px-4 py-3">
</td> <span class="text-sm text-slate-600 dark:text-slate-400">#{{ alert.notify_channel_name }}</span>
</tr> </td>
{% endfor %} <td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 max-w-xs">
</tbody> <div class="line-clamp-2">{{ alert.message }}</div>
</table> </td>
<td class="px-4 py-3">
<div class="flex items-center justify-end gap-2">
<a href="{{ url_for('toggleLiveAlert', id = alert.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors" title="{{ 'Désactiver' if alert.enable else 'Activer' }}">
{% if alert.enable %}
<span class="text-green-600 dark:text-green-500">Actif</span>
{% else %}
<span class="text-slate-400">Inactif</span>
{% endif %}
</a>
<a href="{{ url_for('openEditLiveAlert', id = alert.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
Modifier
</a>
<a href="{{ url_for('delLiveAlert', id = alert.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette alerte ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
Supprimer
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucune alerte configurée
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %} {% endif %}
<h2>{{ 'Editer une alerte' if alert else 'Ajouter une alerte de Live' }}</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
<form action="{{ url_for('submitEditLiveAlert', id = alert.id) if alert else url_for('addLiveAlert') }}" method="POST"> <h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">
<label for="login">Chaine</label> {% if alert %}Modifier l'alerte{% else %}Ajouter une alerte{% endif %}
<input name="login" type="text" maxlength="32" required="required" value="{{alert.login if alert}}"/> </h2>
<label for="notify_channel">Canal de Notification</label>
<select name="notify_channel"> <form action="{{ url_for('submitEditLiveAlert', id = alert.id) if alert else url_for('addLiveAlert') }}" method="POST" class="space-y-6">
{% for channel in channels %} <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<option value="{{channel.id}}"{% if alert and alert.notify_channel == channel.id %} <div>
selected="selected" {% endif %}>{{channel.name}}</option> <label for="login" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Chaîne Twitch</label>
{% endfor %} <input type="text" name="login" id="login" maxlength="32" required value="{{ alert.login if alert else '' }}" placeholder="chainesteve" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</select> <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">Le login de la chaîne, ex: chainesteve</p>
<label for="message">Message</label> </div>
<textarea name="message" rows="5" cols="50" required="required">{{alert.message if alert}}</textarea>
<input type="Submit" value="Ajouter"> <div>
<p> <label for="notify_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de Notification</label>
La chaine est le login de la chaine, par exemple <strong>chainesteve</strong> pour <strong>https://www.twitch.tv/chainesteve</strong>. <select name="notify_channel" id="notify_channel" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</p> {% for channel in channels %}
<p> <option value="{{ channel.id }}" {% if alert and alert.notify_channel == channel.id %}selected{% endif %}>{{ channel.name }}</option>
Pour le message vous avez acces à ces variables : {% endfor %}
<ul> </select>
<li>{0.user_login} : pour le lien vers la chaine</li> </div>
<li>{0.user_name} : à priviligier pour le text</li> </div>
<li>{0.game_name}</li>
<li>{0.title}</li> <div>
<li>{0.language}</li> <label for="message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message de notification</label>
</ul> <textarea name="message" id="message" rows="4" required placeholder="🔴 **{0.user_name}** est en live !" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ alert.message if alert else '' }}</textarea>
Le message est au format <a href="https://commonmark.org/" target="_blank">common-mark</a> dans la limite de ce que </div>
support discord.
Pour mettre un lien vers la chaine : [description](https://www.twitch.tv/{0.user_login}) <div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
</p> <p class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Variables disponibles :</p>
</form> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
<div class="flex items-center gap-2">
{% endblock %} <code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.user_login}</code>
<span class="text-slate-600 dark:text-slate-400">Lien vers la chaîne</span>
</div>
<div class="flex items-center gap-2">
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.user_name}</code>
<span class="text-slate-600 dark:text-slate-400">Nom du streamer</span>
</div>
<div class="flex items-center gap-2">
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.game_name}</code>
<span class="text-slate-600 dark:text-slate-400">Jeu en cours</span>
</div>
<div class="flex items-center gap-2">
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.title}</code>
<span class="text-slate-600 dark:text-slate-400">Titre du stream</span>
</div>
<div class="flex items-center gap-2">
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.language}</code>
<span class="text-slate-600 dark:text-slate-400">Langue du stream</span>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3">
{% if alert %}
<a href="{{ url_for('openLiveAlert') }}" class="px-4 py-2 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
Annuler
</a>
{% endif %}
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
{% if alert %}Enregistrer{% else %}Ajouter{% endif %}
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,110 +1,215 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Modération Discord</h1> <div class="mb-6">
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Modération</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">
Historique des actions de modération sur le serveur Discord.
</p>
</div>
<p> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
Historique des actions de modération effectuées sur le serveur Discord. <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">
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 sanctions</h2>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Utilisateurs les plus sanctionnés</p>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-700">
{% for row in top_sanctioned %}
<div class="px-5 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
<div class="min-w-0">
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.username or '—' }}</span>
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.discord_id }}</span>
</div>
</div>
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} sanction{{ 's' if row.count > 1 else '' }}</span>
</div>
{% else %}
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune sanction enregistrée</div>
{% endfor %}
</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">
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 modérateurs</h2>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Staff ayant effectué le plus d'actions</p>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-700">
{% for row in top_moderators %}
<div class="px-5 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
<div class="min-w-0">
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.staff_name or '—' }}</span>
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.staff_id }}</span>
</div>
</div>
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} action{{ 's' if row.count > 1 else '' }}</span>
</div>
{% else %}
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune action enregistrée</div>
{% endfor %}
</div>
</div>
</div>
Le bot enregistre automatiquement les avertissements, exclusions et bannissements. <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<details class="group">
<table> <summary class="flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<thead> <span class="font-medium text-slate-800 dark:text-white">Commandes de modération disponibles</span>
<tr> <svg class="w-5 h-5 text-slate-400 group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
<th>Commande</th> </summary>
<th>Description</th> <div class="border-t border-slate-200 dark:border-slate-700">
</tr> <div class="divide-y divide-slate-200 dark:divide-slate-700 text-sm">
</thead> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
<tbody> <code class="text-slate-700 dark:text-slate-300 font-mono">!averto @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Avertit un utilisateur</span>
<td><strong>!averto @utilisateur raison</strong><br><small>Alias : !warn, !av, !avertissement</small></td> </div>
<td>Avertit un utilisateur et enregistre l'avertissement dans la base de données</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!delaverto id</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Retire un avertissement</span>
<td><strong>!delaverto id</strong><br><small>Alias : !removewarn, !delwarn</small></td> </div>
<td>Retire un avertissement en utilisant son numéro d'ID</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!warnings [@user]</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Liste les événements de modération</span>
<td><strong>!warnings</strong> ou <strong>!warnings @utilisateur</strong><br><small>Alias : !listevent, !listwarn</small></td> </div>
<td>Affiche la liste des événements de modération (tous ou pour un utilisateur spécifique)</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!inspect @user</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Informations sur un utilisateur</span>
<td><strong>!inspect @utilisateur</strong> ou <strong>!inspect id</strong></td> </div>
<td>Affiche des informations détaillées sur un utilisateur : création du compte, date d'arrivée, historique de modération</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!kick @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Expulse un utilisateur</span>
<td><strong>!kick @utilisateur raison</strong></td> </div>
<td>Expulse un utilisateur du serveur</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!ban @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Bannit un utilisateur</span>
<td><strong>!ban @utilisateur raison</strong></td> </div>
<td>Bannit définitivement un utilisateur du serveur</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!unban id</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Révoque un bannissement</span>
<td><strong>!unban discord_id</strong> ou <strong>!unban #sanction_id raison</strong></td> </div>
<td>Révoque le bannissement d'un utilisateur et lui envoie une invitation</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!banlist</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Liste des utilisateurs bannis</span>
<td><strong>!banlist</strong></td> </div>
<td>Affiche la liste des utilisateurs actuellement bannis du serveur</td> </div>
</tr> </div>
<tr> </details>
<td><strong>!aide</strong><br><small>Alias : !help</small></td> </div>
<td>Affiche l'aide avec toutes les commandes disponibles</td>
</tr>
</tbody>
</table>
</p>
{% if not event %} {% if not event %}
<h2>Événements de modération</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<table class="moderation"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<thead> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Événements de modération</h2>
<tr> </div>
<th>Type</th> <div class="overflow-x-auto">
<th>Utilisateur</th> <table class="w-full">
<th>Discord ID</th> <thead>
<th>Date & Heure</th> <tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
<th>Raison</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Type</th>
<th>Staff</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Utilisateur</th>
<th>#</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Date</th>
</tr> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Raison</th>
</thead> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Staff</th>
<tbody> <th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
{% for mod_event in events %} </tr>
<tr> </thead>
<td>{{ mod_event.type }}</td> <tbody class="divide-y divide-slate-200 dark:divide-slate-700">
<td>{{ mod_event.username }}</td> {% for mod_event in events %}
<td>{{ mod_event.discord_id }}</td> <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<td>{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}</td> <td class="px-4 py-3">
<td>{{ mod_event.reason }}</td> {% if mod_event.type == 'ban' %}
<td>{{ mod_event.staff_name }}</td> <span class="text-xs font-medium text-red-600 dark:text-red-400">Ban</span>
<td> {% elif mod_event.type == 'kick' %}
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="icon"></a> <span class="text-xs font-medium text-orange-600 dark:text-orange-400">Kick</span>
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="icon">🗑</a> {% elif mod_event.type == 'warn' or mod_event.type == 'warning' %}
</td> <span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
</tr> {% elif mod_event.type == 'unban' %}
{% endfor %} <span class="text-xs font-medium text-green-600 dark:text-green-400">Unban</span>
</tbody> {% else %}
</table> <span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
{% endif %}
</td>
<td class="px-4 py-3">
<div class="flex flex-col">
<span class="text-sm font-medium text-slate-800 dark:text-white">{{ mod_event.username }}</span>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono">{{ mod_event.discord_id }}</span>
</div>
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 whitespace-nowrap">
{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 max-w-xs">
<div class="line-clamp-2">{{ mod_event.reason }}</div>
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">
{{ mod_event.staff_name }}
</td>
<td class="px-4 py-3">
<div class="flex items-center justify-end gap-2">
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
Modifier
</a>
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
Supprimer
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucun événement de modération
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %} {% endif %}
{% if event %} {% if event %}
<h2>Editer un événement</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
<form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST"> <h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Modifier l'événement</h2>
<label for="type">Type</label>
<input name="type" type="text" value="{{ event.type }}" disabled /> <form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST" class="space-y-6">
<label for="username">Utilisateur</label> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<input name="username" type="text" value="{{ event.username }}" disabled /> <div>
<label for="discord_id">Discord ID</label> <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Type</label>
<input name="discord_id" type="text" value="{{ event.discord_id }}" disabled /> <input type="text" value="{{ event.type }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
<label for="reason">Raison</label> </div>
<input name="reason" type="text" value="{{ event.reason }}" required="required" /> <div>
<label for="staff_name">Staff</label> <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Staff</label>
<input name="staff_name" type="text" value="{{ event.staff_name }}" disabled /> <input type="text" value="{{ event.staff_name }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
<input type="Submit" value="Modifier"> </div>
<a href="{{ url_for('moderation') }}">Annuler</a> <div>
</form> <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Utilisateur</label>
<input type="text" value="{{ event.username }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Discord ID</label>
<input type="text" value="{{ event.discord_id }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed font-mono">
</div>
</div>
<div>
<label for="reason" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Raison</label>
<input type="text" name="reason" id="reason" value="{{ event.reason }}" required class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<div class="flex items-center justify-end gap-3">
<a href="{{ url_for('moderation') }}" class="px-4 py-2 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
Annuler
</a>
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Enregistrer
</button>
</div>
</form>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,61 +1,124 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Proton DB</h1> <div class="mb-6">
<p>ProtonDB évalue la compatibilité des jeux Windows sur Linux via Steam Play.</p> <h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">ProtonDB</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">
Compatibilité des jeux Windows sur Linux via Steam Play.
</p>
</div>
{% if configuration.getValue('proton_db_enable_enable') %} {% if configuration.getValue('proton_db_enable_enable') %}
<h2>Game alias</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<table> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<thead> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Alias de jeux</h2>
<tr> </div>
<th>Alias</th> <div class="overflow-x-auto">
<th>Game</th> <table class="w-full">
<th>#</th> <thead>
</tr> <tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
</thead> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Alias</th>
<tbody> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Nom du jeu</th>
{% for a in aliases %} <th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
<tr> </tr>
<td>{{a.alias}}</td> </thead>
<td>{{a.name}}</td> <tbody class="divide-y divide-slate-200 dark:divide-slate-700">
<td><a href="{{ url_for('delGameAlias', id = a.id) }}" {% for a in aliases %}
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet alias ?')">Supprimer</a></td> <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
</tr> <td class="px-4 py-3">
{% endfor %} <code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded text-xs font-mono">{{ a.alias }}</code>
</tbody> </td>
</table> <td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">
{{ a.name }}
</td>
<td class="px-4 py-3 text-right">
<a href="{{ url_for('delGameAlias', id = a.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet alias ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
Supprimer
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucun alias configuré
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<h2>Ajouter un Alias</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5 mb-6">
<form action="{{ url_for('addGameAlias') }}" method="POST"> <h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Ajouter un alias</h2>
<label for="alias">Alias</label>
<input name="alias" type="text" maxlength="32" required="required" /> <form action="{{ url_for('addGameAlias') }}" method="POST" class="space-y-6">
<label for="name">Nom</label> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input name="name" type="text" maxlength="256" required="required" /> <div>
<input type="Submit" value="Ajouter"> <label for="alias" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Alias</label>
<p>Si vous créez un alias <strong>GTA : Grand Theft Auto</strong> alors si un utilisateur rentre la commande <input type="text" name="alias" id="alias" maxlength="32" required placeholder="GTA" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
<strong>!protondb GTA 5</strong> cela fera une recherche sur <strong>Grand Theft Auto 5</strong>. </div>
</p> <div>
</form> <label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Nom complet</label>
<input type="text" name="name" id="name" maxlength="256" required placeholder="Grand Theft Auto" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Ajouter
</button>
</div>
</form>
<div class="mt-5 bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
<p class="text-sm text-slate-600 dark:text-slate-400">
<strong class="text-slate-800 dark:text-white">Exemple :</strong> Si vous créez un alias GTA → Grand Theft Auto, alors !protondb GTA 5 fera une recherche sur Grand Theft Auto 5.
</p>
</div>
</div>
{% endif %} {% endif %}
<h2>Configuration</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<form action="{{ url_for('updateConfiguration') }}" method="POST"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<label for="proton_db_enable_enable">Activer</label> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Configuration</h2>
<input type="checkbox" name="proton_db_enable_enable" {% if configuration.getValue('proton_db_enable_enable') %} </div>
checked="checked" {% endif %}>
<label>Activer la commande Proton DB</label> <form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
<label for="proton_db_api_id">API ID</label> <label class="flex items-center gap-3 cursor-pointer">
<input name="proton_db_api_id" type="text" value="{{ configuration.getValue('proton_db_api_id') }}" /> <input type="checkbox" name="proton_db_enable_enable" {% if configuration.getValue('proton_db_enable_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
<label for="proton_db_api_key">Clé API</label> <span class="text-sm text-slate-700 dark:text-slate-300">Activer la commande ProtonDB</span>
<input name="proton_db_api_key" type="text" value="{{ configuration.getValue('proton_db_api_key') }}" /> </label>
<input type="Submit" value="Définir">
<p>Pour trouver les clés, dans votre navigateur avec l'outil d'inspection ouvert (F12 ou clic droit > Inspecter
l'élément dans Firefox/Chrome) faites une recherche de jeux sur protondb,
puis cherchez les clés dans les requêtes (onglet Réseau/Network),
<a href="/static/img/algolia-key.jpg" target="_blank">comme le montre cet exemple</a>
</p>
</form>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="proton_db_api_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">API ID</label>
<input type="text" name="proton_db_api_id" id="proton_db_api_id" value="{{ configuration.getValue('proton_db_api_id') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<div>
<label for="proton_db_api_key" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Clé API</label>
<input type="text" name="proton_db_api_key" id="proton_db_api_key" value="{{ configuration.getValue('proton_db_api_key') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
</div>
{% endblock %} <button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Enregistrer
</button>
</form>
<div class="px-5 pb-5">
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
<p class="text-sm font-medium text-slate-800 dark:text-white mb-2">Comment trouver les clés API ?</p>
<ol class="list-decimal list-inside space-y-1 text-sm text-slate-600 dark:text-slate-400">
<li>Ouvrez l'outil d'inspection de votre navigateur (F12)</li>
<li>Allez dans l'onglet Réseau/Network</li>
<li>Faites une recherche de jeu sur ProtonDB</li>
<li>Cherchez les clés dans les requêtes réseau</li>
</ol>
<a href="/static/img/algolia-key.jpg" target="_blank" class="inline-block mt-3 text-sm text-slate-600 dark:text-slate-400 hover:underline">
Voir l'exemple en image
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,39 +1,249 @@
<!DOCTYPE html> <!DOCTYPE html>
<html color-mode="user"> <html lang="fr" class="h-full">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes">
<title>Mamie Henriette</title> <title>Mamie Henriette</title>
<link rel="stylesheet" href="/static/css/mvp.css" /> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/css/style.css" /> <script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
accent: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
}
}
}
}
}
</script>
<link rel="icon" href="/static/ico/favicon.ico" type="image/x-icon"> <link rel="icon" href="/static/ico/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/ico/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="/static/ico/favicon.ico" type="image/x-icon">
<style>
/* Animations personnalisées */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scrollbar personnalisée */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.dark ::-webkit-scrollbar-thumb { background: #475569; }
.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
</style>
</head> </head>
<body> <body class="h-full bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
<header> <!-- Navbar -->
<nav> <nav class="fixed top-0 left-0 right-0 z-50 bg-white dark:bg-gray-800 shadow-md border-b border-gray-200 dark:border-gray-700">
<a href="/"><img src="/static/ico/favicon.ico"></a> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ul> <div class="flex items-center justify-between h-16">
<li><a href="/live-alert">Alerte live</a></li> <!-- Logo -->
<li><a href="/commandes">Commandes</a></li> <a href="/" class="flex items-center gap-3 group">
<li><a href="/humeurs">Humeurs</a></li> <img src="/static/ico/favicon.ico" alt="Mamie Henriette" class="w-10 h-10 rounded-full ring-2 ring-slate-200 dark:ring-slate-600 group-hover:ring-slate-300 dark:group-hover:ring-slate-500 transition-all">
<li><a href="/moderation">Modération</a></li> <span class="font-bold text-xl text-slate-800 dark:text-white hidden sm:block">Mamie Henriette</span>
<li><a href="/protondb">ProtonDB</a></li> </a>
<li><a href="/configurations">Configurations</a></li>
</ul> <!-- Navigation Desktop -->
</nav> <div class="hidden md:flex items-center gap-1">
</header> <!-- Discord (sous-menus) -->
<main> <div class="relative group">
{% block content %}{% endblock %} <button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
Discord
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
<a href="/humeurs" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Humeur
</a>
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Notification Twitch
</a>
<a href="/youtube" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
Notification YouTube
</a>
<a href="/protondb" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
ProtonDB
</a>
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
<a href="/commandes" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Commandes
</a>
<a href="/moderation" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Modération
</a>
</div>
</div>
</div>
<!-- Twitch (futur bot) -->
<div class="relative group">
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57l-.002-5.143zm3.43 0H16.714v5.143H15V4.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"/></svg>
Twitch
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<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="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Alerte live
</a>
<span class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-400 dark:text-gray-500 italic">
Bot Twitch — à venir
</span>
</div>
</div>
</div>
<!-- Configuration locale -->
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
<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.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Configuration
</a>
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<!-- Dark Mode Toggle -->
<button onclick="toggleDarkMode()" class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all" title="Mode sombre">
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
<svg class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
</button>
<!-- Mobile Menu Button -->
<button onclick="toggleMobileMenu()" class="md:hidden p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="px-4 py-3 space-y-1">
<a href="/live-alert" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Alerte live
</a>
<a href="/youtube" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
YouTube
</a>
<a href="/commandes" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Commandes
</a>
<a href="/humeurs" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Humeurs
</a>
<a href="/moderation" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Modération
</a>
<a href="/protondb" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
ProtonDB
</a>
<a href="/configurations" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Configurations
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="pt-20 pb-12 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="fade-in">
{% block content %}{% endblock %}
</div>
</div>
</main> </main>
<footer>
<hr> <!-- Footer -->
<p><a href="https://github.com/skylanix/MamieHenriette" target="_blank">MamieHenriette</a> créé par la communauté <a href="https://discord.com/invite/UwAPqMJnx3" target="_blank">Discord</a> de <a href="https://www.youtube.com/@513v3" target="_blank">573v3</a> - Projet open source sous licence <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPLv3</a></p> <footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<img src="/static/ico/favicon.ico" alt="" class="w-6 h-6 rounded-full">
<span>Mamie Henriette</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
Créé par la communauté
<a href="https://discord.com/invite/UwAPqMJnx3" target="_blank" class="text-primary-600 dark:text-primary-400 hover:underline">Discord</a>
de
<a href="https://www.youtube.com/@513v3" target="_blank" class="text-primary-600 dark:text-primary-400 hover:underline">573v3</a>
</p>
<div class="flex items-center gap-4">
<a href="https://github.com/skylanix/MamieHenriette" target="_blank" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
</a>
<span class="text-xs text-gray-400 dark:text-gray-500">AGPLv3</span>
</div>
</div>
</div>
</footer> </footer>
<script>
// Dark mode
function toggleDarkMode() {
document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
}
// Initialize dark mode from preference
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// Mobile menu
function toggleMobileMenu() {
document.getElementById('mobile-menu').classList.toggle('hidden');
}
</script>
</body> </body>
</html> </html>

View File

@@ -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 %}

View File

@@ -0,0 +1,245 @@
{% extends "template.html" %}
{% block content %}
<h1>Notifications YouTube</h1>
{% if msg %}
<div id="alert-msg" class="alert alert-{{ msg_type }}" style="padding: 10px; margin: 10px 0; border: 1px solid {{ '#f00' if msg_type == 'error' else '#0f0' }}; background-color: {{ '#ffe0e0' if msg_type == 'error' else '#e0ffe0' }};">
{{ msg }}
</div>
<script>
setTimeout(function() {
var el = document.getElementById('alert-msg');
if (el) el.style.display = 'none';
}, 5000);
</script>
{% endif %}
<p>
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.
</p>
{% if not notification %}
<h2>Notifications</h2>
<table class="live-alert">
<thead>
<tr>
<th>Chaîne YouTube</th>
<th>Canal Discord</th>
<th>Type</th>
<th>Message</th>
<th>#</th>
</tr>
</thead>
<tbody>
{% for notification in notifications %}
<tr>
<td>{{notification.channel_id}}</td>
<td>{{notification.notify_channel_name}}</td>
<td>
{% if notification.video_type == 'all' %}
Toutes
{% elif notification.video_type == 'video' %}
Vidéos uniquement
{% elif notification.video_type == 'short' %}
Shorts uniquement
{% endif %}
</td>
<td>{{notification.message}}</td>
<td>
<a href="{{ url_for('toggleYouTube', id = notification.id) }}" class="icon">{{ '✅' if notification.enable else '❌' }}</a>
<a href="{{ url_for('openEditYouTube', id = notification.id) }}" class="icon"></a>
<a href="{{ url_for('delYouTube', id = notification.id) }}"
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette notification ?')" class="icon">🗑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2>{{ 'Editer une notification' if notification else 'Ajouter une notification YouTube' }}</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<form id="youtube-form" action="{{ url_for('submitEditYouTube', id = notification.id) if notification else url_for('addYouTube') }}" method="POST">
<fieldset>
<legend>Configuration de base</legend>
<label for="channel_id">Lien ou ID de la chaîne YouTube</label>
<input name="channel_id" id="channel_id" type="text" maxlength="256" required="required" value="{{notification.channel_id if notification}}" placeholder="https://www.youtube.com/@513v3 ou https://www.youtube.com/channel/UC... ou UC..."/>
<label for="notify_channel">Canal de Notification Discord</label>
<select name="notify_channel" id="notify_channel">
{% for channel in channels %}
<option value="{{channel.id}}"{% if notification and notification.notify_channel == channel.id %}
selected="selected" {% endif %}>{{channel.name}}</option>
{% endfor %}
</select>
<label for="video_type">Type de vidéo à notifier</label>
<select name="video_type" id="video_type">
<option value="all"{% if notification and notification.video_type == 'all' %} selected="selected" {% endif %}>Toutes (vidéos + shorts)</option>
<option value="video"{% if notification and notification.video_type == 'video' %} selected="selected" {% endif %}>Vidéos uniquement</option>
<option value="short"{% if notification and notification.video_type == 'short' %} selected="selected" {% endif %}>Shorts uniquement</option>
</select>
<label for="message">Message (optionnel, envoyé avant l'embed)</label>
<textarea name="message" id="message" rows="3" cols="50">{{notification.message if notification}}</textarea>
</fieldset>
<fieldset>
<legend>Personnalisation de l'embed Discord</legend>
<label for="embed_title">Titre de l'embed</label>
<input name="embed_title" id="embed_title" type="text" maxlength="256" value="{{notification.embed_title if notification}}" placeholder="{video_title} (par défaut: titre de la vidéo)"/>
<small>Variables: {video_title}, {channel_name}, {video_url}, {video_id}</small>
<label for="embed_description">Description de l'embed</label>
<textarea name="embed_description" id="embed_description" rows="4" cols="50" placeholder="Description optionnelle de l'embed">{{notification.embed_description if notification}}</textarea>
<small>Variables: {video_title}, {channel_name}, {video_url}, {published_at}, {is_short}</small>
<label for="embed_color">Couleur de l'embed (hexadécimal)</label>
<input name="embed_color" id="embed_color" type="color" value="#{{notification.embed_color if notification else 'FF0000'}}" style="width: 100px; height: 40px;"/>
<input type="text" id="embed_color_text" value="{{notification.embed_color if notification else 'FF0000'}}" placeholder="FF0000" style="width: 100px; margin-left: 10px;" maxlength="6"/>
<small>Format: FF0000 (rouge YouTube par défaut)</small>
<label for="embed_author_name">Nom de l'auteur</label>
<input name="embed_author_name" id="embed_author_name" type="text" maxlength="256" value="{{notification.embed_author_name if notification}}" placeholder="{channel_name} (par défaut: nom de la chaîne)"/>
<label for="embed_author_icon">Icône de l'auteur (URL)</label>
<input name="embed_author_icon" id="embed_author_icon" type="text" maxlength="512" value="{{notification.embed_author_icon if notification}}" placeholder="https://www.youtube.com/img/desktop/yt_1200.png"/>
<label for="embed_footer">Pied de page</label>
<input name="embed_footer" id="embed_footer" type="text" maxlength="2048" value="{{notification.embed_footer if notification}}" placeholder="Texte optionnel en bas de l'embed"/>
<label>
<input type="checkbox" name="embed_thumbnail" id="embed_thumbnail" {% if not notification or notification.embed_thumbnail %}checked="checked"{% endif %}>
Afficher la miniature (thumbnail) en haut à droite
</label>
<label>
<input type="checkbox" name="embed_image" id="embed_image" {% if not notification or notification.embed_image %}checked="checked"{% endif %}>
Afficher l'image principale (image de la vidéo)
</label>
</fieldset>
<input type="Submit" value="{{ 'Modifier' if notification else 'Ajouter' }}">
</form>
</div>
<div>
<h3>Prévisualisation de l'embed Discord</h3>
<div id="embed-preview" style="background-color: #2f3136; border-radius: 4px; padding: 16px; font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #dcddde; max-width: 520px; border-left: 4px solid #FF0000;">
<div id="embed-author" style="display: flex; align-items: center; margin-bottom: 8px; font-size: 14px;">
<img id="embed-author-icon" src="https://www.youtube.com/img/desktop/yt_1200.png" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 8px;" onerror="this.style.display='none'"/>
<span id="embed-author-name" style="font-weight: 600;">Nom de la chaîne</span>
</div>
<a id="embed-title" href="#" style="color: #00aff4; text-decoration: none; font-size: 16px; font-weight: 600; display: block; margin-bottom: 8px;">Titre de la vidéo</a>
<div id="embed-description" style="font-size: 14px; line-height: 1.375; margin-bottom: 8px; color: #dcddde;"></div>
<div id="embed-thumbnail-container" style="margin: 8px 0;">
<img id="embed-thumbnail" src="" style="max-width: 80px; max-height: 80px; border-radius: 4px; float: right; margin-left: 16px; display: none;"/>
</div>
<div id="embed-image-container" style="margin-top: 16px;">
<img id="embed-image" src="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" style="max-width: 100%; border-radius: 4px; display: none;"/>
</div>
<div id="embed-footer" style="margin-top: 8px; font-size: 12px; color: #72767d;"></div>
</div>
<small style="color: #666;">Cette prévisualisation est approximative. L'apparence réelle sur Discord peut varier légèrement.</small>
</div>
</div>
<script>
function formatText(text, vars) {
if (!text) return '';
return text.replace(/\{(\w+)\}/g, function(match, key) {
return vars[key] || match;
});
}
function updatePreview() {
const embedTitle = document.getElementById('embed_title').value || '{video_title}';
const embedDescription = document.getElementById('embed_description').value || '';
const embedColor = document.getElementById('embed_color_text').value || 'FF0000';
const embedAuthorName = document.getElementById('embed_author_name').value || '{channel_name}';
const embedAuthorIcon = document.getElementById('embed_author_icon').value || 'https://www.youtube.com/img/desktop/yt_1200.png';
const embedFooter = document.getElementById('embed_footer').value || '';
const embedThumbnail = document.getElementById('embed_thumbnail').checked;
const embedImage = document.getElementById('embed_image').checked;
const vars = {
video_title: 'Nouvelle vidéo de test',
channel_name: 'Ma Chaîne YouTube',
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
video_id: 'dQw4w9WgXcQ',
thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
published_at: '2026-01-25T12:00:00Z',
is_short: false
};
document.getElementById('embed-title').textContent = formatText(embedTitle, vars);
document.getElementById('embed-title').href = vars.video_url;
document.getElementById('embed-description').textContent = formatText(embedDescription, vars);
document.getElementById('embed-author-name').textContent = formatText(embedAuthorName, vars);
document.getElementById('embed-author-icon').src = embedAuthorIcon;
document.getElementById('embed-footer').textContent = formatText(embedFooter, vars);
document.getElementById('embed-preview').style.borderLeftColor = '#' + embedColor;
if (embedThumbnail) {
document.getElementById('embed-thumbnail').src = vars.thumbnail;
document.getElementById('embed-thumbnail').style.display = 'block';
} else {
document.getElementById('embed-thumbnail').style.display = 'none';
}
if (embedImage) {
document.getElementById('embed-image').src = vars.thumbnail;
document.getElementById('embed-image').style.display = 'block';
} else {
document.getElementById('embed-image').style.display = 'none';
}
}
document.getElementById('embed_color').addEventListener('input', function(e) {
document.getElementById('embed_color_text').value = e.target.value.substring(1).toUpperCase();
updatePreview();
});
document.getElementById('embed_color_text').addEventListener('input', function(e) {
const val = e.target.value.replace(/[^0-9A-Fa-f]/g, '').substring(0, 6);
e.target.value = val;
if (val.length === 6) {
document.getElementById('embed_color').value = '#' + val;
updatePreview();
}
});
const formFields = ['embed_title', 'embed_description', 'embed_author_name', 'embed_author_icon', 'embed_footer', 'embed_thumbnail', 'embed_image'];
formFields.forEach(field => {
const el = document.getElementById(field);
if (el) {
el.addEventListener('input', updatePreview);
el.addEventListener('change', updatePreview);
}
});
updatePreview();
</script>
<p>
<strong>Variables disponibles pour l'embed :</strong>
<ul>
<li><code>{channel_name}</code> : nom de la chaîne YouTube</li>
<li><code>{video_title}</code> : titre de la vidéo</li>
<li><code>{video_url}</code> : lien vers la vidéo</li>
<li><code>{video_id}</code> : ID de la vidéo</li>
<li><code>{thumbnail}</code> : URL de la miniature</li>
<li><code>{published_at}</code> : date de publication</li>
<li><code>{is_short}</code> : True si c'est un short, False sinon</li>
</ul>
</p>
{% endblock %}

181
webapp/youtube.py Normal file
View File

@@ -0,0 +1,181 @@
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'<link rel="canonical" href="https://www\.youtube\.com/channel/([^"]{24})"', response.text)
if canonical_match:
return canonical_match.group(1)
return None
except Exception:
return None
@webapp.route("/youtube")
def openYouTube():
notifications: list[YouTubeNotification] = YouTubeNotification.query.all()
channels = bot.getAllTextChannel()
for notification in notifications:
for channel in channels:
if notification.notify_channel == channel.id:
notification.notify_channel_name = channel.name
msg = request.args.get('msg')
msg_type = request.args.get('type', 'info')
return render_template("youtube.html", notifications=notifications, channels=channels, msg=msg, msg_type=msg_type)
@webapp.route("/youtube/add", methods=['POST'])
def addYouTube():
channel_input = request.form.get('channel_id', '').strip()
channel_id = extract_channel_id(channel_input)
if not channel_id:
return redirect(url_for("openYouTube") + "?" + 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("openYouTube") + "?" + 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("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'),
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()
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': f"Notification ajoutée avec succès pour la chaîne {channel_id}", 'type': 'success'}))
@webapp.route("/youtube/toggle/<int:id>")
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/<int:id>")
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/<int:id>", 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'}))
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'}))
@webapp.route("/youtube/del/<int:id>")
def delYouTube(id):
notification = YouTubeNotification.query.get_or_404(id)
db.session.delete(notification)
db.session.commit()
return redirect(url_for("openYouTube"))