From afa27271fa39befbbae20a61084a79a7f9122f1c Mon Sep 17 00:00:00 2001 From: ramforth Date: Tue, 18 Nov 2025 01:33:19 +0100 Subject: [PATCH] Swapped twitch connector twitchio.Client --- FAILED_APPROACH_6.md | 61 ++++++++++++++++++++++++++++++++++++++++++++ chat_listener.py | 35 ++++--------------------- listener_manager.py | 7 +---- 3 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 FAILED_APPROACH_6.md diff --git a/FAILED_APPROACH_6.md b/FAILED_APPROACH_6.md new file mode 100644 index 0000000..a30dc3a --- /dev/null +++ b/FAILED_APPROACH_6.md @@ -0,0 +1,61 @@ +# Analysis of Failed Approach #6: `twitchio` Initialization Conflict + +## 1. Executive Summary + +This document outlines the reasons for the persistent failure to initialize the `twitchio` chat listeners within our FastAPI application. Our attempts have been caught in a cyclical error pattern, switching between a `TypeError` for a missing argument and an `OSError` for a port conflict. + +The root cause is a fundamental design conflict: we are attempting to use a high-level abstraction (`twitchio.ext.commands.Bot`) in a way it is not designed for. This class is architected as a **standalone application** that includes its own web server for handling Twitch EventSub. Our project requires a simple, "headless" IRC client component to be embedded within our existing FastAPI web server. The `commands.Bot` class is not this component, and our attempts to force it into this role have failed. + +**Conclusion:** Continuing to patch this approach is inefficient and unreliable. A new strategy is required. + +## 2. The Cyclical Error Pattern + +Our efforts have resulted in a loop between three primary, contradictory errors, demonstrating the library's conflicting internal states. + +### Error A: `OSError: [Errno 98] address already in use` + +* **Trigger:** Occurs with a default `twitchio.ext.commands.Bot` initialization. +* **Implication:** The library, by default, attempts to start its own `AiohttpAdapter` web server (e.g., on port 4343) for EventSub, which immediately conflicts with our main Uvicorn process or any other service. + +### Error B: `TypeError: Bot.__init__() missing 1 required keyword-only argument: 'bot_id'` + +* **Trigger:** Occurs when we successfully disable the internal web server using a `NullAdapter`. +* **Implication:** By disabling the web server, we seem to place the `Bot` into a different initialization path that now strictly requires the `bot_id` argument, which it previously did not. + +### Error C: Back to `OSError: [Errno 98] address already in use` + +* **Trigger:** Occurs when we satisfy Error B by providing the `bot_id` while the `NullAdapter` is active. +* **Implication:** This is the most critical failure. It demonstrates that providing the `bot_id` causes the library's constructor to **ignore our `NullAdapter`** and fall back to its default behavior of starting a web server, thus bringing us back to Error A. + +### Error D: `TypeError: Client.start() got an unexpected keyword argument 'web_server'` + +* **Trigger:** Occurred when we attempted to bypass the adapter system entirely and use `bot.start(web_server=False)`. +* **Implication:** This proves the `start()` method's API does not support this parameter, closing another potential avenue for controlling the library's behavior. + +## 3. The Homelab & Nginx Proxy Manager Conflict + +This architectural mismatch is especially problematic in our homelab environment using Nginx Proxy Manager. + +1. **Single Entry Point:** Our architecture is designed for a single entry point. Nginx Proxy Manager accepts all traffic on ports 80/443 and forwards it to a single backend service: our FastAPI application on port 8000. + +2. **Unwanted Second Service:** `twitchio`'s attempt to start a second web server on a different port (e.g., 4343) is fundamentally incompatible with this model. It forces us to treat our single Python application as two distinct backend services. + +3. **Unnecessary Complexity:** To make this work, we would have to configure Nginx Proxy Manager with complex location-based routing rules (e.g., route `/` to port 8000, but route `/eventsub` to port 4343). This is brittle, hard to maintain, and completely unnecessary for our goal, which is **IRC chat only**. + +4. **Port Conflicts:** In a managed homelab environment (using Docker, etc.), ports are explicitly allocated resources. A library that randomly tries to bind to an arbitrary port is an unstable and unpredictable component that will inevitably conflict with other services. + +## 4. Root Cause: Architectural Mismatch + +The `twitchio.ext.commands.Bot` class is a powerful, feature-rich tool designed for building **standalone bots**. It is not designed to be a simple, embeddable component within a larger web application that has its own server. + +Our application architecture requires a "headless" IRC client—a component that does nothing but connect to Twitch's chat servers and listen for messages. The `commands.Bot` class is not that component. It brings along a suite of other features, including its own web server, which we cannot reliably disable. + +Our attempts to "trick" the library into behaving like a simple client have failed because we are fighting against its core design: + +## 5. Recommendation: Pivot to a Low-Level Client + +We must abandon the use of `twitchio.ext.commands.Bot`. + +The correct path forward is to use the lower-level `twitchio.Client` class instead. This class is designed to be a more fundamental IRC client without the high-level command handling and, crucially, without the tightly coupled web server components. + +By switching to `twitchio.Client`, we can build a `TwitchBot` class that is truly "headless" and will integrate cleanly into our existing FastAPI application and `ListenerManager` without causing port conflicts or argument mismatches. This aligns our implementation with our architectural needs. \ No newline at end of file diff --git a/chat_listener.py b/chat_listener.py index ce16246..17ab34c 100644 --- a/chat_listener.py +++ b/chat_listener.py @@ -1,35 +1,14 @@ -from twitchio.ext import commands +import twitchio -class NullAdapter: - """A dummy web adapter that does nothing to prevent twitchio from starting a webserver.""" - async def start(self): - pass - - async def stop(self): - pass - -class TwitchBot(commands.Bot): - def __init__(self, access_token: str, client_id: str, client_secret: str, bot_id: str, channel_name: str, websocket_manager, db_user_id: int): +class TwitchBot(twitchio.Client): + def __init__(self, access_token: str, channel_name: str, 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 - # Force the bot to use our dummy adapter that does nothing. - # This is the definitive fix to prevent any port conflicts. - adapter = NullAdapter() - super().__init__( token=access_token, - # Mandate: Explicitly disable the internal web server. - web_server_adapter=adapter, - eventsub_url=None, - prefix='!', # A prefix is required but won't be used for reading chat - initial_channels=[channel_name], - # These are required by twitchio - client_id=client_id, - client_secret=client_secret, - ssl=True, # Mandate: Explicitly use SSL for the IRC connection. - bot_id=bot_id # This is required by the parent Bot class. + initial_channels=[channel_name] ) self.channel_name = channel_name @@ -38,11 +17,7 @@ class TwitchBot(commands.Bot): print(f"Listener ready for #{self.channel_name}") async def event_message(self, message): # Mandate: Type hint removed to prevent import errors. - """Runs every time a message is sent in chat.""" - # This is CRITICAL when overriding a listener in commands.Bot - # to ensure the bot can still process commands if any are added. - await super().event_message(message) - + """Runs every time a message is sent in chat.""" # Ignore messages sent by the bot itself to prevent loops. if message.echo: return diff --git a/listener_manager.py b/listener_manager.py index 75f4455..6a8b0d4 100644 --- a/listener_manager.py +++ b/listener_manager.py @@ -32,15 +32,10 @@ class ListenerManager: bot = TwitchBot( access_token=access_token, channel_name=user.username, - client_id=settings.TWITCH_CLIENT_ID, - client_secret=settings.TWITCH_CLIENT_SECRET, - bot_id=user.platform_user_id, websocket_manager=websocket_manager, db_user_id=user.id ) - # The `web_server=False` argument is the most direct way to prevent - # twitchio from attempting to start its own web server. - task = asyncio.create_task(bot.start(web_server=False)) + task = asyncio.create_task(bot.start()) self.active_listeners[user.id] = task async def stop_listener_for_user(self, user_id: int):