Files
MamieHenriette/twitchbot/__init__.py
T
Mow910andCursor b274dcaa51 Ajout de fonctionnalités majeures Twitch, Discord et interface web
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>
2026-02-12 22:37:22 +01:00

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()