Attempting to add websocket support
This commit is contained in:
@@ -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.
|
||||
|
||||
12
TASKS.md
12
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
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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):
|
||||
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
|
||||
@@ -26,6 +27,16 @@ class TwitchBot(commands.Bot):
|
||||
# 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}")
|
||||
# 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)
|
||||
@@ -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
|
||||
|
||||
16
main.py
16
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
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.")
|
||||
@@ -4,38 +4,47 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
body {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: #00FF41;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 5px #00FF41, 0 0 10px #00FF41;
|
||||
background-color: transparent;
|
||||
color: #0f0;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 0 5px #0f0;
|
||||
}
|
||||
.chat-message {
|
||||
margin-bottom: 8px;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
}
|
||||
.username::after {
|
||||
content: ':~$ ';
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Example Message -->
|
||||
<div class="chat-message">
|
||||
<span class="username">system</span>
|
||||
<span class="message">Hacker Green Theme Initialized.</span>
|
||||
<div id="chat-container" class="chat-container">
|
||||
<!-- Messages will be injected here -->
|
||||
</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>
|
||||
</html>
|
||||
22
websocket_manager.py
Normal file
22
websocket_manager.py
Normal 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)
|
||||
Reference in New Issue
Block a user