heystreamer/app/routers/actions.py
Bashy 6ad661c220 Initial commit — BashyOverlay v1
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>
2026-04-22 23:48:35 +03:00

140 lines
4.3 KiB
Python

from typing import Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlmodel import Session, select
from app.auth import require_user
from app.database import get_session
from app.models import ActivityLog, EventAction, MediaFile
from app.templates import templates
from app.ws import manager
router = APIRouter(prefix="/actions")
EVENT_TYPES = ["sub", "gift_sub", "bits", "channel_points", "command", "follow", "raid"]
ACTION_TYPES = ["sound", "video", "alert"]
@router.get("", response_class=HTMLResponse)
async def actions_page(request: Request, session: Session = Depends(get_session), user=Depends(require_user)):
actions = session.exec(select(EventAction)).all()
media_files = session.exec(select(MediaFile)).all()
return templates.TemplateResponse("actions.html", {
"request": request,
"user": user,
"actions": actions,
"media_files": media_files,
"event_types": EVENT_TYPES,
"action_types": ACTION_TYPES,
})
@router.post("", response_class=HTMLResponse)
async def create_action(
request: Request,
event_type: str = Form(...),
event_detail: Optional[str] = Form(default=None),
action_type: str = Form(...),
media_file_id: Optional[int] = Form(default=None),
alert_text: Optional[str] = Form(default=None),
cooldown_seconds: int = Form(default=0),
session: Session = Depends(get_session),
user=Depends(require_user),
):
action = EventAction(
event_type=event_type,
event_detail=event_detail.strip() if event_detail and event_detail.strip() else None,
action_type=action_type,
media_file_id=media_file_id or None,
alert_text=alert_text.strip() if alert_text and alert_text.strip() else None,
cooldown_seconds=max(0, cooldown_seconds),
)
session.add(action)
session.commit()
session.refresh(action)
media_files = session.exec(select(MediaFile)).all()
return templates.TemplateResponse("partials/action_row.html", {
"request": request,
"action": action,
"media_files": media_files,
})
@router.post("/{action_id}/test", response_class=HTMLResponse)
async def test_action(
request: Request,
action_id: int,
session: Session = Depends(get_session),
user=Depends(require_user),
):
action = session.get(EventAction, action_id)
if not action:
raise HTTPException(status_code=404)
media_url = None
if action.media_file_id:
media = session.get(MediaFile, action.media_file_id)
if media:
media_url = f"/static/media/{media.filename}"
await manager.broadcast({
"type": "execute",
"action_type": action.action_type,
"media_url": media_url,
"alert_text": action.alert_text or "Test!",
})
session.add(ActivityLog(
event_type="test",
username=user.twitch_display_name,
detail=f"{action.event_type}:{action.event_detail or 'any'}",
action_taken=f"{action.action_type}: {media_url or action.alert_text or ''}",
))
session.commit()
media_files = session.exec(select(MediaFile)).all()
return templates.TemplateResponse("partials/action_row.html", {
"request": request,
"action": action,
"media_files": media_files,
"just_tested": True,
})
@router.delete("/{action_id}", response_class=HTMLResponse)
async def delete_action(
action_id: int,
session: Session = Depends(get_session),
user=Depends(require_user),
):
action = session.get(EventAction, action_id)
if not action:
raise HTTPException(status_code=404)
session.delete(action)
session.commit()
return HTMLResponse("")
@router.post("/{action_id}/toggle", response_class=HTMLResponse)
async def toggle_action(
request: Request,
action_id: int,
session: Session = Depends(get_session),
user=Depends(require_user),
):
action = session.get(EventAction, action_id)
if not action:
raise HTTPException(status_code=404)
action.enabled = not action.enabled
session.add(action)
session.commit()
session.refresh(action)
media_files = session.exec(select(MediaFile)).all()
return templates.TemplateResponse("partials/action_row.html", {
"request": request,
"action": action,
"media_files": media_files,
})