Swapped twitch connector twitchio.Client
This commit is contained in:
61
FAILED_APPROACH_6.md
Normal file
61
FAILED_APPROACH_6.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user