diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0ab6d37 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Syntax check + run: | + python3 -c " + import ast, sys, pathlib + errors = [] + for f in pathlib.Path('app').rglob('*.py'): + try: + ast.parse(f.read_text()) + except SyntaxError as e: + errors.append(f'{f}: {e}') + if errors: + for e in errors: print('SYNTAX ERROR:', e) + sys.exit(1) + files = list(pathlib.Path('app').rglob('*.py')) + print(f'All {len(files)} Python files OK') + " + + - name: Import check + run: python -c "from app.main import app; print('App import OK')" + env: + DATABASE_URL: sqlite:///./test.db + SECRET_KEY: ci-test-secret-not-real + TWITCH_CLIENT_ID: ci-placeholder + TWITCH_CLIENT_SECRET: ci-placeholder + TWITCH_REDIRECT_URI: http://localhost:8000/auth/callback + APP_BASE_URL: http://localhost:8000 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8cd3931 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md + +## Running the app + +```bash +source .venv/bin/activate +uvicorn app.main:app --reload +``` + +Requires `.env` — copy from `.env.example`. Database is created automatically on startup (`SQLModel.metadata.create_all`). + +## Architecture + +``` +Twitch ──EventSub WebSocket──► app/eventsub.py ──► dispatch.py ──► ws.py ──► OBS overlay +Twitch ──IRC (tmi.js)────────────────────────────────────────────────────► OBS overlay +``` + +- `eventsub.py` runs as an asyncio background task started in `main.py` lifespan +- When it receives a notification, it calls `dispatch.dispatch_event()` +- `dispatch_event()` opens its own DB session (not request-scoped), resolves matching `EventAction` rows, checks cooldowns, logs to `ActivityLog`, and calls `ws.manager.broadcast()` +- `ws.manager` holds a list of active WebSocket connections from `/ws/overlay` +- The overlay JS receives the broadcast and plays the action + +## Key conventions + +**No passwords anywhere.** Auth is Twitch OAuth only. Session is a signed cookie via `SessionMiddleware`. `request.session["user_id"]` stores the DB user ID after login. + +**Singleton tables.** `Settings` and `OverlayLayout` each have one row. `database.get_settings()` and `database.get_layout()` create the row on first call. Both return detached objects (safe to use after session close — no lazy relationships). + +**Background DB sessions.** `eventsub.py` and `dispatch.py` use `with Session(engine) as session` directly — not FastAPI's `Depends(get_session)` which is request-scoped. Never pass a request-scoped session to a background task. + +**Cooldowns are in-memory.** `app/cooldowns.py` tracks `{action_id: (triggered_at, duration)}`. Restarting the server resets all cooldowns. This is intentional — keeping them in the DB is not worth the complexity for this use case. + +**Multiple actions per event.** `dispatch_event()` loops through ALL matching `EventAction` rows for the event type and fires each one (subject to its own cooldown). There is no "first match wins" logic. + +**Event matching rules:** +- `event_type="command"`: `event_detail` must match the command name (case-insensitive, required) +- `event_type="bits"`: if `event_detail` is set, it's treated as a minimum bits threshold (`bits_amount >= int(event_detail)`) +- `event_type="channel_points"`: `event_detail` is the reward ID; None matches any reward +- `event_type="sub"/"gift_sub"`: `event_detail` is the tier ("1000", "2000", "3000"); None matches any tier +- `event_type="follow"/"raid"`: `event_detail` not used for matching + +## Route map + +| Method | Path | File | Notes | +|---|---|---|---| +| GET | `/` | routers/dashboard.py | Activity log, EventSub status | +| GET | `/auth/login` | routers/auth.py | Login page | +| GET | `/auth/twitch` | routers/auth.py | Redirect to Twitch OAuth | +| GET | `/auth/callback` | routers/auth.py | Exchange code, save user, start EventSub | +| GET | `/auth/logout` | routers/auth.py | Clear session | +| GET | `/overlay` | routers/overlay.py | Live OBS browser source | +| GET | `/overlay/preview` | routers/overlay.py | Mock preview for layout editor | +| WS | `/ws/overlay` | routers/overlay.py | Bidirectional: commands up, actions down | +| GET/POST | `/actions` | routers/actions.py | List and create actions (HTMX) | +| POST | `/actions/{id}/test` | routers/actions.py | Fire action immediately, skip cooldown | +| POST | `/actions/{id}/toggle` | routers/actions.py | Enable/disable (HTMX) | +| DELETE | `/actions/{id}` | routers/actions.py | Delete (HTMX) | +| GET/POST | `/media` | routers/media.py | Upload list (HTMX) | +| DELETE | `/media/{id}` | routers/media.py | Delete file from disk and DB | +| GET/POST | `/layout` | routers/layout.py | Style form | +| POST | `/layout/positions` | routers/layout.py | JSON endpoint called by drag JS | +| GET/POST | `/settings` | routers/settings.py | Channel, gap audio, custom CSS/JS | +| POST | `/settings/reconnect` | routers/settings.py | Restart EventSub task | + +## Overlay CSS variable system + +All overlay positioning and styling is driven by CSS custom properties injected into `overlay.html` and `overlay_preview.html` from `OverlayLayout`: + +| Variable | Controls | +|---|---| +| `--chat-left`, `--chat-top` | Chat box position (%) | +| `--chat-width`, `--chat-height` | Chat box size (%) | +| `--chat-font-size` | Chat font size (px) | +| `--chat-bg-opacity` | Chat message background opacity | +| `--chat-text-color` | Chat message text color | +| `--alert-left`, `--alert-top` | Alert position (%, centered via transform) | +| `--alert-font-size` | Alert font size (px) | +| `--alert-bg-opacity` | Alert background opacity | +| `--alert-text-color` | Alert text color | + +The layout editor (`/layout`) has drag handles in the parent document overlaid on an iframe pointing to `/overlay/preview`. Since both are same-origin, the editor updates the iframe's CSS variables directly via `iframe.contentDocument.documentElement.style.setProperty(...)` for live preview. Positions are persisted via `POST /layout/positions` (JSON) on drag end. + +## Custom events dispatched to overlay JS + +When the overlay WebSocket receives an `execute` message, it dispatches a `CustomEvent` on `document` before playing the alert: + +```javascript +document.dispatchEvent(new CustomEvent(`bashyoverlay:${msg.event_type}`, { detail: msg })); +``` + +Available event types: `sub`, `gift_sub`, `bits`, `channel_points`, `command`, `follow`, `raid`. + +`msg.detail` contains `{ event_type, action_type, media_url, alert_text }`. + +## Data model + +``` +User ──────────────────────────────── Twitch OAuth user (one per login) +Settings ──────────────────────────── Singleton: channel, gap audio, custom CSS/JS +OverlayLayout ─────────────────────── Singleton: widget positions and styles +MediaFile ──────────────────────────── Uploaded sound or video file +EventAction ──── media_file (FK) ───── Event → action mapping +ActivityLog ────────────────────────── Append-only event log +``` + +`Claim` unique constraint on `(item_id, attendee_id)` prevents double-claiming — wait, that's IllBring. BashyOverlay has no Claim model. `EventAction` has no unique constraints; multiple rows with the same `event_type` are allowed and all fire. + +## EventSub subscriptions + +| Twitch event | Condition | Scope required | +|---|---|---| +| `channel.subscribe` v1 | `broadcaster_user_id` | `channel:read:subscriptions` | +| `channel.subscription.message` v1 | `broadcaster_user_id` | `channel:read:subscriptions` | +| `channel.subscription.gift` v1 | `broadcaster_user_id` | `channel:read:subscriptions` | +| `channel.cheer` v1 | `broadcaster_user_id` | `bits:read` | +| `channel.channel_points_custom_reward_redemption.add` v1 | `broadcaster_user_id` | `channel:read:redemptions` | +| `channel.follow` v2 | `broadcaster_user_id` + `moderator_user_id` | `moderator:read:followers` | +| `channel.raid` v1 | `to_broadcaster_user_id` | none | + +Token refresh is handled in `eventsub._refresh_token()` — triggered on 401 during subscription. + +## Not yet implemented + +- Alert variable interpolation (`{username}` in alert text) +- Mod access (currently streamer-only login) +- Blerp / HudFX integrations +- Crypto payment notifications +- Morning-of reminder emails (IllBring feature, not applicable here) diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..ac48bd7 --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,274 @@ +# Deployment & Git Instructions + +## Prerequisites + +- Python 3.10 or higher +- A Twitch account (the channel you stream on) +- Git + +--- + +## Initial Deploy + +### 1. Register a Twitch application + +1. Go to [dev.twitch.tv/console/apps](https://dev.twitch.tv/console/apps) +2. Click **Register Your Application** +3. Fill in: + - **Name:** anything (e.g. `BashyOverlay`) + - **OAuth Redirect URLs:** `http://localhost:8000/auth/callback` + - **Category:** Application Integration +4. Click **Create** +5. On the next screen, click **Manage** to see your app +6. Copy the **Client ID** +7. Click **New Secret** and copy the **Client Secret** + +Keep both — you will need them in the next step. + +--- + +### 2. Clone the repository + +```bash +git clone https://github.com/yourusername/BashyOverlay.git +cd BashyOverlay +``` + +--- + +### 3. Configure environment variables + +```bash +cp .env.example .env +``` + +Open `.env` and fill in: + +```env +TWITCH_CLIENT_ID=paste_your_client_id_here +TWITCH_CLIENT_SECRET=paste_your_client_secret_here +TWITCH_REDIRECT_URI=http://localhost:8000/auth/callback + +SECRET_KEY=paste_a_random_string_here +APP_BASE_URL=http://localhost:8000 +DATABASE_URL=sqlite:///./bashyoverlay.db +``` + +Generate a `SECRET_KEY` with: + +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +--- + +### 4. Run the deploy script + +```bash +chmod +x deploy.sh +./deploy.sh +``` + +The script will: +- Check that Python 3.10+ is installed +- Create a `.venv` virtual environment +- Install all Python dependencies +- Verify `.env` is filled in +- Create the `static/media/` directory +- Start the server at `http://localhost:8000` + +To stop the server: `Ctrl+C` + +To start again without the setup steps: + +```bash +source .venv/bin/activate +uvicorn app.main:app --reload +``` + +--- + +### 5. Log in with Twitch + +1. Open `http://localhost:8000` in your browser +2. Click **Login with Twitch** +3. Authorise the app — you will be asked to grant permissions for subscriptions, bits, channel points, and follower data +4. You are redirected to the dashboard; EventSub connects automatically + +**Bookmark the dashboard URL.** There are no accounts or passwords — the session cookie keeps you logged in. + +--- + +### 6. Configure OBS browser source + +1. In OBS, go to **Sources → + → Browser** +2. Set the URL to the value shown on the Settings page (default: `http://localhost:8000/overlay`) +3. Set **Width** and **Height** to match your stream resolution (typically `1920` × `1080`) +4. Check **Shutdown source when not visible** +5. In the **Custom CSS** box paste: + ```css + body { background: transparent !important; } + ``` +6. Click OK + +The overlay will appear transparent in OBS. Chat messages and alerts show on top of your game/content. + +--- + +### 7. Upload media + +1. Go to **Media** in the management UI +2. Click **Choose File** and upload sounds (MP3, OGG, WAV) and/or videos (MP4, WebM) +3. Preview them in the browser before assigning them to actions + +--- + +### 8. Configure actions + +1. Go to **Actions** +2. For each event you want to react to, fill in the **Add action** form: + - **Event type:** `sub`, `gift_sub`, `bits`, `channel_points`, `command`, `follow`, or `raid` + - **Event detail:** command name (for commands), minimum bits (for bits), reward ID (for channel points), tier (for subs) — leave blank to match any + - **Action type:** `sound`, `video`, or `alert` + - **Media file:** pick an uploaded file (for sound/video) + - **Alert text:** message shown on screen (for alert type) + - **Cooldown:** seconds before this action can fire again +3. Click **Test** on each action to verify it fires before going live + +To find a channel point reward ID: open your Twitch dashboard, go to Channel Points → Manage Rewards, then click a reward. The ID is in the page URL. + +--- + +### 9. Configure the layout + +1. Go to **Layout** +2. Drag the **Chat** handle to where you want chat to appear on screen +3. Drag the bottom-right corner of the Chat handle to resize it +4. Drag the **Alert** handle to where you want alerts to appear +5. Adjust font size, opacity, and text color in the form below the canvas +6. Click **▶ Preview alert** to see the animation with your current settings +7. Click **Save styles** when done +8. **Refresh the OBS browser source** (`Right-click source → Refresh`) to apply layout changes + +--- + +### 10. Optional: Custom CSS and JS + +Go to **Settings** and scroll to the bottom. + +**Custom CSS example** — glow effect on alerts: +```css +#alert { + box-shadow: 0 0 30px #9146ff; + border: 2px solid #9146ff; +} +.chat-msg { + border-left: 3px solid; +} +``` + +**Custom JS example** — react to a raid: +```javascript +document.addEventListener('bashyoverlay:raid', (e) => { + console.log('Raided!', e.detail.alert_text); +}); +``` + +Available events: `bashyoverlay:sub`, `bashyoverlay:gift_sub`, `bashyoverlay:bits`, `bashyoverlay:channel_points`, `bashyoverlay:command`, `bashyoverlay:follow`, `bashyoverlay:raid`. + +Click **Save settings** then refresh the OBS browser source. + +--- + +## Git Actions + +### First push to GitHub + +After creating a new empty repository on GitHub: + +```bash +git remote add origin https://github.com/yourusername/BashyOverlay.git +git branch -M main +git push -u origin main +``` + +--- + +### Day-to-day workflow + +```bash +# Check what has changed +git status +git diff + +# Stage and commit changes +git add app/routers/actions.py templates/actions.html +git commit -m "add sub tier filtering to action matching" + +# Push to remote +git push +``` + +--- + +### GitHub Actions CI + +The repository includes `.github/workflows/ci.yml`. It runs automatically on every push and pull request and does the following: + +1. Installs Python 3.11 +2. Installs all dependencies from `requirements.txt` +3. Syntax-checks every Python file under `app/` +4. Verifies the FastAPI app imports without error + +No secrets are required for CI — it uses placeholder values for Twitch credentials. + +To view CI results: go to your repository on GitHub → **Actions** tab. + +--- + +### Adding GitHub repository secrets (for future CD) + +If you later want to deploy automatically, add secrets in GitHub: + +1. Go to your repository → **Settings → Secrets and variables → Actions** +2. Click **New repository secret** +3. Add: + - `TWITCH_CLIENT_ID` + - `TWITCH_CLIENT_SECRET` + - `SECRET_KEY` + +Reference them in a workflow step with `${{ secrets.TWITCH_CLIENT_ID }}`. + +--- + +### Branches + +```bash +# Create a feature branch +git checkout -b feature/alert-interpolation + +# Work, commit, push +git push -u origin feature/alert-interpolation + +# Merge back to main when done (on GitHub via pull request, or locally) +git checkout main +git merge feature/alert-interpolation +git push +``` + +--- + +### Updating a deployment + +If BashyOverlay is running on another machine or you pull in new changes: + +```bash +git pull +source .venv/bin/activate +pip install -r requirements.txt # in case dependencies changed +uvicorn app.main:app --reload +``` + +The SQLite database is updated automatically on startup — new tables and columns are created by `SQLModel.metadata.create_all`. Existing data is preserved. + +> **Note:** `create_all` only adds new tables and columns — it does not handle column renames or deletions. If a migration requires dropping a column, do it manually with `sqlite3 bashyoverlay.db "ALTER TABLE ..."` before restarting. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d20d721 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# BashyOverlay + +Self-hosted Twitch stream alert and overlay platform. Subs, bits, channel points, follows, raids, and chat commands trigger configurable sound and video reactions — all running locally on your machine, no third-party alert service required. + +## Features + +- **OBS browser source overlay** — transparent chat display with gap audio (tone plays when chat goes quiet, volume and pitch scale with silence duration) +- **Twitch EventSub** — real-time events for subs, gift subs, bits, channel points, follows, and raids +- **Chat commands** — `!command` triggers via tmi.js, no bot account needed for public channels +- **Action system** — map any event to a sound, video clip, or on-screen alert; multiple actions per event; per-action cooldowns and enable/disable toggle +- **Test button** — fire any action instantly from the management UI without waiting for a real event +- **Self-hosted media library** — upload your own MP3s and MP4s +- **Layout editor** — WYSIWYG iframe editor; drag chat and alert widgets, resize chat, live style preview +- **Custom CSS / JS** — inject code directly into the overlay; hook into `bashyoverlay:sub`, `bashyoverlay:raid`, etc. +- **Management UI** — dashboard with activity log, EventSub status, action config, media library +- **Twitch OAuth** — login with your Twitch account; no passwords anywhere + +## Stack + +- Python 3.10+ · FastAPI · SQLModel · SQLite +- Jinja2 · HTMX · Pico CSS (dark theme) +- tmi.js · Twitch EventSub WebSocket +- interact.js (layout editor drag and resize) + +## Quick start + +See **[INSTRUCTIONS.md](INSTRUCTIONS.md)** for the full setup guide including Twitch app registration and OBS configuration. + +```bash +cp .env.example .env +# fill in your Twitch credentials and a SECRET_KEY +./deploy.sh +``` + +## Project structure + +``` +app/ + main.py — FastAPI app, lifespan, EventSub startup + models.py — SQLModel table definitions + database.py — engine, session dependency, singleton helpers + auth.py — Twitch OAuth helpers, session dependency + eventsub.py — EventSub WebSocket background task, token refresh + dispatch.py — event → action resolution, cooldowns, DB log, broadcast + ws.py — WebSocket connection manager for overlay clients + cooldowns.py — in-memory cooldown tracking per action + routers/ + auth.py — /auth/login, /auth/callback, /auth/logout + dashboard.py — / dashboard with activity log + overlay.py — /overlay (live), /overlay/preview, /ws/overlay + actions.py — /actions CRUD, test, toggle + media.py — /media upload and delete + layout.py — /layout editor, /layout/positions (drag save) + settings.py — /settings +templates/ + overlay.html — OBS browser source (live tmi.js + WebSocket) + overlay_preview.html — layout editor preview (mock data, no live connections) + layout.html — iframe-based drag-and-drop layout editor + base.html — management UI shell + dashboard.html — activity log + actions.html — action configuration + media.html — media library + settings.html — settings including custom CSS/JS + login.html — Twitch OAuth login page + partials/ — HTMX response fragments +static/ + overlay.css — overlay styles using CSS custom properties + style.css — management UI styles + media/ — uploaded sounds and videos (gitignored) +``` + +## Deferred / upcoming + +- Blerp sound library integration +- HudFX video library integration +- Crypto payment notifications (Base/ETH wallet watching) +- Alert variable interpolation (`{username}` in alert text) +- Mod access (currently streamer-only login)