Transition to Docker-based framework

This commit is contained in:
Jo Eskil
2025-11-13 17:27:18 +01:00
parent 2ce96386e3
commit 5475698fc5
14 changed files with 185 additions and 40 deletions

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
# 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"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

13
auth.py
View File

@@ -1,4 +1,4 @@
# auth.py
import os
from fastapi import APIRouter, Depends
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
@@ -6,13 +6,12 @@ import httpx
from database import get_db, User
from itsdangerous import URLSafeTimedSerializer
# IMPORTANT: These must be replaced with your actual Twitch application credentials
TWITCH_CLIENT_ID = "YOUR_TWITCH_CLIENT_ID"
TWITCH_CLIENT_SECRET = "YOUR_TWITCH_CLIENT_SECRET"
REDIRECT_URI = "http://localhost:8000/auth/twitch/callback"
# 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
# IMPORTANT: This should be a long, random string kept secret in a production environment
SECRET_KEY = "YOUR_SECRET_KEY"
serializer = URLSafeTimedSerializer(SECRET_KEY)
router = APIRouter()

65
chat_listeners.py Normal file
View File

@@ -0,0 +1,65 @@
# 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,10 +1,11 @@
import os
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./multichat.db"
DATABASE_URL = os.environ.get("DATABASE_URL")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
@@ -32,6 +33,3 @@ def get_db():
def create_tables():
Base.metadata.create_all(bind=engine)
if __name__ == "__main__":
create_tables()

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
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=YOUR_TWITCH_CLIENT_ID
- TWITCH_CLIENT_SECRET=YOUR_TWITCH_CLIENT_SECRET
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:

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!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>

35
main.py
View File

@@ -8,10 +8,16 @@ 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
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)
@@ -28,33 +34,6 @@ class SessionMiddleware(BaseHTTPMiddleware):
request.state.user = None
return response
app.add_middleware(SessionMiddleware)
def get_current_user(request: Request):
return request.state.user
app.include_router(auth_router, prefix="/auth") # Include the auth router
connected_clients = []
async def broadcast_message(message: dict):
# Convert the message dictionary to a JSON string before sending
message_json = json.dumps(message)
for client in connected_clients:
try:
await client.send_text(message_json)
except RuntimeError:
# Handle cases where client might have disconnected
connected_clients.remove(client)
@app.on_event("startup")
async def startup_event():
# Start chat listeners in the background
# Replace with actual video ID and Twitch token/channel
# For now, using placeholders. These will need to be configured.
asyncio.create_task(listen_youtube_chat("YOUR_YOUTUBE_VIDEO_ID", broadcast_message))
asyncio.create_task(listen_twitch_chat("YOUR_TWITCH_OAUTH_TOKEN", "YOUR_TWITCH_CHANNEL", broadcast_message))
@app.get("/")
async def read_root():
return {"Hello": "World"}

BIN
multichat.db Normal file

Binary file not shown.

37
requirements.txt Normal file
View File

@@ -0,0 +1,37 @@
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

0
uvicorn.log Normal file
View File