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>
193 lines
6.8 KiB
Python
193 lines
6.8 KiB
Python
import logging
|
|
import discord
|
|
|
|
from twitchAPI.twitch import Twitch
|
|
from twitchAPI.object.api import Stream
|
|
|
|
from database import db
|
|
from database.models import LiveAlert
|
|
from discordbot import bot
|
|
from webapp import webapp
|
|
|
|
logger = logging.getLogger('live-alert')
|
|
logger.setLevel(logging.INFO)
|
|
|
|
_live_alert_first_check = True
|
|
|
|
|
|
def _stream_thumbnail_url(stream: Stream) -> str:
|
|
"""URL de la miniature du stream (preview Twitch)."""
|
|
url = getattr(stream, 'thumbnail_url', None) or ''
|
|
if '{width}' in url or '{height}' in url:
|
|
url = url.replace('{width}', '320').replace('{height}', '180')
|
|
if not url:
|
|
url = f"https://static-cdn.jtvnw.net/previews-ttv/live_user_{stream.user_login}-320x180.jpg"
|
|
return url
|
|
|
|
|
|
def _format_embed_text(text: str, stream: Stream, stream_url: str, thumbnail: str) -> str:
|
|
"""Formate un texte d'embed avec les variables stream."""
|
|
if not text:
|
|
return ''
|
|
try:
|
|
return text.format(
|
|
user_login=stream.user_login or '',
|
|
user_name=stream.user_name or '',
|
|
game_name=getattr(stream, 'game_name', None) or '',
|
|
title=stream.title or '',
|
|
language=getattr(stream, 'language', None) or '',
|
|
stream_url=stream_url,
|
|
thumbnail=thumbnail,
|
|
)
|
|
except KeyError:
|
|
return text
|
|
|
|
|
|
async def checkOnlineStreamer(twitch: Twitch) :
|
|
global _live_alert_first_check
|
|
with webapp.app_context() :
|
|
alerts : list[LiveAlert] = LiveAlert.query.all()
|
|
streams = await _retreiveStreams(twitch, alerts)
|
|
watch_stream = None
|
|
|
|
# Récupération du statut du live principal (channel configuré)
|
|
from database.helpers import ConfigurationHelper
|
|
main_channel = ConfigurationHelper().getValue('twitch_channel')
|
|
main_stream = None
|
|
if main_channel:
|
|
main_stream = next((s for s in streams if s.user_login.lower() == main_channel.lower()), None)
|
|
|
|
# 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)
|
|
else:
|
|
webapp.config["BOT_STATUS"]["twitch_is_live"] = False
|
|
webapp.config["BOT_STATUS"]["twitch_viewer_count"] = 0
|
|
|
|
# Premier check : synchronisation sans notification
|
|
if _live_alert_first_check:
|
|
logger.info('Live Alert: première vérification, synchronisation sans notification')
|
|
for alert in alerts:
|
|
stream = next((s for s in streams if s.user_login == alert.login), None)
|
|
if stream:
|
|
alert.online = True
|
|
if alert.watch_activity and alert.enable:
|
|
watch_stream = stream
|
|
else:
|
|
alert.online = False
|
|
await _updateBotActivity(watch_stream)
|
|
db.session.commit()
|
|
_live_alert_first_check = False
|
|
return
|
|
|
|
# Vérifications normales ensuite
|
|
for alert in alerts :
|
|
stream = next((s for s in streams if s.user_login == alert.login), None)
|
|
if stream :
|
|
logger.info(f'Streamer en ligne : {alert.login}')
|
|
if not alert.online and alert.enable :
|
|
logger.info(f'N\'etait pas en ligne auparavant : {alert.login}')
|
|
await _notifyAlert(alert, stream)
|
|
alert.online = True
|
|
if alert.watch_activity and alert.enable:
|
|
watch_stream = stream
|
|
else :
|
|
logger.info(f'Streamer hors ligne : {alert.login}')
|
|
alert.online = False
|
|
|
|
await _updateBotActivity(watch_stream)
|
|
db.session.commit()
|
|
|
|
|
|
async def _updateBotActivity(stream: Stream | None):
|
|
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)
|
|
else:
|
|
logger.info('Aucun stream à regarder, retour à l\'activité normale')
|
|
# Remettre une humeur aléatoire
|
|
from database.models import Humeur
|
|
import random
|
|
humeurs = Humeur.query.all()
|
|
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))
|
|
else:
|
|
# Si pas de humeur, remettre un statut par défaut
|
|
await bot.change_presence(status=discord.Status.online, activity=None)
|
|
|
|
async def _notifyAlert(alert: LiveAlert, stream: Stream):
|
|
stream_url = f'https://www.twitch.tv/{stream.user_login}'
|
|
thumbnail = _stream_thumbnail_url(stream)
|
|
|
|
# Message texte optionnel (avant l'embed)
|
|
message_text = None
|
|
if alert.message and alert.message.strip():
|
|
try:
|
|
message_text = alert.message.format(stream)
|
|
except KeyError:
|
|
message_text = alert.message
|
|
|
|
# Construction de l'embed Discord
|
|
try:
|
|
embed_color = int(alert.embed_color or '9146FF', 16)
|
|
except ValueError:
|
|
embed_color = 0x9146FF
|
|
|
|
embed_title = _format_embed_text(alert.embed_title, stream, stream_url, thumbnail) if alert.embed_title else (stream.title or f"{stream.user_name} est en live")
|
|
embed_description = _format_embed_text(alert.embed_description, stream, stream_url, thumbnail) if alert.embed_description else None
|
|
|
|
embed = discord.Embed(
|
|
title=embed_title,
|
|
url=stream_url,
|
|
color=embed_color
|
|
)
|
|
if embed_description:
|
|
embed.description = embed_description
|
|
|
|
author_name = _format_embed_text(alert.embed_author_name, stream, stream_url, thumbnail) if alert.embed_author_name else stream.user_name
|
|
user_id = getattr(stream, 'user_id', None)
|
|
author_icon = alert.embed_author_icon or (f"https://static-cdn.jtvnw.net/jtv_user_pictures/{user_id}-profile_image-70x70.png" if user_id else "https://static-cdn.jtvnw.net/ttv-favicon/favicon-32x32.png")
|
|
embed.set_author(name=author_name, icon_url=author_icon)
|
|
|
|
if alert.embed_thumbnail and thumbnail:
|
|
embed.set_thumbnail(url=thumbnail)
|
|
if alert.embed_image and thumbnail:
|
|
embed.set_image(url=thumbnail)
|
|
|
|
if alert.embed_footer:
|
|
footer_text = _format_embed_text(alert.embed_footer, stream, stream_url, thumbnail)
|
|
if footer_text:
|
|
embed.set_footer(text=footer_text)
|
|
|
|
logger.info(f'Envoi de notification live (embed) : {stream.user_login}')
|
|
bot.loop.create_task(_sendMessage(alert.notify_channel, message_text, embed))
|
|
|
|
async def _sendMessage(channel_id: int, message: str | None, embed: discord.Embed):
|
|
try:
|
|
discord_channel = bot.get_channel(channel_id)
|
|
if not discord_channel:
|
|
logger.error(f"Canal Discord {channel_id} introuvable")
|
|
return
|
|
if message and message.strip():
|
|
await discord_channel.send(content=message, embed=embed)
|
|
else:
|
|
await discord_channel.send(embed=embed)
|
|
logger.info('Notification live envoyée')
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'envoi de la notification live : {e}")
|
|
|
|
async def _retreiveStreams(twitch: Twitch, alerts: list[LiveAlert]) -> list[Stream]:
|
|
streams: list[Stream] = []
|
|
logger.info(f'Recherche de streams pour : {alerts}')
|
|
async for stream in twitch.get_streams(user_login=[alert.login for alert in alerts]):
|
|
streams.append(stream)
|
|
logger.info(f'Ces streams sont en ligne : {streams}')
|
|
return streams
|