Compare commits
2 Commits
bdd8674645
...
3f2f0fcb4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f2f0fcb4e | |||
| 036e9da25e |
23
CONTEXT.md
23
CONTEXT.md
@@ -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
|
||||||
|
|||||||
@@ -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
24
main.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user