diff --git a/database/models.py b/database/models.py index 0234412..2259340 100644 --- a/database/models.py +++ b/database/models.py @@ -227,3 +227,11 @@ class TwitchBannedWord(db.Model): timeout_duration = db.Column(db.Integer, default=60) # durée du timeout en secondes created_at = db.Column(db.DateTime, default=datetime.utcnow) + +class ModShoutboxMessage(db.Model): + __tablename__ = 'mod_shoutbox_message' + id = db.Column(db.Integer, primary_key=True) + author = db.Column(db.String(64), nullable=False) + message = db.Column(db.String(500), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + diff --git a/database/schema.sql b/database/schema.sql index 152dc93..3a3ff8b 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -198,3 +198,10 @@ CREATE TABLE IF NOT EXISTS `twitch_event_notification` ( embed_thumbnail BOOLEAN NOT NULL DEFAULT TRUE, last_clip_id VARCHAR(128) NULL ); + +CREATE TABLE IF NOT EXISTS `mod_shoutbox_message` ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + `author` VARCHAR(64) NOT NULL, + `message` VARCHAR(500) NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/webapp/templates/twitch-moderation.html b/webapp/templates/twitch-moderation.html index e795a70..956245c 100644 --- a/webapp/templates/twitch-moderation.html +++ b/webapp/templates/twitch-moderation.html @@ -289,6 +289,29 @@ {% if not is_live %}{% endif %} + +
+
+
+ + Shoutbox Modos + +
+ +
+
+
Aucun message
+
+
+
+ > + + +
+
+
+
@@ -859,5 +882,139 @@ function saveEditCommand() { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeEditModal(); }); + +// ============================= +// Shoutbox modérateurs (IRC) +// ============================= +var shoutboxKnownIds = new Set(); +var shoutboxLastTimestamp = ''; +var shoutboxAutoScroll = true; + +var SHOUTBOX_COLORS = [ + '#6366f1', '#8b5cf6', '#ec4899', '#14b8a6', '#f59e0b', + '#3b82f6', '#10b981', '#ef4444', '#06b6d4', '#84cc16', +]; +var shoutboxColorMap = {}; + +function shoutboxColor(name) { + if (!shoutboxColorMap[name]) { + var hash = 0; + for (var i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); + shoutboxColorMap[name] = SHOUTBOX_COLORS[Math.abs(hash) % SHOUTBOX_COLORS.length]; + } + return shoutboxColorMap[name]; +} + +function shoutboxTime(isoStr) { + var d = new Date(isoStr); + return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0'); +} + +function addShoutboxItem(item) { + if (shoutboxKnownIds.has(item.id)) return; + shoutboxKnownIds.add(item.id); + + var display = document.getElementById('shoutboxDisplay'); + var ph = document.getElementById('shoutboxPlaceholder'); + if (ph) ph.remove(); + + var line = document.createElement('div'); + line.className = 'py-0.5 leading-relaxed'; + + var time = '[' + shoutboxTime(item.created_at) + '] '; + + if (item.type === 'message') { + var color = shoutboxColor(item.author); + line.innerHTML = time + + '<' + escapeHtml(item.author) + '> ' + + '' + escapeHtml(item.text) + ''; + } else { + var actionUpper = (item.action || '').toUpperCase(); + var sanctionColor = 'text-red-400'; + if (['timeout', 'clean', 'permit'].indexOf(item.action) >= 0) sanctionColor = 'text-orange-400'; + if (['subon', 'suboff', 'emoteon', 'emoteoff', 'follon', 'folloff'].indexOf(item.action) >= 0) sanctionColor = 'text-blue-400'; + if (item.action === 'unban') sanctionColor = 'text-green-400'; + + var text = '*** ' + actionUpper; + if (item.moderator) text += ' par ' + item.moderator; + if (item.target) text += ' → ' + item.target; + if (item.details && item.details !== '-') text += ' (' + item.details + ')'; + text += ' ***'; + + line.innerHTML = time + '' + escapeHtml(text) + ''; + } + + display.appendChild(line); + + while (display.children.length > 200) display.removeChild(display.firstChild); + if (shoutboxAutoScroll) display.scrollTop = display.scrollHeight; +} + +function fetchShoutbox() { + var url = '{{ url_for("shoutbox_messages") }}'; + if (shoutboxLastTimestamp) url += '?since=' + encodeURIComponent(shoutboxLastTimestamp); + + fetch(url) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.items && data.items.length > 0) { + data.items.forEach(addShoutboxItem); + } + if (data.timestamp) shoutboxLastTimestamp = data.timestamp; + var cnt = document.getElementById('shoutboxCount'); + if (cnt) cnt.textContent = '(' + shoutboxKnownIds.size + ')'; + }) + .catch(function(e) { console.error('Shoutbox error:', e); }); +} + +function sendShoutboxMessage(event) { + event.preventDefault(); + var input = document.getElementById('shoutboxInput'); + var message = input.value.trim(); + if (!message) return; + + var btn = event.target.querySelector('button[type="submit"]'); + btn.disabled = true; + + fetch('{{ url_for("shoutbox_send") }}', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: message }) + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.success) { + input.value = ''; + fetchShoutbox(); + } else { + showNotification(data.error || 'Erreur shoutbox', 'error'); + } + }) + .catch(function() { showNotification('Erreur réseau', 'error'); }) + .finally(function() { btn.disabled = false; }); +} + +function clearShoutbox() { + if (!confirm('Effacer tous les messages de la shoutbox ?')) return; + fetch('{{ url_for("shoutbox_clear") }}') + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.success) { + var display = document.getElementById('shoutboxDisplay'); + display.innerHTML = '
Aucun message
'; + shoutboxKnownIds.clear(); + shoutboxLastTimestamp = ''; + showNotification('Shoutbox effacée', 'success'); + } + }) + .catch(function() { showNotification('Erreur réseau', 'error'); }); +} + +document.getElementById('shoutboxDisplay').addEventListener('scroll', function() { + shoutboxAutoScroll = this.scrollHeight - this.scrollTop <= this.clientHeight + 30; +}); + +setInterval(fetchShoutbox, 3000); +fetchShoutbox(); {% endblock %} diff --git a/webapp/twitch_moderation.py b/webapp/twitch_moderation.py index e573e11..48e2c65 100644 --- a/webapp/twitch_moderation.py +++ b/webapp/twitch_moderation.py @@ -2,7 +2,8 @@ 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.models import Commande, TwitchModerationLog, TwitchLinkFilter, TwitchBannedWord, ModShoutboxMessage +from flask_login import current_user from database.helpers import ConfigurationHelper from datetime import datetime, timedelta import asyncio @@ -483,3 +484,87 @@ def execute_moderation_action(): except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 + + +# ============================= +# Shoutbox modérateurs +# ============================= + +@webapp.route("/twitch-moderation/shoutbox/send", methods=['POST']) +@require_page("twitch_moderation") +def shoutbox_send(): + if not can_write_page("twitch_moderation"): + return jsonify({"success": False, "error": "Permission refusée"}), 403 + + data = request.get_json() + message = (data.get('message') or '').strip()[:500] + if not message: + return jsonify({"success": False, "error": "Message vide"}), 400 + + msg = ModShoutboxMessage( + author=current_user.username, + message=message, + created_at=datetime.now(), + ) + db.session.add(msg) + db.session.commit() + return jsonify({"success": True, "id": msg.id}) + + +@webapp.route("/twitch-moderation/shoutbox/messages") +@require_page("twitch_moderation") +def shoutbox_messages(): + since_str = request.args.get('since', '') + since = None + if since_str: + try: + since = datetime.fromisoformat(since_str) + except ValueError: + pass + + chat_query = ModShoutboxMessage.query + log_query = TwitchModerationLog.query + if since: + chat_query = chat_query.filter(ModShoutboxMessage.created_at > since) + log_query = log_query.filter(TwitchModerationLog.created_at > since) + + chat_msgs = chat_query.order_by(ModShoutboxMessage.created_at.desc()).limit(100).all() + log_msgs = log_query.order_by(TwitchModerationLog.created_at.desc()).limit(100).all() + + items = [] + for m in chat_msgs: + items.append({ + "type": "message", + "id": f"msg-{m.id}", + "author": m.author, + "text": m.message, + "created_at": m.created_at.isoformat() if m.created_at else '', + }) + for log in log_msgs: + items.append({ + "type": "sanction", + "id": f"log-{log.id}", + "action": log.action, + "moderator": log.moderator, + "target": log.target or '', + "details": log.details or '', + "created_at": log.created_at.isoformat() if log.created_at else '', + }) + + items.sort(key=lambda x: x["created_at"]) + items = items[-100:] + + return jsonify({ + "items": items, + "timestamp": datetime.now().isoformat(), + }) + + +@webapp.route("/twitch-moderation/shoutbox/clear") +@require_page("twitch_moderation") +def shoutbox_clear(): + if not can_write_page("twitch_moderation"): + return jsonify({"success": False, "error": "Permission refusée"}), 403 + ModShoutboxMessage.query.delete() + db.session.commit() + return jsonify({"success": True})