Compare commits

...

171 Commits

Author SHA1 Message Date
ba480e8409 Working on: The Twitch authentication 2025-12-25 21:22:24 +01:00
e69d423deb Working on: The Twitch authentication 2025-12-25 21:05:11 +01:00
773288faf0 Working on: The Twitch authentication 2025-12-25 18:40:54 +01:00
7a18b5b402 Working on: The Twitch authentication 2025-12-25 18:39:14 +01:00
cfc082a6f8 Working on: The Twitch authentication 2025-12-25 18:34:44 +01:00
8521510215 Working on: The Twitch authentication 2025-12-25 18:22:46 +01:00
4b640861a6 rftdgsh 2025-11-18 01:57:12 +01:00
7748e55a71 sdfghb 2025-11-18 01:53:39 +01:00
edfe38113a edrsfx 2025-11-18 01:51:49 +01:00
c249010cbd ergqa 2025-11-18 01:50:12 +01:00
1650f9343e ruining a good night's sleep 2025-11-18 01:48:36 +01:00
12c69f4797 oauth token mis-read 2025-11-18 01:47:39 +01:00
845e74bd5e ssl back on 2025-11-18 01:45:53 +01:00
1c93a09c74 dsf 2025-11-18 01:43:43 +01:00
53c1966494 fucked 2025-11-18 01:40:58 +01:00
303c8430e8 re-formatting client_ID and client__Secret 2025-11-18 01:37:17 +01:00
afa27271fa Swapped twitch connector twitchio.Client 2025-11-18 01:33:19 +01:00
45a511b8e9 debug text in logs 2025-11-18 01:18:54 +01:00
67ae5065f5 reverted bot_id 2025-11-18 01:17:27 +01:00
8abb76a55b Later bot start 2025-11-18 01:14:42 +01:00
555ba5a2d0 garrr! 2025-11-18 01:13:08 +01:00
c6450bc7be test3 on the listener 2025-11-18 01:11:18 +01:00
9eb614b9a3 Incorrectly passed user_id corrected to bot_id and id=bot_id 2025-11-18 01:09:22 +01:00
c52560aaae repairing listener 2025-11-18 01:01:46 +01:00
aea75918b2 Trying to bring chat messages past proxy by ensuring SSL traffic 2025-11-18 00:58:11 +01:00
a0877d8276 proxy handling of css 2025-11-18 00:55:32 +01:00
4b96b17368 css moar correction 2025-11-18 00:53:12 +01:00
e74553a482 Trying to bring back default css 2025-11-18 00:49:06 +01:00
2fffb4318a -- 2025-11-18 00:45:51 +01:00
08526cfa60 Re-arranged call of static/ 2025-11-18 00:44:01 +01:00
2392d21e17 Small correction in main.py 2025-11-18 00:39:41 +01:00
3f2f0fcb4e eod: status 2025-11-17 19:08:36 +01:00
036e9da25e asyncio improvement 2025-11-17 18:43:32 +01:00
bdd8674645 New eventsub 2025-11-17 18:34:15 +01:00
c3d77974e3 class NullAdapter 2025-11-17 18:27:00 +01:00
2f0e817e12 added kwargs 2025-11-17 18:24:15 +01:00
fdda47da7b remove AIOhttp 2025-11-17 18:22:12 +01:00
9b7c9e8671 Rewrite adapter port 2025-11-17 18:18:37 +01:00
ab832e928f twitchio==3.1.0 2025-11-17 18:11:06 +01:00
691c704ecc Edite requirements 2025-11-17 18:08:43 +01:00
5806cb08e1 rework twitchIOMessage statement 2025-11-17 17:59:47 +01:00
e7801287c2 from twitchio import Message as TwitchIOMessage 2025-11-17 17:45:02 +01:00
d808c17ab6 twitchio.client import Message 2025-11-17 17:43:38 +01:00
3e604a9816 event_messenger 2025-11-17 17:41:32 +01:00
31f7e00538 twitchio.ext.commands correction 2025-11-17 17:39:38 +01:00
4d360abd21 chat listener updated 2025-11-17 17:37:19 +01:00
35410dec0a cahat_listener.py 2025-11-17 17:29:03 +01:00
8e6bd96d44 adjust listener 2025-11-17 17:27:28 +01:00
4936966c32 adjustment 2025-11-17 17:22:34 +01:00
63780b19b1 new chat_listener.py 2025-11-17 16:32:32 +01:00
ae2cfc60a0 Troubleshooting message, self statement 2025-11-17 16:29:49 +01:00
6520ea0b63 Troubleshooting db_user_id statement 2025-11-17 16:25:00 +01:00
7f22cec452 Swapping mapping for websocket return traffic 2025-11-17 15:55:18 +01:00
6b2d296774 Adding twitchio port 8123 2025-11-17 15:47:23 +01:00
d1c4c931ef Changing websocket port 2025-11-17 15:45:16 +01:00
d8086afab2 Added websockets in imports in main.py 2025-11-17 15:36:38 +01:00
b60c642fd6 Attempting to add websocket support 2025-11-17 15:34:01 +01:00
2fe07abecf Not excluding messages from "self" in the return statement 2025-11-17 15:29:37 +01:00
30b9df0ebb Editing return message statement in chat_listener.py 2025-11-17 15:25:52 +01:00
c061f2ff78 Forgot a comma in the code 2025-11-17 15:22:33 +01:00
034c8fe604 Correction to init for access token 2025-11-17 15:20:10 +01:00
5dc73bd06a Updated chat_listener and listener_manager 2025-11-17 15:08:29 +01:00
4013d3d23d Addidng chat_listener function for Twitch, adding custom overlay template for testing. 2025-11-17 14:46:50 +01:00
98cda57d90 Phase 2 update. TASKS and Development_plan edited 2025-11-17 14:36:13 +01:00
aa9688d811 Starting adding help-file for user made CSS templates 2025-11-17 14:31:54 +01:00
8bc24fc80a Corrected Response import in main.py 2025-11-17 14:26:44 +01:00
1535d90842 Still updating database schemas 2025-11-17 14:23:54 +01:00
0fa46d4cca Correcting schemas in database 2025-11-17 14:17:30 +01:00
589ac73b25 Starting implementation of user made custom CSS templates 2025-11-17 14:14:45 +01:00
a120e30e03 Set dark mode to default regardless of user browser settings 2025-11-17 13:24:14 +01:00
65adbf1aaa added light mode to site 2025-11-17 13:08:51 +01:00
5ff361b97c Added back the css templates and added logout button 2025-11-17 13:03:25 +01:00
883439e66f Changes to login 2025-11-17 12:59:25 +01:00
67c0d0124b Small corrections 2025-11-17 12:46:24 +01:00
9d95e1fd3c css restore 2025-11-17 12:20:57 +01:00
e0502f93d6 start rebuild main.css 2025-11-17 12:18:29 +01:00
519cc907af main.css removed 2025-11-17 12:15:11 +01:00
77f0a8a395 Corrected error in file path to templates 2025-11-17 12:13:59 +01:00
7f08ad86b0 Updated file paths for CSS 2025-11-17 12:10:42 +01:00
0e0135be55 Correcting requirements 2025-11-17 12:00:16 +01:00
6af58808ad Phase 2: User experience enhancements to templates handling, dashboard and overlays. 2025-11-17 11:55:53 +01:00
6010666dcf Fix: small type-o 2025-11-17 10:35:51 +01:00
43b8610aa6 Added: CSS template switch in Dashboard. 2025-11-17 03:06:25 +01:00
d72e27ad2e Added: Temporary overlay text and 2 css layouts. 2025-11-17 03:04:26 +01:00
0c00ff7ee0 gitea logo png
For embeds
2025-11-17 02:53:12 +01:00
bce05652ed refactor: Organize templates into templates directory 2025-11-17 02:38:25 +01:00
830a16d693 Small update to main.py 2025-11-17 02:36:27 +01:00
ba1d486d07 Dashboard: Changed stylesheet and started prepping html for population. 2025-11-17 02:17:15 +01:00
0967983483 Update: Logic with Twitch login now works. 2025-11-17 02:10:00 +01:00
8829228295 change: Added separate stylesheet under /static and added task to TASKS.md 2025-11-17 01:59:35 +01:00
8236d5e837 feat: Implement session management and dashboard page 2025-11-17 01:56:03 +01:00
f2ebde841d Added: Dashboard html file and routing 2025-11-17 01:50:06 +01:00
cafb39d9df Day close: Added memory for AI agent. Added small corrections to DEVELOPMENT_PLAN. 2025-11-17 01:45:25 +01:00
35c2210305 prep: Making way for the planned 'Dashboard' page and correcting Twitch bot scope, corrected. 2025-11-17 01:28:47 +01:00
6ed78cf3fb prep: Making way for the planned 'Dashboard' page and correcting Twitch bot scope 2025-11-17 01:16:21 +01:00
3fb360aa77 prep: Making way for the planned 'Dashboard' page 2025-11-17 01:03:31 +01:00
f15f339de8 refactor: Move login.html to static directory 2025-11-17 01:01:36 +01:00
603eb84d34 feat: Implement static login page and fix server errors. Attempt 2 2025-11-17 00:54:13 +01:00
6a7c96a82c feat: Implement static login page and fix server errors 2025-11-17 00:49:39 +01:00
9b44c088b8 chore: Added redirect URL. APP_BASE_URL set in .env 2025-11-17 00:46:15 +01:00
98c1417bf1 chore: Add static directory for frontend assets 2025-11-17 00:37:44 +01:00
c4edd88b71 Update: Building start of login page, based on previous attempt. 2025-11-17 00:34:13 +01:00
3827744154 Update: requirements were missing security features. Module 'itsdangerous' incorporated 2025-11-17 00:28:21 +01:00
11254095ff docs: Update task board and align development plan 2025-11-17 00:23:54 +01:00
264e4c276d Connecting local program to Twitch dev-portal credentials 2025-11-17 00:19:13 +01:00
60417d4594 Preparing storage of generated keys 2025-11-17 00:10:47 +01:00
8825421335 Starting implementation of helper functions to encrypt and decrypt Oauth tokens before storing them. Added requirement python-dotenv and cryptography 2025-11-17 00:05:46 +01:00
e4df8d0e15 Update: Finished automated updates for Discord. Added some details concerning communications 2025-11-16 23:58:50 +01:00
21f175af9a chore: Trigger webhook for testing 2025-11-16 23:47:50 +01:00
aa5c63296a chore: Trigger webhook for testing 2025-11-16 23:40:00 +01:00
5931a0bdfe chore: Trigger webhook for testing 2025-11-16 23:38:17 +01:00
407a6447fd chore: Trigger webhook for testing 2025-11-16 23:34:38 +01:00
643f6f4272 chore: Trigger webhook for testing 2025-11-16 23:29:45 +01:00
dab3fb4bb4 chore: Trigger webhook for testing 2025-11-16 23:27:50 +01:00
582a12fbe7 chore: Trigger webhook for testing 2025-11-16 23:27:08 +01:00
55672d4631 chore: Trigger webhook for testing 2025-11-16 23:26:33 +01:00
ecd8518bd9 chore: Trigger webhook for testing 2025-11-16 23:25:35 +01:00
3f462e34a1 Update: Added info in the Readme 2025-11-16 23:21:20 +01:00
a661ab0b9f Update: Added info in the Readme 2025-11-16 23:17:07 +01:00
2b6a4cc3ab Update: Corrected info in the Readme 2025-11-16 23:09:04 +01:00
bd9e801042 Update: Corrected info in the Readme 2025-11-16 23:06:04 +01:00
761659fffb Update: Added info in the Readme 2025-11-16 23:04:20 +01:00
1a53135050 Update: Changed minor detail in the task list. 2025-11-16 22:51:59 +01:00
3d6e78b356 chore: Trigger webhook for testing 2025-11-16 22:46:31 +01:00
49dcf3b8a8 chore: Trigger webhook for testing 2025-11-16 22:45:15 +01:00
8c09a9ac82 chore: Trigger webhook for testing 2025-11-16 22:43:53 +01:00
7ee83501ba chore: Trigger webhook for testing 2025-11-16 22:40:02 +01:00
48747edb9c chore: Trigger webhook for testing 2025-11-16 22:25:08 +01:00
78da04fece chore: Trigger webhook for testing 2025-11-16 22:20:26 +01:00
f6e2fedebc chore: Trigger webhook for testing 2025-11-16 22:17:33 +01:00
1ad2be8dea chore: Trigger webhook for testing 2025-11-16 22:12:35 +01:00
0475315bab chore: Trigger webhook for testing 2025-11-16 21:57:08 +01:00
36efa91e1f chore: Trigger webhook for testing 2025-11-16 21:55:05 +01:00
545ec0ad0f chore: Trigger webhook for testing 2025-11-16 21:52:39 +01:00
fb551af208 chore: Trigger webhook for testing 2025-11-16 21:46:52 +01:00
e31b6701db chore: Trigger webhook for testing 2025-11-16 21:00:33 +01:00
8678b508b7 chore: Trigger webhook for testing 2025-11-16 20:59:38 +01:00
9e4f7da306 chore: Trigger webhook for testing 2025-11-16 20:57:04 +01:00
4a0b08fa25 chore: Trigger webhook for testing 2025-11-16 20:56:23 +01:00
81338c1ecf chore: Trigger webhook for testing 2025-11-16 20:55:45 +01:00
ce8dd866ec chore: Trigger webhook for testing 2025-11-16 20:55:07 +01:00
f157c8e7df chore: Trigger webhook for testing 2025-11-16 20:54:43 +01:00
2265d24578 chore: Trigger webhook for testing 2025-11-16 20:48:46 +01:00
b7c7ca6745 chore: Trigger webhook for testing 2025-11-16 20:45:37 +01:00
c4dbb7eca9 chore: Trigger webhook for testing 2025-11-16 20:45:12 +01:00
7aa6b8a675 chore: Trigger webhook for testing 2025-11-16 20:43:17 +01:00
ca096e8639 chore: Trigger webhook for testing 2025-11-16 20:38:22 +01:00
99c46f2e47 chore: Trigger webhook for testing 2025-11-16 20:37:33 +01:00
bb70dcfa05 chore: Trigger webhook for testing 2025-11-16 20:37:08 +01:00
be23f184c9 chore: Trigger webhook for testing 2025-11-16 20:36:54 +01:00
d48188cfa7 chore: Trigger webhook for testing 2025-11-16 20:36:04 +01:00
098dc52248 chore: Trigger webhook for testing 2025-11-16 20:35:16 +01:00
2e6e605f24 chore: Trigger webhook for testing 2025-11-16 20:13:30 +01:00
1b9bf938d6 chore: Trigger webhook for testing 2025-11-16 20:12:29 +01:00
232d24fd10 chore: Trigger webhook for testing 2025-11-16 20:10:13 +01:00
8ff003e393 Update: Tasks updated. More icons. 2025-11-16 20:05:07 +01:00
ac87daec68 Update: Tasks updated 2025-11-16 20:03:16 +01:00
d09e15ae86 chore: Trigger webhook for testing 2025-11-16 20:02:13 +01:00
873dc6e736 chore: Trigger webhook for testing 2025-11-16 20:00:49 +01:00
55c55a7055 chore: Trigger webhook for testing 2025-11-16 20:00:20 +01:00
2e19fd6f40 chore: Trigger webhook for testing 2025-11-16 19:56:38 +01:00
6aec15c408 chore: Trigger webhook for testing 2025-11-16 19:53:27 +01:00
d702343db6 Update: Tasks updated 2025-11-16 19:49:59 +01:00
0eacb12c73 Update: main.py and models.py had incorrect imports 2025-11-16 19:34:19 +01:00
1186c91dcf chore: Update dependencies and task list 2025-11-16 18:27:38 +01:00
56bf044bb6 Set up database.py
:wq

A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
B
B
B
B
B
B
A
A
2025-11-16 18:16:10 +01:00
4a951df300 Fix: Correct gitignore 2025-11-16 17:58:27 +01:00
8f16c2c4df Updated tasks 2025-11-16 17:21:38 +01:00
4d2566b48a Fix: Correct Markdown links in README 2025-11-16 17:14:28 +01:00
62ee9ad954 howto, tasks and readme update 2025-11-16 17:04:19 +01:00
e31ccbbb19 Start 2025-11-16 16:58:28 +01:00
45 changed files with 2423 additions and 27 deletions

9
.env.example Normal file
View 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
View 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/

182
CONTEXT.md Normal file
View File

@@ -0,0 +1,182 @@
# 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
**Phases 1, 2, and 3 are complete.** The application is now a fully functional chat overlay service for Twitch.
* **Phase 1 (Authentication):** A secure Twitch OAuth2 flow is implemented, with user data and encrypted tokens stored in a SQLite database.
* **Phase 2 (Dashboard & Configuration):** A dynamic user dashboard is available after login. It includes a theme switcher (light/dark), a theme selector for the overlay, and a full CRUD system for users to create and manage their own private CSS themes.
* **Phase 3 (Real-time Chat):** A decoupled background listener manager successfully starts `twitchio` listeners for each user. A WebSocket manager broadcasts incoming chat messages to the correct user's overlay in real-time.
* A basic HTML login page is served at the root URL (`/`).
### 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
### 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. Now that Phase 1 is done, these tasks are ready to be worked on.
### To Do
* `[ ]` **2.4: Create Logo and Favicon:** The project should have a logo and a favicon.
### Done
* `[✔️]` **2.0: CSS Refactor & Styling:** Improved the general look and feel of the application pages, including a light/dark theme switcher.
* `[✔️]` **2.1: Dashboard UI:** Created `dashboard.html` for logged-in users to manage settings.
* `[✔️]` **2.2: Config API:** Created API endpoints for `/api/settings` to save user preferences.
* `[✔️]` **2.3: Overlay URL:** Generated and displayed the unique overlay URL for the user on the dashboard.
* `[✔️]` **2.5: Custom CSS Themes:** Implemented a system for users to create, preview, and delete their own private CSS overlay themes.
* `[✔️]` **2.6: CSS Help Page:** Created a guide for users on how to write custom CSS for their overlays.
---
## 💬 Phase 3: Real-time Chat & 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
---
### Done
* `[✔️]` **3.1: Dynamic Listener Manager:** Designed and implemented a system to start/stop listener processes for users on application startup/shutdown.
* `[✔️]` **3.2: User-Specific Broadcasting:** Implemented a WebSocket manager and endpoint (`/ws/{user_id}`) to broadcast messages to the correct user's overlay.
* `[✔️]` **3.3: Basic Overlay UI:** Created dynamic overlay templates that connect to the WebSocket and display incoming chat messages.
## 💡 Backlog & Future Features
* `[ ]` Implement YouTube OAuth & `pytchat` listener (Phase 4).
* `[ ]` "Single Message Focus" feature (Issue #1).
* `[ ]` Moderator panels (Issue #2).
```

99
CONTRIBUTING.md Normal file
View 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.

View File

@@ -8,39 +8,46 @@ The goal is to create a service where streamers can log in using their platform
## 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).
* **Database:** MySQL
* **Frontend:** HTML, CSS, JavaScript
* **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: Basic Setup & Twitch Chat Listener (Python)
1. **Project Structure:** Establish a clear directory structure for PHP, Python, Node.js, and static assets.
2. **Python Environment:** Set up a Python virtual environment and install `twitchio`.
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.
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.
### Phase 1: User Authentication & Database (FastAPI)
**Status: ✔️ Complete**
### Phase 2: MySQL Database & User Management (PHP)
1. **MySQL Setup:** Set up a MySQL database and create a `users` table to store user information (Twitch ID, username, access token, refresh token).
2. **PHP Web Server:** Configure a basic PHP web server.
3. **User Registration/Login (PHP):** Implement PHP scripts for user registration and login, integrating with the MySQL database.
4. **Dashboard (PHP/HTML):** Create a basic dashboard where logged-in users can see their Twitch connection status and their unique overlay URL.
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 3: WebSocket Server (Node.js) & Overlay (HTML/CSS/JS)
1. **Node.js Environment:** Set up a Node.js environment and install `ws` (WebSocket library).
2. **WebSocket Server (Node.js):** Create a Node.js WebSocket server that:
* Accepts connections from overlay clients.
* 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 2: User Dashboard & Configuration
**Status: ✔️ Complete**
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.
3. **Overlay URL Generation:** Display a unique, persistent overlay URL for each user.
4. **Theming System:** Implement a site-wide light/dark theme switcher.
5. **Custom CSS Themes:** Develop a full CRUD (Create, Read, Update, Delete) system allowing users to create, manage, and preview their own private CSS overlay themes.
6. **Help & Documentation:** Add a help page to guide users in creating their custom CSS.
### Phase 3: Dynamic Listeners & Basic Overlay
**Status: ✔️ Complete**
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. **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.
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.
3. **Advanced Overlay Customization:** Implement options for users to customize their overlay.
**Status: ⏳ Not Started**
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)

61
FAILED_APPROACH_6.md Normal file
View File

@@ -0,0 +1,61 @@
# Analysis of Failed Approach #6: `twitchio` Initialization Conflict
## 1. Executive Summary
This document outlines the reasons for the persistent failure to initialize the `twitchio` chat listeners within our FastAPI application. Our attempts have been caught in a cyclical error pattern, switching between a `TypeError` for a missing argument and an `OSError` for a port conflict.
The root cause is a fundamental design conflict: we are attempting to use a high-level abstraction (`twitchio.ext.commands.Bot`) in a way it is not designed for. This class is architected as a **standalone application** that includes its own web server for handling Twitch EventSub. Our project requires a simple, "headless" IRC client component to be embedded within our existing FastAPI web server. The `commands.Bot` class is not this component, and our attempts to force it into this role have failed.
**Conclusion:** Continuing to patch this approach is inefficient and unreliable. A new strategy is required.
## 2. The Cyclical Error Pattern
Our efforts have resulted in a loop between three primary, contradictory errors, demonstrating the library's conflicting internal states.
### Error A: `OSError: [Errno 98] address already in use`
* **Trigger:** Occurs with a default `twitchio.ext.commands.Bot` initialization.
* **Implication:** The library, by default, attempts to start its own `AiohttpAdapter` web server (e.g., on port 4343) for EventSub, which immediately conflicts with our main Uvicorn process or any other service.
### Error B: `TypeError: Bot.__init__() missing 1 required keyword-only argument: 'bot_id'`
* **Trigger:** Occurs when we successfully disable the internal web server using a `NullAdapter`.
* **Implication:** By disabling the web server, we seem to place the `Bot` into a different initialization path that now strictly requires the `bot_id` argument, which it previously did not.
### Error C: Back to `OSError: [Errno 98] address already in use`
* **Trigger:** Occurs when we satisfy Error B by providing the `bot_id` while the `NullAdapter` is active.
* **Implication:** This is the most critical failure. It demonstrates that providing the `bot_id` causes the library's constructor to **ignore our `NullAdapter`** and fall back to its default behavior of starting a web server, thus bringing us back to Error A.
### Error D: `TypeError: Client.start() got an unexpected keyword argument 'web_server'`
* **Trigger:** Occurred when we attempted to bypass the adapter system entirely and use `bot.start(web_server=False)`.
* **Implication:** This proves the `start()` method's API does not support this parameter, closing another potential avenue for controlling the library's behavior.
## 3. The Homelab & Nginx Proxy Manager Conflict
This architectural mismatch is especially problematic in our homelab environment using Nginx Proxy Manager.
1. **Single Entry Point:** Our architecture is designed for a single entry point. Nginx Proxy Manager accepts all traffic on ports 80/443 and forwards it to a single backend service: our FastAPI application on port 8000.
2. **Unwanted Second Service:** `twitchio`'s attempt to start a second web server on a different port (e.g., 4343) is fundamentally incompatible with this model. It forces us to treat our single Python application as two distinct backend services.
3. **Unnecessary Complexity:** To make this work, we would have to configure Nginx Proxy Manager with complex location-based routing rules (e.g., route `/` to port 8000, but route `/eventsub` to port 4343). This is brittle, hard to maintain, and completely unnecessary for our goal, which is **IRC chat only**.
4. **Port Conflicts:** In a managed homelab environment (using Docker, etc.), ports are explicitly allocated resources. A library that randomly tries to bind to an arbitrary port is an unstable and unpredictable component that will inevitably conflict with other services.
## 4. Root Cause: Architectural Mismatch
The `twitchio.ext.commands.Bot` class is a powerful, feature-rich tool designed for building **standalone bots**. It is not designed to be a simple, embeddable component within a larger web application that has its own server.
Our application architecture requires a "headless" IRC client—a component that does nothing but connect to Twitch's chat servers and listen for messages. The `commands.Bot` class is not that component. It brings along a suite of other features, including its own web server, which we cannot reliably disable.
Our attempts to "trick" the library into behaving like a simple client have failed because we are fighting against its core design:
## 5. Recommendation: Pivot to a Low-Level Client
We must abandon the use of `twitchio.ext.commands.Bot`.
The correct path forward is to use the lower-level `twitchio.Client` class instead. This class is designed to be a more fundamental IRC client without the high-level command handling and, crucially, without the tightly coupled web server components.
By switching to `twitchio.Client`, we can build a `TwitchBot` class that is truly "headless" and will integrate cleanly into our existing FastAPI application and `ListenerManager` without causing port conflicts or argument mismatches. This aligns our implementation with our architectural needs.

43
README.md Normal file
View 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 2: User Dashboard & Configuration**.
## 🚀 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.
## 🖥️ 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

69
TASKS.md Normal file
View File

@@ -0,0 +1,69 @@
# 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
### 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. Now that Phase 1 is done, these tasks are ready to be worked on.
### To Do
* `[ ]` **2.4: Create Logo and Favicon:** The project should have a logo and a favicon.
### Done
* `[✔️]` **2.0: CSS Refactor & Styling:** Improved the general look and feel of the application pages, including a light/dark theme switcher.
* `[✔️]` **2.1: Dashboard UI:** Created `dashboard.html` for logged-in users to manage settings.
* `[✔️]` **2.2: Config API:** Created API endpoints for `/api/settings` to save user preferences.
* `[✔️]` **2.3: Overlay URL:** Generated and displayed the unique overlay URL for the user on the dashboard.
* `[✔️]` **2.5: Custom CSS Themes:** Implemented a system for users to create, preview, and delete their own private CSS overlay themes.
* `[✔️]` **2.6: CSS Help Page:** Created a guide for users on how to write custom CSS for their overlays.
---
## 💬 Phase 3: Real-time Chat & 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
---
### Done
* `[✔️]` **3.1: Dynamic Listener Manager:** Designed and implemented a system to start/stop listener processes for users on application startup/shutdown.
* `[✔️]` **3.2: User-Specific Broadcasting:** Implemented a WebSocket manager and endpoint (`/ws/{user_id}`) to broadcast messages to the correct user's overlay.
* `[✔️]` **3.3: Basic Overlay UI:** Created dynamic overlay templates that connect to the WebSocket and display incoming chat messages.
## 💡 Backlog & Future Features
* `[ ]` Implement YouTube OAuth & `pytchat` listener (Phase 4).
* `[ ]` "Single Message Focus" feature (Issue #1).
* `[ ]` Moderator panels (Issue #2).

120
auth.py Normal file
View File

@@ -0,0 +1,120 @@
import httpx
import secrets
import logging
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()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 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
logger.info(f"Generated OAuth state: {state} for session.")
# 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
session_state = request.session.pop('oauth_state', None)
if state != session_state:
logger.error(f"OAuth state mismatch! Received state: '{state}', Session state: '{session_state}'")
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)
# Also create a default settings object for the new user
new_settings = models.Setting(owner=user)
db.add(new_settings)
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")

80
chat_listener.py Normal file
View File

@@ -0,0 +1,80 @@
import logging
import twitchio
from sqlalchemy.orm import Session
from database import SessionLocal
import models
import security
logger = logging.getLogger(__name__)
class TwitchBot(twitchio.Client):
def __init__(self, websocket_manager, db_user_id: int):
self.websocket_manager = websocket_manager
# Store our application's database user ID to avoid conflict with twitchio's internal 'owner_id'
self.db_user_id = db_user_id
self.is_initialized = False # Health check flag
async def start(self, access_token: str, refresh_token: str, client_id: str, client_secret: str, channel_name: str):
"""
A custom start method that also handles initialization. This makes the
entire setup process an awaitable, atomic operation.
"""
logger.info(f"DIAGNOSTIC: Initializing and connecting for user {self.db_user_id}...")
# The sensitive __init__ call is now inside the awaitable task.
# FIX: Do not pass client_secret/refresh_token to super().__init__.
# This prevents twitchio from starting its internal web server (AiohttpAdapter) on port 4343,
# which causes "Address already in use" errors when multiple bots run.
super().__init__(token=access_token, client_id=client_id, initial_channels=[channel_name], ssl=True)
self.channel_name = channel_name
self.is_initialized = True
try:
await super().start()
except Exception as e:
logger.error(f"Twitch connection failed for user {self.db_user_id}: {e}")
async def event_ready(self):
"""Called once when the bot goes online."""
# Diagnostic Logging: Confirming the bot is ready and joined the channel.
logger.info(f"DIAGNOSTIC: Listener connected and ready for user_id: {self.db_user_id}, channel: #{self.channel_name}")
async def event_token_refresh(self, token: str, refresh_token: str):
"""
Called when twitchio automatically refreshes the token.
We must save the new tokens back to our database.
"""
logger.info(f"DIAGNOSTIC: Token refreshed for user {self.db_user_id}. Saving new tokens to database.")
db: Session = SessionLocal()
try:
user = db.query(models.User).filter(models.User.id == self.db_user_id).first()
if user:
user.encrypted_tokens = security.encrypt_tokens(token, refresh_token)
db.commit()
finally:
db.close()
async def event_message(self, message): # Mandate: Type hint removed to prevent import errors.
"""Runs every time a message is sent in chat."""
# Diagnostic Logging: Checkpoint 1 - A raw message is received from Twitch.
logger.info(f"DIAGNOSTIC: Message received for user {self.db_user_id} in channel {self.channel_name}: '{message.content}'")
# Ignore messages sent by the bot itself to prevent loops.
if message.echo:
return
# Prepare the message data to be sent to the frontend
chat_data = {
"author": message.author.name if message.author else "Twitch",
"text": message.content,
"platform": "twitch"
}
# Diagnostic Logging: Checkpoint 2 - The message data has been prepared for broadcasting.
logger.info(f"DIAGNOSTIC: Prepared chat_data for user {self.db_user_id}: {chat_data}")
# Broadcast the message to the specific user's overlay
# We need the user's ID to know which WebSocket connection to send to.
user_id = self.db_user_id
await self.websocket_manager.broadcast_to_user(user_id, chat_data)
# Diagnostic Logging: Checkpoint 3 - The broadcast function was called.
logger.info(f"DIAGNOSTIC: Broadcast called for user {self.db_user_id}.")

18
config.py Normal file
View 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()

19
database.py Normal file
View 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()

75
filename Normal file
View File

@@ -0,0 +1,75 @@
import asyncio
from typing import Dict
from chat_listener import TwitchBot
import security # To decrypt tokens
from config import settings # To get client_id and client_secret
class ListenerManager:
def __init__(self):
# This dictionary will hold our running listener tasks.
# The key will be the user_id and the value will be the asyncio.Task.
self.active_listeners: Dict[int, Dict] = {}
print("ListenerManager initialized.")
async def start_listener_for_user(self, user, websocket_manager):
"""
Starts a chat listener for a given user if one isn't already running.
"""
if user.id in self.active_listeners:
print(f"Listener for user {user.id} is already running.")
return
# Guard Clause: Ensure the user has a valid platform ID required by twitchio.
if not user.platform_user_id:
print(f"ERROR: Cannot start listener for user {user.id}. Missing platform_user_id.")
return
print(f"Starting listener for user {user.id} ({user.username})...")
try:
tokens = security.decrypt_tokens(user.encrypted_tokens)
access_token = tokens['access_token']
refresh_token = tokens['refresh_token']
# Initialize the bot object without credentials first.
bot = TwitchBot(
websocket_manager=websocket_manager,
db_user_id=user.id
)
# Create a task that runs our new start method with all credentials.
# If super().__init__ fails inside bot.start(), the exception will be
# caught by our try/except block here.
task = asyncio.create_task(bot.start(
access_token=access_token, refresh_token=refresh_token,
client_id=settings.TWITCH_CLIENT_ID, client_secret=settings.TWITCH_CLIENT_SECRET,
channel_name=user.username
))
# Store both the task and the bot instance for graceful shutdown
self.active_listeners[user.id] = {"task": task, "bot": bot}
except Exception as e:
# This will catch errors during bot instantiation (e.g., bad token)
print(f"ERROR: Failed to instantiate or start listener for user {user.id}: {e}")
async def stop_listener_for_user(self, user_id: int):
"""Stops a chat listener for a given user."""
if user_id not in self.active_listeners:
print(f"No active listener found for user {user_id}.")
return
print(f"Stopping listener for user {user_id}...")
listener_info = self.active_listeners.pop(user_id)
task = listener_info["task"]
bot = listener_info["bot"]
# Gracefully close the bot's connection
if bot and not bot.is_closed():
await bot.close()
# Cancel the asyncio task
task.cancel()
try:
await task
except asyncio.CancelledError:
print(f"Listener for user {user_id} successfully stopped.")

79
listener_manager.py Normal file
View File

@@ -0,0 +1,79 @@
import asyncio
import logging
from typing import Dict
from chat_listener import TwitchBot
import security # To decrypt tokens
from config import settings # To get client_id and client_secret
logger = logging.getLogger(__name__)
class ListenerManager:
def __init__(self):
# This dictionary will hold our running listener tasks.
# The key will be the user_id and the value will be the asyncio.Task.
self.active_listeners: Dict[int, Dict] = {}
logger.info("ListenerManager initialized.")
async def start_listener_for_user(self, user, websocket_manager):
"""
Starts a chat listener for a given user if one isn't already running.
"""
if user.id in self.active_listeners:
logger.info(f"Listener for user {user.id} is already running.")
return
# Guard Clause: Ensure the user has a valid platform ID required by twitchio.
if not user.platform_user_id:
logger.error(f"Cannot start listener for user {user.id}. Missing platform_user_id.")
return
logger.info(f"Starting listener for user {user.id} ({user.username})...")
try:
tokens = security.decrypt_tokens(user.encrypted_tokens)
access_token = tokens['access_token']
refresh_token = tokens['refresh_token']
# Initialize the bot object without credentials first. It's just a lightweight container.
bot = TwitchBot(
websocket_manager=websocket_manager,
db_user_id=user.id
)
# Create a task that runs our new start method with all credentials.
# If super().__init__ fails inside bot.start(), the exception will be
# caught by our try/except block here, preventing hollow objects.
task = asyncio.create_task(bot.start(
access_token=access_token, refresh_token=refresh_token,
client_id=settings.TWITCH_CLIENT_ID, client_secret=settings.TWITCH_CLIENT_SECRET,
channel_name=user.username
))
# Store both the task and the bot instance for graceful shutdown
self.active_listeners[user.id] = {"task": task, "bot": bot}
except Exception as e:
# This will catch errors during bot instantiation (e.g., bad token)
logger.error(f"Failed to instantiate or start listener for user {user.id}: {e}")
async def stop_listener_for_user(self, user_id: int):
"""Stops a chat listener for a given user."""
if user_id not in self.active_listeners:
logger.info(f"No active listener found for user {user_id}.")
return
logger.info(f"Stopping listener for user {user_id}...")
listener_info = self.active_listeners.pop(user_id)
task = listener_info["task"]
bot = listener_info["bot"]
# Gracefully close the bot's connection
# The getattr check prevents the shutdown crash if the bot was never initialized.
if bot and getattr(bot, 'is_initialized', False) and not bot.is_closed():
await bot.close()
# Cancel the asyncio task
task.cancel()
try:
await task
except asyncio.CancelledError:
logger.info(f"Listener for user {user_id} successfully stopped.")

0
main.css Normal file
View File

211
main.py Normal file
View File

@@ -0,0 +1,211 @@
import os
import logging
import asyncio
from fastapi import FastAPI, Request, Depends, HTTPException
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.staticfiles import StaticFiles
from starlette.responses import FileResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from starlette.websockets import WebSocket
from contextlib import asynccontextmanager
from sqlalchemy.orm import Session
import models
from database import engine, SessionLocal
import auth # Import the new auth module
import schemas
from starlette.responses import Response
from config import settings # Import settings to get the secret key
from listener_manager import ListenerManager
from websocket_manager import WebSocketManager
# --- 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")
# --- Logging Configuration ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def background_listener_startup(app: FastAPI):
"""A non-blocking task to start listeners after the app has started."""
logger.info("Background task: Starting listeners for all users...")
db = SessionLocal()
users = db.query(models.User).all()
db.close()
for user in users:
# Use try/except to ensure one failing listener doesn't stop others
try:
await app.state.listener_manager.start_listener_for_user(user, app.state.websocket_manager)
except Exception as e:
logger.error(f"Failed to start listener for user {user.id} ({user.username}): {e}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# This code runs on startup
logger.info("Application startup: Creating database tables...")
app.state.websocket_manager = WebSocketManager()
app.state.listener_manager = ListenerManager()
models.Base.metadata.create_all(bind=engine)
logger.info("Application startup: Database tables created.")
# Decouple listener startup from the main application startup
asyncio.create_task(background_listener_startup(app))
yield
# This code runs on shutdown
logger.info("Application shutdown: Stopping all listeners...")
manager = app.state.listener_manager
# Create a copy of keys to avoid runtime errors from changing dict size
for user_id in list(manager.active_listeners.keys()):
await manager.stop_listener_for_user(user_id)
app = FastAPI(lifespan=lifespan)
# 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.
# Note: Middleware is applied in reverse order (last added is first executed).
# We want ProxyHeaders to run FIRST (outermost) to fix the scheme/host,
# then SessionMiddleware to run SECOND (inner) so it sees the correct scheme.
app.add_middleware(SessionMiddleware, secret_key=settings.ENCRYPTION_KEY)
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
# Mount the 'static' directory using an absolute path for reliability
# This MUST be done before the routes that depend on it are defined.
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Add the authentication router
app.include_router(auth.router)
# --- Template Dependency ---
def get_templates():
return Jinja2Templates(directory=TEMPLATES_DIR)
@app.get("/")
async def read_root(request: Request, templates: Jinja2Templates = Depends(get_templates)):
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/dashboard")
async def read_dashboard(request: Request, db: Session = Depends(auth.get_db),
templates: Jinja2Templates = Depends(get_templates)):
# 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}"
# Ensure user has settings, create if they don't for some reason
if not user.settings:
user.settings = models.Setting()
db.commit()
return templates.TemplateResponse("dashboard.html", {
"request": request,
"user": user,
"overlay_url": overlay_url,
"current_theme": user.settings.overlay_theme,
"settings": settings,
"custom_themes": user.custom_themes
})
@app.get("/logout")
async def logout(request: Request):
# Clear the session cookie
request.session.clear()
return RedirectResponse(url="/")
@app.get("/help/css")
async def css_help(request: Request, templates: Jinja2Templates = Depends(get_templates)):
return templates.TemplateResponse("help_css.html", {"request": request})
@app.get("/overlay/{user_id}")
async def read_overlay(request: Request, user_id: int, theme_override: str = None,
db: Session = Depends(auth.get_db), templates: Jinja2Templates = Depends(get_templates)):
# This endpoint serves the overlay page.
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
theme_name = "dark-purple" # Default theme
if theme_override:
theme_name = theme_override
elif user.settings and user.settings.overlay_theme:
theme_name = user.settings.overlay_theme
# Check if it's a custom theme
if theme_name.startswith("custom-"):
theme_id = int(theme_name.split("-")[1])
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id, models.CustomTheme.owner_id == user.id).first()
if not theme:
raise HTTPException(status_code=404, detail="Custom theme not found")
# Use a generic overlay template that will link to the dynamic CSS
return templates.TemplateResponse("overlay-custom.html", {"request": request, "theme_id": theme.id})
return templates.TemplateResponse(f"overlay-{theme_name}.html", {"request": request})
@app.post("/api/settings")
async def update_settings(settings_data: schemas.SettingsUpdate, request: Request, db: Session = Depends(auth.get_db)):
user_id = request.session.get('user_id')
if not user_id:
raise HTTPException(status_code=401, detail="Not authenticated")
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user.settings:
user.settings = models.Setting()
user.settings.overlay_theme = settings_data.overlay_theme
db.commit()
return {"message": "Settings updated successfully"}
@app.post("/api/themes", response_model=schemas.CustomTheme)
async def create_theme(theme_data: schemas.CustomThemeCreate, request: Request, db: Session = Depends(auth.get_db)):
user_id = request.session.get('user_id')
if not user_id:
raise HTTPException(status_code=401, detail="Not authenticated")
new_theme = models.CustomTheme(**theme_data.dict(), owner_id=user_id)
db.add(new_theme)
db.commit()
db.refresh(new_theme)
return new_theme
@app.delete("/api/themes/{theme_id}")
async def delete_theme(theme_id: int, request: Request, db: Session = Depends(auth.get_db)):
user_id = request.session.get('user_id')
if not user_id:
raise HTTPException(status_code=401, detail="Not authenticated")
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id, models.CustomTheme.owner_id == user_id).first()
if not theme:
raise HTTPException(status_code=404, detail="Theme not found")
db.delete(theme)
db.commit()
return {"message": "Theme deleted successfully"}
@app.get("/css/custom/{theme_id}")
async def get_custom_css(theme_id: int, db: Session = Depends(auth.get_db)):
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id).first()
if not theme:
raise HTTPException(status_code=404, detail="Custom theme not found")
return Response(content=theme.css_content, media_type="text/css")
@app.websocket("/ws/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: int):
manager = websocket.app.state.websocket_manager
await manager.connect(user_id, websocket)
try:
while True:
# Keep the connection alive
await websocket.receive_text()
except Exception:
manager.disconnect(user_id, websocket)
logger.info(f"WebSocket for user {user_id} disconnected.")

39
models.py Normal file
View File

@@ -0,0 +1,39 @@
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)
custom_themes = relationship("CustomTheme", back_populates="owner")
class Setting(Base):
__tablename__ = "settings"
id = Column(Integer, primary_key=True, index=True)
custom_css = Column(Text, nullable=True)
overlay_theme = Column(String, default="dark-purple")
user_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="settings")
class CustomTheme(Base):
__tablename__ = "custom_themes"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
css_content = Column(Text, nullable=False)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
owner = relationship("User", back_populates="custom_themes")

11
requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
fastapi
uvicorn[standard]
sqlalchemy
httpx
cryptography
python-dotenv
itsdangerous
jinja2
pydantic
python-jose[cryptography]
twitchio==3.1.0

19
schemas.py Normal file
View File

@@ -0,0 +1,19 @@
from pydantic import BaseModel
class SettingsUpdate(BaseModel):
overlay_theme: str
class CustomThemeBase(BaseModel):
name: str
css_content: str
class CustomThemeCreate(CustomThemeBase):
pass
class CustomTheme(CustomThemeBase):
id: int
owner_id: int
class Config:
# This allows the Pydantic model to be created from a SQLAlchemy ORM object
from_attributes = True

47
security.py Normal file
View 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
View File

@@ -0,0 +1 @@
# This file ensures the 'static' directory is tracked by Git.

290
static/css/main.css Normal file
View File

@@ -0,0 +1,290 @@
/* --- Theme Variables and CSS Reset --- */
:root,
[data-theme="dark"] {
--background-color: #1a1a2e;
--surface-color: #16213e;
--text-color: #e0e0e0;
--text-muted-color: #a0a0a0;
--text-inverted-color: #ffffff;
--border-color: #4f4f7a;
--primary-color: #7f5af0;
--primary-hover-color: #6a48d7;
}
[data-theme="light"] {
--background-color: #f0f2f5;
--surface-color: #ffffff;
--text-color: #1c1e21;
--text-muted-color: #606770;
--text-inverted-color: #ffffff;
--border-color: #dddfe2;
--primary-color: #7f5af0;
--primary-hover-color: #6a48d7;
}
/* Box-sizing reset */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Reset margins and paddings on most elements */
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
figure,
blockquote,
dl,
dd {
margin: 0;
}
/* Basic body styling */
body {
min-height: 100vh;
line-height: 1.5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.2s, color 0.2s;
}
/* Make images responsive */
img,
picture {
max-width: 100%;
display: block;
}
/* General Container for login and dashboard */
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 1rem;
}
/* Login Page Styles */
.login-container {
width: 100%;
padding: 1rem;
}
.login-box {
width: 100%;
max-width: 400px;
margin: 0 auto;
margin-top: 5vh;
padding: 2.5rem;
background-color: var(--surface-color);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
text-align: center;
}
.login-box h1 {
color: var(--text-color);
margin-bottom: 0.5rem;
}
.login-box p {
margin-bottom: 2rem;
color: var(--text-muted-color);
}
.input-group {
margin-bottom: 1.5rem;
text-align: left;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--text-muted-color);
}
.input-group input {
width: 100%;
padding: 0.75rem 1rem;
background-color: var(--background-color);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 1rem;
}
.btn-primary {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 6px;
background-color: var(--primary-color);
color: var(--text-inverted-color);
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-primary:hover {
background-color: var(--primary-hover-color);
}
.login-footer {
margin-top: 1.5rem;
}
.login-footer a {
color: var(--primary-color);
text-decoration: none;
font-size: 0.9rem;
}
.login-footer a:hover {
text-decoration: underline;
}
/* Header Styles */
.logo h1 {
font-size: 1.5rem;
color: var(--text-color);
}
.logo {
text-decoration: none;
}
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
width: 100%;
margin-bottom: 2rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.logout-btn {
display: inline-block;
padding: 0.5rem 1rem;
background-color: var(--primary-color);
color: var(--text-inverted-color);
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.logout-btn:hover {
background-color: var(--primary-hover-color);
}
.theme-btn {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 5px;
border-radius: 50%;
}
.theme-btn:hover {
background-color: var(--surface-color);
}
[data-theme="light"] .moon-icon { display: block; }
[data-theme="light"] .sun-icon { display: none; }
[data-theme="dark"] .moon-icon { display: none; }
[data-theme="dark"] .sun-icon { display: block; }
/* Dashboard Card Styles */
.card {
background-color: var(--surface-color);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h2 {
margin-top: 0;
margin-bottom: 0.5rem;
}
.card p {
margin-top: 0;
color: var(--text-muted-color);
}
/* Custom Theme Form Styles */
.theme-form {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
border-radius: 6px;
font-family: inherit;
font-size: 1rem;
}
.custom-theme-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.5rem;
background-color: var(--background-color);
}
.delete-theme-btn {
background-color: #e43f5a;
color: white;
border: none;
padding: 0.3rem 0.8rem;
border-radius: 4px;
cursor: pointer;
}
/* Styles for help page code examples */
pre {
background-color: var(--background-color);
border: 1px solid var(--border-color);
padding: 1rem;
border-radius: 6px;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
}

View File

@@ -0,0 +1,12 @@
body {
background-color: transparent;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
overflow: hidden;
}
.chat-container {
padding: 10px;
height: 100vh;
}

View File

@@ -0,0 +1,28 @@
body {
background-color: transparent;
color: #f0f0f0;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
padding: 10px;
overflow: hidden;
text-shadow: 0 0 5px rgba(57, 255, 20, 0.5);
}
.chat-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-message {
padding: 6px 10px;
background-color: rgba(10, 30, 10, 0.6);
border-radius: 6px;
border-left: 3px solid #39FF14; /* Neon green accent */
}
.username {
font-weight: 800;
color: #39FF14; /* Neon green */
}

View File

@@ -0,0 +1,28 @@
body {
background-color: transparent;
color: #e0e0e0;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
padding: 10px;
overflow: hidden;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
}
.chat-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-message {
padding: 6px 10px;
background-color: rgba(26, 26, 46, 0.7); /* Dark blue/purple transparent background */
border-radius: 6px;
border: 1px solid rgba(127, 90, 240, 0.3);
}
.username {
font-weight: 800;
color: #a970ff; /* Twitch-like purple */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

21
static/login.html Normal file
View 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>

View File

@@ -0,0 +1,26 @@
body {
background-color: transparent;
color: #1a1a1a; /* Dark text for readability on light backgrounds */
font-family: 'Inter', sans-serif;
font-size: 18px;
margin: 0;
padding: 10px;
font-weight: 600;
-webkit-text-stroke: 0.5px white; /* White outline for text */
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5);
}
.chat-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-message {
padding: 5px 10px;
background-color: rgba(240, 240, 240, 0.5); /* Semi-transparent light background */
border-radius: 5px;
border-left: 3px solid #00ff7f; /* Spring green accent */
}
.username {
font-weight: 800;
color: #008000; /* Dark green for usernames */
}

View File

@@ -0,0 +1,24 @@
body {
background-color: transparent;
color: #e6edf3; /* Light text for dark backgrounds */
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
padding: 10px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.9);
}
.chat-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-message {
padding: 4px 8px;
background-color: rgba(22, 27, 34, 0.6); /* Semi-transparent dark background */
border-radius: 4px;
border-left: 3px solid #9146FF; /* Twitch purple accent */
}
.username {
font-weight: 800;
color: #a970ff; /* Lighter purple for usernames */
}

77
static/style.css Normal file
View 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 */
}

55
templates/base.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MultiChat Overlay{% endblock %}</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;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}">
</head>
<body>
<div class="container">
<header class="main-header">
<a href="/" class="logo"><h1>MultiChat Overlay</h1></a>
<div class="header-actions">
<button id="theme-toggle" class="theme-btn" title="Toggle theme">
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.64 5.64c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0s.39-1.02 0-1.41L5.64 5.64zm12.72 12.72c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.06-1.06zM5.64 18.36l-1.06-1.06c-.39-.39-.39-1.02 0-1.41s1.02-.39 1.41 0l1.06 1.06c.39.39.39 1.02 0 1.41s-1.02.39-1.41 0zm12.72-12.72l-1.06-1.06c-.39-.39-.39-1.02 0-1.41s1.02-.39 1.41 0l1.06 1.06c.39.39.39 1.02 0 1.41-.39.39-1.02.39-1.41 0z"></path></svg>
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M9.37 5.51A7.35 7.35 0 0 0 9 6c0 4.42 3.58 8 8 8 .36 0 .72-.02 1.08-.06A7.5 7.5 0 0 1 9.37 5.51zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10A9.96 9.96 0 0 0 12 2z"></path></svg>
</button>
{% if user %}
<a href="/logout" class="logout-btn">Logout</a>
{% endif %}
</div>
</header>
{% block content %}{% endblock %}
</div>
<script>
(function() {
const themeToggle = document.getElementById('theme-toggle');
const getTheme = () => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) return storedTheme;
// For new visitors, default to dark mode regardless of their OS setting.
return 'dark';
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
};
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
// Set initial theme on page load
setTheme(getTheme());
})();
</script>
</body>
</html>

240
templates/dashboard.html Normal file
View File

@@ -0,0 +1,240 @@
{% extends "base.html" %}
{% block title %}Dashboard - {{ super() }}{% endblock %}
{% block content %}
<div class="card">
<h2>Welcome, {{ user.username }}!</h2>
<p>This is your personal dashboard. Here you can manage your overlay settings and find your unique URL.</p>
</div>
<div class="card">
<h2>Your Overlay URL</h2>
<p>Copy this URL and add it as a "Browser Source" in your streaming software (e.g., OBS, Streamlabs).</p>
<div class="url-box">
<input type="text" id="overlayUrl" value="{{ overlay_url }}" readonly>
<button id="copyButton" class="btn">Copy</button>
</div>
</div>
<div class="card">
<h2>Overlay Theme</h2>
<p>Choose a theme for your chat overlay. Your selection will be saved automatically.</p>
<div class="theme-selector-container">
<div class="theme-options">
<label for="theme-select">Select a theme:</label>
<select id="theme-select" name="theme">
<option value="dark-purple" {% if current_theme == 'dark-purple' %}selected{% endif %}>Dark Purple</option>
<option value="bright-green" {% if current_theme == 'bright-green' %}selected{% endif %}>Bright Green</option>
<option value="minimal-light" {% if current_theme == 'minimal-light' %}selected{% endif %}>Minimal Light</option>
<option value="hacker-green" {% if current_theme == 'hacker-green' %}selected{% endif %}>Hacker Green</option>
{% if custom_themes %}
<optgroup label="Your Themes">
{% for theme in custom_themes %}
<option value="custom-{{ theme.id }}" {% if current_theme == 'custom-' ~ theme.id %}selected{% endif %}>{{ theme.name }}</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
</div>
<div class="theme-preview">
<h3>Preview</h3>
<iframe id="theme-preview-frame" src="" frameborder="0" scrolling="no"></iframe>
</div>
</div>
</div>
<div class="card">
<h2>
Custom Themes
<a href="/help/css" target="_blank" class="help-link" title="Open CSS guide in new window">(?)</a>
</h2>
<p>Create your own themes with CSS. These are private to your account.</p>
<div id="custom-themes-list">
{% for theme in custom_themes %}
<div class="custom-theme-item" id="theme-item-{{ theme.id }}">
<span>{{ theme.name }}</span>
<button class="delete-theme-btn" data-theme-id="{{ theme.id }}">Delete</button>
</div>
{% endfor %}
</div>
<form id="theme-form" class="theme-form">
<h3>Create New Theme</h3>
<div class="form-group">
<label for="theme-name">Theme Name</label>
<input type="text" id="theme-name" name="name" required>
</div>
<div class="form-group">
<label for="theme-css">CSS Content</label>
<textarea id="theme-css" name="css_content" rows="8" required placeholder="body { color: red; }"></textarea>
</div>
<button type="submit" class="btn-primary">Save Theme</button>
</form>
</div>
<style>
.help-link {
font-size: 0.9rem;
vertical-align: middle;
text-decoration: none;
color: var(--primary-color);
}
.url-box {
display: flex;
gap: 10px;
}
.url-box input {
flex-grow: 1;
padding: 10px;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
border-radius: 6px;
font-family: monospace;
}
.theme-selector-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
.theme-options select {
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--surface-color);
color: var(--text-color);
font-size: 1rem;
}
.theme-preview {
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--background-color);
}
.theme-preview h3 {
margin: 0;
padding: 0.75rem 1rem;
font-size: 1rem;
border-bottom: 1px solid var(--border-color);
}
#theme-preview-frame {
width: 100%;
height: 150px;
border-radius: 0 0 8px 8px;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- Copy URL Logic ---
const copyButton = document.getElementById('copyButton');
const overlayUrlInput = document.getElementById('overlayUrl');
copyButton.addEventListener('click', () => {
overlayUrlInput.select();
document.execCommand('copy');
copyButton.textContent = 'Copied!';
setTimeout(() => { copyButton.textContent = 'Copy'; }, 2000);
});
// --- Theme Selector Logic ---
const themeSelect = document.getElementById('theme-select');
const previewFrame = document.getElementById('theme-preview-frame');
const baseUrl = "{{ settings.APP_BASE_URL }}";
const userId = "{{ user.id }}";
// Function to update preview and save settings
function updateTheme(selectedTheme) {
// Update iframe preview
previewFrame.src = `${baseUrl}/overlay/${userId}?theme_override=${selectedTheme}`;
// Save the setting to the backend
fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ overlay_theme: selectedTheme }),
})
.then(response => response.json())
.then(data => {
console.log('Settings saved:', data.message);
})
.catch(error => console.error('Error saving settings:', error));
}
// Event listener for dropdown change
themeSelect.addEventListener('change', (event) => {
updateTheme(event.target.value);
});
// Set initial preview on page load
updateTheme(themeSelect.value);
// --- Custom Theme Creation Logic ---
const themeForm = document.getElementById('theme-form');
themeForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(themeForm);
const data = Object.fromEntries(formData.entries());
fetch('/api/themes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
.then(response => response.json())
.then(newTheme => {
// Add new theme to the list and dropdown without reloading
addThemeToList(newTheme);
addThemeToSelect(newTheme);
themeForm.reset(); // Clear the form
})
.catch(error => console.error('Error creating theme:', error));
});
// --- Custom Theme Deletion Logic ---
const themesListContainer = document.getElementById('custom-themes-list');
themesListContainer.addEventListener('click', function(event) {
if (event.target.classList.contains('delete-theme-btn')) {
const themeId = event.target.dataset.themeId;
if (confirm('Are you sure you want to delete this theme?')) {
fetch(`/api/themes/${themeId}`, { method: 'DELETE' })
.then(response => {
if (response.ok) {
// Remove theme from the list and dropdown
document.getElementById(`theme-item-${themeId}`).remove();
document.querySelector(`#theme-select option[value="custom-${themeId}"]`).remove();
}
})
.catch(error => console.error('Error deleting theme:', error));
}
}
});
function addThemeToList(theme) {
const list = document.getElementById('custom-themes-list');
const item = document.createElement('div');
item.className = 'custom-theme-item';
item.id = `theme-item-${theme.id}`;
item.innerHTML = `<span>${theme.name}</span><button class="delete-theme-btn" data-theme-id="${theme.id}">Delete</button>`;
list.appendChild(item);
}
function addThemeToSelect(theme) {
let optgroup = document.querySelector('#theme-select optgroup[label="Your Themes"]');
if (!optgroup) {
optgroup = document.createElement('optgroup');
optgroup.label = 'Your Themes';
themeSelect.appendChild(optgroup);
}
const option = document.createElement('option');
option.value = `custom-${theme.id}`;
option.textContent = theme.name;
optgroup.appendChild(option);
}
});
</script>
{% endblock %}

46
templates/help_css.html Normal file
View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}CSS Overlay Help - {{ super() }}{% endblock %}
{% block content %}
<div class="card">
<h2>Custom Overlay CSS Guide</h2>
<p>This guide will help you create your own custom CSS for the chat overlay. Your CSS is applied on top of a base stylesheet, so you only need to override the styles you want to change.</p>
</div>
<div class="card">
<h3>HTML Structure</h3>
<p>Your CSS will be applied to a simple HTML structure. Here are the key classes you can target:</p>
<ul>
<li><code>.chat-container</code>: The main container holding all messages.</li>
<li><code>.chat-message</code>: The container for a single message, including the username and text.</li>
<li><code>.username</code>: The part of the message that shows the chatter's name.</li>
<li><code>.message-text</code>: The actual content of the chat message.</li>
</ul>
<p>The <code>body</code> of the overlay is transparent by default, so you only need to style the message elements.</p>
</div>
<div class="card">
<h3>Example: "Bubbly" Theme</h3>
<p>Here is a simple example to get you started. This creates chat bubbles with a gradient background.</p>
<pre><code>body {
font-family: 'Comic Sans MS', cursive, sans-serif;
font-size: 18px;
text-shadow: 1px 1px 2px #333;
}
.chat-message {
background: linear-gradient(135deg, #7f5af0, #a970ff);
color: white;
padding: 10px 15px;
border-radius: 20px;
margin-bottom: 10px;
display: inline-block;
}
.username {
font-weight: bold;
color: #e0e0e0;
}</code></pre>
</div>
{% endblock %}

16
templates/login.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Login - MultiChat Overlay{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-box">
<h1>MultiChat Overlay</h1>
<p>Connect your streaming accounts to get started.</p>
<a href="/login/twitch" class="btn-primary twitch-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 10px;"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0h1.714v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"></path></svg>
Login with Twitch
</a>
</div>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Overlay - Bright Green</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="{{ url_for('static', path='css/overlay-bright-green.css') }}">
</head>
<body>
<div class="chat-container">
<div class="chat-message"><span class="username">Streamer:</span> This is the bright green theme!</div>
<div class="chat-message"><span class="username">VerdantViewer:</span> Looks so fresh and clean!</div>
<div class="chat-message"><span class="username">NightBot:</span> Welcome to the channel!</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Chat Overlay</title>
<link rel="stylesheet" href="{{ url_for('static', path='css/overlay-base.css') }}">
<link rel="stylesheet" href="/css/custom/{{ theme_id }}">
</head>
<body>
<div class="chat-container">
<!-- Chat messages will be injected here by the WebSocket client -->
</div>
</body>
</html>

View File

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Overlay - Dark Purple</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="{{ url_for('static', path='css/overlay-dark-purple.css') }}">
</head>
<body>
<div class="chat-container">
<div class="chat-message"><span class="username">Streamer:</span> This is the dark purple theme!</div>
<div class="chat-message"><span class="username">PurpleHaze:</span> Very cool, feels like Twitch!</div>
<div class="chat-message"><span class="username">NightBot:</span> Welcome to the channel!</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hacker Green Overlay</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=VT323&display=swap" rel="stylesheet">
<style>
body {
background-color: transparent;
color: #0f0;
font-family: 'VT323', monospace;
font-size: 20px;
margin: 0;
padding: 10px;
overflow: hidden;
text-shadow: 0 0 5px #0f0;
}
.chat-container {
display: flex;
flex-direction: column;
gap: 5px;
}
.username {
font-weight: bold;
}
</style>
</head>
<body>
<div id="chat-container" class="chat-container">
<!-- Messages will be injected here -->
</div>
<script>
const userId = window.location.pathname.split('/')[2];
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws/${userId}`);
ws.onmessage = function(event) {
const messageData = JSON.parse(event.data);
const chatContainer = document.getElementById('chat-container');
const messageElement = document.createElement('div');
messageElement.innerHTML = `<span class="username">${messageData.author}:</span> ${messageData.text}`;
chatContainer.appendChild(messageElement);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minimal Light Overlay</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
body {
background-color: rgba(0, 0, 0, 0);
color: #111827;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
padding: 10px;
overflow: hidden;
}
.chat-message {
background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 8px;
backdrop-filter: blur(4px);
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.username {
font-weight: 600;
}
</style>
</head>
<body>
<!-- Example Message -->
<div class="chat-message">
<span class="username">User:</span>
<span class="message">This is the minimal light theme.</span>
</div>
</body>
</html>

42
templates/overlay.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Overlay</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">
<style>
body {
background-color: transparent;
color: white;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
padding: 10px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
.chat-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-message {
padding: 4px 8px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 4px;
}
.username {
font-weight: 800;
color: #a970ff; /* A nice purple for usernames */
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-message"><span class="username">User123:</span> This is an example chat message!</div>
<div class="chat-message"><span class="username">StreamFan:</span> Looks great! Can't wait to use this.</div>
<div class="chat-message"><span class="username">AnotherViewer:</span> Hello world!</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Test Overlay</title>
<style>
body {
background-color: black;
color: white;
font-family: sans-serif;
margin: 0;
padding: 20px;
}
.message {
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>Live Test Overlay</h1>
<div id="chat-messages">
<!-- Messages will appear here -->
</div>
<script>
// JavaScript to connect to WebSocket and display messages will go here
</script>
</body>
</html>

63
twitch_test.py Normal file
View File

@@ -0,0 +1,63 @@
import asyncio
import os
from dotenv import load_dotenv
import twitchio
from twitchio.ext import commands
# --- Standalone Twitch IRC Connection Test ---
class TestBot(twitchio.Client):
"""A minimal twitchio client for testing IRC connectivity."""
def __init__(self, channel_name: str):
# Load credentials from environment variables
self.TMI_TOKEN = os.getenv("TWITCH_TEST_TOKEN")
self.CLIENT_ID = os.getenv("TWITCH_CLIENT_ID")
self.TARGET_CHANNEL = channel_name
# Pre-flight checks
if not all([self.TMI_TOKEN, self.CLIENT_ID, self.TARGET_CHANNEL]):
raise ValueError("Missing required environment variables. Ensure TWITCH_TEST_TOKEN, TWITCH_CLIENT_ID, and a channel are provided.")
print("--- Configuration ---")
print(f"CLIENT_ID: {self.CLIENT_ID[:4]}...{self.CLIENT_ID[-4:]}")
print(f"TOKEN: {self.TMI_TOKEN[:12]}...")
print(f"TARGET CHANNEL: {self.TARGET_CHANNEL}")
print("-----------------------")
super().__init__(
token=f"oauth:{self.TMI_TOKEN}",
client_id=self.CLIENT_ID,
initial_channels=[self.TARGET_CHANNEL],
ssl=True
)
async def event_ready(self):
"""Called once when the bot goes online."""
print("\n--- Connection Successful ---")
print(f"Logged in as: {self.nick}")
print(f"Listening for messages in #{self.TARGET_CHANNEL}...")
print("---------------------------\n")
async def event_message(self, message):
"""Runs every time a message is sent in chat."""
if message.echo:
return
print(f"#{message.channel.name} | {message.author.name}: {message.content}")
async def main():
"""Main function to run the test bot."""
# IMPORTANT: Replace 'ramforth' with the channel you want to test.
channel_to_test = "ramforth"
print(f"Attempting to connect to Twitch IRC for channel: {channel_to_test}")
try:
bot = TestBot(channel_name=channel_to_test)
await bot.start()
except Exception as e:
print(f"\n--- AN ERROR OCCURRED ---")
print(f"Error: {e}")
print("Please check your credentials and network connection.")
if __name__ == "__main__":
asyncio.run(main())

22
websocket_manager.py Normal file
View File

@@ -0,0 +1,22 @@
from typing import Dict, List
from fastapi import WebSocket
class WebSocketManager:
def __init__(self):
# Maps user_id to a list of active WebSocket connections
self.active_connections: Dict[int, List[WebSocket]] = {}
print("WebSocketManager initialized.")
async def connect(self, user_id: int, websocket: WebSocket):
await websocket.accept()
if user_id not in self.active_connections:
self.active_connections[user_id] = []
self.active_connections[user_id].append(websocket)
def disconnect(self, user_id: int, websocket: WebSocket):
self.active_connections[user_id].remove(websocket)
async def broadcast_to_user(self, user_id: int, message: dict):
if user_id in self.active_connections:
for connection in self.active_connections[user_id]:
await connection.send_json(message)