Compare commits
84 Commits
cdc5c32429
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
| ba1d486d07 | |||
| 0967983483 | |||
| 8829228295 | |||
| 8236d5e837 | |||
| f2ebde841d | |||
| cafb39d9df | |||
| 35c2210305 | |||
| 6ed78cf3fb | |||
| 3fb360aa77 | |||
| f15f339de8 | |||
| 603eb84d34 | |||
| 6a7c96a82c | |||
| 9b44c088b8 | |||
| 98c1417bf1 | |||
| c4edd88b71 | |||
| 3827744154 | |||
| 11254095ff | |||
| 264e4c276d | |||
| 60417d4594 | |||
| 8825421335 | |||
| e4df8d0e15 | |||
| 21f175af9a | |||
| aa5c63296a | |||
| 5931a0bdfe | |||
| 407a6447fd | |||
| 643f6f4272 | |||
| dab3fb4bb4 | |||
| 582a12fbe7 | |||
| 55672d4631 | |||
| ecd8518bd9 | |||
| 3f462e34a1 | |||
| a661ab0b9f | |||
| 2b6a4cc3ab | |||
| bd9e801042 | |||
| 761659fffb | |||
| 1a53135050 | |||
| 3d6e78b356 | |||
| 49dcf3b8a8 | |||
| 8c09a9ac82 | |||
| 7ee83501ba | |||
| 48747edb9c | |||
| 78da04fece | |||
| f6e2fedebc | |||
| 1ad2be8dea | |||
| 0475315bab | |||
| 36efa91e1f | |||
| 545ec0ad0f | |||
| fb551af208 | |||
| e31b6701db | |||
| 8678b508b7 | |||
| 9e4f7da306 | |||
| 4a0b08fa25 | |||
| 81338c1ecf | |||
| ce8dd866ec | |||
| f157c8e7df | |||
| 2265d24578 | |||
| b7c7ca6745 | |||
| c4dbb7eca9 | |||
| 7aa6b8a675 | |||
| ca096e8639 | |||
| 99c46f2e47 | |||
| bb70dcfa05 | |||
| be23f184c9 | |||
| d48188cfa7 | |||
| 098dc52248 | |||
| 2e6e605f24 | |||
| 1b9bf938d6 | |||
| 232d24fd10 | |||
| 8ff003e393 | |||
| ac87daec68 | |||
| d09e15ae86 | |||
| 873dc6e736 | |||
| 55c55a7055 | |||
| 2e19fd6f40 | |||
| 6aec15c408 | |||
| d702343db6 | |||
| 0eacb12c73 | |||
| 1186c91dcf | |||
| 56bf044bb6 | |||
| 4a951df300 | |||
| 8f16c2c4df | |||
| 4d2566b48a | |||
| 62ee9ad954 | |||
| e31ccbbb19 |
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# This is an example file. Copy it to .env and fill in your actual secrets.
|
||||||
|
# The .env file is ignored by Git and should NEVER be committed.
|
||||||
|
|
||||||
|
ENCRYPTION_KEY=your_32_byte_url_safe_base64_encoded_key_goes_here
|
||||||
|
|
||||||
|
TWITCH_CLIENT_ID=your_twitch_client_id_goes_here
|
||||||
|
TWITCH_CLIENT_SECRET=your_twitch_client_secret_goes_here
|
||||||
|
|
||||||
|
APP_BASE_URL=http://localhost:8000
|
||||||
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
.env/
|
||||||
|
|
||||||
|
# SQLite databases
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Personal notes - will not be tracked by git
|
||||||
|
personal_notes.md
|
||||||
|
|
||||||
|
# Local-only directory for untracked notes and context
|
||||||
|
local/
|
||||||
189
CONTEXT.md
Normal file
189
CONTEXT.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Gemini Code Assist: Project Context
|
||||||
|
|
||||||
|
This document outlines my core instructions and my current understanding of the MultiChatOverlay project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: My Core Instructions
|
||||||
|
|
||||||
|
My primary function is to act as a world-class senior software engineering assistant. My goal is to provide insightful answers that prioritize code quality, clarity, and adherence to best practices.
|
||||||
|
|
||||||
|
My core instructions are:
|
||||||
|
* **Persona:** Gemini Code Assist, a very experienced coding assistant.
|
||||||
|
* **Objective:** Answer questions thoroughly, review code, and suggest improvements.
|
||||||
|
* **Output Format:**
|
||||||
|
* Provide all code changes as diffs in the unified format, using full absolute file paths.
|
||||||
|
* Ensure all code blocks are valid and well-formatted.
|
||||||
|
* Suggest relevant next steps or prompts for our workflow.
|
||||||
|
* Maintain a conversational, accurate, and helpful tone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: My Understanding of the Project
|
||||||
|
|
||||||
|
### Project Goal
|
||||||
|
The objective is to build a multi-platform chat overlay SaaS (Software as a Service) for streamers. The service will aggregate chat from Twitch and YouTube into a single, customizable browser source for use in streaming software like OBS.
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
The project is in **Phase 1: User Authentication & Database**. Most of this phase is complete.
|
||||||
|
* A solid FastAPI application skeleton is in place.
|
||||||
|
* The database schema (`User`, `Setting` models) is defined using SQLAlchemy and a SQLite database.
|
||||||
|
* A secure Twitch OAuth2 authentication flow is fully functional. It correctly:
|
||||||
|
1. Redirects users to Twitch.
|
||||||
|
2. Handles the callback.
|
||||||
|
3. Exchanges the authorization code for tokens.
|
||||||
|
4. Fetches user details from the Twitch API.
|
||||||
|
5. Encrypts the tokens using the `cryptography` library.
|
||||||
|
6. Saves or updates the user's record in the database.
|
||||||
|
* A basic HTML login page is served at the root URL (`/`).
|
||||||
|
* Configuration and secrets are managed securely via a `config.py` file that reads from a `.env` file.
|
||||||
|
|
||||||
|
### Core Architecture
|
||||||
|
The project is built on the "hybrid architecture" detailed in the `RESEARCH_REPORT.md`:
|
||||||
|
* **Authentication:** Always use the official, secure OAuth2 flows for each platform.
|
||||||
|
* **Twitch Chat Ingestion:** Use the stable and scalable Twitch IRC protocol (via `twitchio`).
|
||||||
|
* **YouTube Chat Ingestion:** Use an unofficial, reverse-engineered "InnerTube" API (via `pytchat`). This is the primary technical risk of the project due to its fragility and will require careful implementation with proxy rotation and monitoring.
|
||||||
|
|
||||||
|
### Immediate Next Task
|
||||||
|
Based on the `TASKS.md` file, the only remaining task for Phase 1 is:
|
||||||
|
* **Task 1.4: Basic Session Management:** After a user successfully logs in, we need to create a persistent session for them. This will allow us to "remember" who is logged in, protect routes like the future `/dashboard`, and provide a seamless user experience. The current flow correctly authenticates the user but does not yet establish this persistent session.
|
||||||
|
|
||||||
|
## References:
|
||||||
|
|
||||||
|
### Development plan
|
||||||
|
```
|
||||||
|
# Multi-Platform Chat Overlay Development Plan (v4 - Simplified Stack)
|
||||||
|
|
||||||
|
This document outlines the development plan for a multi-user, web-based chat overlay service using a simplified technology stack.
|
||||||
|
|
||||||
|
## 1. Project Overview
|
||||||
|
|
||||||
|
The goal is to create a service where streamers can log in using their platform accounts (Twitch, YouTube), configure a personalized chat overlay, and use it in their streaming software (e.g., OBS). The service will aggregate chat from their connected accounts and provide moderation tools.
|
||||||
|
|
||||||
|
## 2. Technology Stack
|
||||||
|
|
||||||
|
* **Team Communications:** Discord and Nextcloud, primarily. This can change. There's a list of links in the [README.md](README.md)
|
||||||
|
* **Backend:** Python 3.13+ (FastAPI)
|
||||||
|
* **Database:** SQLite (for initial development) with SQLAlchemy ORM
|
||||||
|
* **Frontend:** Vanilla HTML, CSS, and JavaScript
|
||||||
|
* **Chat Listeners:** `twitchio` (Twitch), `pytchat` (YouTube)
|
||||||
|
|
||||||
|
## 3. Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: User Authentication & Database (FastAPI)
|
||||||
|
1. **Project Skeleton:** Establish the core FastAPI application structure, dependencies, and version control.
|
||||||
|
2. **Database Schema:** Define the data models for users and settings using SQLAlchemy.
|
||||||
|
3. **Twitch OAuth2:** Implement the server-side OAuth2 flow within FastAPI to authenticate users and securely store encrypted tokens in the database.
|
||||||
|
4. **Session Management:** Create a system to manage logged-in user sessions.
|
||||||
|
5. **Basic Frontend:** Develop a simple login page.
|
||||||
|
|
||||||
|
### Phase 2: User Dashboard & Configuration
|
||||||
|
1. **Dashboard UI:** Create a dashboard page accessible only to authenticated users.
|
||||||
|
2. **Settings API:** Build API endpoints for users to save and retrieve their overlay settings (e.g., custom CSS).
|
||||||
|
3. **Overlay URL Generation:** Display a unique, persistent overlay URL for each user on their dashboard.
|
||||||
|
|
||||||
|
### Phase 3: Dynamic Listeners & Basic Overlay
|
||||||
|
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.
|
||||||
|
3. **Basic Overlay UI:** Create the `overlay.html` page that connects to the WebSocket and renders incoming chat messages.
|
||||||
|
|
||||||
|
### Phase 4: Integration & Refinement
|
||||||
|
1. **YouTube Integration:** Implement the full YouTube OAuth2 flow and integrate the `pytchat` listener into the dynamic listener manager.
|
||||||
|
2. **Advanced Overlay Customization:** Add more features for users to customize their overlay's appearance and behavior.
|
||||||
|
3. **Twitch Chat Writeback:** Re-introduce the `chat:write` scope during authentication to allow the service (and potentially moderators, as per Issue #2) to send messages to the user's Twitch chat.
|
||||||
|
|
||||||
|
## 4. Requirements for Completion (Initial Version)
|
||||||
|
|
||||||
|
The project will be considered complete for its initial version when Phases 1, 2, and 3 are functional:
|
||||||
|
1. Users can log in with their Twitch account.
|
||||||
|
2. Users can see their unique overlay URL on a dashboard.
|
||||||
|
3. The overlay successfully connects to their Twitch chat and displays messages when opened in a browser source.
|
||||||
|
|
||||||
|
## 6. Future Enhancements from Gitea Issues
|
||||||
|
|
||||||
|
These are enhancement suggestions gathered from the project's Gitea issues, representing potential future features or considerations:
|
||||||
|
|
||||||
|
* **Issue #1: Multi select chat display order**
|
||||||
|
* Allow streamer to click on messages that appear whilst discussing chat message already on screen. This will enable quick progress through important messages without having to scroll back up chat.
|
||||||
|
* **Issue #2: Moderator chat assistance with streamer over ride**
|
||||||
|
* Moderators can select messages on their end, marking them for discussion, freeing up the streamer to simply stream. The streamer would have override to reject messages as the stream owner.
|
||||||
|
* **Issue #3: Chat Speed toggle for busier chat streams**
|
||||||
|
* Implement a toggle to adjust the display speed of chat messages, useful for very active streams.
|
||||||
|
* **Issue #4: Auto add YT Superchats to Highlights**
|
||||||
|
* Add a setting to automatically include YouTube Superchats in the highlighted messages.
|
||||||
|
* **Issue #5: Donations page somewhere**
|
||||||
|
* Consider integrating a donations page or feature within the service.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
```
|
||||||
|
# Project Task List
|
||||||
|
|
||||||
|
This file tracks all active development tasks. It is based on the official `DEVELOPMENT_PLAN.md`.
|
||||||
|
|
||||||
|
## 📋 How to Use This List
|
||||||
|
|
||||||
|
1. Find a task in the "To Do" section that you want to work on.
|
||||||
|
2. Add your name next to it (e.g., `[ ] Task Name - @YourName`).
|
||||||
|
3. When you start, move it to "In Progress" and follow the `CONTRIBUTING.md` workflow.
|
||||||
|
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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Phase 1: User Authentication & Database
|
||||||
|
|
||||||
|
* **Goal:** Get the basic API, database, and Twitch login flow working.
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
|
||||||
|
* `[ ]` **1.4: Basic Session Management:** Create a simple session/JWT system to know *who* is logged in.
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
|
||||||
|
### Done
|
||||||
|
* `[✔️]` **1.0: Project Skeleton** - @ramforth
|
||||||
|
* *Task:* Setup `main.py`, `requirements.txt`, and `.gitignore`.
|
||||||
|
* `[✔️]` **1.1: Database Schema:** Define SQLAlchemy models for `User` (id, username, platform, encrypted_tokens) and `Settings`. @ramforth
|
||||||
|
* `[✔️]` **1.1.5: Discord Overview:** Create an automated 'TASK-LIST' and post to Discord whenever someone pushes a change to the repository. @ramforth
|
||||||
|
* `[✔️]` **1.2: Twitch OAuth API:** Create FastAPI endpoints for `/login/twitch` (redirect) and `/auth/twitch/callback` (handles token exchange). @ramforth
|
||||||
|
* `[✔️]` **1.3: Secure Token Storage:** Implement helper functions to `encrypt` and `decrypt` OAuth tokens before storing them in the database. @ramforth
|
||||||
|
* `[✔️]` **1.5: Login Frontend:** Create a basic `login.html` file with a "Login with Twitch" button. @ramforth
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ Phase 2: User Dashboard & Configuration
|
||||||
|
|
||||||
|
* **Goal:** Allow logged-in users to see a dashboard, get their overlay URL, and save settings.
|
||||||
|
* *(All tasks for this phase are on hold until Phase 1 is complete)*
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
* `[ ]` **2.1: Dashboard UI:** Create `dashboard.html` (only for logged-in users).
|
||||||
|
* `[ ]` **2.2: Config API:** Create API endpoints (`GET`, `POST`) for `/api/settings` to save/load user preferences (e.g., custom CSS).
|
||||||
|
* `[ ]` **2.3: Overlay URL:** Generate and display the unique overlay URL for the user (e.g., `/overlay/{user_id}`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Phase 3: Dynamic Listeners & Basic Overlay
|
||||||
|
|
||||||
|
* **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)*
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
* `[ ]` **3.1: Dynamic Listener Manager (The Big One):** Design a system (e.g., background service) to start/stop listener processes for users.
|
||||||
|
* `[ ]` **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Backlog & Future Features
|
||||||
|
|
||||||
|
* *(Tasks from Phase 4, Gitea Issues, etc., will be added here as we go)*
|
||||||
|
* `[ ]` Implement YouTube OAuth & `pytchat` listener (Phase 4).
|
||||||
|
* `[ ]` "Single Message Focus" feature (Issue #1).
|
||||||
|
* `[ ]` Moderator panels (Issue #2).
|
||||||
|
* `[ ]` Custom CSS storage & injection (Issue #6).
|
||||||
|
|
||||||
|
```
|
||||||
99
CONTRIBUTING.md
Normal file
99
CONTRIBUTING.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Contribution & Workflow Guide
|
||||||
|
|
||||||
|
Welcome to the MultiChatOverlay project! To ensure we can collaborate effectively and avoid errors, we follow a strict and professional development workflow.
|
||||||
|
|
||||||
|
## 📜 The Golden Rules
|
||||||
|
|
||||||
|
1. **Gitea is the Source of Truth.** The `main` branch on our Gitea server is the *only* source of truth.
|
||||||
|
2. **NEVER Commit to `main`.** All work must be done in a separate "feature branch" and submitted as a Pull Request.
|
||||||
|
3. **NEVER Work on the Server.** The staging server (`192.168.10.33`) is for *testing* the `main` branch. It is **NOT** a development environment. All development must be done on your **local machine**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Your Local Setup (One Time)
|
||||||
|
|
||||||
|
You only need to do this once.
|
||||||
|
|
||||||
|
1. **Install Tools:**
|
||||||
|
* [Git](https://git-scm.com/downloads)
|
||||||
|
* [Visual Studio Code](https://code.visualstudio.com/)
|
||||||
|
* [Python 3.9+](https://www.python.org/downloads/)
|
||||||
|
2. **Clone the Repo:** Clone the project from our Gitea server to your local computer:
|
||||||
|
```bash
|
||||||
|
git clone [https://gitea.ramforth.net/ramforth/MultiChatOverlay.git](https://gitea.ramforth.net/ramforth/MultiChatOverlay.git)
|
||||||
|
cd MultiChatOverlay
|
||||||
|
```
|
||||||
|
3. **Install VS Code Extensions:**
|
||||||
|
* Open the `MultiChatOverlay` folder in VS Code.
|
||||||
|
* Go to the Extensions tab and install:
|
||||||
|
* `Python` (Microsoft)
|
||||||
|
* `Gemini` (Google)
|
||||||
|
4. **Create Your Virtual Environment:**
|
||||||
|
```bash
|
||||||
|
# From the terminal in VS Code
|
||||||
|
python -m venv venv
|
||||||
|
```
|
||||||
|
* VS Code should auto-detect this and ask to use it. Click "Yes."
|
||||||
|
5. **Install Dependencies:**
|
||||||
|
```bash
|
||||||
|
# Make sure your 'venv' is activated
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
You are now ready to develop!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Your Daily Workflow (The "Loop")
|
||||||
|
|
||||||
|
This is the process you will follow *every time* you want to add a new feature or fix a bug.
|
||||||
|
|
||||||
|
1. **Get Latest Code:** Make sure your local `main` branch is up-to-date.
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
2. **Create a New Branch:** Create a new branch for your task. Name it clearly (e.g., `feature/twitch-auth`, `bugfix/css-error`).
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/my-new-feature
|
||||||
|
```
|
||||||
|
3. **Write Code!**
|
||||||
|
* This is where you do your work.
|
||||||
|
* Use the **Gemini plugin** in VS Code to help you.
|
||||||
|
* **Pro-tip:** Open the Gemini chat and give it context by pasting in files like `DEVELOPMENT_PLAN.md` or the `TASKS.md` file so it understands the goal.
|
||||||
|
4. **Test Locally:** Run the FastAPI server on your *local* machine to make sure your feature works and doesn't break anything.
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
5. **Commit Your Work:** Once it's working, save your changes.
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add new feature: brief description here"
|
||||||
|
```
|
||||||
|
6. **Push Your Branch:** Push your *new branch* (not `main`) to Gitea.
|
||||||
|
```bash
|
||||||
|
git push -u origin feature/my-new-feature
|
||||||
|
```
|
||||||
|
7. **Open a Pull Request:**
|
||||||
|
* Go to the Gitea website.
|
||||||
|
* You will see a prompt to "Open a Pull Request" for your new branch.
|
||||||
|
* Fill it out, describe your changes, and submit it for review.
|
||||||
|
|
||||||
|
A project lead will then review your code, and once approved, it will be merged into the `main` branch and deployed to the staging server for final testing.
|
||||||
|
|
||||||
|
|
||||||
|
🚀 Automatic Deployment (The Webhook)
|
||||||
|
|
||||||
|
We have set up an automated "hotline" that connects our code storage (Gitea) to our live server.
|
||||||
|
|
||||||
|
Here's how it works:
|
||||||
|
|
||||||
|
**Code is Saved**: A developer saves new code to our Gitea project.
|
||||||
|
|
||||||
|
**Gitea Calls the Server**: Gitea immediately "calls" a special, secret address on our server.
|
||||||
|
|
||||||
|
**Server Verifies the Call**: A "listener" program on the server answers and checks a secret password to make sure the call is genuinely from Gitea and not an impostor.
|
||||||
|
|
||||||
|
**Server Updates Itself**: Once verified, the listener automatically runs our deploy.sh script. This script fetches all the new code and restarts the application.
|
||||||
|
|
||||||
|
The result: The server is always running the latest version of the code, and no one has to log in to update it manually. It's completely automatic.
|
||||||
@@ -8,44 +8,40 @@ The goal is to create a service where streamers can log in using their platform
|
|||||||
|
|
||||||
## 2. Technology Stack
|
## 2. Technology Stack
|
||||||
|
|
||||||
* **Backend (API & Chat Listeners):** Python (for Twitch/YouTube chat listeners), Node.js (for WebSocket server and potentially other APIs), PHP (for user management and web serving).
|
* **Team Communications:** Discord and Nextcloud, primarily. This can change. There's a list of links in the [README.md](README.md)
|
||||||
* **Database:** MySQL
|
* **Backend:** Python 3.13+ (FastAPI)
|
||||||
* **Frontend:** HTML, CSS, JavaScript
|
* **Database:** SQLite (for initial development) with SQLAlchemy ORM
|
||||||
|
* **Frontend:** Vanilla HTML, CSS, and JavaScript
|
||||||
|
* **Chat Listeners:** `twitchio` (Twitch), `pytchat` (YouTube)
|
||||||
|
|
||||||
## 3. Implementation Roadmap
|
## 3. Implementation Roadmap
|
||||||
|
|
||||||
### Phase 1: Basic Setup & Twitch Chat Listener (Python)
|
### Phase 1: User Authentication & Database (FastAPI)
|
||||||
1. **Project Structure:** Establish a clear directory structure for PHP, Python, Node.js, and static assets.
|
1. **Project Skeleton:** Establish the core FastAPI application structure, dependencies, and version control.
|
||||||
2. **Python Environment:** Set up a Python virtual environment and install `twitchio`.
|
2. **Database Schema:** Define the data models for users and settings using SQLAlchemy.
|
||||||
3. **Twitch Chat Listener (Python Script):** Create a standalone Python script that connects to Twitch chat, listens for messages, and prints them to standard output. This script will be run as a background process.
|
3. **Twitch OAuth2:** Implement the server-side OAuth2 flow within FastAPI to authenticate users and securely store encrypted tokens in the database.
|
||||||
4. **Twitch OAuth2 (Python):** Implement a simple Python script or a PHP endpoint to handle Twitch OAuth2 to obtain user access tokens. Store these securely in MySQL.
|
4. **Session Management:** Create a system to manage logged-in user sessions.
|
||||||
|
5. **Basic Frontend:** Develop a simple login page.
|
||||||
|
|
||||||
### Phase 2: MySQL Database & User Management (PHP)
|
### Phase 2: User Dashboard & Configuration
|
||||||
1. **MySQL Setup:** Set up a MySQL database and create a `users` table to store user information (Twitch ID, username, access token, refresh token).
|
1. **Dashboard UI:** Create a dashboard page accessible only to authenticated users.
|
||||||
2. **PHP Web Server:** Configure a basic PHP web server.
|
2. **Settings API:** Build API endpoints for users to save and retrieve their overlay settings (e.g., custom CSS).
|
||||||
3. **User Registration/Login (PHP):** Implement PHP scripts for user registration and login, integrating with the MySQL database.
|
3. **Overlay URL Generation:** Display a unique, persistent overlay URL for each user on their dashboard.
|
||||||
4. **Dashboard (PHP/HTML):** Create a basic dashboard where logged-in users can see their Twitch connection status and their unique overlay URL.
|
|
||||||
|
|
||||||
### Phase 3: WebSocket Server (Node.js) & Overlay (HTML/CSS/JS)
|
### Phase 3: Dynamic Listeners & Basic Overlay
|
||||||
1. **Node.js Environment:** Set up a Node.js environment and install `ws` (WebSocket library).
|
1. **Dynamic Listener Manager:** Design and build a background service that starts and stops chat listener processes (`twitchio`, `pytchat`) based on user activity.
|
||||||
2. **WebSocket Server (Node.js):** Create a Node.js WebSocket server that:
|
2. **Real-time Message Broadcasting:** Implement a WebSocket system within FastAPI to push chat messages to the correct user's overlay in real-time.
|
||||||
* Accepts connections from overlay clients.
|
3. **Basic Overlay UI:** Create the `overlay.html` page that connects to the WebSocket and renders incoming chat messages.
|
||||||
* Receives chat messages from the Python Twitch listener (via a simple inter-process communication mechanism, e.g., writing to a file or a local socket).
|
|
||||||
* Broadcasts messages to connected overlay clients.
|
|
||||||
3. **Overlay Frontend (HTML/CSS/JS):** Create a basic `overlay.html` that:
|
|
||||||
* Connects to the Node.js WebSocket server.
|
|
||||||
* Displays incoming chat messages.
|
|
||||||
4. **Inter-process Communication:** Implement a mechanism for the Python Twitch listener to send messages to the Node.js WebSocket server.
|
|
||||||
|
|
||||||
### Phase 4: Integration & Refinement
|
### Phase 4: Integration & Refinement
|
||||||
1. **Dynamic Listener Management:** Develop a system (e.g., a PHP script or a Node.js API) to start and stop Python Twitch listener processes based on user activity.
|
1. **YouTube Integration:** Implement the full YouTube OAuth2 flow and integrate the `pytchat` listener into the dynamic listener manager.
|
||||||
2. **YouTube Integration:** Add YouTube chat listening capabilities (Python `pytchat`) and integrate with the existing system. This will be implemented after the core Twitch functionality is stable.
|
2. **Advanced Overlay Customization:** Add more features for users to customize their overlay's appearance and behavior.
|
||||||
3. **Advanced Overlay Customization:** Implement options for users to customize their overlay.
|
3. **Twitch Chat Writeback:** Re-introduce the `chat:write` scope during authentication to allow the service (and potentially moderators, as per Issue #2) to send messages to the user's Twitch chat.
|
||||||
|
|
||||||
## 4. Requirements for Completion (Initial Version)
|
## 4. Requirements for Completion (Initial Version)
|
||||||
|
|
||||||
The project will be considered complete for its initial version when Phases 1, 2, and 3 are functional:
|
The project will be considered complete for its initial version when Phases 1, 2, and 3 are functional:
|
||||||
1. Users can log in with their Twitch account.
|
1. Users can log in with their Twitch account.
|
||||||
2. Users can see their unique overlay URL on a dashboard.
|
2. Users can see their unique overlay URL on a dashboard.
|
||||||
3. The overlay successfully connects to their Twitch chat and displays messages when opened in a browser source.
|
3. The overlay successfully connects to their Twitch chat and displays messages when opened in a browser source.
|
||||||
|
|
||||||
|
|||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# MultiChatOverlay
|
||||||
|
|
||||||
|
MultiChatOverlay is a web-based, multi-platform chat overlay service designed for streamers. The goal is to create a "SaaS" (Software as a Service) project where users can log in with their platform accounts (Twitch, YouTube, etc.) and get a single, unified, and customizable chat overlay for their stream.
|
||||||
|
|
||||||
|
This project is currently in **Phase 1: Initial Development**.
|
||||||
|
|
||||||
|
## 🚀 Project Goal
|
||||||
|
|
||||||
|
* **Unified Chat:** Aggregate chat from multiple platforms (starting with Twitch & YouTube) into one browser source.
|
||||||
|
* **Customization:** Allow users to save their own custom CSS and use templates.
|
||||||
|
* **Interaction:** Provide "single message focus" and other moderation tools for streamers and their teams.
|
||||||
|
* **Self-Hosted:** The service is hosted by the project owner (you) and provided to users.
|
||||||
|
|
||||||
|
## 🔒 Security & Privacy
|
||||||
|
|
||||||
|
User privacy and security are paramount. All sensitive user credentials, such as OAuth access and refresh tokens from external platforms, are **always encrypted** before being stored in the database. They are never stored in plain text, ensuring a high standard of security for user data.
|
||||||
|
|
||||||
|
## <20> Technology Stack
|
||||||
|
|
||||||
|
* **Backend:** Python 3.9+ (FastAPI)
|
||||||
|
* **Database:** SQLite (initially, for simplicity) with SQLAlchemy
|
||||||
|
* **Chat Listeners:** `twitchio` (Twitch), `pytchat` (YouTube)
|
||||||
|
* **Frontend:** Vanilla HTML5, CSS3, and JavaScript (Fetch API, WebSockets)
|
||||||
|
* **Authentication:** OAuth2 for all external platforms.
|
||||||
|
|
||||||
|
## 📖 Development & Contribution
|
||||||
|
|
||||||
|
This project follows a professional development workflow. Gitea is our single source of truth.
|
||||||
|
|
||||||
|
* **Want to contribute?** See our [CONTRIBUTING.md](CONTRIBUTING.md) file for the complete setup guide and workflow rules.
|
||||||
|
* **Looking for a task?** See the [TASKS.md](TASKS.md) file for a list of current jobs, broken down by phase.
|
||||||
|
* **Want the full plan?** See the [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) for the complete project roadmap.
|
||||||
|
|
||||||
|
## ⁉️ Acknowledgements
|
||||||
|
|
||||||
|
For the project we are using Discord 💬 and Nextcloud ☁️ for communications.
|
||||||
|
* ☁️ [Nextcloud](https://cloud9.ramforth.net/)
|
||||||
|
* 💬 [Discord](https://discord.gg/Zaxp6ch9hs)
|
||||||
|
* 🌐 [Public website](https://multichat.ramforth.net/)
|
||||||
|
|
||||||
|
##
|
||||||
|
|
||||||
|
👨💻 - Coded on and for Linux - 2025
|
||||||
67
TASKS.md
Normal file
67
TASKS.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Project Task List
|
||||||
|
|
||||||
|
This file tracks all active development tasks. It is based on the official `DEVELOPMENT_PLAN.md`.
|
||||||
|
|
||||||
|
## 📋 How to Use This List
|
||||||
|
|
||||||
|
1. Find a task in the "To Do" section that you want to work on.
|
||||||
|
2. Add your name next to it (e.g., `[ ] Task Name - @YourName`).
|
||||||
|
3. When you start, move it to "In Progress" and follow the `CONTRIBUTING.md` workflow.
|
||||||
|
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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Phase 1: User Authentication & Database
|
||||||
|
|
||||||
|
* **Goal:** Get the basic API, database, and Twitch login flow working.
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
|
||||||
|
### Done
|
||||||
|
* `[✔️]` **1.0: Project Skeleton** - @ramforth
|
||||||
|
* *Task:* Setup `main.py`, `requirements.txt`, and `.gitignore`.
|
||||||
|
* `[✔️]` **1.1: Database Schema:** Define SQLAlchemy models for `User` (id, username, platform, encrypted_tokens) and `Settings`. @ramforth
|
||||||
|
* `[✔️]` **1.1.5: Discord Overview:** Create an automated 'TASK-LIST' and post to Discord whenever someone pushes a change to the repository. @ramforth
|
||||||
|
* `[✔️]` **1.2: Twitch OAuth API:** Create FastAPI endpoints for `/login/twitch` (redirect) and `/auth/twitch/callback` (handles token exchange). @ramforth
|
||||||
|
* `[✔️]` **1.3: Secure Token Storage:** Implement helper functions to `encrypt` and `decrypt` OAuth tokens before storing them in the database. @ramforth
|
||||||
|
* `[✔️]` **1.4: Basic Session Management:** Create a simple session/JWT system to know *who* is logged in. @ramforth
|
||||||
|
* `[✔️]` **1.5: Login Frontend:** Create a basic `login.html` file with a "Login with Twitch" button. @ramforth
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ Phase 2: User Dashboard & Configuration
|
||||||
|
|
||||||
|
* **Goal:** Allow logged-in users to see a dashboard, get their overlay URL, and save settings.
|
||||||
|
* *(All tasks for this phase are on hold until Phase 1 is complete)*
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
* `[ ]` **2.0: CSS Refactor & Styling:** Improve the general look and feel of the application pages.
|
||||||
|
* `[ ]` **2.1: Dashboard UI:** Create `dashboard.html` (only for logged-in users).
|
||||||
|
* `[ ]` **2.2: Config API:** Create API endpoints (`GET`, `POST`) for `/api/settings` to save/load user preferences (e.g., custom CSS).
|
||||||
|
* `[ ]` **2.3: Overlay URL:** Generate and display the unique overlay URL for the user (e.g., `/overlay/{user_id}`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Phase 3: Dynamic Listeners & Basic Overlay
|
||||||
|
|
||||||
|
* **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)*
|
||||||
|
|
||||||
|
### To Do
|
||||||
|
* `[ ]` **3.1: Dynamic Listener Manager (The Big One):** Design a system (e.g., background service) to start/stop listener processes for users.
|
||||||
|
* `[ ]` **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Backlog & Future Features
|
||||||
|
|
||||||
|
* *(Tasks from Phase 4, Gitea Issues, etc., will be added here as we go)*
|
||||||
|
* `[ ]` Implement YouTube OAuth & `pytchat` listener (Phase 4).
|
||||||
|
* `[ ]` "Single Message Focus" feature (Issue #1).
|
||||||
|
* `[ ]` Moderator panels (Issue #2).
|
||||||
|
* `[ ]` Custom CSS storage & injection (Issue #6).
|
||||||
110
auth.py
Normal file
110
auth.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import httpx
|
||||||
|
import secrets
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
from database import SessionLocal
|
||||||
|
import models
|
||||||
|
import security
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Dependency to get a DB session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@router.get("/login/twitch")
|
||||||
|
async def login_with_twitch(request: Request):
|
||||||
|
"""
|
||||||
|
Step 1 of OAuth flow: Redirect the user to Twitch's authorization page.
|
||||||
|
"""
|
||||||
|
# Generate a random state token for CSRF protection
|
||||||
|
state = secrets.token_urlsafe(16)
|
||||||
|
request.session['oauth_state'] = state
|
||||||
|
|
||||||
|
# As per RESEARCH_REPORT.md, these are the minimum required scopes
|
||||||
|
scopes = "chat:read"
|
||||||
|
|
||||||
|
# Construct the authorization URL
|
||||||
|
auth_url = (
|
||||||
|
f"https://id.twitch.tv/oauth2/authorize"
|
||||||
|
f"?response_type=code"
|
||||||
|
f"&client_id={settings.TWITCH_CLIENT_ID}"
|
||||||
|
f"&redirect_uri={settings.APP_BASE_URL}/auth/twitch/callback"
|
||||||
|
f"&scope={scopes}"
|
||||||
|
f"&state={state}"
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=auth_url)
|
||||||
|
|
||||||
|
@router.get("/auth/twitch/callback")
|
||||||
|
async def auth_twitch_callback(code: str, state: str, request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Step 2 of OAuth flow: Handle the callback from Twitch after user authorization.
|
||||||
|
"""
|
||||||
|
# CSRF Protection: Validate the state
|
||||||
|
if state != request.session.pop('oauth_state', None):
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid state parameter. CSRF attack suspected.")
|
||||||
|
|
||||||
|
# Step 4: Exchange the authorization code for an access token
|
||||||
|
token_url = "https://id.twitch.tv/oauth2/token"
|
||||||
|
token_data = {
|
||||||
|
"client_id": settings.TWITCH_CLIENT_ID,
|
||||||
|
"client_secret": settings.TWITCH_CLIENT_SECRET,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"redirect_uri": f"{settings.APP_BASE_URL}/auth/twitch/callback",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
token_response = await client.post(token_url, data=token_data)
|
||||||
|
if token_response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=400, detail="Failed to retrieve access token from Twitch.")
|
||||||
|
|
||||||
|
token_json = token_response.json()
|
||||||
|
access_token = token_json["access_token"]
|
||||||
|
refresh_token = token_json["refresh_token"]
|
||||||
|
|
||||||
|
# Step 5: Validate the user and get their details from Twitch API
|
||||||
|
users_url = "https://api.twitch.tv/helix/users"
|
||||||
|
headers = {
|
||||||
|
"Client-ID": settings.TWITCH_CLIENT_ID,
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
}
|
||||||
|
user_response = await client.get(users_url, headers=headers)
|
||||||
|
user_data = user_response.json()["data"][0]
|
||||||
|
|
||||||
|
# Encrypt the tokens for storage
|
||||||
|
encrypted_tokens = security.encrypt_tokens(access_token, refresh_token)
|
||||||
|
|
||||||
|
# --- Database Upsert Logic ---
|
||||||
|
# Check if the user already exists in our database
|
||||||
|
user = db.query(models.User).filter(models.User.platform_user_id == user_data['id']).first()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# If user exists, update their details
|
||||||
|
user.username = user_data['login']
|
||||||
|
user.encrypted_tokens = encrypted_tokens
|
||||||
|
else:
|
||||||
|
# If user does not exist, create a new record
|
||||||
|
user = models.User(
|
||||||
|
platform_user_id=user_data['id'],
|
||||||
|
username=user_data['login'],
|
||||||
|
platform="twitch",
|
||||||
|
encrypted_tokens=encrypted_tokens
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Create a session for the user by storing their database ID.
|
||||||
|
request.session['user_id'] = user.id
|
||||||
|
|
||||||
|
# Redirect to a future dashboard page for a better user experience
|
||||||
|
# This prepares us for Task 1.4 (Session Management) and Task 2.1 (Dashboard UI)
|
||||||
|
return RedirectResponse(url="/dashboard")
|
||||||
18
config.py
Normal file
18
config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""
|
||||||
|
A simple class to hold all application settings, loaded from environment variables.
|
||||||
|
"""
|
||||||
|
ENCRYPTION_KEY: str = os.getenv("ENCRYPTION_KEY")
|
||||||
|
TWITCH_CLIENT_ID: str = os.getenv("TWITCH_CLIENT_ID")
|
||||||
|
TWITCH_CLIENT_SECRET: str = os.getenv("TWITCH_CLIENT_SECRET")
|
||||||
|
|
||||||
|
# The full URL where our app is running, needed for the redirect_uri
|
||||||
|
APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
27
dashboard.html
Normal file
27
dashboard.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard - MultiChatOverlay</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=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome, <strong>{{ user.username }}</strong>! You are successfully logged in.</p>
|
||||||
|
|
||||||
|
<div class="overlay-url-container">
|
||||||
|
<p>Your unique overlay URL:</p>
|
||||||
|
<code>{{ overlay_url }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/logout">Logout</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
database.py
Normal file
19
database.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# Define the location of our SQLite database file.
|
||||||
|
# The ./. indicates it will be in the same directory as our project.
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./multichat_overlay.db"
|
||||||
|
|
||||||
|
# Create the SQLAlchemy engine. The `connect_args` is needed only for SQLite
|
||||||
|
# to allow it to be used by multiple threads, which FastAPI does.
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Each instance of SessionLocal will be a database session.
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# This Base will be used by our model classes to inherit from.
|
||||||
|
Base = declarative_base()
|
||||||
65
main.py
Normal file
65
main.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import os
|
||||||
|
from fastapi import FastAPI, Request, Depends
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
from starlette.responses import FileResponse, RedirectResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import models
|
||||||
|
from database import engine
|
||||||
|
import auth # Import the new auth module
|
||||||
|
from config import settings # Import settings to get the secret key
|
||||||
|
|
||||||
|
# --- Absolute Path Configuration ---
|
||||||
|
# Get the absolute path of the directory where this file is located
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
||||||
|
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# This code runs on startup
|
||||||
|
print("Application startup: Creating database tables...")
|
||||||
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
print("Application startup: Database tables created.")
|
||||||
|
yield
|
||||||
|
# Code below yield runs on shutdown, if needed
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
# Configure Jinja2 templates
|
||||||
|
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
||||||
|
|
||||||
|
# Mount the 'static' directory using an absolute path for reliability
|
||||||
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
|
# Add the authentication router
|
||||||
|
app.include_router(auth.router)
|
||||||
|
|
||||||
|
# Add session middleware. A secret key is required for signing the session cookie.
|
||||||
|
# We can reuse our encryption key for this, but in production you might want a separate key.
|
||||||
|
app.add_middleware(SessionMiddleware, secret_key=settings.ENCRYPTION_KEY)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_root():
|
||||||
|
return FileResponse(os.path.join(STATIC_DIR, "login.html"))
|
||||||
|
|
||||||
|
@app.get("/dashboard")
|
||||||
|
async def read_dashboard(request: Request, db: Session = Depends(auth.get_db)):
|
||||||
|
# This is our protected route. It checks if a user_id exists in the session.
|
||||||
|
user_id = request.session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
# If not, redirect them to the login page.
|
||||||
|
return RedirectResponse(url="/")
|
||||||
|
|
||||||
|
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
|
overlay_url = f"{settings.APP_BASE_URL}/overlay/{user.id}"
|
||||||
|
|
||||||
|
return templates.TemplateResponse("dashboard.html", {"request": request, "user": user, "overlay_url": overlay_url})
|
||||||
|
|
||||||
|
@app.get("/logout")
|
||||||
|
async def logout(request: Request):
|
||||||
|
# Clear the session cookie
|
||||||
|
request.session.clear()
|
||||||
|
return RedirectResponse(url="/")
|
||||||
27
models.py
Normal file
27
models.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# The user's unique ID from the platform (e.g., Twitch ID, YouTube Channel ID)
|
||||||
|
platform_user_id = Column(String, unique=True, index=True, nullable=False)
|
||||||
|
username = Column(String, index=True, nullable=False)
|
||||||
|
platform = Column(String, nullable=False) # e.g., "twitch", "youtube"
|
||||||
|
|
||||||
|
# A JSON string or other format holding the encrypted access and refresh tokens
|
||||||
|
encrypted_tokens = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
settings = relationship("Setting", back_populates="owner", uselist=False)
|
||||||
|
|
||||||
|
class Setting(Base):
|
||||||
|
__tablename__ = "settings"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
custom_css = Column(Text, nullable=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
|
||||||
|
owner = relationship("User", back_populates="settings")
|
||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy
|
||||||
|
httpx
|
||||||
|
cryptography
|
||||||
|
python-dotenv
|
||||||
|
itsdangerous
|
||||||
|
jinja2
|
||||||
47
security.py
Normal file
47
security.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from a .env file for local development
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def _get_fernet_instance() -> Fernet:
|
||||||
|
"""
|
||||||
|
Helper function to get the Fernet instance.
|
||||||
|
This ensures the key is checked only when encryption/decryption is needed.
|
||||||
|
"""
|
||||||
|
# It is CRITICAL that this key is set in your environment and kept secret.
|
||||||
|
# It should be a 32-url-safe-base64-encoded key.
|
||||||
|
encryption_key = os.getenv("ENCRYPTION_KEY")
|
||||||
|
|
||||||
|
if not encryption_key:
|
||||||
|
raise ValueError("ENCRYPTION_KEY is not set in the environment. Please generate a key and add it to your .env file.")
|
||||||
|
|
||||||
|
# Ensure the key is in bytes for the Fernet instance
|
||||||
|
return Fernet(encryption_key.encode())
|
||||||
|
|
||||||
|
def encrypt_tokens(access_token: str, refresh_token: str) -> str:
|
||||||
|
"""
|
||||||
|
Combines access and refresh tokens into a JSON object, then encrypts it.
|
||||||
|
"""
|
||||||
|
fernet = _get_fernet_instance()
|
||||||
|
tokens = {"access_token": access_token, "refresh_token": refresh_token}
|
||||||
|
tokens_json_string = json.dumps(tokens)
|
||||||
|
encrypted_data = fernet.encrypt(tokens_json_string.encode())
|
||||||
|
return encrypted_data.decode()
|
||||||
|
|
||||||
|
def decrypt_tokens(encrypted_data_str: str) -> dict:
|
||||||
|
"""
|
||||||
|
Decrypts the token string back into a dictionary of tokens.
|
||||||
|
"""
|
||||||
|
fernet = _get_fernet_instance()
|
||||||
|
decrypted_data_bytes = fernet.decrypt(encrypted_data_str.encode())
|
||||||
|
tokens_json_string = decrypted_data_bytes.decode()
|
||||||
|
return json.loads(tokens_json_string)
|
||||||
|
|
||||||
|
def generate_key():
|
||||||
|
"""
|
||||||
|
Utility function to generate a new encryption key. Run this once.
|
||||||
|
"""
|
||||||
|
return Fernet.generate_key().decode()
|
||||||
1
static/.gitkeep
Normal file
1
static/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This file ensures the 'static' directory is tracked by Git.
|
||||||
16
static/dashboard.html
Normal file
16
static/dashboard.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard - MultiChatOverlay</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome! You are successfully logged in.</p>
|
||||||
|
<p><a href="/logout">Logout</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
static/login.html
Normal file
21
static/login.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - MultiChatOverlay</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=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Welcome to MultiChatOverlay</h1>
|
||||||
|
<p>Connect your streaming accounts to get started.</p>
|
||||||
|
<a href="/login/twitch" class="twitch-btn">Login with Twitch</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
77
static/style.css
Normal file
77
static/style.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
body {
|
||||||
|
/* Use a very dark grey background for contrast, making the container pop */
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
/* Suggest a modern font (requires import) */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
/* Use min-height for responsiveness */
|
||||||
|
background-color: #0d1117;
|
||||||
|
/* Dark Mode base color */
|
||||||
|
background-image: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
|
||||||
|
/* Subtle gradient */
|
||||||
|
margin: 0;
|
||||||
|
color: #e6edf3;
|
||||||
|
/* Light text color for contrast */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
/* Slightly more padding */
|
||||||
|
background-color: #161b22;
|
||||||
|
/* Lighter dark-mode color for the box */
|
||||||
|
border-radius: 12px;
|
||||||
|
/* Smoother corners */
|
||||||
|
|
||||||
|
/* Modern, subtle layered shadows for depth */
|
||||||
|
box-shadow:
|
||||||
|
0 4px 15px rgba(0, 0, 0, 0.4),
|
||||||
|
/* Primary shadow */
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.7);
|
||||||
|
/* Deep, soft shadow */
|
||||||
|
|
||||||
|
/* Optional: Small border for definition */
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
|
||||||
|
/* Slightly increase size */
|
||||||
|
max-width: 380px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.twitch-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
/* Use flex for easy icon alignment if you add one */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
/* Use a slightly brighter, but still core Twitch purple */
|
||||||
|
background-color: #9146FF;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
/* Slightly larger padding */
|
||||||
|
border-radius: 8px;
|
||||||
|
/* Smoother corners */
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
/* Medium bold */
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
/* Better readability */
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
/* Enable smooth transitions */
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* Subtle inner shadow for 'pressed' look */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a sleek hover effect */
|
||||||
|
.twitch-btn:hover {
|
||||||
|
background-color: #772ce8;
|
||||||
|
/* Slightly darker purple on hover */
|
||||||
|
transform: translateY(-2px);
|
||||||
|
/* Lift the button slightly */
|
||||||
|
box-shadow: 0 6px 12px rgba(145, 70, 255, 0.3);
|
||||||
|
/* Glow effect on hover */
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user