Twitch stream event platform with OBS browser source overlay. Features: - Twitch OAuth login (streamer + mod access) - EventSub WebSocket for subs, gift subs, bits, channel points, follows, raids - Chat commands via tmi.js in the overlay - Gap audio — tone plays on new chat message after silence, volume/pitch scale with gap length - Self-hosted media library (upload sounds and videos) - Action configuration — map any event to sound, video, or alert - Per-action cooldowns, test button, enable/disable toggle - Multiple actions per event (all matching actions fire) - Activity log dashboard with EventSub connection status - Layout editor — iframe-based WYSIWYG with drag handles, live style preview - Custom CSS and custom JS injection into overlay - Custom DOM events (bashyoverlay:sub, bashyoverlay:raid, etc.) for custom JS hooks - deploy.sh — one-shot setup and launch script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
import os
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
from fastapi import Depends, HTTPException, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlmodel import Session, select
|
|
|
|
from app.database import get_session
|
|
from app.models import User
|
|
|
|
TWITCH_CLIENT_ID = os.getenv("TWITCH_CLIENT_ID", "")
|
|
TWITCH_CLIENT_SECRET = os.getenv("TWITCH_CLIENT_SECRET", "")
|
|
TWITCH_REDIRECT_URI = os.getenv("TWITCH_REDIRECT_URI", "http://localhost:8000/auth/callback")
|
|
|
|
SCOPES = "channel:read:subscriptions bits:read channel:read:redemptions moderator:read:followers"
|
|
|
|
TWITCH_AUTH_URL = (
|
|
"https://id.twitch.tv/oauth2/authorize"
|
|
f"?client_id={TWITCH_CLIENT_ID}"
|
|
f"&redirect_uri={TWITCH_REDIRECT_URI}"
|
|
"&response_type=code"
|
|
f"&scope={SCOPES.replace(' ', '+')}"
|
|
)
|
|
|
|
|
|
async def exchange_code(code: str) -> dict:
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.post("https://id.twitch.tv/oauth2/token", data={
|
|
"client_id": TWITCH_CLIENT_ID,
|
|
"client_secret": TWITCH_CLIENT_SECRET,
|
|
"code": code,
|
|
"grant_type": "authorization_code",
|
|
"redirect_uri": TWITCH_REDIRECT_URI,
|
|
})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def get_twitch_user(access_token: str) -> dict:
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(
|
|
"https://api.twitch.tv/helix/users",
|
|
headers={"Authorization": f"Bearer {access_token}", "Client-Id": TWITCH_CLIENT_ID},
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()["data"][0]
|
|
|
|
|
|
def get_current_user(request: Request, session: Session = Depends(get_session)) -> Optional[User]:
|
|
user_id = request.session.get("user_id")
|
|
if not user_id:
|
|
return None
|
|
return session.get(User, user_id)
|
|
|
|
|
|
def require_user(request: Request, session: Session = Depends(get_session)) -> User:
|
|
user_id = request.session.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=307, headers={"Location": "/auth/login"})
|
|
user = session.get(User, user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=307, headers={"Location": "/auth/login"})
|
|
return user
|