Addidng chat_listener function for Twitch, adding custom overlay template for testing.

This commit is contained in:
2025-11-17 14:46:50 +01:00
parent 98cda57d90
commit 4013d3d23d
6 changed files with 119 additions and 4 deletions

View File

@@ -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. * **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)* * *(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 ### 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.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. * `[ ]` **3.3: Basic Overlay UI:** Create the `overlay.html` page that connects to the WebSocket and displays messages.

23
chat_listener.py Normal file
View File

@@ -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}")

42
listener_manager.py Normal file
View File

@@ -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.")

20
main.py
View File

@@ -8,11 +8,12 @@ from contextlib import asynccontextmanager
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import models import models
from database import engine from database import engine, SessionLocal
import auth # Import the new auth module import auth # Import the new auth module
import schemas import schemas
from starlette.responses import Response from starlette.responses import Response
from config import settings # Import settings to get the secret key from config import settings # Import settings to get the secret key
from listener_manager import ListenerManager
# --- Absolute Path Configuration --- # --- Absolute Path Configuration ---
# Get the absolute path of the directory where this file is located # 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): async def lifespan(app: FastAPI):
# This code runs on startup # This code runs on startup
print("Application startup: Creating database tables...") print("Application startup: Creating database tables...")
app.state.listener_manager = ListenerManager()
models.Base.metadata.create_all(bind=engine) models.Base.metadata.create_all(bind=engine)
print("Application startup: Database tables created.") 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 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) app = FastAPI(lifespan=lifespan)

View File

@@ -7,4 +7,5 @@ python-dotenv
itsdangerous itsdangerous
jinja2 jinja2
pydantic pydantic
python-jose[cryptography] python-jose[cryptography]
twitchio

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Test Overlay</title>
<style>
body {
background-color: black;
color: white;
font-family: sans-serif;
margin: 0;
padding: 20px;
}
.message {
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>Live Test Overlay</h1>
<div id="chat-messages">
<!-- Messages will appear here -->
</div>
<script>
// JavaScript to connect to WebSocket and display messages will go here
</script>
</body>
</html>