diff --git a/chat_listener.py b/chat_listener.py index 354e89a..aedc1a7 100644 --- a/chat_listener.py +++ b/chat_listener.py @@ -5,36 +5,28 @@ import models import security class TwitchBot(twitchio.Client): - def __init__(self, access_token: str, refresh_token: str, client_id: str, client_secret: str, channel_name: str, websocket_manager, db_user_id: int): + def __init__(self, websocket_manager, db_user_id: int): self.websocket_manager = websocket_manager # Store our application's database user ID to avoid conflict with twitchio's internal 'owner_id' self.db_user_id = db_user_id + self.is_initialized = False # Health check flag + + async def start(self, access_token: str, refresh_token: str, client_id: str, client_secret: str, channel_name: str): + """ + A custom start method that also handles initialization. This makes the + entire setup process an awaitable, atomic operation. + """ + print(f"DIAGNOSTIC: Initializing and connecting for user {self.db_user_id}...") # Per twitchio documentation, the token must be prefixed with 'oauth:'. - # This is a common cause of silent authentication failures. formatted_token = f"oauth:{access_token}" - - super().__init__( - token=formatted_token, - # The client_id and client_secret are required for the client to - # identify itself with Twitch's services. - client_id=client_id, - client_secret=client_secret, - refresh_token=refresh_token, - initial_channels=[channel_name], - ssl=True # Mandate: Explicitly use SSL for the IRC connection. - ) + + # The sensitive __init__ call is now inside the awaitable task. + super().__init__(token=formatted_token, client_id=client_id, client_secret=client_secret, + refresh_token=refresh_token, initial_channels=[channel_name], ssl=True) self.channel_name = channel_name - # Health Check: If __init__ completes successfully, this flag will exist. - # If super().__init__ fails silently, this line is never reached. self.is_initialized = True - async def start(self): - """ - A custom start method to bypass the default start() which can - unpredictably start a webserver. This gives us direct control. - """ - print(f"DIAGNOSTIC: Initiating secure connection for user {self.db_user_id}...") await self.connect() await self.join_channels([self.channel_name]) diff --git a/listener_manager.py b/listener_manager.py index ca57a87..a85bbd0 100644 --- a/listener_manager.py +++ b/listener_manager.py @@ -32,22 +32,20 @@ class ListenerManager: access_token = tokens['access_token'] refresh_token = tokens['refresh_token'] + # Initialize the bot object without credentials first. It's just a lightweight container. bot = TwitchBot( - access_token=access_token, - refresh_token=refresh_token, - channel_name=user.username, - client_id=settings.TWITCH_CLIENT_ID, - client_secret=settings.TWITCH_CLIENT_SECRET, websocket_manager=websocket_manager, db_user_id=user.id ) - - # Verify that the bot was initialized correctly before proceeding. - # This catches silent failures from the twitchio library. - if not getattr(bot, 'is_initialized', False): - raise Exception("TwitchBot failed to initialize, likely due to an invalid token.") - task = asyncio.create_task(bot.start()) + # Create a task that runs our new start method with all credentials. + # If super().__init__ fails inside bot.start(), the exception will be + # caught by our try/except block here, preventing hollow objects. + task = asyncio.create_task(bot.start( + access_token=access_token, refresh_token=refresh_token, + client_id=settings.TWITCH_CLIENT_ID, client_secret=settings.TWITCH_CLIENT_SECRET, + channel_name=user.username + )) # Store both the task and the bot instance for graceful shutdown self.active_listeners[user.id] = {"task": task, "bot": bot} except Exception as e: @@ -66,7 +64,8 @@ class ListenerManager: bot = listener_info["bot"] # Gracefully close the bot's connection - if bot and not bot.is_closed(): + # The getattr check prevents the shutdown crash if the bot was never initialized. + if bot and getattr(bot, 'is_initialized', False) and not bot.is_closed(): await bot.close() # Cancel the asyncio task