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>
262 lines
10 KiB
Python
262 lines
10 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
from twitchAPI.twitch import Twitch
|
|
from twitchAPI.type import AuthScope, ChatEvent
|
|
from twitchAPI.chat import Chat, ChatEvent, ChatMessage, EventData
|
|
|
|
from database.helpers import ConfigurationHelper
|
|
from database.models import Commande
|
|
|
|
|
|
def _user_has_twitch_permission(msg: ChatMessage, required: str) -> bool:
|
|
"""Vérifie si l'utilisateur a le niveau de permission requis (viewer, sub, vip, moderator)."""
|
|
if not required or required == 'viewer':
|
|
return True
|
|
is_broadcaster = msg.user.name.lower() == msg.room.name.lower()
|
|
is_mod = msg.user.mod or is_broadcaster
|
|
if required == 'moderator':
|
|
return is_mod
|
|
if required == 'vip':
|
|
return msg.user.vip or is_mod
|
|
if required == 'sub':
|
|
return msg.user.subscriber or msg.user.vip or is_mod
|
|
return True
|
|
from twitchbot.live_alert import checkOnlineStreamer
|
|
from twitchbot.announcements import checkAndSendAnnouncements, incrementMessageCount
|
|
from twitchbot import moderation
|
|
from twitchbot import link_filter
|
|
from twitchbot import event_notifications
|
|
from webapp import webapp
|
|
|
|
USER_SCOPE = [
|
|
AuthScope.CHAT_READ,
|
|
AuthScope.CHAT_EDIT,
|
|
AuthScope.MODERATOR_MANAGE_BANNED_USERS,
|
|
AuthScope.MODERATOR_MANAGE_CHAT_MESSAGES,
|
|
AuthScope.MODERATOR_MANAGE_CHAT_SETTINGS,
|
|
AuthScope.MODERATOR_MANAGE_SHIELD_MODE,
|
|
AuthScope.CHANNEL_MANAGE_BROADCAST,
|
|
AuthScope.MODERATOR_READ_FOLLOWERS,
|
|
AuthScope.CHANNEL_READ_SUBSCRIPTIONS, # EventSub channel.subscribe (notifs sub)
|
|
]
|
|
|
|
|
|
async def _onReady(ready_event: EventData):
|
|
logging.info('Bot Twitch prêt')
|
|
with webapp.app_context():
|
|
channel = ConfigurationHelper().getValue('twitch_channel')
|
|
webapp.config["BOT_STATUS"]["twitch_connected"] = True
|
|
webapp.config["BOT_STATUS"]["twitch_channel_name"] = channel
|
|
await ready_event.chat.join_room(channel)
|
|
# EventSub (follow, sub, raid) : besoin du broadcaster_id
|
|
try:
|
|
from twitchAPI.helper import first
|
|
user = await first(twitchBot.twitch.get_users(logins=[channel]))
|
|
if user:
|
|
twitchBot._eventsub = event_notifications.create_eventsub(twitchBot.twitch, asyncio.get_event_loop())
|
|
asyncio.create_task(event_notifications.register_eventsub_handlers(twitchBot._eventsub, user.id, ready_event.chat, channel))
|
|
except Exception as e:
|
|
logging.warning('EventSub non démarré: %s', e)
|
|
asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())
|
|
asyncio.get_event_loop().create_task(twitchBot._runAnnouncements())
|
|
asyncio.get_event_loop().create_task(twitchBot._checkClips())
|
|
|
|
|
|
async def _onMessage(msg: ChatMessage):
|
|
logging.info(f'Dans {msg.room.name}, {msg.user.name} a dit : {msg.text}')
|
|
incrementMessageCount()
|
|
|
|
# Stocker le message dans BOT_STATUS pour l'affichage web
|
|
with webapp.app_context():
|
|
from datetime import datetime
|
|
message_data = {
|
|
'username': msg.user.name,
|
|
'text': msg.text,
|
|
'timestamp': datetime.now().isoformat(),
|
|
'is_mod': msg.user.mod,
|
|
'is_subscriber': msg.user.subscriber,
|
|
'is_vip': msg.user.vip,
|
|
'color': getattr(msg.user, 'color', None) or '#9146FF'
|
|
}
|
|
messages = webapp.config["BOT_STATUS"]["twitch_chat_messages"]
|
|
messages.append(message_data)
|
|
# Limiter à 100 messages
|
|
if len(messages) > 100:
|
|
messages.pop(0)
|
|
|
|
if not await link_filter.check_message_for_links(msg, twitchBot.twitch):
|
|
return
|
|
if not await moderation.check_message_for_banned_words(msg, twitchBot.twitch):
|
|
return
|
|
await _handleCustomCommand(msg)
|
|
|
|
|
|
async def _handleCustomCommand(msg: ChatMessage):
|
|
if not msg.text.startswith('!'):
|
|
return
|
|
trigger = msg.text.split()[0].lower()
|
|
with webapp.app_context():
|
|
# Vérifier si les commandes Twitch sont activées globalement
|
|
if not ConfigurationHelper().getValue('twitch_commands_enable'):
|
|
return
|
|
commande = Commande.query.filter_by(trigger=trigger, twitch_enable=True).first()
|
|
if commande:
|
|
permission = commande.twitch_permission or 'viewer'
|
|
if not _user_has_twitch_permission(msg, permission):
|
|
return # Pas de réponse = l'utilisateur n'a pas la permission
|
|
response = commande.response.replace('{user}', msg.user.name)
|
|
await msg.reply(response)
|
|
|
|
|
|
async def _helloCommand(msg: ChatMessage):
|
|
await msg.reply(f'Bonjour {msg.user.name}')
|
|
|
|
|
|
def _isConfigured():
|
|
helper = ConfigurationHelper()
|
|
return (helper.getValue('twitch_client_id') is not None and
|
|
helper.getValue('twitch_client_secret') is not None and
|
|
helper.getValue('twitch_access_token') is not None and
|
|
helper.getValue('twitch_refresh_token') is not None and
|
|
helper.getValue('twitch_channel') is not None)
|
|
|
|
|
|
class TwitchBot():
|
|
_eventsub = None
|
|
|
|
async def _connect(self):
|
|
with webapp.app_context():
|
|
if _isConfigured():
|
|
try:
|
|
helper = ConfigurationHelper()
|
|
self.twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
|
|
await self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token'))
|
|
self.chat = await Chat(self.twitch)
|
|
self.chat.register_event(ChatEvent.READY, _onReady)
|
|
self.chat.register_event(ChatEvent.MESSAGE, _onMessage)
|
|
self.chat.register_command('hello', _helloCommand)
|
|
self._register_moderation_commands()
|
|
self.chat.start()
|
|
except Exception as e:
|
|
logging.error(f'Échec de l\'authentification Twitch : {e}')
|
|
else:
|
|
logging.info("Twitch n'est pas configuré")
|
|
|
|
def _register_moderation_commands(self):
|
|
# Créer des wrappers async pour chaque commande
|
|
async def cmd_timeout(msg): await moderation.timeout_command(msg, self.twitch)
|
|
async def cmd_ban(msg): await moderation.ban_command(msg, self.twitch)
|
|
async def cmd_unban(msg): await moderation.unban_command(msg, self.twitch)
|
|
async def cmd_clean(msg): await moderation.clean_command(msg, self.twitch)
|
|
async def cmd_shieldmode(msg): await moderation.shieldmode_command(msg, self.twitch)
|
|
async def cmd_settitle(msg): await moderation.settitle_command(msg, self.twitch)
|
|
async def cmd_setgame(msg): await moderation.setgame_command(msg, self.twitch)
|
|
async def cmd_subon(msg): await moderation.subon_command(msg, self.twitch)
|
|
async def cmd_suboff(msg): await moderation.suboff_command(msg, self.twitch)
|
|
async def cmd_follon(msg): await moderation.follon_command(msg, self.twitch)
|
|
async def cmd_folloff(msg): await moderation.folloff_command(msg, self.twitch)
|
|
async def cmd_emoteon(msg): await moderation.emoteon_command(msg, self.twitch)
|
|
async def cmd_emoteoff(msg): await moderation.emoteoff_command(msg, self.twitch)
|
|
async def cmd_ann(msg): await moderation.ann_command(msg, self.twitch)
|
|
async def cmd_no_game(msg): await moderation.no_game_command(msg, self.twitch)
|
|
async def cmd_multitwitch(msg): await moderation.multitwitch_command(msg, self.twitch)
|
|
async def cmd_permit(msg): await link_filter.permit_command(msg, self.twitch)
|
|
|
|
self.chat.register_command('kick', cmd_timeout)
|
|
self.chat.register_command('to', cmd_timeout)
|
|
self.chat.register_command('timeout', cmd_timeout)
|
|
self.chat.register_command('tm', cmd_timeout)
|
|
self.chat.register_command('ban', cmd_ban)
|
|
self.chat.register_command('unban', cmd_unban)
|
|
self.chat.register_command('clean', cmd_clean)
|
|
self.chat.register_command('shieldmode', cmd_shieldmode)
|
|
self.chat.register_command('settitle', cmd_settitle)
|
|
self.chat.register_command('setgame', cmd_setgame)
|
|
self.chat.register_command('setcateg', cmd_setgame)
|
|
self.chat.register_command('subon', cmd_subon)
|
|
self.chat.register_command('suboff', cmd_suboff)
|
|
self.chat.register_command('follon', cmd_follon)
|
|
self.chat.register_command('folloff', cmd_folloff)
|
|
self.chat.register_command('emoteon', cmd_emoteon)
|
|
self.chat.register_command('emoteoff', cmd_emoteoff)
|
|
self.chat.register_command('ann', cmd_ann)
|
|
self.chat.register_command('no_game', cmd_no_game)
|
|
self.chat.register_command('multitwitch', cmd_multitwitch)
|
|
self.chat.register_command('permit', cmd_permit)
|
|
|
|
async def _checkOnlineStreamers(self):
|
|
while True:
|
|
try:
|
|
await checkOnlineStreamer(self.twitch)
|
|
except Exception as e:
|
|
logging.error(f'Erreur check streamers online : {e}')
|
|
await asyncio.sleep(5 * 60)
|
|
|
|
async def _runAnnouncements(self):
|
|
with webapp.app_context():
|
|
channel = ConfigurationHelper().getValue('twitch_channel')
|
|
while True:
|
|
try:
|
|
await checkAndSendAnnouncements(self.chat, channel, self.twitch)
|
|
except Exception as e:
|
|
logging.error(f'Erreur envoi annonces : {e}')
|
|
await asyncio.sleep(2 * 60) # Vérification toutes les 2 min pour limiter le spam
|
|
|
|
async def _checkClips(self):
|
|
"""Vérifie le clip le plus récent (polling) et notifie si nouveau."""
|
|
with webapp.app_context():
|
|
channel = ConfigurationHelper().getValue('twitch_channel')
|
|
if not channel:
|
|
return
|
|
from twitchAPI.helper import first
|
|
try:
|
|
user = await first(self.twitch.get_users(logins=[channel]))
|
|
if not user:
|
|
return
|
|
except Exception:
|
|
return
|
|
while True:
|
|
try:
|
|
with webapp.app_context():
|
|
from database.models import TwitchEventNotification
|
|
cfg = TwitchEventNotification.query.filter_by(event_type='clip', enable=True).first()
|
|
if not cfg:
|
|
await asyncio.sleep(120)
|
|
continue
|
|
clip = await first(self.twitch.get_clips(broadcaster_id=user.id, first=1))
|
|
if not clip:
|
|
await asyncio.sleep(120)
|
|
continue
|
|
if cfg.last_clip_id is None:
|
|
with webapp.app_context():
|
|
from database import db
|
|
cfg = TwitchEventNotification.query.filter_by(event_type='clip', enable=True).first()
|
|
if cfg:
|
|
cfg.last_clip_id = clip.id
|
|
db.session.commit()
|
|
elif clip.id != cfg.last_clip_id:
|
|
with webapp.app_context():
|
|
await event_notifications.notify_clip(
|
|
self.chat,
|
|
channel,
|
|
user=getattr(clip, 'creator_name', None) or getattr(clip, 'user_name', '') or clip.id,
|
|
title=getattr(clip, 'title', '') or 'Clip',
|
|
url=getattr(clip, 'url', '') or f'https://clips.twitch.tv/{clip.id}',
|
|
thumbnail_url=getattr(clip, 'thumbnail_url', '') or '',
|
|
clip_id=clip.id,
|
|
)
|
|
except Exception as e:
|
|
logging.error('Erreur check clips: %s', e)
|
|
await asyncio.sleep(120)
|
|
|
|
def begin(self):
|
|
asyncio.run(self._connect())
|
|
|
|
async def _close(self):
|
|
self.chat.stop()
|
|
await self.twitch.close()
|
|
|
|
|
|
twitchBot = TwitchBot()
|