Ajout d'un système de shoutbox pour les modérateurs Twitch. Création d'un modèle de base de données pour les messages de shoutbox et implémentation d'endpoints pour envoyer, récupérer et effacer les messages. Mise à jour de l'interface utilisateur pour afficher les messages et permettre l'envoi via un formulaire.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -289,6 +289,29 @@
|
||||
</div>
|
||||
{% if not is_live %}</div>{% endif %}
|
||||
|
||||
<!-- Shoutbox modérateurs (IRC) -->
|
||||
<div class="section-card flex flex-col" style="height: 260px;">
|
||||
<div class="section-card-header flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-6a2 2 0 012-2h8z"></path></svg>
|
||||
<span>Shoutbox Modos</span>
|
||||
<span class="text-xs text-gray-400" id="shoutboxCount"></span>
|
||||
</div>
|
||||
<button onclick="clearShoutbox()" class="text-xs text-red-500 hover:text-red-600" title="Effacer">Effacer</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto font-mono text-xs p-2 bg-gray-950 dark:bg-gray-950 bg-opacity-95" id="shoutboxDisplay" style="scrollbar-width: thin;">
|
||||
<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 p-2 bg-gray-50 dark:bg-gray-800">
|
||||
<form onsubmit="sendShoutboxMessage(event)" class="flex gap-1.5">
|
||||
<span class="text-xs text-indigo-400 font-mono flex items-center">></span>
|
||||
<input type="text" id="shoutboxInput" placeholder="Message..." maxlength="500" autocomplete="off"
|
||||
class="flex-1 px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs font-mono focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<button type="submit" class="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded text-xs font-medium transition-colors">Envoyer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commandes de modération & Logs -->
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="section-card">
|
||||
@@ -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 = '<span class="text-gray-500">[' + shoutboxTime(item.created_at) + ']</span> ';
|
||||
|
||||
if (item.type === 'message') {
|
||||
var color = shoutboxColor(item.author);
|
||||
line.innerHTML = time +
|
||||
'<span style="color:' + color + '" class="font-semibold"><' + escapeHtml(item.author) + '></span> ' +
|
||||
'<span class="text-gray-200">' + escapeHtml(item.text) + '</span>';
|
||||
} 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 + '<span class="' + sanctionColor + ' font-semibold">' + escapeHtml(text) + '</span>';
|
||||
}
|
||||
|
||||
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 = '<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>';
|
||||
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();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user