From 4013d3d23da2656e06e086a32a9d0752bda53d07 Mon Sep 17 00:00:00 2001 From: ramforth Date: Mon, 17 Nov 2025 14:46:50 +0100 Subject: [PATCH] Addidng chat_listener function for Twitch, adding custom overlay template for testing. --- TASKS.md | 5 ++++- chat_listener.py | 23 ++++++++++++++++++++ listener_manager.py | 42 +++++++++++++++++++++++++++++++++++++ main.py | 20 ++++++++++++++++-- requirements.txt | 3 ++- templates/overlay_test.html | 30 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 chat_listener.py create mode 100644 listener_manager.py create mode 100644 templates/overlay_test.html diff --git a/TASKS.md b/TASKS.md index bc773b8..740f227 100644 --- a/TASKS.md +++ b/TASKS.md @@ -54,8 +54,11 @@ If you want to use emojis for visibility, here's some I have used: * **Goal:** The core magic. Start chat listeners for users and show messages in the overlay. * *(All tasks for this phase are on hold until Phase 2 is complete)* +### In Progress +* `[🧑‍🔧]` **3.1: Dynamic Listener Manager (The Big One):** Design a system (e.g., background service) to start/stop listener processes for users. @ramforth + + ### To Do -* `[ ]` **3.1: Dynamic Listener Manager (The Big One):** Design a system (e.g., background service) to start/stop listener processes for users. * `[ ]` **3.2: User-Specific Broadcasting:** Update the WebSocket system to use "rooms" (e.g., `/ws/{user_id}`) so users only get their *own* chat. * `[ ]` **3.3: Basic Overlay UI:** Create the `overlay.html` page that connects to the WebSocket and displays messages. diff --git a/chat_listener.py b/chat_listener.py new file mode 100644 index 0000000..11ce1b3 --- /dev/null +++ b/chat_listener.py @@ -0,0 +1,23 @@ +from twitchio.ext import commands + +class TwitchBot(commands.Bot): + def __init__(self, access_token: str, channel_name: str): + super().__init__( + token=access_token, + prefix='!', # A prefix is required but won't be used for reading chat + initial_channels=[channel_name] + ) + self.channel_name = channel_name + + async def event_ready(self): + """Called once when the bot goes online.""" + print(f"Listener ready for #{self.channel_name}") + + async def event_message(self, message): + """Runs every time a message is sent in chat.""" + if message.echo: + return # Ignore messages sent by the bot itself + + # For now, we just print the message to the console. + # Later, this will push messages to a queue for the WebSocket. + print(f"Message from {message.author.name} in #{self.channel_name}: {message.content}") \ No newline at end of file diff --git a/listener_manager.py b/listener_manager.py new file mode 100644 index 0000000..058ebcc --- /dev/null +++ b/listener_manager.py @@ -0,0 +1,42 @@ +import asyncio +from typing import Dict + +from chat_listener import TwitchBot +import security # To decrypt tokens + +class ListenerManager: + def __init__(self): + # This dictionary will hold our running listener tasks. + # The key will be the user_id and the value will be the asyncio.Task. + self.active_listeners: Dict[int, asyncio.Task] = {} + print("ListenerManager initialized.") + + async def start_listener_for_user(self, user): + """ + Starts a chat listener for a given user if one isn't already running. + """ + if user.id in self.active_listeners: + print(f"Listener for user {user.id} is already running.") + return + + print(f"Starting listener for user {user.id} ({user.username})...") + + tokens = security.decrypt_tokens(user.encrypted_tokens) + access_token = tokens['access_token'] + bot = TwitchBot(access_token=access_token, channel_name=user.username) + task = asyncio.create_task(bot.start()) + self.active_listeners[user.id] = task + + async def stop_listener_for_user(self, user_id: int): + """Stops a chat listener for a given user.""" + if user_id not in self.active_listeners: + print(f"No active listener found for user {user_id}.") + return + + print(f"Stopping listener for user {user_id}...") + task = self.active_listeners.pop(user_id) + task.cancel() + try: + await task + except asyncio.CancelledError: + print(f"Listener for user {user_id} successfully stopped.") \ No newline at end of file diff --git a/main.py b/main.py index fa6ccb5..f3de320 100644 --- a/main.py +++ b/main.py @@ -8,11 +8,12 @@ from contextlib import asynccontextmanager from sqlalchemy.orm import Session import models -from database import engine +from database import engine, SessionLocal import auth # Import the new auth module import schemas from starlette.responses import Response from config import settings # Import settings to get the secret key +from listener_manager import ListenerManager # --- Absolute Path Configuration --- # Get the absolute path of the directory where this file is located @@ -24,10 +25,25 @@ TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") async def lifespan(app: FastAPI): # This code runs on startup print("Application startup: Creating database tables...") + app.state.listener_manager = ListenerManager() models.Base.metadata.create_all(bind=engine) print("Application startup: Database tables created.") + + # Start listeners for all existing users + db = SessionLocal() + users = db.query(models.User).all() + db.close() + for user in users: + await app.state.listener_manager.start_listener_for_user(user) + yield - # Code below yield runs on shutdown, if needed + + # This code runs on shutdown + print("Application shutdown: Stopping all listeners...") + manager = app.state.listener_manager + # Create a copy of keys to avoid runtime errors from changing dict size + for user_id in list(manager.active_listeners.keys()): + await manager.stop_listener_for_user(user_id) app = FastAPI(lifespan=lifespan) diff --git a/requirements.txt b/requirements.txt index b33b740..724a68e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ python-dotenv itsdangerous jinja2 pydantic -python-jose[cryptography] \ No newline at end of file +python-jose[cryptography] +twitchio \ No newline at end of file diff --git a/templates/overlay_test.html b/templates/overlay_test.html new file mode 100644 index 0000000..461e5ff --- /dev/null +++ b/templates/overlay_test.html @@ -0,0 +1,30 @@ + + + + + + Live Test Overlay + + + +

Live Test Overlay

+
+ +
+ + + + \ No newline at end of file