Attempting to add websocket support

This commit is contained in:
2025-11-17 15:34:01 +01:00
parent 2fe07abecf
commit b60c642fd6
7 changed files with 92 additions and 37 deletions

View File

@@ -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. 6. **Help & Documentation:** Add a help page to guide users in creating their custom CSS.
### Phase 3: Dynamic Listeners & Basic Overlay ### 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. 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. 2. **Real-time Message Broadcasting:** Implement a WebSocket system within FastAPI to push chat messages to the correct user's overlay in real-time.

View File

@@ -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." 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: 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. * **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.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 ## 💡 Backlog & Future Features

View File

@@ -1,7 +1,8 @@
from twitchio.ext import commands from twitchio.ext import commands
class TwitchBot(commands.Bot): class TwitchBot(commands.Bot):
def __init__(self, access_token: str, client_id: str, client_secret: str, bot_id: str, channel_name: str): 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__( super().__init__(
token=access_token, token=access_token,
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
@@ -26,6 +27,16 @@ class TwitchBot(commands.Bot):
# if message.echo: # if message.echo:
# return # Ignore messages sent by the bot itself # return # Ignore messages sent by the bot itself
# For now, we just print the message to the console. # Prepare the message data to be sent to the frontend
# Later, this will push messages to a queue for the WebSocket. chat_data = {
print(f"Message from {message.author.name} in #{self.channel_name}: {message.content}") "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)

View File

@@ -12,7 +12,7 @@ class ListenerManager:
self.active_listeners: Dict[int, asyncio.Task] = {} self.active_listeners: Dict[int, asyncio.Task] = {}
print("ListenerManager initialized.") 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. Starts a chat listener for a given user if one isn't already running.
""" """
@@ -29,7 +29,8 @@ class ListenerManager:
channel_name=user.username, channel_name=user.username,
client_id=settings.TWITCH_CLIENT_ID, client_id=settings.TWITCH_CLIENT_ID,
client_secret=settings.TWITCH_CLIENT_SECRET, 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()) task = asyncio.create_task(bot.start())
self.active_listeners[user.id] = task self.active_listeners[user.id] = task

16
main.py
View File

@@ -14,6 +14,7 @@ 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 from listener_manager import ListenerManager
from websocket_manager import WebSocketManager
# --- 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
@@ -25,6 +26,7 @@ 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.websocket_manager = WebSocketManager()
app.state.listener_manager = ListenerManager() 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.")
@@ -34,7 +36,7 @@ async def lifespan(app: FastAPI):
users = db.query(models.User).all() users = db.query(models.User).all()
db.close() db.close()
for user in users: 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 yield
@@ -171,3 +173,15 @@ async def get_custom_css(theme_id: int, db: Session = Depends(auth.get_db)):
raise HTTPException(status_code=404, detail="Custom theme not found") raise HTTPException(status_code=404, detail="Custom theme not found")
return Response(content=theme.css_content, media_type="text/css") 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.")

View File

@@ -4,38 +4,47 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hacker Green Overlay</title> <title>Hacker Green Overlay</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<style> <style>
body { body {
background-color: rgba(0, 0, 0, 0); background-color: transparent;
color: #00FF41; color: #0f0;
font-family: 'Courier New', Courier, monospace; font-family: 'VT323', monospace;
font-size: 18px; font-size: 20px;
text-shadow: 0 0 5px #00FF41, 0 0 10px #00FF41;
margin: 0; margin: 0;
padding: 10px; padding: 10px;
overflow: hidden; overflow: hidden;
text-shadow: 0 0 5px #0f0;
} }
.chat-message { .chat-container {
margin-bottom: 8px; display: flex;
animation: fadeIn 0.5s ease-in-out; flex-direction: column;
gap: 5px;
} }
.username { .username {
font-weight: bold; font-weight: bold;
} }
.username::after {
content: ':~$ ';
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style> </style>
</head> </head>
<body> <body>
<!-- Example Message --> <div id="chat-container" class="chat-container">
<div class="chat-message"> <!-- Messages will be injected here -->
<span class="username">system</span>
<span class="message">Hacker Green Theme Initialized.</span>
</div> </div>
<script>
const userId = window.location.pathname.split('/')[2];
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws/${userId}`);
ws.onmessage = function(event) {
const messageData = JSON.parse(event.data);
const chatContainer = document.getElementById('chat-container');
const messageElement = document.createElement('div');
messageElement.innerHTML = `<span class="username">${messageData.author}:</span> ${messageData.text}`;
chatContainer.appendChild(messageElement);
};
</script>
</body> </body>
</html> </html>

22
websocket_manager.py Normal file
View File

@@ -0,0 +1,22 @@
from typing import Dict, List
from fastapi import WebSocket
class WebSocketManager:
def __init__(self):
# Maps user_id to a list of active WebSocket connections
self.active_connections: Dict[int, List[WebSocket]] = {}
print("WebSocketManager initialized.")
async def connect(self, user_id: int, websocket: WebSocket):
await websocket.accept()
if user_id not in self.active_connections:
self.active_connections[user_id] = []
self.active_connections[user_id].append(websocket)
def disconnect(self, user_id: int, websocket: WebSocket):
self.active_connections[user_id].remove(websocket)
async def broadcast_to_user(self, user_id: int, message: dict):
if user_id in self.active_connections:
for connection in self.active_connections[user_id]:
await connection.send_json(message)