Transition to Docker-based framework
This commit is contained in:
17
Dockerfile
Normal file
17
Dockerfile
Normal 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"]
|
||||
BIN
__pycache__/auth.cpython-313.pyc
Normal file
BIN
__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/chat_listeners.cpython-313.pyc
Normal file
BIN
__pycache__/chat_listeners.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/database.cpython-313.pyc
Normal file
BIN
__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-313.pyc
Normal file
BIN
__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
13
auth.py
13
auth.py
@@ -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
65
chat_listeners.py
Normal 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())
|
||||
@@ -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
29
docker-compose.yml
Normal 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
21
index.html
Normal 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
35
main.py
@@ -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
BIN
multichat.db
Normal file
Binary file not shown.
37
requirements.txt
Normal file
37
requirements.txt
Normal 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
0
uvicorn.log
Normal file
Reference in New Issue
Block a user