Cleaned project directory and added new development plan for simplified stack

This commit is contained in:
Jo Eskil
2025-11-13 23:43:39 +01:00
parent 384d5364a8
commit 2ed89fecbc
21 changed files with 0 additions and 513 deletions

1
.gitignore vendored
View File

@@ -1 +0,0 @@
.gemini/

View File

@@ -1,17 +0,0 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the requirements file into the container
COPY requirements.txt .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code into the container
COPY . .
# Command to run the application
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000", "main:app"]

View File

@@ -1,38 +0,0 @@
# Project Progress Report
This document summarizes the key steps and decisions made during the initial setup and planning of the MultiChat Overlay project.
## 1. Initial Setup & Environment Assessment
- **Environment Assessed:** The initial container environment was assessed, and `python3` and `git` were confirmed to be installed.
- **Project Directory:** The project directory was created at `/home/joe/MultiChatOverlay`.
- **Collaboration:** The directory permissions were configured to allow for collaboration by setting the group to `collaborators` and permissions to `775`.
- **Git Repository:** The project was cloned from the Gitea repository at `https://gitea.ramforth.net/ramforth/MultiChatOverlay`.
## 2. Initial Development (Single-User Model)
- **Virtual Environment:** A Python virtual environment was created at `/home/joe/MultiChatOverlay/venv`.
- **Dependencies:** Initial Python dependencies (`fastapi`, `uvicorn`, `websockets`, `pytchat`, `twitchio`) were installed.
- **Basic Framework:** A basic FastAPI application was created with a WebSocket endpoint and a simple HTML overlay.
- **Chat Listeners:** Placeholder chat listener modules were created for YouTube and Twitch.
## 3. Pivot to a Multi-User Service
- **New Requirement:** The project direction was updated to create a multi-user, web-based service where users can log in with their streaming accounts.
- **Revised Development Plan:** A new development plan (`DEVELOPMENT_PLAN.md` v3) was created to reflect this change. The new plan focuses on user authentication, a database, and dynamic management of chat listeners.
- **Plan Synced:** The revised development plan was pushed to the Gitea repository.
## 4. Implementation of the Multi-User Framework
- **Database:** A SQLite database was initialized, and a `users` table schema was defined using SQLAlchemy.
- **Authentication:** A placeholder Twitch OAuth2 implementation was created in `auth.py`.
- **Login Frontend:** A `login.html` page and a corresponding FastAPI endpoint were created.
- **Session Management:** Basic session management using signed cookies was implemented to keep users logged in.
- **Dashboard:** A protected `/dashboard` endpoint and a simple `dashboard.html` page were created.
- **Usage Guide:** A `USAGE.md` file was created to document the login process for end-users.
- **Code Synced:** All changes for the initial multi-user framework were pushed to the Gitea repository.
## 5. Pivot to a Production-Ready Docker-Based Framework
- **New Requirement:** The need for a more robust, scalable, and easily accessible framework was identified.
- **Revised Plan:** A new plan was created to use Docker Compose to manage the application services.
- **Docker Compose Plan:**
- A `web` service for the FastAPI application.
- A `db` service using PostgreSQL.
- The use of `gunicorn` with `uvicorn` workers for the production server.
- The use of Docker volumes to allow for easy collaboration on frontend files.

View File

@@ -1,8 +0,0 @@
There is a need for a chat overlay in live streaming circles that allows streamers to read chat from several of the primary platforms. Twitch and Youtube at the very least.
The idea:
Use self hosted web server to present the user integration with account linking and setup instructions. Let the end user log in with Youtube and Twitch. Have preset templates for html overlay for OBS use.
Must have functions to highlight single messages, preferably from a dockable html browser dock in OBS studio.
This is a specific extension of project: /home/joe/Cloud9/Documents/Obisdian/projects/youtube-chat-webhook-v2

View File

@@ -1,30 +0,0 @@
# Multi-Platform Chat Overlay
A self-hosted tool to aggregate chat from multiple streaming platforms (YouTube, Twitch) and display it in a customizable OBS overlay. This project also provides a suite of tools for both the streamer (host) and their moderators to manage the chat.
## Core Features
* **Combined Chat:** Aggregates chat from YouTube and Twitch into a single feed.
* **OBS Overlay:** A customizable web-based overlay to display the chat in your stream.
* **Host Control Panel:** A dockable OBS panel for the streamer to:
* Highlight important messages to feature them on the overlay.
* View a queue of messages tagged by moderators.
* Dismiss queued messages after they have been addressed.
* **Moderator Panel:** A separate, secure web page for moderators to:
* View the combined chat feed.
* "Tag" or "Queue" important messages for the host's attention.
## Technology Stack
* **Backend:** Python 3.9+ with FastAPI and WebSockets.
* **Frontend:** HTML5, CSS3, and vanilla JavaScript.
* **Chat Libraries:** `pytchat` for YouTube and `TwitchIO` for Twitch.
## Getting Started
_(Instructions will be added here once the initial version is complete)_
1. Clone the repository.
2. Install Python dependencies.
3. Run the backend server.
4. Add the frontend components to OBS as Browser Sources/Docks.

View File

@@ -1,41 +0,0 @@
# MultiChat Overlay Usage Guide
This guide explains how to log in to the MultiChat Overlay service and connect your streaming accounts to generate a personalized chat overlay.
## 1. Logging In
To use the MultiChat Overlay, you first need to log in with your streaming platform account.
1. **Access the Login Page:** Open your web browser and navigate to the service's login page (e.g., `http://localhost:8000/login`).
2. **Choose Your Platform:** On the login page, you will see options to log in with different streaming platforms.
3. **Authorize MultiChat Overlay:**
* Click on the "Login with Twitch" button.
* You will be redirected to Twitch's authorization page. Review the permissions requested by MultiChat Overlay and click "Authorize" to grant access.
* After successful authorization, you will be redirected back to the MultiChat Overlay dashboard.
## 2. Connecting Services
Currently, only Twitch is supported for connection. YouTube integration will be added in a future update.
### Connecting Twitch
Once you have logged in with your Twitch account, your Twitch channel will automatically be connected. You can verify this on your dashboard.
## 3. Your Personalized Overlay
After logging in and connecting your services, you will find your unique overlay URL on the dashboard.
1. **Copy Overlay URL:** On the dashboard, locate and copy the provided "Overlay URL". This URL is unique to your account.
2. **Add to OBS (or other streaming software):**
* In OBS Studio, add a new "Browser Source".
* Paste your copied Overlay URL into the "URL" field of the Browser Source properties.
* Adjust the width and height of the browser source to match your desired overlay size.
* Click "OK".
Your chat overlay should now appear in your streaming software, displaying messages from your connected Twitch channel.
## 4. Future Integrations (Coming Soon)
* **YouTube Integration:** Connect your YouTube channel to aggregate chat from both platforms.
* **Customization Options:** Personalize the look and feel of your chat overlay directly from the dashboard.
* **Moderator & Host Panels:** Access dedicated panels for advanced moderation and stream management.

Binary file not shown.

Binary file not shown.

Binary file not shown.

87
auth.py
View File

@@ -1,87 +0,0 @@
import os
from fastapi import APIRouter, Depends
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
import httpx
from database import get_db, User
from itsdangerous import URLSafeTimedSerializer
# Get configuration from environment variables
TWITCH_CLIENT_ID = os.environ.get("TWITCH_CLIENT_ID")
TWITCH_CLIENT_SECRET = os.environ.get("TWITCH_CLIENT_SECRET")
SECRET_KEY = os.environ.get("SECRET_KEY")
REDIRECT_URI = "http://localhost:8000/auth/twitch/callback" # This will need to be updated for production
serializer = URLSafeTimedSerializer(SECRET_KEY)
router = APIRouter()
@router.get("/login/twitch")
async def login_with_twitch():
# Scopes required to get user's email and channel info
scopes = "user:read:email"
auth_url = (
f"https://id.twitch.tv/oauth2/authorize"
f"?client_id={TWITCH_CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope={scopes}"
)
return RedirectResponse(url=auth_url)
@router.get("/auth/twitch/callback")
async def auth_twitch_callback(code: str, db: Session = Depends(get_db)):
# Exchange the authorization code for an access token
token_url = "https://id.twitch.tv/oauth2/token"
token_data = {
"client_id": TWITCH_CLIENT_ID,
"client_secret": TWITCH_CLIENT_SECRET,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": REDIRECT_URI,
}
async with httpx.AsyncClient() as client:
token_response = await client.post(token_url, data=token_data)
token_json = token_response.json()
access_token = token_json.get("access_token")
refresh_token = token_json.get("refresh_token")
if not access_token:
return {"error": "Could not fetch access token"}
# Get user info from Twitch API
user_info_url = "https://api.twitch.tv/helix/users"
headers = {
"Authorization": f"Bearer {access_token}",
"Client-Id": TWITCH_CLIENT_ID,
}
user_response = await client.get(user_info_url, headers=headers)
user_data = user_response.json()["data"][0]
twitch_id = user_data["id"]
twitch_username = user_data["login"]
# Check if user exists in the database, otherwise create them
user = db.query(User).filter(User.twitch_id == twitch_id).first()
if not user:
user = User(
twitch_id=twitch_id,
twitch_username=twitch_username,
twitch_access_token=access_token, # TODO: Encrypt this
twitch_refresh_token=refresh_token, # TODO: Encrypt this
)
db.add(user)
db.commit()
db.refresh(user)
else:
# Update tokens for existing user
user.twitch_access_token = access_token # TODO: Encrypt this
user.twitch_refresh_token = refresh_token # TODO: Encrypt this
db.commit()
# Create a session cookie
response = RedirectResponse(url="/dashboard")
session_data = {"user_id": user.id}
session_cookie = serializer.dumps(session_data)
response.set_cookie(key="session", value=session_cookie)
return response

View File

@@ -1,65 +0,0 @@
# chat_listeners.py
import asyncio
from pytchat import LiveChat
from twitchio.ext import commands
# YouTube Chat Listener (Placeholder)
async def listen_youtube_chat(video_id: str, callback):
livechat = LiveChat(video_id=video_id)
while True:
try:
chatdata = await livechat.get().as_dict()
for c in chatdata['items']:
message = {
"platform": "youtube",
"author": c['author']['name'],
"message": c['message'],
"is_moderator": False # Placeholder
}
await callback(message)
except Exception as e:
print(f"YouTube chat error: {e}")
await asyncio.sleep(1) # Don't hammer the API
# Twitch Chat Listener (Placeholder)
class TwitchBot(commands.Bot):
def __init__(self, token: str, channel: str, callback):
super().__init__(token=token, prefix='!', initial_channels=[channel])
self.callback = callback
async def event_ready(self):
print(f'Logged in as | {self.nick}')
print(f'User ID is | {self.user_id}')
async def event_message(self, message):
if message.echo:
return
is_moderator = 'moderator' in message.tags and message.tags['moderator'] == '1'
chat_message = {
"platform": "twitch",
"author": message.author.name,
"message": message.content,
"is_moderator": is_moderator
}
await self.callback(chat_message)
async def listen_twitch_chat(token: str, channel: str, callback):
bot = TwitchBot(token, channel, callback)
await bot.start()
# Example usage (for testing purposes, not part of the main application flow)
async def main():
async def print_message(message):
print(f"[{message['platform']}] {message['author']}: {message['message']} (Mod: {message['is_moderator']})")
# Replace with actual YouTube video ID and Twitch token/channel
# asyncio.create_task(listen_youtube_chat("YOUR_YOUTUBE_VIDEO_ID", print_message))
# asyncio.create_task(listen_twitch_chat("YOUR_TWITCH_OAUTH_TOKEN", "YOUR_TWITCH_CHANNEL", print_message))
while True:
await asyncio.sleep(3600) # Keep main running
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>MultiChat Overlay Dashboard</title>
</head>
<body>
<h1>Welcome to your Dashboard</h1>
<p>This is your personalized dashboard. You can manage your connected accounts and configure your overlay here.</p>
<h2>Your Overlay URL</h2>
<p>Use this URL as a browser source in your streaming software:</p>
<pre id="overlay-url"></pre>
<script>
// In a real application, we would fetch the user's unique overlay URL
// from an API and display it here. For now, we'll just show a placeholder.
const overlayUrlElement = document.getElementById('overlay-url');
// This would be something like `http://localhost:8000/overlay/USER_ID`
overlayUrlElement.textContent = `http://localhost:8000/overlay/YOUR_USER_ID`;
</script>
</body>
</html>

View File

@@ -1,35 +0,0 @@
import os
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.environ.get("DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
twitch_id = Column(String, unique=True, index=True)
twitch_username = Column(String)
# We will store encrypted tokens
twitch_access_token = Column(String)
twitch_refresh_token = Column(String)
youtube_id = Column(String, unique=True, index=True, nullable=True)
youtube_username = Column(String, nullable=True)
youtube_access_token = Column(String, nullable=True)
youtube_refresh_token = Column(String, nullable=True)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def create_tables():
Base.metadata.create_all(bind=engine)

View File

@@ -1,30 +0,0 @@
version: '3.8'
services:
web:
build: .
command: gunicorn -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000 main:app
volumes:
- .:/app
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db/mydatabase
- SECRET_KEY=YOUR_SECRET_KEY # This should be a long, random string
- TWITCH_CLIENT_ID=ogxx1fhpxbg8g89rov6oswuxeup2pb
- TWITCH_CLIENT_SECRET=2660uqpk2e1leayhpwcu35a27zidmh
- REDIRECT_URI=https://multichat.ramforth.net/auth/twitch/callback
depends_on:
- db
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydatabase
volumes:
postgres_data:

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>MultiChat Overlay</title>
</head>
<body>
<h1>Chat Messages</h1>
<ul id="messages"></ul>
<script>
const messages = document.getElementById('messages');
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = function(event) {
const chatMessage = JSON.parse(event.data);
const messageElement = document.createElement('li');
messageElement.textContent = `[${chatMessage.platform}] ${chatMessage.author}: ${chatMessage.message}`;
messages.appendChild(messageElement);
};
</script>
</body>
</html>

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Login to MultiChat Overlay</title>
</head>
<body>
<h1>Login to MultiChat Overlay</h1>
<p>Connect your streaming accounts to get started.</p>
<a href="/auth/login/twitch">
<button>Login with Twitch</button>
</a>
<!-- YouTube login will be added in a later phase -->
</body>
</html>

67
main.py
View File

@@ -1,67 +0,0 @@
import asyncio
import json
from fastapi import FastAPI, WebSocket, Request, Depends
from fastapi.responses import HTMLResponse, RedirectResponse
from starlette.websockets import WebSocketDisconnect
from starlette.middleware.base import BaseHTTPMiddleware
from sqlalchemy.orm import Session
from chat_listeners import listen_youtube_chat, listen_twitch_chat
from auth import router as auth_router, serializer
from database import get_db, User, create_tables
app = FastAPI()
@app.on_event("startup")
async def startup_event():
create_tables()
# The chat listeners will be started dynamically based on user activity
# and not on application startup.
class SessionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
session_cookie = request.cookies.get("session")
if session_cookie:
try:
data = serializer.loads(session_cookie, max_age=3600 * 24 * 7) # 1 week
db = next(get_db())
user = db.query(User).filter(User.id == data["user_id"]).first()
request.state.user = user
except Exception:
request.state.user = None
else:
request.state.user = None
return response
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.get("/login", response_class=HTMLResponse)
async def get_login_page():
with open("login.html", "r") as f:
return f.read()
@app.get("/dashboard", response_class=HTMLResponse)
async def get_dashboard(user: User = Depends(get_current_user)):
if not user:
return RedirectResponse(url="/login")
with open("dashboard.html", "r") as f:
return f.read()
@app.get("/overlay", response_class=HTMLResponse)
async def get_overlay():
with open("index.html", "r") as f:
return f.read()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
connected_clients.append(websocket)
try:
while True:
# Keep the connection alive, or handle incoming messages if needed
await websocket.receive_text()
except WebSocketDisconnect:
connected_clients.remove(websocket)

Binary file not shown.

View File

@@ -1,37 +0,0 @@
aiohappyeyeballs==2.6.1
aiohttp==3.13.2
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.11.0
attrs==25.4.0
certifi==2025.11.12
click==8.3.0
fastapi==0.121.1
frozenlist==1.8.0
greenlet==3.2.4
gunicorn==23.0.0
h11==0.16.0
h2==4.3.0
hpack==4.1.0
httpcore==1.0.9
httpx==0.28.1
hyperframe==6.1.0
idna==3.11
itsdangerous==2.2.0
multidict==6.7.0
packaging==25.0
propcache==0.4.1
psycopg2-binary==2.9.11
pydantic==2.12.4
pydantic_core==2.41.5
pytchat==0.5.5
sniffio==1.3.1
SQLAlchemy==2.0.44
starlette==0.49.3
twitchio==3.1.0
typing-inspection==0.4.2
typing_extensions==4.15.0
uvicorn==0.38.0
websockets==15.0.1
yarl==1.22.0

View File