Compare commits

..

2 Commits

Author SHA1 Message Date
3f2f0fcb4e eod: status 2025-11-17 19:08:36 +01:00
036e9da25e asyncio improvement 2025-11-17 18:43:32 +01:00
3 changed files with 25 additions and 29 deletions

View File

@@ -25,19 +25,11 @@ My core instructions are:
The objective is to build a multi-platform chat overlay SaaS (Software as a Service) for streamers. The service will aggregate chat from Twitch and YouTube into a single, customizable browser source for use in streaming software like OBS. The objective is to build a multi-platform chat overlay SaaS (Software as a Service) for streamers. The service will aggregate chat from Twitch and YouTube into a single, customizable browser source for use in streaming software like OBS.
### Current Status ### Current Status
The project is in **Phase 1: User Authentication & Database**. Most of this phase is complete. **Phases 1, 2, and 3 are complete.** The application is now a fully functional chat overlay service for Twitch.
* A solid FastAPI application skeleton is in place. * **Phase 1 (Authentication):** A secure Twitch OAuth2 flow is implemented, with user data and encrypted tokens stored in a SQLite database.
* The database schema (`User`, `Setting` models) is defined using SQLAlchemy and a SQLite database. * **Phase 2 (Dashboard & Configuration):** A dynamic user dashboard is available after login. It includes a theme switcher (light/dark), a theme selector for the overlay, and a full CRUD system for users to create and manage their own private CSS themes.
* A secure Twitch OAuth2 authentication flow is fully functional. It correctly: * **Phase 3 (Real-time Chat):** A decoupled background listener manager successfully starts `twitchio` listeners for each user. A WebSocket manager broadcasts incoming chat messages to the correct user's overlay in real-time.
1. Redirects users to Twitch.
2. Handles the callback.
3. Exchanges the authorization code for tokens.
4. Fetches user details from the Twitch API.
5. Encrypts the tokens using the `cryptography` library.
6. Saves or updates the user's record in the database.
* A basic HTML login page is served at the root URL (`/`). * A basic HTML login page is served at the root URL (`/`).
* Configuration and secrets are managed securely via a `config.py` file that reads from a `.env` file.
### Core Architecture ### Core Architecture
The project is built on the "hybrid architecture" detailed in the `RESEARCH_REPORT.md`: The project is built on the "hybrid architecture" detailed in the `RESEARCH_REPORT.md`:
* **Authentication:** Always use the official, secure OAuth2 flows for each platform. * **Authentication:** Always use the official, secure OAuth2 flows for each platform.
@@ -48,13 +40,6 @@ The project is built on the "hybrid architecture" detailed in the `RESEARCH_REPO
Based on the `TASKS.md` file, the only remaining task for Phase 1 is: Based on the `TASKS.md` file, the only remaining task for Phase 1 is:
* **Task 1.4: Basic Session Management:** After a user successfully logs in, we need to create a persistent session for them. This will allow us to "remember" who is logged in, protect routes like the future `/dashboard`, and provide a seamless user experience. The current flow correctly authenticates the user but does not yet establish this persistent session. * **Task 1.4: Basic Session Management:** After a user successfully logs in, we need to create a persistent session for them. This will allow us to "remember" who is logged in, protect routes like the future `/dashboard`, and provide a seamless user experience. The current flow correctly authenticates the user but does not yet establish this persistent session.
### Dashboard Progress (Phase 2)
We have started work on Phase 2, focusing on the user dashboard. The following features have been implemented on the `feature/dashboard-ui` branch:
* **Dynamic Dashboard:** The dashboard now uses the Jinja2 templating engine to dynamically display the logged-in user's username and their unique overlay URL.
* **Theme Selection:** A theme selection dropdown has been added to the dashboard, allowing users to choose between a "Dark Purple" and a "Bright Green" overlay theme.
* **Settings API:** A `/api/settings` endpoint has been created. This endpoint handles saving the user's chosen theme to the database.
* **Dynamic Overlay Theming:** The `/overlay/{user_id}` endpoint now dynamically loads the correct HTML template based on the user's saved theme preference, providing a personalized overlay.
## References: ## References:
### Development plan ### Development plan

View File

@@ -20,11 +20,12 @@ class TwitchBot(commands.Bot):
super().__init__( super().__init__(
token=access_token, token=access_token,
# Mandate: Explicitly disable the internal web server.
web_server_adapter=adapter, web_server_adapter=adapter,
eventsub_url=None, # Explicitly disable EventSub eventsub_url=None,
prefix='!', # A prefix is required but won't be used for reading chat prefix='!', # A prefix is required but won't be used for reading chat
initial_channels=[channel_name], initial_channels=[channel_name],
# These are required by twitchio for authentication # These are required by twitchio
client_id=client_id, client_id=client_id,
client_secret=client_secret, client_secret=client_secret,
# The 'bot_id' is the Twitch ID of the user account the bot is running as # The 'bot_id' is the Twitch ID of the user account the bot is running as
@@ -36,7 +37,7 @@ class TwitchBot(commands.Bot):
"""Called once when the bot goes online.""" """Called once when the bot goes online."""
print(f"Listener ready for #{self.channel_name}") print(f"Listener ready for #{self.channel_name}")
async def event_message(self, message): async def event_message(self, message): # Mandate: Type hint removed to prevent import errors.
"""Runs every time a message is sent in chat.""" """Runs every time a message is sent in chat."""
# This is CRITICAL when overriding a listener in commands.Bot # This is CRITICAL when overriding a listener in commands.Bot
# to ensure the bot can still process commands if any are added. # to ensure the bot can still process commands if any are added.

24
main.py
View File

@@ -1,4 +1,5 @@
import os import os
import asyncio
from fastapi import FastAPI, Request, Depends, HTTPException from fastapi import FastAPI, Request, Depends, HTTPException
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
@@ -23,6 +24,20 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, "static") STATIC_DIR = os.path.join(BASE_DIR, "static")
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
@asynccontextmanager
async def background_listener_startup(app: FastAPI):
"""A non-blocking task to start listeners after the app has started."""
print("Background task: Starting listeners for all users...")
db = SessionLocal()
users = db.query(models.User).all()
db.close()
for user in users:
# Use try/except to ensure one failing listener doesn't stop others
try:
await app.state.listener_manager.start_listener_for_user(user, app.state.websocket_manager)
except Exception as e:
print(f"ERROR: Failed to start listener for user {user.id} ({user.username}): {e}")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# This code runs on startup # This code runs on startup
@@ -32,13 +47,8 @@ async def lifespan(app: FastAPI):
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 # Decouple listener startup from the main application startup
db = SessionLocal() asyncio.create_task(background_listener_startup(app))
users = db.query(models.User).all()
db.close()
for user in users:
await app.state.listener_manager.start_listener_for_user(user, app.state.websocket_manager)
yield yield
# This code runs on shutdown # This code runs on shutdown