EmperorFred/twitch_bot/bot.py

108 lines
4.6 KiB
Python
Raw Permalink Normal View History

2025-12-06 02:21:47 +00:00
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 "<unknown>"
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)