import importlib import os from typing import Optional, Type, List from data.models.TwitchComponent import TwitchComponent from twitchio import eventsub from twitchio.ext import commands as twitch_commands import twitch_bot.config as cfg from libs.Db import Db class TwitchBot(twitch_commands.Bot): # def __init__(self, restart_controller=None, tokens: Optional[Dict[str, Dict[str, str]]] = None, db: Optional[Db] = None): def __init__(self, restart_controller=None, db: Optional[Db] = None): super().__init__( client_id=cfg.TWITCH_CLIENT_ID, client_secret=cfg.TWITCH_CLIENT_SECRET, bot_id=cfg.TWITCH_BOT_ID, owner_id=cfg.TWITCH_OWNER_ID, prefix=cfg.TWITCH_COMMAND_PREFIX ) # Store channel from config for reference/logging. TwitchIO v3 does not expose join_channels. self._initial_channel: Optional[str] = cfg.TWITCH_CHANNEL # Expose restart controller for commands (e.g., !restart) self.restart_controller = restart_controller self.db = db or Db(cfg.DB_CONN_STR, False) async def setup_hook(self) -> None: # Prefer explicit IDs from config; these should be numeric strings from Twitch. broadcaster_id = cfg.TWITCH_OWNER_ID bot_id = cfg.TWITCH_BOT_ID # If broadcaster_id is missing, we cannot subscribe. Ask the user to provide it. if not broadcaster_id: print("⚠️ TWITCH_OWNER_ID is not set. Please set it to your broadcaster (channel) numeric user ID.") if cfg.TWITCH_CHANNEL: print( "ℹ️ TWITCH_CHANNEL is set to '", cfg.TWITCH_CHANNEL, "'. Provide TWITCH_OWNER_ID for that channel to enable chat subscription.", ) return if not bot_id: print("⚠️ TWITCH_BOT_ID is not set. Please set it to the numeric user ID of the bot account.") return try: payload = eventsub.ChatMessageSubscription( broadcaster_user_id=broadcaster_id, user_id=bot_id, ) await self.subscribe_websocket(payload=payload) channel_hint = cfg.TWITCH_CHANNEL or "" print(f"✅ Subscribed to EventSub ChatMessage for broadcaster {broadcaster_id} (channel hint: {channel_hint})") except Exception as e: print(f"❌ Failed to subscribe to EventSub ChatMessage: {e}") async def load_commands(self) -> None: components: List[str] = [] package_name = "twitch_bot.commands" for filename in os.listdir(f"./{package_name.replace('.', '/')}"): if filename.endswith(".py") and not filename.startswith("_"): components.append(f"{package_name}.{filename[:-3]}") for cmp in components: try: module = importlib.import_module(cmp) except Exception as e: print(f"⚠️ Failed to import {cmp}: {e}") continue # Find a component class to load component_cls: Optional[Type[twitch_commands.Component]] = None for attr in module.__dict__.values(): if isinstance(attr, type) and issubclass(attr, twitch_commands.Component): component_cls = attr break if component_cls is None: print(f"⚠️ No Component found in {cmp}; skipping.") continue component_record = self.db.session.query(TwitchComponent).filter_by(name=cmp).first() if not component_record: self.db.session.add(TwitchComponent(name=cmp)) self.db.session.commit() component_record = self.db.session.query(TwitchComponent).filter_by(name=cmp).first() if component_record.active: instance = None try: instance = component_cls(self) # type: ignore[call-arg] except Exception as e: print(f"⚠️ Failed to instantiate {component_cls.__name__} in {cmp}: {e}") continue try: await self.add_component(instance) # type: ignore[arg-type] print(f"✅ Loaded Twitch component from {cmp}: {component_cls.__name__}") except Exception as e: print(f"⚠️ Failed to add component from {cmp}: {e}") else: print(f"⏭️ Component inactive in DB, skipping: {cmp}") print(f"bot commands:", self.commands)