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>
138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
import logging
|
|
from datetime import datetime, timedelta
|
|
|
|
from twitchAPI.chat import Chat
|
|
from twitchAPI.twitch import Twitch
|
|
|
|
from database import db
|
|
from database.models import TwitchAnnouncement
|
|
from webapp import webapp
|
|
|
|
logger = logging.getLogger('twitch-announcements')
|
|
logger.setLevel(logging.INFO)
|
|
|
|
# Délai minimum entre deux annonces (quelle qu'elles soient) pour éviter le spam
|
|
MIN_DELAY_BETWEEN_ANNOUNCEMENTS_MINUTES = 5
|
|
|
|
_message_count: int = 0
|
|
_last_announcement_index: int = -1 # Pour la rotation round-robin
|
|
|
|
|
|
def incrementMessageCount():
|
|
global _message_count
|
|
_message_count += 1
|
|
|
|
|
|
def _getAndResetMessageCount() -> int:
|
|
global _message_count
|
|
count = _message_count
|
|
_message_count = 0
|
|
return count
|
|
|
|
|
|
async def _is_channel_live(twitch: Twitch, channel: str) -> bool:
|
|
"""Vérifie si la chaîne Twitch est actuellement en live."""
|
|
try:
|
|
async for _ in twitch.get_streams(user_login=[channel]):
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f'Impossible de vérifier le statut live de {channel}: {e}')
|
|
return False
|
|
|
|
|
|
async def checkAndSendAnnouncements(chat: Chat, channel: str, twitch: Twitch):
|
|
"""
|
|
Envoie une annonce en rotation parmi celles dont la périodicité est écoulée,
|
|
uniquement si la chaîne est en live et qu'il y a assez d'activité dans le chat.
|
|
Appelé périodiquement par le bot Twitch.
|
|
"""
|
|
global _last_announcement_index
|
|
|
|
with webapp.app_context():
|
|
if not await _is_channel_live(twitch, channel):
|
|
return
|
|
|
|
announcements: list[TwitchAnnouncement] = TwitchAnnouncement.query.filter_by(enable=True).order_by(TwitchAnnouncement.id).all()
|
|
if not announcements:
|
|
return
|
|
|
|
now = datetime.now()
|
|
|
|
# Ne pas envoyer si une annonce (quelle qu'elle soit) a été envoyée récemment
|
|
last_any = max((a.last_sent for a in announcements if a.last_sent), default=None)
|
|
if last_any and (now - last_any) < timedelta(minutes=MIN_DELAY_BETWEEN_ANNOUNCEMENTS_MINUTES):
|
|
return
|
|
|
|
# Filtrer les annonces dont la périodicité est écoulée
|
|
due = [a for a in announcements if _shouldSend(a, now)]
|
|
if not due:
|
|
return
|
|
|
|
# Rotation round-robin : chercher la prochaine annonce après la dernière envoyée
|
|
announcement = _selectNextAnnouncement(due, announcements)
|
|
if not announcement:
|
|
return
|
|
|
|
# Vérifier le nombre minimum de messages dans le chat
|
|
message_count = _getAndResetMessageCount()
|
|
if message_count < announcement.min_chat_messages:
|
|
logger.debug(f'Annonce "{announcement.name}" ignorée : seulement {message_count} messages (minimum requis : {announcement.min_chat_messages})')
|
|
return
|
|
|
|
try:
|
|
await _sendAnnouncement(chat, channel, announcement)
|
|
announcement.last_sent = now
|
|
_last_announcement_index = announcements.index(announcement)
|
|
db.session.commit()
|
|
logger.info(f'Annonce envoyée : {announcement.name} (après {message_count} messages)')
|
|
except Exception as e:
|
|
logger.error(f'Erreur lors de l\'envoi de l\'annonce "{announcement.name}": {e}')
|
|
|
|
|
|
def _selectNextAnnouncement(due: list[TwitchAnnouncement], all_announcements: list[TwitchAnnouncement]) -> TwitchAnnouncement | None:
|
|
"""
|
|
Sélectionne la prochaine annonce selon un système de rotation round-robin.
|
|
Cherche la première annonce éligible après la dernière envoyée.
|
|
"""
|
|
global _last_announcement_index
|
|
|
|
if not due:
|
|
return None
|
|
|
|
# Si c'est la première annonce ou si l'index est invalide, prendre la première de la liste
|
|
if _last_announcement_index == -1 or _last_announcement_index >= len(all_announcements):
|
|
return due[0]
|
|
|
|
# Chercher la prochaine annonce éligible après la dernière envoyée
|
|
start_index = _last_announcement_index + 1
|
|
|
|
# Parcourir depuis l'index suivant jusqu'à la fin, puis revenir au début
|
|
for i in range(len(all_announcements)):
|
|
idx = (start_index + i) % len(all_announcements)
|
|
announcement = all_announcements[idx]
|
|
if announcement in due:
|
|
return announcement
|
|
|
|
# Fallback : retourner la première annonce éligible
|
|
return due[0]
|
|
|
|
|
|
def _shouldSend(announcement: TwitchAnnouncement, now: datetime) -> bool:
|
|
"""
|
|
Vérifie si une annonce doit être envoyée basée sur sa périodicité.
|
|
"""
|
|
if announcement.last_sent is None:
|
|
return True
|
|
|
|
time_since_last = now - announcement.last_sent
|
|
periodicity_delta = timedelta(minutes=announcement.periodicity)
|
|
|
|
return time_since_last >= periodicity_delta
|
|
|
|
|
|
async def _sendAnnouncement(chat: Chat, channel: str, announcement: TwitchAnnouncement):
|
|
"""
|
|
Envoie une annonce dans le chat Twitch.
|
|
"""
|
|
await chat.send_message(channel, announcement.text)
|