diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 556cc81..035e5be 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -36,7 +36,7 @@ The goal is to create a service where streamers can log in using their platform 6. **Help & Documentation:** Add a help page to guide users in creating their custom CSS. ### Phase 3: Dynamic Listeners & Basic Overlay -**Status: ⏳ Not Started** +**Status: ✔️ Complete** 1. **Dynamic Listener Manager:** Design and build a background service that starts and stops chat listener processes (`twitchio`, `pytchat`) based on user activity. 2. **Real-time Message Broadcasting:** Implement a WebSocket system within FastAPI to push chat messages to the correct user's overlay in real-time. diff --git a/TASKS.md b/TASKS.md index 740f227..71c1205 100644 --- a/TASKS.md +++ b/TASKS.md @@ -10,7 +10,7 @@ This file tracks all active development tasks. It is based on the official `DEVE 4. When your Pull Request is *merged*, move it to "Done." If you want to use emojis for visibility, here's some I have used: -✔️ - Done | 🧑🔧 - In progress | ↗️ - Task evolved (should correspond with an edit in the [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) +✔️ - Done | 🧑🔧 - In progress | ↗️ - Task evolved (should correspond with an edit in the [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) ) --- @@ -54,15 +54,13 @@ 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.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. --- +### Done +* `[✔️]` **3.1: Dynamic Listener Manager:** Designed and implemented a system to start/stop listener processes for users on application startup/shutdown. +* `[✔️]` **3.2: User-Specific Broadcasting:** Implemented a WebSocket manager and endpoint (`/ws/{user_id}`) to broadcast messages to the correct user's overlay. +* `[✔️]` **3.3: Basic Overlay UI:** Created dynamic overlay templates that connect to the WebSocket and display incoming chat messages. ## 💡 Backlog & Future Features diff --git a/chat_listener.py b/chat_listener.py index c525d68..8924d6d 100644 --- a/chat_listener.py +++ b/chat_listener.py @@ -1,8 +1,9 @@ from twitchio.ext import commands class TwitchBot(commands.Bot): - def __init__(self, access_token: str, client_id: str, client_secret: str, bot_id: str, channel_name: str): - super().__init__( + def __init__(self, access_token: str, client_id: str, client_secret: str, bot_id: str, channel_name: str, websocket_manager): + self.websocket_manager = websocket_manager + super().__init__( token=access_token, prefix='!', # A prefix is required but won't be used for reading chat initial_channels=[channel_name], @@ -25,7 +26,17 @@ class TwitchBot(commands.Bot): # The 'echo' check is temporarily disabled to allow self-messaging for testing. # 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 + + # Prepare the message data to be sent to the frontend + chat_data = { + "author": message.author.name, + "text": message.content, + "platform": "twitch" + } + + # Broadcast the message to the specific user's overlay + # We need the user's ID to know which WebSocket connection to send to. + # For now, we'll assume the bot instance is tied to a user ID. + # Let's find the user ID from the channel name. + user_id = self.owner_id + await self.websocket_manager.broadcast_to_user(user_id, chat_data) \ No newline at end of file diff --git a/listener_manager.py b/listener_manager.py index 8ea9925..165f024 100644 --- a/listener_manager.py +++ b/listener_manager.py @@ -12,7 +12,7 @@ class ListenerManager: self.active_listeners: Dict[int, asyncio.Task] = {} print("ListenerManager initialized.") - async def start_listener_for_user(self, user): + async def start_listener_for_user(self, user, websocket_manager): """ Starts a chat listener for a given user if one isn't already running. """ @@ -29,7 +29,8 @@ class ListenerManager: channel_name=user.username, client_id=settings.TWITCH_CLIENT_ID, client_secret=settings.TWITCH_CLIENT_SECRET, - bot_id=user.platform_user_id + bot_id=user.platform_user_id, + websocket_manager=websocket_manager ) task = asyncio.create_task(bot.start()) self.active_listeners[user.id] = task diff --git a/main.py b/main.py index f3de320..a682517 100644 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ import schemas from starlette.responses import Response from config import settings # Import settings to get the secret key from listener_manager import ListenerManager +from websocket_manager import WebSocketManager # --- Absolute Path Configuration --- # Get the absolute path of the directory where this file is located @@ -25,6 +26,7 @@ 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.websocket_manager = WebSocketManager() app.state.listener_manager = ListenerManager() models.Base.metadata.create_all(bind=engine) print("Application startup: Database tables created.") @@ -34,7 +36,7 @@ async def lifespan(app: FastAPI): users = db.query(models.User).all() db.close() for user in users: - await app.state.listener_manager.start_listener_for_user(user) + await app.state.listener_manager.start_listener_for_user(user, app.state.websocket_manager) yield @@ -170,4 +172,16 @@ async def get_custom_css(theme_id: int, db: Session = Depends(auth.get_db)): if not theme: raise HTTPException(status_code=404, detail="Custom theme not found") - return Response(content=theme.css_content, media_type="text/css") \ No newline at end of file + return Response(content=theme.css_content, media_type="text/css") + +@app.websocket("/ws/{user_id}") +async def websocket_endpoint(websocket: WebSocket, user_id: int): + manager = websocket.app.state.websocket_manager + await manager.connect(user_id, websocket) + try: + while True: + # Keep the connection alive + await websocket.receive_text() + except Exception: + manager.disconnect(user_id, websocket) + print(f"WebSocket for user {user_id} disconnected.") \ No newline at end of file diff --git a/templates/overlay-hacker-green.html b/templates/overlay-hacker-green.html index d8dd93a..3f77801 100644 --- a/templates/overlay-hacker-green.html +++ b/templates/overlay-hacker-green.html @@ -4,38 +4,47 @@