Enhance Twitch and Discord bot functionality by updating BOT_STATUS management. Added tracking for Twitch message timestamps and messages per minute, improved connection handling, and refined live status updates. Updated webapp to reflect new status attributes and modified Twitch moderation template for better chat message handling.
This commit is contained in:
@@ -84,6 +84,10 @@ class DiscordBot(discord.Client):
|
||||
|
||||
async def updateStatus(self):
|
||||
while not self.is_closed():
|
||||
bot_status = webapp.config.get("BOT_STATUS", {})
|
||||
if bot_status.get("twitch_is_live") or bot_status.get("discord_streaming_activity"):
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
humeurs = Humeur.query.all()
|
||||
if len(humeurs)>0 :
|
||||
humeur = random.choice(humeurs)
|
||||
|
||||
+37
-1
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from twitchAPI.twitch import Twitch
|
||||
from twitchAPI.type import AuthScope, ChatEvent
|
||||
@@ -71,6 +72,13 @@ async def _onMessage(msg: ChatMessage):
|
||||
# Stocker le message dans BOT_STATUS pour l'affichage web
|
||||
with webapp.app_context():
|
||||
from datetime import datetime
|
||||
now_ts = time.time()
|
||||
msg_timestamps = webapp.config["BOT_STATUS"].setdefault("twitch_msg_timestamps", [])
|
||||
msg_timestamps.append(now_ts)
|
||||
cutoff = now_ts - 60
|
||||
webapp.config["BOT_STATUS"]["twitch_msg_timestamps"] = [ts for ts in msg_timestamps if ts >= cutoff]
|
||||
webapp.config["BOT_STATUS"]["twitch_msg_per_minute"] = len(webapp.config["BOT_STATUS"]["twitch_msg_timestamps"])
|
||||
|
||||
message_data = {
|
||||
'username': msg.user.name,
|
||||
'text': msg.text,
|
||||
@@ -176,6 +184,19 @@ class TwitchBot():
|
||||
self.chat.start()
|
||||
except Exception as e:
|
||||
logging.error(f'Échec de l\'authentification Twitch : {e}')
|
||||
finally:
|
||||
webapp.config["BOT_STATUS"]["twitch_connected"] = False
|
||||
self._loop = None
|
||||
try:
|
||||
if hasattr(self, 'chat') and self.chat:
|
||||
self.chat.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self, 'twitch') and self.twitch:
|
||||
await self.twitch.close()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logging.info("Twitch n'est pas configuré")
|
||||
|
||||
@@ -287,7 +308,22 @@ class TwitchBot():
|
||||
await asyncio.sleep(120)
|
||||
|
||||
def begin(self):
|
||||
asyncio.run(self._connect())
|
||||
retry_delay = 15
|
||||
while True:
|
||||
try:
|
||||
if not _isConfigured():
|
||||
logging.info("Twitch non configuré, nouvelle tentative dans 60s")
|
||||
with webapp.app_context():
|
||||
webapp.config["BOT_STATUS"]["twitch_connected"] = False
|
||||
time.sleep(60)
|
||||
continue
|
||||
asyncio.run(self._connect())
|
||||
logging.warning("Session Twitch terminée, reconnexion dans %ss", retry_delay)
|
||||
except Exception as e:
|
||||
logging.error("Déconnexion/erreur Twitch: %s", e)
|
||||
with webapp.app_context():
|
||||
webapp.config["BOT_STATUS"]["twitch_connected"] = False
|
||||
time.sleep(retry_delay)
|
||||
|
||||
async def _close(self):
|
||||
self.chat.stop()
|
||||
|
||||
+57
-13
@@ -1,4 +1,6 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import discord
|
||||
|
||||
from twitchAPI.twitch import Twitch
|
||||
@@ -47,6 +49,8 @@ async def checkOnlineStreamer(twitch: Twitch) :
|
||||
global _live_alert_first_check
|
||||
with webapp.app_context() :
|
||||
alerts : list[LiveAlert] = LiveAlert.query.all()
|
||||
bot_status = webapp.config["BOT_STATUS"]
|
||||
was_live = bot_status.get("twitch_is_live", False)
|
||||
|
||||
try:
|
||||
streams = await _retreiveStreams(twitch, alerts)
|
||||
@@ -65,17 +69,37 @@ async def checkOnlineStreamer(twitch: Twitch) :
|
||||
|
||||
# Mise à jour du BOT_STATUS pour la webapp
|
||||
if main_stream:
|
||||
webapp.config["BOT_STATUS"]["twitch_is_live"] = True
|
||||
webapp.config["BOT_STATUS"]["twitch_viewer_count"] = getattr(main_stream, 'viewer_count', 0)
|
||||
webapp.config["BOT_STATUS"]["twitch_stream_title"] = getattr(main_stream, 'title', '') or ''
|
||||
webapp.config["BOT_STATUS"]["twitch_game_name"] = getattr(main_stream, 'game_name', '') or ''
|
||||
webapp.config["BOT_STATUS"]["twitch_started_at"] = main_stream.started_at.isoformat() if getattr(main_stream, 'started_at', None) else None
|
||||
bot_status["twitch_is_live"] = True
|
||||
bot_status["twitch_viewer_count"] = getattr(main_stream, 'viewer_count', 0)
|
||||
bot_status["twitch_stream_title"] = getattr(main_stream, 'title', '') or ''
|
||||
bot_status["twitch_game_name"] = getattr(main_stream, 'game_name', '') or ''
|
||||
bot_status["twitch_started_at"] = main_stream.started_at.isoformat() if getattr(main_stream, 'started_at', None) else None
|
||||
bot_status["twitch_ended_at"] = None
|
||||
bot_status["twitch_chat_clear_notice_sent"] = False
|
||||
else:
|
||||
webapp.config["BOT_STATUS"]["twitch_is_live"] = False
|
||||
webapp.config["BOT_STATUS"]["twitch_viewer_count"] = 0
|
||||
webapp.config["BOT_STATUS"]["twitch_stream_title"] = ""
|
||||
webapp.config["BOT_STATUS"]["twitch_game_name"] = ""
|
||||
webapp.config["BOT_STATUS"]["twitch_started_at"] = None
|
||||
bot_status["twitch_is_live"] = False
|
||||
bot_status["twitch_viewer_count"] = 0
|
||||
bot_status["twitch_stream_title"] = ""
|
||||
bot_status["twitch_game_name"] = ""
|
||||
bot_status["twitch_started_at"] = None
|
||||
if was_live and not bot_status.get("twitch_ended_at"):
|
||||
bot_status["twitch_ended_at"] = datetime.now().isoformat()
|
||||
if was_live and not bot_status.get("twitch_chat_clear_notice_sent"):
|
||||
messages = bot_status.setdefault("twitch_chat_messages", [])
|
||||
now_iso = datetime.now().isoformat()
|
||||
messages.append({
|
||||
'username': 'System',
|
||||
'text': 'Live terminé, ce chat sera vidé dans 1h.',
|
||||
'timestamp': now_iso,
|
||||
'is_mod': False,
|
||||
'is_subscriber': False,
|
||||
'is_vip': False,
|
||||
'color': '#22c55e',
|
||||
'panel_only': True,
|
||||
})
|
||||
if len(messages) > 100:
|
||||
messages.pop(0)
|
||||
bot_status["twitch_chat_clear_notice_sent"] = True
|
||||
|
||||
# Premier check : synchronisation sans notification
|
||||
if _live_alert_first_check:
|
||||
@@ -113,15 +137,27 @@ async def checkOnlineStreamer(twitch: Twitch) :
|
||||
|
||||
|
||||
async def _updateBotActivity(stream: Stream | None):
|
||||
if not bot.loop or bot.loop.is_closed():
|
||||
logger.warning("Loop Discord non disponible pour mise à jour de présence")
|
||||
return
|
||||
|
||||
if stream:
|
||||
logger.info(f'Mise à jour de l\'activité : Regarde le live de {stream.user_name}')
|
||||
activity = discord.Streaming(
|
||||
name=f'Regarde le live de {stream.user_name}',
|
||||
url=f'https://www.twitch.tv/{stream.user_login}'
|
||||
)
|
||||
await bot.change_presence(status=discord.Status.online, activity=activity)
|
||||
with webapp.app_context():
|
||||
webapp.config["BOT_STATUS"]["discord_streaming_activity"] = True
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
bot.change_presence(status=discord.Status.online, activity=activity),
|
||||
bot.loop
|
||||
)
|
||||
future.result(timeout=10)
|
||||
else:
|
||||
logger.info('Aucun stream à regarder, retour à l\'activité normale')
|
||||
with webapp.app_context():
|
||||
webapp.config["BOT_STATUS"]["discord_streaming_activity"] = False
|
||||
# Remettre une humeur aléatoire
|
||||
from database.models import Humeur
|
||||
import random
|
||||
@@ -129,10 +165,18 @@ async def _updateBotActivity(stream: Stream | None):
|
||||
if humeurs:
|
||||
humeur = random.choice(humeurs)
|
||||
logger.info(f'Réinitialisation du statut : {humeur.text}')
|
||||
await bot.change_presence(status=discord.Status.online, activity=discord.CustomActivity(humeur.text))
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
bot.change_presence(status=discord.Status.online, activity=discord.CustomActivity(humeur.text)),
|
||||
bot.loop
|
||||
)
|
||||
future.result(timeout=10)
|
||||
else:
|
||||
# Si pas de humeur, remettre un statut par défaut
|
||||
await bot.change_presence(status=discord.Status.online, activity=None)
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
bot.change_presence(status=discord.Status.online, activity=None),
|
||||
bot.loop
|
||||
)
|
||||
future.result(timeout=10)
|
||||
|
||||
async def _notifyAlert(alert: LiveAlert, stream: Stream):
|
||||
stream_url = f'https://www.twitch.tv/{stream.user_login}'
|
||||
|
||||
@@ -11,6 +11,7 @@ webapp.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in
|
||||
webapp.config["BOT_STATUS"] = {
|
||||
"discord_connected": False,
|
||||
"discord_guild_count": 0,
|
||||
"discord_streaming_activity": False,
|
||||
"twitch_connected": False,
|
||||
"twitch_channel_name": None,
|
||||
"twitch_is_live": False,
|
||||
@@ -18,6 +19,10 @@ webapp.config["BOT_STATUS"] = {
|
||||
"twitch_stream_title": "",
|
||||
"twitch_game_name": "",
|
||||
"twitch_started_at": None,
|
||||
"twitch_ended_at": None,
|
||||
"twitch_chat_clear_notice_sent": False,
|
||||
"twitch_msg_per_minute": 0,
|
||||
"twitch_msg_timestamps": [], # Unix timestamps des 60 dernières secondes
|
||||
"twitch_chat_messages": [], # Derniers messages du chat (max 100)
|
||||
"shoutbox_heartbeats": {}, # {"username": datetime} — présence des modos
|
||||
}
|
||||
|
||||
@@ -135,116 +135,69 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_live %}
|
||||
<!-- ===== LAYOUT EN LIVE ===== -->
|
||||
<!-- Ligne 1 : Player (2/3) | Infos stream dynamiques (1/3) -->
|
||||
<div class="grid lg:grid-cols-3 gap-4">
|
||||
<!-- Player Twitch (2/3) -->
|
||||
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4">
|
||||
<!-- Colonne 1 : Live Twitch -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col xl:h-[600px]">
|
||||
<div class="aspect-video bg-gray-900">
|
||||
<iframe
|
||||
src="https://player.twitch.tv/?channel={{ twitch_channel }}&enableExtensions=true&muted=false&parent={{ embed_parent }}&player=popout&quality=auto&volume=0.5"
|
||||
src="https://player.twitch.tv/?channel={{ twitch_channel }}&enableExtensions=true&muted={{ 'false' if is_live else 'true' }}&parent={{ embed_parent }}&player=popout&quality=auto&volume={{ '0.5' if is_live else '0' }}"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700 flex items-center justify-between">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
{% if is_live %}
|
||||
<span class="flex h-2 w-2 relative" id="liveIndicator">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white" id="liveStatusLabel">EN DIRECT</span>
|
||||
{% else %}
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-gray-400"></span>
|
||||
{% endif %}
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white" id="liveStatusLabel">{{ 'EN DIRECT' if is_live else 'HORS LIGNE' }}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400" id="playerViewerCount">• {{ viewer_count }} viewers</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<svg class="w-3 h-3" 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>
|
||||
Ouvrir sur Twitch
|
||||
Ouvrir
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Infos stream dynamiques (1/3) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4 flex flex-col gap-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-purple-500" 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>
|
||||
Infos du stream
|
||||
</h2>
|
||||
|
||||
<div class="space-y-3 flex-1">
|
||||
<!-- Titre -->
|
||||
<div class="p-3 space-y-2 text-sm overflow-y-auto">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Titre</div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white leading-snug" id="streamTitle">{{ stream_title or 'N/A' }}</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white leading-snug" id="streamTitle">{{ stream_title or 'N/A' }}</div>
|
||||
</div>
|
||||
<!-- Jeu / Catégorie -->
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Catégorie</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Catégorie</div>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300" id="streamGame">{{ game_name or 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Viewers</div>
|
||||
<div class="text-xl font-bold text-purple-600 dark:text-purple-400" id="streamViewers">{{ viewer_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Viewers -->
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Viewers</div>
|
||||
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400" id="streamViewers">{{ viewer_count }}</div>
|
||||
</div>
|
||||
<!-- Uptime -->
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">Uptime</div>
|
||||
<div class="text-sm font-mono font-medium text-gray-900 dark:text-white" id="streamUptime">--:--:--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="block w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm text-center">
|
||||
Voir sur Twitch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ligne 2 : Chat (pleine largeur) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col" style="height: 500px;">
|
||||
{% else %}
|
||||
<!-- ===== LAYOUT HORS LIVE ===== -->
|
||||
<div class="grid lg:grid-cols-2 gap-4">
|
||||
<!-- Player Twitch (offline) + infos -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="aspect-video bg-gray-900">
|
||||
<iframe
|
||||
src="https://player.twitch.tv/?channel={{ twitch_channel }}&enableExtensions=true&muted=true&parent={{ embed_parent }}&player=popout&quality=auto&volume=0"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-gray-400"></span>
|
||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400" id="liveStatusLabel">HORS LIGNE</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<svg class="w-3 h-3" 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>
|
||||
Ouvrir sur Twitch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat (1/2) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col" style="min-height: 480px; max-height: 600px;">
|
||||
{% endif %}
|
||||
<!-- Colonne 2 : Chat bot Twitch -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col xl:h-[600px]">
|
||||
|
||||
<!-- === Contenu du chat (commun live / hors live) === -->
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Chat en direct</h3>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Chat Twitch (bot)</h3>
|
||||
<span class="flex h-2 w-2 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}/chat" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<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>
|
||||
Ouvrir sur Twitch
|
||||
</a>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">Msg/min: <span id="msgPerMinValue" class="font-semibold text-purple-600 dark:text-purple-400">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 dark:bg-gray-900/50 p-4" id="chatDisplay">
|
||||
@@ -300,11 +253,25 @@
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Envoyé via le bot • <a href="https://www.twitch.tv/popout/{{ twitch_channel }}/chat" target="_blank" class="text-purple-600 dark:text-purple-400 hover:underline">Ouvrir le chat</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if not is_live %}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Shoutbox modérateurs (IRC) -->
|
||||
<div class="section-card flex flex-col" style="height: 260px;">
|
||||
<!-- Colonne 3 : Chat Twitch (compte personnel) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col xl:h-[600px]">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Chat Twitch (compte perso)</h3>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}/chat" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline">Ouvrir</a>
|
||||
</div>
|
||||
<div class="flex-1 bg-gray-900">
|
||||
<iframe
|
||||
src="https://www.twitch.tv/embed/{{ twitch_channel }}/chat?parent={{ embed_parent }}"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne 4 : Shoutbox modérateurs -->
|
||||
<div class="section-card flex flex-col xl:h-[600px]">
|
||||
<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>
|
||||
@@ -354,6 +321,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commandes de modération & Logs -->
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
@@ -659,6 +627,13 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeJsString(text) {
|
||||
return String(text || '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/\r?\n/g, ' ');
|
||||
}
|
||||
|
||||
function addMessageToDisplay(username, message, timestamp, badges, userColor) {
|
||||
var chatDisplay = document.getElementById('chatDisplay');
|
||||
var messagesContainer = chatDisplay.querySelector('.space-y-2');
|
||||
@@ -678,14 +653,19 @@ function addMessageToDisplay(username, message, timestamp, badges, userColor) {
|
||||
if (badges && badges.is_subscriber) badgeIcons += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400" title="Abonné">SUB</span>';
|
||||
|
||||
var safeUser = escapeHtml(username);
|
||||
var safeUserJs = escapeJsString(username);
|
||||
var safeMessageJs = escapeJsString(message);
|
||||
var modButtons = '<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity ml-auto flex-shrink-0">'
|
||||
+ '<button onclick="executeModerationAction(\'clean\', { username: \'' + safeUser + '\' })" class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 transition-colors" title="Supprimer les messages">'
|
||||
+ '<button onclick="transferToShoutbox(\'' + safeUserJs + '\', \'' + safeMessageJs + '\')" class="p-1 rounded hover:bg-indigo-100 dark:hover:bg-indigo-900/30 text-gray-400 hover:text-indigo-500 transition-colors" title="Transférer vers shoutbox">'
|
||||
+ '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h6m7-9l-5 5m0 0l5 5m-5-5h12"/></svg>'
|
||||
+ '</button>'
|
||||
+ '<button onclick="executeModerationAction(\'clean\', { username: \'' + safeUserJs + '\' })" class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 transition-colors" title="Supprimer les messages">'
|
||||
+ '<svg class="w-3.5 h-3.5" 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"/></svg>'
|
||||
+ '</button>'
|
||||
+ '<button onclick="promptTimeout(\'' + safeUser + '\')" class="p-1 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/30 text-gray-400 hover:text-yellow-500 transition-colors" title="Timeout">'
|
||||
+ '<button onclick="promptTimeout(\'' + safeUserJs + '\')" class="p-1 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/30 text-gray-400 hover:text-yellow-500 transition-colors" title="Timeout">'
|
||||
+ '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
||||
+ '</button>'
|
||||
+ '<button onclick="promptBan(\'' + safeUser + '\')" class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-600 transition-colors" title="Ban">'
|
||||
+ '<button onclick="promptBan(\'' + safeUserJs + '\')" class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-600 transition-colors" title="Ban">'
|
||||
+ '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>'
|
||||
+ '</button>'
|
||||
+ '</div>';
|
||||
@@ -725,6 +705,9 @@ function fetchChatMessages() {
|
||||
fetch('{{ url_for("get_twitch_messages") }}')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var msgPerMinEl = document.getElementById('msgPerMinValue');
|
||||
if (msgPerMinEl) msgPerMinEl.textContent = String(data.msg_per_min || 0);
|
||||
if (data.clear_chat) clearChatDisplay(data.clear_reason || 'Chat vidé automatiquement.');
|
||||
if (data.messages && data.messages.length > 0) {
|
||||
data.messages.forEach(function(msg) {
|
||||
addMessageToDisplay(msg.username, msg.text, msg.timestamp, { is_mod: msg.is_mod, is_vip: msg.is_vip, is_subscriber: msg.is_subscriber }, msg.color);
|
||||
@@ -734,6 +717,36 @@ function fetchChatMessages() {
|
||||
.catch(function(e) { console.error('Erreur chat:', e); });
|
||||
}
|
||||
|
||||
function clearChatDisplay(reason) {
|
||||
var chatDisplay = document.getElementById('chatDisplay');
|
||||
var messagesContainer = chatDisplay.querySelector('.space-y-2');
|
||||
if (!messagesContainer) return;
|
||||
displayedMessages.clear();
|
||||
messagesContainer.innerHTML = '';
|
||||
var notice = document.createElement('div');
|
||||
notice.className = 'text-center text-sm text-gray-500 dark:text-gray-400 py-8';
|
||||
notice.textContent = reason || 'Chat vidé.';
|
||||
messagesContainer.appendChild(notice);
|
||||
}
|
||||
|
||||
function transferToShoutbox(username, message) {
|
||||
fetch('{{ url_for("shoutbox_transfer") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: username, message: message })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
showNotification('Message transféré vers la shoutbox', 'success');
|
||||
fetchShoutbox();
|
||||
} else {
|
||||
showNotification(data.error || 'Erreur de transfert', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() { showNotification('Erreur réseau', 'error'); });
|
||||
}
|
||||
|
||||
setInterval(fetchChatMessages, 2000);
|
||||
fetchChatMessages();
|
||||
|
||||
|
||||
@@ -306,8 +306,31 @@ def send_twitch_message():
|
||||
@require_page("twitch_moderation")
|
||||
def get_twitch_messages():
|
||||
"""Retourne les derniers messages du chat Twitch"""
|
||||
messages = list(webapp.config["BOT_STATUS"].get("twitch_chat_messages", []))
|
||||
return jsonify({"messages": messages})
|
||||
bot_status = webapp.config["BOT_STATUS"]
|
||||
clear_chat = False
|
||||
clear_reason = None
|
||||
|
||||
ended_at_raw = bot_status.get("twitch_ended_at")
|
||||
if ended_at_raw:
|
||||
try:
|
||||
ended_at = datetime.fromisoformat(ended_at_raw)
|
||||
if datetime.now(ended_at.tzinfo) >= ended_at + timedelta(hours=1):
|
||||
if bot_status.get("twitch_chat_messages"):
|
||||
bot_status["twitch_chat_messages"] = []
|
||||
bot_status["twitch_msg_timestamps"] = []
|
||||
bot_status["twitch_msg_per_minute"] = 0
|
||||
clear_chat = True
|
||||
clear_reason = "Chat vidé automatiquement 1h après la fin du live."
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
messages = list(bot_status.get("twitch_chat_messages", []))
|
||||
return jsonify({
|
||||
"messages": messages,
|
||||
"msg_per_min": int(bot_status.get("twitch_msg_per_minute", 0)),
|
||||
"clear_chat": clear_chat,
|
||||
"clear_reason": clear_reason,
|
||||
})
|
||||
|
||||
|
||||
@webapp.route("/twitch-moderation/stream-info")
|
||||
@@ -321,6 +344,7 @@ def twitch_stream_info():
|
||||
"title": bot_status.get("twitch_stream_title", ""),
|
||||
"game_name": bot_status.get("twitch_game_name", ""),
|
||||
"started_at": bot_status.get("twitch_started_at"),
|
||||
"msg_per_min": int(bot_status.get("twitch_msg_per_minute", 0)),
|
||||
})
|
||||
|
||||
@webapp.route("/twitch-moderation/logs/poll")
|
||||
@@ -523,6 +547,29 @@ def shoutbox_send():
|
||||
return jsonify({"success": True, "id": msg.id})
|
||||
|
||||
|
||||
@webapp.route("/twitch-moderation/shoutbox/transfer", methods=['POST'])
|
||||
@require_page("twitch_moderation")
|
||||
def shoutbox_transfer():
|
||||
if not can_write_page("twitch_moderation"):
|
||||
return jsonify({"success": False, "error": "Permission refusée"}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
username = (data.get('username') or '').strip().lstrip('@')
|
||||
message = (data.get('message') or '').strip()
|
||||
if not username or not message:
|
||||
return jsonify({"success": False, "error": "Données incomplètes"}), 400
|
||||
|
||||
text = f"@{username}: {message}"
|
||||
msg = ModShoutboxMessage(
|
||||
author=current_user.username,
|
||||
message=text[:500],
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user