diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..696187f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/__pycache__/auth.cpython-313.pyc b/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..6878527 Binary files /dev/null and b/__pycache__/auth.cpython-313.pyc differ diff --git a/__pycache__/chat_listeners.cpython-313.pyc b/__pycache__/chat_listeners.cpython-313.pyc new file mode 100644 index 0000000..ddc0905 Binary files /dev/null and b/__pycache__/chat_listeners.cpython-313.pyc differ diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..a1b4728 Binary files /dev/null and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..1c6c0e5 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/auth.py b/auth.py index 3d724bc..850c706 100644 --- a/auth.py +++ b/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() diff --git a/chat_listeners.py b/chat_listeners.py new file mode 100644 index 0000000..81676c5 --- /dev/null +++ b/chat_listeners.py @@ -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()) diff --git a/database.py b/database.py index 6c09ba1..cbf4b39 100644 --- a/database.py +++ b/database.py @@ -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() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6071f69 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/index.html b/index.html new file mode 100644 index 0000000..c4fdf02 --- /dev/null +++ b/index.html @@ -0,0 +1,21 @@ + + + + MultiChat Overlay + + +

Chat Messages

+ + + + diff --git a/main.py b/main.py index 6303ab7..8c73251 100644 --- a/main.py +++ b/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"} diff --git a/multichat.db b/multichat.db new file mode 100644 index 0000000..1479c2b Binary files /dev/null and b/multichat.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3c044ba --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/uvicorn.log b/uvicorn.log new file mode 100644 index 0000000..e69de29