Files
MamieHenriette/webapp/twitch_moderation.py
T
Mow910andCursor b274dcaa51 Ajout de fonctionnalités majeures Twitch, Discord et interface web
Nouvelles fonctionnalités :
- Système de modération Twitch complet (bans, timeouts, avertissements)
- Filtre de liens intelligent pour Twitch avec whitelist/blacklist
- Notifications d'événements Twitch (follows, subs, raids, etc.)
- Système Freeloot Discord avec flux RSS dédié
- Salons automatiques Discord (création/suppression dynamique)
- Authentification utilisateur pour l'interface web (login/register)
- Gestion des utilisateurs et permissions
- Interface de modération Twitch dans la webapp
- Interface de gestion des événements Twitch
- Configuration des paramètres utilisateur
- Migration BDD pour les mots bannis

Améliorations de l'interface :
- Refonte complète des templates (configurations, commandes, humeurs, etc.)
- Nouvelle page de settings utilisateur
- Page de gestion des utilisateurs (admin)
- Page d'erreur 403 personnalisée
- Amélioration de la navigation et du design global
- Intégration de nouvelles sections dans le menu principal

Modifications techniques :
- Ajout de nouveaux modèles en base de données
- Extension des helpers database
- Mise à jour des dépendances (requirements.txt)
- Amélioration de la gestion des annonces Twitch
- Refactorisation du code pour meilleure maintenabilité

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 22:37:22 +01:00

404 lines
17 KiB
Python

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 <viewer> [minutes] [raison]",
"description": "Ejection temporaire d'un viewer (3 minutes par defaut) avec raison optionnelle",
"permission": "Moderateur"
},
{
"commands": ["!ban"],
"usage": "!ban <viewer1> [viewer2] ...",
"description": "Bannissement d'un ou plusieurs viewers (max 5)",
"permission": "Moderateur"
},
{
"commands": ["!unban"],
"usage": "!unban <viewer1> [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 <on/off>",
"description": "Active/desactive le mode Shield de Twitch",
"permission": "Moderateur"
},
{
"commands": ["!settitle"],
"usage": "!settitle <titre>",
"description": "Changement du titre du live",
"permission": "Moderateur"
},
{
"commands": ["!setgame", "!setcateg"],
"usage": "!setgame <jeu>",
"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 <alias> <on/off/toggle>",
"description": "Activer/desactiver/inverser une liste d'annonce par alias",
"permission": "Moderateur"
},
{
"commands": ["!no_game"],
"usage": "!no_game <on/off>",
"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 <viewer> [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/<int:word_id>")
@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