heystreamer/CLAUDE.md
Bashy c46f2f2fc3 Add README, CLAUDE.md, INSTRUCTIONS.md, and CI workflow
- README.md: feature list, stack, project structure, deferred items
- CLAUDE.md: architecture, conventions, route map, CSS variable system,
  EventSub subscription table, data model, custom event names
- INSTRUCTIONS.md: step-by-step initial deploy guide (Twitch app
  registration through OBS setup and custom CSS/JS), plus git workflow,
  first push, GitHub Actions, branching, and update procedure
- .github/workflows/ci.yml: syntax check and import check on every push

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:58:23 +03:00

7.5 KiB

CLAUDE.md

Running the app

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:

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)