-
Comment trouver les clés API ?
-
- Ouvrez l'outil d'inspection de votre navigateur (F12)
- Allez dans l'onglet Réseau/Network
- Faites une recherche de jeu sur ProtonDB
- Cherchez les clés dans les requêtes réseau
-
-
- Voir l'exemple en image
-
+{% if configuration.getValue('proton_db_enable_enable') %}
+
+
Alias de jeux
+
+
+
+
+
+ Alias
+ Jeu
+ Action
+
+
+
+ {% for a in aliases %}
+
+
+ {{ a.alias }}
+
+ {{ a.name }}
+
+
+
+
+
+
+ {% else %}
+
+
+ Aucun alias configuré. Ajoutez-en un ci-dessous.
+
+
+ {% endfor %}
+
+
+
+
+{% endif %}
+
+
{% endblock %}
diff --git a/webapp/templates/register.html b/webapp/templates/register.html
new file mode 100644
index 0000000..c54445a
--- /dev/null
+++ b/webapp/templates/register.html
@@ -0,0 +1,41 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+
Créer un compte
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+
+ {% for category, msg in messages %}
+
{{ msg }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+
+
+ Déjà un compte ? Se connecter
+
+
+{% endblock %}
diff --git a/webapp/templates/settings.html b/webapp/templates/settings.html
new file mode 100644
index 0000000..f4b943c
--- /dev/null
+++ b/webapp/templates/settings.html
@@ -0,0 +1,386 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+
+
+
Paramètres et Permissions
+
Configuration des rôles et des accès aux pages (super administrateur uniquement)
+
+
+
+
+
+ Aide
+
+
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+
+ {% for category, msg in messages %}
+
+ {{ msg }}
+
+ {% endfor %}
+
+ {% endif %}
+{% endwith %}
+
+
+
+
+
+
+
+
Inscriptions
+
Autoriser ou bloquer la création de nouveaux comptes par les visiteurs
+
+
+
+
+
+
+
+
+
+
+
+
Hiérarchie des rôles
+
+ Les rôles définissent le niveau d'accès des utilisateurs. Plus le niveau est élevé, plus les permissions sont étendues.
+
+
+
+
+
+
+
+ {% for r in roles %}
+
+
+
+ ●
+
+
+
+
{{ r.name }}
+
+ Niveau {{ r.level }}
+
+
+ {% if r.description %}
+
{{ r.description }}
+ {% else %}
+ {% set default_desc = default_roles_meta.get(r.name, {}).get('description') %}
+ {% if default_desc %}
+
{{ default_desc }}
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+ Modifier ce rôle
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+ Créer un nouveau rôle
+
+
+
+
+
+
+
+
+
+
+
+
+
Accès aux pages
+
+ Définissez le rôle minimum requis pour accéder à chaque section de l'interface
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚡ Modification en masse
+
+
+
+
+
+
+
+ {% for category_key in ['general', 'content', 'moderation', 'config', 'admin'] %}
+ {% if category_key in pages_by_category %}
+ {% set category_info = category_labels[category_key] %}
+
+
+
+ ●
+
+
{{ category_info.label }}
+
+
+
+
+ {% for page_data in pages_by_category[category_key] %}
+ {% set page_key = page_data.key %}
+ {% set meta = page_data.meta %}
+ {% set perm = page_data.permission %}
+ {% set min_lvl = perm.min_level if perm else 0 %}
+
+
+
+
+
{{ meta.label }}
+
{{ meta.description }}
+
+ {% for r in roles %}
+ {% if r.level == min_lvl %}
+
+ {{ r.name }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
Guide des permissions
+
+
+
+
+
+
+
+
+
🎭 Rôles
+
Les rôles définissent le niveau d'autorité d'un utilisateur. Plus le niveau est élevé, plus l'utilisateur a accès à des fonctionnalités.
+
+ Niveau 0-1 : Accès basique en lecture seule
+ Niveau 2-3 : Modification de contenu et gestion basique
+ Niveau 4 : Modération et configuration avancée
+ Niveau 5+ : Administration complète du système
+
+
+
+
🔐 Permissions par page
+
Chaque page peut être restreinte à un rôle minimum. Un utilisateur doit avoir au moins le niveau requis pour y accéder.
+
+
+
📋 Catégories
+
+ ● Général : Pages d'accueil et navigation
+ ● Contenu : Gestion des commandes, alertes, humeurs, etc.
+ ● Modération : Outils de modération Discord/Twitch
+ ● Configuration : Paramètres techniques des bots
+ ● Administration : Gestion des utilisateurs et permissions
+
+
+
+
💡 Bonnes pratiques
+
+ Attribuez le rôle le plus bas possible selon les besoins
+ Testez les permissions avec un compte utilisateur avant de les déployer
+ Documentez les rôles personnalisés avec des descriptions claires
+ Vérifiez régulièrement les accès des utilisateurs
+
+
+
+
+
+
+ Compris !
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/webapp/templates/template.html b/webapp/templates/template.html
index e1387ae..e171b20 100644
--- a/webapp/templates/template.html
+++ b/webapp/templates/template.html
@@ -77,46 +77,117 @@
diff --git a/webapp/templates/twitch-events.html b/webapp/templates/twitch-events.html
new file mode 100644
index 0000000..fcd333c
--- /dev/null
+++ b/webapp/templates/twitch-events.html
@@ -0,0 +1,117 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/webapp/templates/twitch-moderation.html b/webapp/templates/twitch-moderation.html
new file mode 100644
index 0000000..18a968f
--- /dev/null
+++ b/webapp/templates/twitch-moderation.html
@@ -0,0 +1,964 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+
+
+
+
Modération Twitch
+
Commandes et logs de modération pour {{ twitch_channel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Live
+
{{ 'En ligne' if is_live else 'Hors ligne' }}
+
+
+ {% if is_live %}
+
+
{{ viewer_count }}
+
viewers
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
Filtre de liens
+
{{ 'Actif' if link_filter_enabled else 'Inactif' }}
+
+
+
Configurer
+
+
+
+
+
+
+
+
+
+
Mots interdits
+
{{ banned_words|length }} mot{{ 's' if banned_words|length > 1 else '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Commande(s)
+ Usage
+ Permission
+
+
+
+ {% for cmd in commands %}
+
+
+ {% for c in cmd.commands %}
+ {{ c }}
+ {% if not loop.last %} {% endif %}
+ {% endfor %}
+
+ {{ cmd.usage }}
+ {{ cmd.permission }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {% if logs %}
+
+
+
+ Date
+ Action
+ Modo
+ Cible
+ Détails
+
+
+
+ {% for log in logs %}
+
+ {{ log.created_at.strftime('%d/%m %H:%M') }}
+ {{ log.action }}
+ {{ log.moderator }}
+ {{ log.target or '-' }}
+ {{ log.details or '-' }}
+
+ {% endfor %}
+
+
+ {% else %}
+
Aucun log
+ {% endif %}
+
+
+
+
+
+
+ {% if is_live %}
+
+
+
+
+
+
+
+
+
+
+
+ Nouvelle commande
+
+
+
+
+
+
+
+ {% else %}
+
+
+
+
+
+
+
+
+ Ajouter une commande
+
+
+
+
+
+ {% for value, label in twitch_permissions.items() %}
+ {{ label }}
+ {% endfor %}
+
+
+
+ Ajouter
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
Récupération du chat...
+
Les messages du chat Twitch apparaîtront ici en temps réel
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ban
+
+
+
+
+
+ Timeout
+
+
+
+
+
+ Clean
+
+
+
+
+
+ Permit
+
+
+
+
+
+
+
+ Sub ON
+
+
+
+
+
+ Sub OFF
+
+
+ 😀 ON
+
+
+ 😀 OFF
+
+
+
+
+
+
+
+ Envoyer
+
+
+
Envoyé via le bot • Ouvrir le chat
+
+
+
+
+
+ {% if custom_commands %}
+
+
+
Commandes personnalisées Twitch ({{ custom_commands|length }})
+
+
+
+
+
+ Commande
+ Réponse
+ Permission
+ Actions
+
+
+
+ {% for cmd in custom_commands %}
+
+ {{ cmd.trigger }}
+ {{ cmd.response }}
+ {{ twitch_permissions.get(cmd.twitch_permission or 'viewer', 'Tous') }}
+ Supprimer
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
Mots interdits ({{ banned_words|length }})
+
+
+
+
+
+
+ {% if banned_words %}
+
+
+
+
+ Mot
+ Timeout
+ Ajouté le
+ Actions
+
+
+
+ {% for word in banned_words %}
+
+ {{ word.word }}
+ {{ word.timeout_duration }}s
+ {{ word.created_at.strftime('%d/%m/%Y %H:%M') if word.created_at else '-' }}
+ Supprimer
+
+ {% endfor %}
+
+
+
+ {% else %}
+
Aucun mot interdit configuré
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/webapp/templates/users.html b/webapp/templates/users.html
new file mode 100644
index 0000000..038e608
--- /dev/null
+++ b/webapp/templates/users.html
@@ -0,0 +1,292 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+
+
+
+
+
Gestion des utilisateurs
+
Liste des comptes webapp et attribution des rôles
+
+
+
+
+
+
+ Créer un utilisateur
+
+
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+
+ {% for category, msg in messages %}
+
+ {{ msg }}
+
+ {% endfor %}
+
+ {% endif %}
+{% endwith %}
+
+
+ {% if users %}
+
+
+
+
+
+ Utilisateur
+
+
+ E-mail
+
+
+ Rôle actuel
+
+
+ Inscrit le
+
+
+ Modifier le rôle
+
+
+
+
+ {% for u in users %}
+
+
+
+
+ {{ u.username[0].upper() }}
+
+
+
{{ u.username }}
+ {% if u.id == current_user.id %}
+
C'est vous
+ {% endif %}
+
+
+
+
+ {{ u.email }}
+
+
+ {% set user_role = None %}
+ {% for r in roles %}
+ {% if r == u.role %}
+ {% set user_role = r %}
+ {% endif %}
+ {% endfor %}
+ {% if user_role %}
+ {% set role_obj = namespace(found=None) %}
+ {% for role_name in roles %}
+ {% if role_name == user_role %}
+ {% for r_obj in roles %}
+ {# Obtenir l'objet WebappRole complet #}
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+
+
+ {{ role_labels.get(u.role, u.role) }}
+
+ {% else %}
+
+ {{ role_labels.get(u.role, u.role) }}
+
+ {% endif %}
+
+
+
+ {% if u.created_at %}{{ u.created_at.strftime('%d/%m/%Y à %H:%M') }}{% else %}—{% endif %}
+
+
+
+
+
+
+ {% for r in roles %}
+ {{ role_labels.get(r, r) }}
+ {% endfor %}
+
+
+ Appliquer
+
+
+ {% if u.id != current_user.id %}
+
+
+
+
+
+
+
+ {% endif %}
+
+ {% if u.id == current_user.id %}
+ Protection: modification/suppression impossible
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+ {% else %}
+
+
+
+
+
Aucun utilisateur enregistré
+
+ {% endif %}
+
+
+
+{% if users %}
+
+
+
+
+
+ Hiérarchie des rôles
+
+
+ {% for role_name in roles %}
+
+
+
+ {{ role_labels.get(role_name, role_name) }}
+
+
+ {% if role_name == 'viewer_twitch' %}
+ Accès minimal, consultation uniquement
+ {% elif role_name == 'utilisateur_discord' %}
+ Modification de contenu basique
+ {% elif role_name == 'moderateur_discord' %}
+ Outils de modération Discord
+ {% elif role_name == 'expert_discord' %}
+ Gestion avancée du contenu
+ {% elif role_name == 'moderateur_twitch' %}
+ Outils de modération Twitch
+ {% elif role_name == 'super_administrateur' %}
+ Accès complet au système
+ {% else %}
+ Rôle personnalisé
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+ 💡 Conseil : Vous pouvez personnaliser les rôles et leurs permissions dans la page
+ Paramètres .
+
+
+
+{% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
Créer un utilisateur
+
+
+
+
+
+
+
+
+
+
+
+ Annuler
+
+
+ Créer l'utilisateur
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/webapp/templates/youtube.html b/webapp/templates/youtube.html
new file mode 100644
index 0000000..bd9bb15
--- /dev/null
+++ b/webapp/templates/youtube.html
@@ -0,0 +1,342 @@
+{% extends "template.html" %}
+
+{% block content %}
+
+
Notifications YouTube
+
+ {% if msg %}
+
+ {{ msg }}
+
+
+ {% endif %}
+
+
+
+ Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos.
+ Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous.
+ Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
+
+
+
+
+{% if not notification %}
+
+
Notifications configurées
+
+
+
+
+
+ Chaîne YouTube
+ Canal Discord
+ Type
+ Message
+ Actions
+
+
+
+ {% for notification in notifications %}
+
+
+ {{ notification.channel_id }}
+
+ {{ notification.notify_channel_name }}
+
+
+ {% if notification.video_type == 'all' %}Toutes
+ {% elif notification.video_type == 'video' %}Vidéos
+ {% else %}Shorts{% endif %}
+
+
+ {{ notification.message }}
+
+
+
+
+ {% else %}
+
+
+ Aucune notification configurée. Ajoutez-en une ci-dessous.
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+ {{ 'Modifier la notification' if notification else 'Ajouter une notification YouTube' }}
+
+
+
+
+
+
+
Configuration de base
+
+
+ Lien ou ID de la chaîne YouTube
+
+
+
+
+ Canal de notification Discord
+
+ {% for channel in channels %}
+ {{channel.name}}
+ {% endfor %}
+
+
+
+
+ Type de vidéo à notifier
+
+ Toutes (vidéos + shorts)
+ Vidéos uniquement
+ Shorts uniquement
+
+
+
+
+ Message (optionnel)
+ {{notification.message if notification}}
+
+
+
+
+
Personnalisation de l'embed Discord
+
+
+
Titre de l'embed
+
+
Variables: {video_title}, {channel_name}, {video_url}, {video_id}
+
+
+
+ Description de l'embed
+ {{notification.embed_description if notification}}
+
+
+
+
+
+ Nom de l'auteur
+
+
+
+
+
+ Icône de l'auteur (URL)
+
+
+
+
+ Pied de page
+
+
+
+
+
+
+ Miniature
+
+
+
+ Image principale
+
+
+
+
+
+
+ {{ 'Enregistrer' if notification else 'Ajouter la notification' }}
+
+ {% if notification %}
+
+ Annuler
+
+ {% endif %}
+
+
+
+
+
+
Prévisualisation de l'embed Discord
+
+
+
+
Nom de la chaîne
+
+
Titre de la vidéo
+
+
+
+
+
+
+
+
+
+
Cette prévisualisation est approximative.
+
+
+
Variables disponibles
+
+ {channel_name} — Nom de la chaîne
+ {video_title} — Titre de la vidéo
+ {video_url} — Lien vers la vidéo
+ {video_id} — ID de la vidéo
+ {thumbnail} — URL de la miniature
+ {published_at} — Date de publication
+ {is_short} — True si c'est un short
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/webapp/twitch_auth.py b/webapp/twitch_auth.py
index 4994f81..9fbd100 100644
--- a/webapp/twitch_auth.py
+++ b/webapp/twitch_auth.py
@@ -1,50 +1,71 @@
+import asyncio
import logging
-from flask import render_template, request, redirect, url_for
+from flask import render_template, request, redirect, url_for, flash
from twitchAPI.twitch import Twitch
-from twitchAPI.type import TwitchAPIException
+from twitchAPI.type import TwitchAPIException
from twitchAPI.oauth import UserAuthenticator
from database import db
from database.helpers import ConfigurationHelper
from twitchbot import USER_SCOPE
from webapp import webapp
+from webapp.auth import require_page
auth: UserAuthenticator
-@webapp.route("/configurations/twitch/help")
-def twitchConfigurationHelp():
- return render_template("twitch-aide.html", token_redirect_url = _buildUrl())
-@webapp.route("/configurations/twitch/request-token")
-async def twitchRequestToken():
+@webapp.route("/configurations/twitch/help")
+@require_page("configurations")
+def twitchConfigurationHelp():
+ return render_template("twitch-aide.html", token_redirect_url=_buildUrl())
+
+
+@webapp.route("/configurations/twitch/request-token")
+@require_page("configurations")
+def twitchRequestToken():
global auth
helper = ConfigurationHelper()
- twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
+ twitch = asyncio.run(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')))
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
return redirect(auth.return_auth_url())
-@webapp.route("/configurations/twitch/receive-token")
-async def twitchReceiveToken():
+
+@webapp.route("/configurations/twitch/receive-token")
+def twitchReceiveToken():
global auth
state = request.args.get('state')
code = request.args.get('code')
- if state != auth.state :
- logging('bad returned state')
+
+ logging.info("Callback Twitch reçu - state: %s, code: %s", state, code is not None)
+
+ if not hasattr(auth, 'state') or auth is None:
+ logging.error('Objet auth non initialisé - veuillez réessayer')
return redirect(url_for('openConfigurations'))
- if code == None :
- logging('no returned state')
+
+ if state != auth.state:
+ logging.error('State invalide - attendu: %s, reçu: %s', auth.state, state)
return redirect(url_for('openConfigurations'))
-
+ if code is None:
+ logging.error('Pas de code retourné par Twitch')
+ return redirect(url_for('openConfigurations'))
+
try:
- token, refresh = await auth.authenticate(user_token=code)
+ token, refresh = asyncio.run(auth.authenticate(user_token=code))
+ logging.info('Tokens Twitch obtenus avec succès')
helper = ConfigurationHelper()
helper.createOrUpdate('twitch_access_token', token)
helper.createOrUpdate('twitch_refresh_token', refresh)
db.session.commit()
+ logging.info('Tokens Twitch sauvegardés en base de données')
+ flash('Token Twitch enregistré. Redémarrez l\'application pour que le bot utilise le nouveau token.', 'success')
except TwitchAPIException as e:
- logging(e)
+ logging.error('Erreur API Twitch: %s', e)
+ flash(f'Erreur API Twitch : {e}', 'error')
+ except Exception as e:
+ logging.error('Erreur inattendue: %s', e)
+ flash(f'Erreur inattendue : {e}', 'error')
return redirect(url_for('openConfigurations'))
# hack pas fou mais on estime qu'on sera toujours en ssl en connecté
diff --git a/webapp/twitch_events.py b/webapp/twitch_events.py
new file mode 100644
index 0000000..41db9cf
--- /dev/null
+++ b/webapp/twitch_events.py
@@ -0,0 +1,81 @@
+# Notifications d'événements Twitch : sub, follow, raid, clip (chat + Discord)
+from flask import render_template, request, redirect, url_for
+
+from webapp import webapp
+from webapp.auth import require_page, can_write_page
+from database import db
+from database.models import TwitchEventNotification
+from discordbot import bot
+
+EVENT_LABELS = {
+ "sub": "Abonnement (sub)",
+ "follow": "Nouveau follow",
+ "raid": "Raid reçu",
+ "clip": "Nouveau clip",
+}
+
+
+@webapp.route("/twitch-events")
+@require_page("twitch_events")
+def open_twitch_events():
+ configs = TwitchEventNotification.query.order_by(TwitchEventNotification.event_type).all()
+ # S'assurer qu'il existe une config par type
+ existing = {c.event_type for c in configs}
+ for ev in ("sub", "follow", "raid", "clip"):
+ if ev not in existing:
+ cfg = TwitchEventNotification(
+ event_type=ev,
+ message_twitch="Merci {user} !" if ev != "raid" else "Bienvenue aux viewers de {from_broadcaster_name} !",
+ )
+ db.session.add(cfg)
+ configs.append(cfg)
+ db.session.commit()
+ channels = bot.getAllTextChannel()
+ # Nom du canal Discord pour l'affichage
+ for c in configs:
+ if c.discord_channel_id:
+ c.discord_channel_name = next((ch.name for ch in channels if ch.id == c.discord_channel_id), None)
+ else:
+ c.discord_channel_name = None
+ return render_template("twitch-events.html", configs=configs, channels=channels, labels=EVENT_LABELS)
+
+
+@webapp.route("/twitch-events/save", methods=["POST"])
+@require_page("twitch_events")
+def save_twitch_events():
+ if not can_write_page("twitch_events"):
+ return render_template("403.html"), 403
+ for ev in ("sub", "follow", "raid", "clip"):
+ cfg = TwitchEventNotification.query.filter_by(event_type=ev).first()
+ if not cfg:
+ cfg = TwitchEventNotification(event_type=ev)
+ db.session.add(cfg)
+ prefix = f"ev_{ev}_"
+ cfg.enable = request.form.get(prefix + "enable") == "1"
+ cfg.notify_twitch_chat = request.form.get(prefix + "notify_twitch_chat") == "1"
+ cfg.notify_discord = request.form.get(prefix + "notify_discord") == "1"
+ ch_id = request.form.get(prefix + "discord_channel_id")
+ cfg.discord_channel_id = int(ch_id) if ch_id and ch_id.isdigit() else None
+ cfg.message_twitch = (request.form.get(prefix + "message_twitch") or "").strip()[:500]
+ cfg.message_discord = (request.form.get(prefix + "message_discord") or "").strip()[:2000] or None
+ embed_color = (request.form.get(prefix + "embed_color") or "9146FF").strip().lstrip("#")[:6]
+ cfg.embed_color = embed_color if len(embed_color) == 6 else "9146FF"
+ cfg.embed_title = (request.form.get(prefix + "embed_title") or "").strip()[:256] or None
+ cfg.embed_description = (request.form.get(prefix + "embed_description") or "").strip()[:2000] or None
+ cfg.embed_thumbnail = request.form.get(prefix + "embed_thumbnail") == "1"
+ db.session.commit()
+ return redirect(url_for("open_twitch_events"))
+
+
+@webapp.route("/twitch-events/toggle/
")
+@require_page("twitch_events")
+def toggle_twitch_event(event_type):
+ if not can_write_page("twitch_events"):
+ return render_template("403.html"), 403
+ if event_type not in ("sub", "follow", "raid", "clip"):
+ return redirect(url_for("open_twitch_events"))
+ cfg = TwitchEventNotification.query.filter_by(event_type=event_type).first()
+ if cfg:
+ cfg.enable = not cfg.enable
+ db.session.commit()
+ return redirect(url_for("open_twitch_events"))
diff --git a/webapp/twitch_moderation.py b/webapp/twitch_moderation.py
new file mode 100644
index 0000000..75bcb80
--- /dev/null
+++ b/webapp/twitch_moderation.py
@@ -0,0 +1,403 @@
+from flask import render_template, request, redirect, url_for, jsonify
+from webapp import webapp
+from webapp.auth import require_page, can_write_page
+from database import db
+from database.models import Commande, TwitchModerationLog, TwitchLinkFilter, TwitchBannedWord
+from database.helpers import ConfigurationHelper
+from datetime import datetime, timedelta
+import asyncio
+
+MODERATION_COMMANDS = [
+ {
+ "commands": ["!kick", "!to", "!timeout", "!tm"],
+ "usage": "!timeout [minutes] [raison]",
+ "description": "Ejection temporaire d'un viewer (3 minutes par defaut) avec raison optionnelle",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!ban"],
+ "usage": "!ban [viewer2] ...",
+ "description": "Bannissement d'un ou plusieurs viewers (max 5)",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!unban"],
+ "usage": "!unban [viewer2] ...",
+ "description": "Debannissement d'un ou plusieurs viewers (max 5)",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!clean"],
+ "usage": "!clean [viewer]",
+ "description": "Nettoyage du chat ou des messages d'un viewer",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!shieldmode"],
+ "usage": "!shieldmode ",
+ "description": "Active/desactive le mode Shield de Twitch",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!settitle"],
+ "usage": "!settitle ",
+ "description": "Changement du titre du live",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!setgame", "!setcateg"],
+ "usage": "!setgame ",
+ "description": "Changement du jeu/categorie du live",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!subon"],
+ "usage": "!subon",
+ "description": "Activation du mode abonnes uniquement",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!suboff"],
+ "usage": "!suboff",
+ "description": "Desactivation du mode abonnes uniquement",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!follon"],
+ "usage": "!follon [minutes]",
+ "description": "Activation du mode followers-only",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!folloff"],
+ "usage": "!folloff",
+ "description": "Desactivation du mode followers-only",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!emoteon"],
+ "usage": "!emoteon",
+ "description": "Activation du mode emote-only",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!emoteoff"],
+ "usage": "!emoteoff",
+ "description": "Desactivation du mode emote-only",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!ann"],
+ "usage": "!ann ",
+ "description": "Activer/desactiver/inverser une liste d'annonce par alias",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!no_game"],
+ "usage": "!no_game ",
+ "description": "Desactiver/activer tous les jeux de la chaine",
+ "permission": "Moderateur"
+ },
+ {
+ "commands": ["!multitwitch"],
+ "usage": "!multitwitch [live1] [live2] ... | auto | reset",
+ "description": "Creation d'un lien MultiTwitch. '@' = chaine actuelle, 'auto' = depuis le titre, 'reset' = reinitialiser",
+ "permission": "Moderateur (creation) / Tous (affichage)"
+ },
+ {
+ "commands": ["!permit"],
+ "usage": "!permit [minutes]",
+ "description": "Autorise temporairement un viewer a poster un lien (1 minute par defaut)",
+ "permission": "Moderateur"
+ },
+]
+
+TWITCH_PERMISSIONS = {'viewer': 'Tous (viewers)', 'sub': 'Abonnés', 'vip': 'VIP', 'moderator': 'Modérateur'}
+
+
+@webapp.route("/twitch-moderation")
+@require_page("twitch_moderation")
+def twitch_moderation():
+ custom_commands = Commande.query.filter_by(twitch_enable=True).all()
+ logs = TwitchModerationLog.query.order_by(TwitchModerationLog.created_at.desc()).limit(50).all()
+ raw_channel = ConfigurationHelper().getValue("twitch_channel") or webapp.config["BOT_STATUS"].get("twitch_channel_name") or "chainesteve"
+ twitch_channel = (raw_channel or "").strip().lower() or "chainesteve"
+ embed_parent = request.host or "localhost"
+
+ # Link filter status
+ link_filter_config = TwitchLinkFilter.query.first()
+ link_filter_enabled = link_filter_config.enabled if link_filter_config else False
+
+ # Banned words
+ banned_words = TwitchBannedWord.query.filter_by(enabled=True).all()
+
+ # Live status (from BOT_STATUS)
+ bot_status = webapp.config.get("BOT_STATUS", {})
+ is_live = bot_status.get("twitch_is_live", False)
+ viewer_count = bot_status.get("twitch_viewer_count", 0)
+
+ return render_template(
+ "twitch-moderation.html",
+ commands=MODERATION_COMMANDS,
+ custom_commands=custom_commands,
+ logs=logs,
+ twitch_permissions=TWITCH_PERMISSIONS,
+ twitch_channel=twitch_channel,
+ embed_parent=embed_parent,
+ link_filter_enabled=link_filter_enabled,
+ banned_words=banned_words,
+ is_live=is_live,
+ viewer_count=viewer_count,
+ )
+
+@webapp.route("/twitch-moderation/logs/clear")
+@require_page("twitch_moderation")
+def clear_twitch_logs():
+ if not can_write_page("twitch_moderation"):
+ return render_template("403.html"), 403
+ TwitchModerationLog.query.delete()
+ db.session.commit()
+ return redirect(url_for('twitch_moderation'))
+
+@webapp.route("/twitch-moderation/add", methods=['POST'])
+@require_page("twitch_moderation")
+def add_twitch_commande():
+ if not can_write_page("twitch_moderation"):
+ return render_template("403.html"), 403
+ trigger = request.form.get('trigger')
+ response = request.form.get('response')
+ twitch_permission = request.form.get('twitch_permission') or 'viewer'
+ if twitch_permission not in TWITCH_PERMISSIONS:
+ twitch_permission = 'viewer'
+
+ if trigger and response:
+ if not trigger.startswith('!'):
+ trigger = '!' + trigger
+
+ existing = Commande.query.filter_by(trigger=trigger).first()
+ if not existing:
+ commande = Commande(trigger=trigger, response=response, discord_enable=False, twitch_enable=True, twitch_permission=twitch_permission)
+ db.session.add(commande)
+ db.session.commit()
+
+ return redirect(url_for('twitch_moderation'))
+
+@webapp.route("/twitch-moderation/banned-word/add", methods=['POST'])
+@require_page("twitch_moderation")
+def add_banned_word():
+ if not can_write_page("twitch_moderation"):
+ return render_template("403.html"), 403
+
+ word = request.form.get('word', '').strip().lower()
+ timeout_duration = int(request.form.get('timeout_duration', 60))
+
+ if word:
+ existing = TwitchBannedWord.query.filter_by(word=word).first()
+ if not existing:
+ banned_word = TwitchBannedWord(word=word, enabled=True, timeout_duration=timeout_duration)
+ db.session.add(banned_word)
+ db.session.commit()
+
+ return redirect(url_for('twitch_moderation'))
+
+@webapp.route("/twitch-moderation/banned-word/delete/")
+@require_page("twitch_moderation")
+def delete_banned_word(word_id):
+ if not can_write_page("twitch_moderation"):
+ return render_template("403.html"), 403
+
+ banned_word = TwitchBannedWord.query.get_or_404(word_id)
+ db.session.delete(banned_word)
+ db.session.commit()
+
+ return redirect(url_for('twitch_moderation'))
+
+@webapp.route("/twitch-moderation/send-message", methods=['POST'])
+@require_page("twitch_moderation")
+def send_twitch_message():
+ if not can_write_page("twitch_moderation"):
+ return jsonify({"success": False, "error": "Permission refusée"}), 403
+
+ data = request.get_json()
+ message = data.get('message', '').strip()
+
+ if not message:
+ return jsonify({"success": False, "error": "Message vide"}), 400
+
+ # Vérifier que le bot Twitch est connecté
+ from twitchbot import twitchBot
+ if not hasattr(twitchBot, 'chat') or not twitchBot.chat:
+ return jsonify({"success": False, "error": "Bot Twitch non connecté"}), 503
+
+ # Récupérer le nom du channel
+ channel = ConfigurationHelper().getValue('twitch_channel')
+ if not channel:
+ return jsonify({"success": False, "error": "Channel Twitch non configuré"}), 400
+
+ # Envoyer le message de manière asynchrone
+ try:
+ async def send_msg():
+ try:
+ await twitchBot.chat.send_message(channel, message)
+ return True
+ except Exception as e:
+ return str(e)
+
+ # Exécuter la coroutine de manière synchrone
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(send_msg())
+ loop.close()
+
+ if result is True:
+ return jsonify({"success": True})
+ else:
+ return jsonify({"success": False, "error": f"Erreur: {result}"}), 500
+ except Exception as e:
+ return jsonify({"success": False, "error": str(e)}), 500
+
+@webapp.route("/twitch-moderation/messages")
+@require_page("twitch_moderation")
+def get_twitch_messages():
+ """Retourne les derniers messages du chat Twitch"""
+ messages = webapp.config["BOT_STATUS"].get("twitch_chat_messages", [])
+ return jsonify({"messages": messages})
+
+@webapp.route("/twitch-moderation/execute-action", methods=['POST'])
+@require_page("twitch_moderation")
+def execute_moderation_action():
+ """Exécute une action de modération directement"""
+ if not can_write_page("twitch_moderation"):
+ return jsonify({"success": False, "error": "Permission refusée"}), 403
+
+ data = request.get_json()
+ action = data.get('action', '').strip()
+ params = data.get('params', {})
+
+ if not action:
+ return jsonify({"success": False, "error": "Action non spécifiée"}), 400
+
+ # Vérifier que le bot Twitch est connecté
+ from twitchbot import twitchBot
+ if not hasattr(twitchBot, 'chat') or not twitchBot.chat or not hasattr(twitchBot, 'twitch'):
+ return jsonify({"success": False, "error": "Bot Twitch non connecté"}), 503
+
+ # Récupérer le nom du channel
+ channel = ConfigurationHelper().getValue('twitch_channel')
+ if not channel:
+ return jsonify({"success": False, "error": "Channel Twitch non configuré"}), 400
+
+ # Créer un objet ChatMessage simulé pour les commandes qui en ont besoin
+ from twitchAPI.chat import ChatMessage
+ from types import SimpleNamespace
+
+ # Exécuter l'action de manière asynchrone
+ try:
+ async def execute_action():
+ try:
+ if action == 'timeout':
+ from twitchbot.moderation import _get_broadcaster_id, _get_moderator_id, _get_user_id, _log_action
+ username = params.get('username', '').strip().lstrip('@')
+ duration = int(params.get('duration', 600)) # en secondes
+ reason = params.get('reason', 'Timeout')
+
+ broadcaster_id = await _get_broadcaster_id(twitchBot.twitch, channel)
+ moderator_id = await _get_moderator_id(twitchBot.twitch)
+ user_id = await _get_user_id(twitchBot.twitch, username)
+
+ if user_id:
+ await twitchBot.twitch.ban_user(broadcaster_id, moderator_id, user_id, reason=reason, duration=duration)
+ _log_action("timeout", "WebApp", username, f"{duration}s - {reason}")
+ return {"success": True, "message": f"Timeout de {username} pour {duration}s"}
+ return {"success": False, "error": f"Utilisateur {username} introuvable"}
+
+ elif action == 'ban':
+ from twitchbot.moderation import _get_broadcaster_id, _get_moderator_id, _get_user_id, _log_action
+ username = params.get('username', '').strip().lstrip('@')
+ reason = params.get('reason', 'Ban')
+
+ broadcaster_id = await _get_broadcaster_id(twitchBot.twitch, channel)
+ moderator_id = await _get_moderator_id(twitchBot.twitch)
+ user_id = await _get_user_id(twitchBot.twitch, username)
+
+ if user_id:
+ await twitchBot.twitch.ban_user(broadcaster_id, moderator_id, user_id, reason=reason)
+ _log_action("ban", "WebApp", username, reason)
+ return {"success": True, "message": f"Ban de {username}"}
+ return {"success": False, "error": f"Utilisateur {username} introuvable"}
+
+ elif action == 'clean':
+ from twitchbot.moderation import _get_broadcaster_id, _get_moderator_id, _get_user_id, _log_action
+ username = params.get('username', '').strip().lstrip('@')
+
+ broadcaster_id = await _get_broadcaster_id(twitchBot.twitch, channel)
+ moderator_id = await _get_moderator_id(twitchBot.twitch)
+
+ if username:
+ user_id = await _get_user_id(twitchBot.twitch, username)
+ if user_id:
+ await twitchBot.twitch.delete_chat_messages(broadcaster_id, moderator_id, user_id=user_id)
+ _log_action("clean", "WebApp", username)
+ return {"success": True, "message": f"Messages de {username} supprimés"}
+ return {"success": False, "error": f"Utilisateur {username} introuvable"}
+ else:
+ await twitchBot.twitch.delete_chat_messages(broadcaster_id, moderator_id)
+ _log_action("clean", "WebApp", None, "Chat complet")
+ return {"success": True, "message": "Chat nettoyé"}
+
+ elif action == 'permit':
+ from database.models import TwitchPermit
+ username = params.get('username', '').strip().lstrip('@').lower()
+ duration = int(params.get('duration', 60)) # en secondes
+
+ expires_at = datetime.now() + timedelta(seconds=duration)
+
+ with webapp.app_context():
+ existing = TwitchPermit.query.filter_by(username=username).first()
+ if existing:
+ existing.expires_at = expires_at
+ else:
+ permit = TwitchPermit(username=username, expires_at=expires_at)
+ db.session.add(permit)
+ db.session.commit()
+
+ return {"success": True, "message": f"Permit accordé à {username} pour {duration//60}min"}
+
+ elif action in ['subon', 'suboff', 'emoteon', 'emoteoff']:
+ from twitchbot.moderation import _get_broadcaster_id, _get_moderator_id, _log_action
+
+ broadcaster_id = await _get_broadcaster_id(twitchBot.twitch, channel)
+ moderator_id = await _get_moderator_id(twitchBot.twitch)
+
+ if action == 'subon':
+ await twitchBot.twitch.update_chat_settings(broadcaster_id, moderator_id, subscriber_mode=True)
+ _log_action("subon", "WebApp")
+ return {"success": True, "message": "Mode abonnés activé"}
+ elif action == 'suboff':
+ await twitchBot.twitch.update_chat_settings(broadcaster_id, moderator_id, subscriber_mode=False)
+ _log_action("suboff", "WebApp")
+ return {"success": True, "message": "Mode abonnés désactivé"}
+ elif action == 'emoteon':
+ await twitchBot.twitch.update_chat_settings(broadcaster_id, moderator_id, emote_mode=True)
+ _log_action("emoteon", "WebApp")
+ return {"success": True, "message": "Mode emote activé"}
+ elif action == 'emoteoff':
+ await twitchBot.twitch.update_chat_settings(broadcaster_id, moderator_id, emote_mode=False)
+ _log_action("emoteoff", "WebApp")
+ return {"success": True, "message": "Mode emote désactivé"}
+
+ return {"success": False, "error": f"Action '{action}' non reconnue"}
+
+ except Exception as e:
+ import logging
+ logging.error(f"Erreur lors de l'exécution de l'action {action}: {e}")
+ return {"success": False, "error": str(e)}
+
+ # Exécuter la coroutine de manière synchrone
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(execute_action())
+ loop.close()
+
+ return jsonify(result)
+
+ except Exception as e:
+ return jsonify({"success": False, "error": str(e)}), 500
diff --git a/webapp/users.py b/webapp/users.py
new file mode 100644
index 0000000..ab5a169
--- /dev/null
+++ b/webapp/users.py
@@ -0,0 +1,119 @@
+# Gestion des utilisateurs webapp (réservé super administrateur).
+from flask import render_template, request, redirect, url_for, flash
+from werkzeug.security import generate_password_hash
+
+from webapp import webapp
+from webapp.auth import require_page
+from database import db
+from database.models import WebappUser, WebappRole
+
+ROLE_LABELS = {
+ "viewer_twitch": "Viewer Twitch",
+ "utilisateur_discord": "Utilisateur Discord",
+ "moderateur_discord": "Modérateur Discord",
+ "expert_discord": "Expert Discord",
+ "moderateur_twitch": "Modérateur Twitch",
+ "super_administrateur": "Super administrateur",
+}
+
+
+def _role_labels():
+ roles = WebappRole.query.order_by(WebappRole.level).all()
+ return {r.name: r.name.replace("_", " ").title() for r in roles}
+
+
+@webapp.route("/users")
+@require_page("users")
+def users_list():
+ users = WebappUser.query.order_by(WebappUser.created_at.desc()).all()
+ roles = WebappRole.query.order_by(WebappRole.level).all()
+ labels = dict(ROLE_LABELS)
+ labels.update(_role_labels())
+ return render_template(
+ "users.html",
+ users=users,
+ roles=[r.name for r in roles],
+ role_labels=labels,
+ )
+
+
+@webapp.route("/users/role/", methods=["POST"])
+@require_page("users")
+def users_set_role(user_id):
+ user = WebappUser.query.get_or_404(user_id)
+ new_role = request.form.get("role")
+ existing = WebappRole.query.filter_by(name=new_role).first()
+ if new_role and existing:
+ user.role = new_role
+ db.session.commit()
+ return redirect(url_for("users_list"))
+
+
+@webapp.route("/users/create", methods=["POST"])
+@require_page("users")
+def users_create():
+ """Création d'un utilisateur par un administrateur."""
+ username = (request.form.get("username") or "").strip()
+ email = (request.form.get("email") or "").strip().lower()
+ password = request.form.get("password") or ""
+ password_confirm = request.form.get("password_confirm") or ""
+ role = request.form.get("role") or "viewer_twitch"
+
+ errors = []
+
+ # Validations
+ if len(username) < 3:
+ errors.append("Le nom d'utilisateur doit faire au moins 3 caractères.")
+ if len(email) < 5 or "@" not in email:
+ errors.append("Adresse e-mail invalide.")
+ if len(password) < 8:
+ errors.append("Le mot de passe doit faire au moins 8 caractères.")
+ if password != password_confirm:
+ errors.append("Les mots de passe ne correspondent pas.")
+ if WebappUser.query.filter_by(username=username).first():
+ errors.append("Ce nom d'utilisateur est déjà pris.")
+ if WebappUser.query.filter_by(email=email).first():
+ errors.append("Cette adresse e-mail est déjà utilisée.")
+
+ # Vérifier que le rôle existe
+ if not WebappRole.query.filter_by(name=role).first():
+ errors.append("Rôle invalide.")
+
+ if errors:
+ for msg in errors:
+ flash(msg, "error")
+ return redirect(url_for("users_list"))
+
+ # Créer l'utilisateur
+ user = WebappUser(
+ username=username,
+ email=email,
+ password_hash=generate_password_hash(password, method="scrypt"),
+ role=role,
+ )
+ db.session.add(user)
+ db.session.commit()
+
+ flash(f"Utilisateur « {username} » créé avec succès.", "success")
+ return redirect(url_for("users_list"))
+
+
+@webapp.route("/users/delete/", methods=["POST"])
+@require_page("users")
+def users_delete(user_id):
+ """Suppression d'un utilisateur (sauf soi-même)."""
+ from flask_login import current_user
+
+ user = WebappUser.query.get_or_404(user_id)
+
+ # Protection : impossible de se supprimer soi-même
+ if user.id == current_user.id:
+ flash("Vous ne pouvez pas supprimer votre propre compte.", "error")
+ return redirect(url_for("users_list"))
+
+ username = user.username
+ db.session.delete(user)
+ db.session.commit()
+
+ flash(f"Utilisateur « {username} » supprimé.", "success")
+ return redirect(url_for("users_list"))
diff --git a/webapp/youtube.py b/webapp/youtube.py
new file mode 100644
index 0000000..8d156d4
--- /dev/null
+++ b/webapp/youtube.py
@@ -0,0 +1,195 @@
+import re
+import requests
+from urllib.parse import urlencode
+from flask import render_template, request, redirect, url_for
+from webapp import webapp
+from webapp.auth import require_page, can_write_page
+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' ")
+@require_page("youtube")
+def toggleYouTube(id):
+ if not can_write_page("youtube"):
+ return render_template("403.html"), 403
+ 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/")
+@require_page("youtube")
+def openEditYouTube(id):
+ notification = YouTubeNotification.query.get_or_404(id)
+ channels = bot.getAllTextChannel()
+ msg = request.args.get('msg')
+ msg_type = request.args.get('type', 'info')
+ return render_template("youtube.html", notification=notification, channels=channels, notifications=YouTubeNotification.query.all(), msg=msg, msg_type=msg_type)
+
+
+@webapp.route("/youtube/edit/", methods=['POST'])
+@require_page("youtube")
+def submitEditYouTube(id):
+ if not can_write_page("youtube"):
+ return render_template("403.html"), 403
+ 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/")
+@require_page("youtube")
+def delYouTube(id):
+ if not can_write_page("youtube"):
+ return render_template("403.html"), 403
+ notification = YouTubeNotification.query.get_or_404(id)
+ db.session.delete(notification)
+ db.session.commit()
+ return redirect(url_for("openYouTube"))