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>
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
from typing import Optional
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
from app.cooldowns import is_on_cooldown, set_cooldown
|
|
from app.database import engine
|
|
from app.models import ActivityLog, EventAction, MediaFile
|
|
from app.ws import manager
|
|
|
|
|
|
async def dispatch_event(
|
|
event_type: str,
|
|
username: Optional[str] = None,
|
|
event_detail: Optional[str] = None,
|
|
bits_amount: Optional[int] = None,
|
|
) -> None:
|
|
with Session(engine) as session:
|
|
actions = session.exec(
|
|
select(EventAction)
|
|
.where(EventAction.event_type == event_type)
|
|
.where(EventAction.enabled == True)
|
|
).all()
|
|
|
|
fired = []
|
|
for action in actions:
|
|
if not _matches(action, event_detail, bits_amount):
|
|
continue
|
|
if is_on_cooldown(action.id):
|
|
continue
|
|
|
|
payload = _resolve_payload(action, session)
|
|
alert_text = action.alert_text or _default_alert(event_type, username, event_detail, bits_amount)
|
|
|
|
set_cooldown(action.id, action.cooldown_seconds)
|
|
await manager.broadcast({
|
|
"type": "execute",
|
|
"event_type": event_type,
|
|
"action_type": action.action_type,
|
|
"media_url": payload.get("media_url"),
|
|
"alert_text": alert_text,
|
|
})
|
|
fired.append(f"{action.action_type}: {payload.get('media_url') or alert_text or ''}")
|
|
|
|
action_taken = ", ".join(fired) if fired else None
|
|
_log(session, event_type, username, event_detail, bits_amount, action_taken)
|
|
|
|
session.commit()
|
|
|
|
|
|
def _matches(action: EventAction, event_detail: Optional[str], bits_amount: Optional[int]) -> bool:
|
|
if not action.event_detail:
|
|
return True
|
|
if action.event_type == "bits" and bits_amount is not None:
|
|
try:
|
|
return bits_amount >= int(action.event_detail)
|
|
except ValueError:
|
|
return False
|
|
if action.event_type == "command":
|
|
return action.event_detail.lower() == (event_detail or "").lower()
|
|
return action.event_detail == event_detail
|
|
|
|
|
|
def _resolve_payload(action: EventAction, session: Session) -> dict:
|
|
if action.media_file_id:
|
|
media = session.get(MediaFile, action.media_file_id)
|
|
if media:
|
|
return {"media_url": f"/static/media/{media.filename}"}
|
|
return {}
|
|
|
|
|
|
def _log(session, event_type, username, event_detail, bits_amount, action_taken):
|
|
session.add(ActivityLog(
|
|
event_type=event_type,
|
|
username=username,
|
|
detail=_detail_str(event_type, event_detail, bits_amount),
|
|
action_taken=action_taken,
|
|
))
|
|
|
|
|
|
def _detail_str(event_type, event_detail, bits_amount) -> Optional[str]:
|
|
if event_type == "sub":
|
|
return f"tier {event_detail}" if event_detail else None
|
|
if event_type == "bits":
|
|
return f"{bits_amount} bits" if bits_amount else None
|
|
if event_type == "channel_points":
|
|
return event_detail
|
|
if event_type == "command":
|
|
return f"!{event_detail}" if event_detail else None
|
|
if event_type == "raid":
|
|
return f"{event_detail} viewers" if event_detail else None
|
|
return None
|
|
|
|
|
|
def _default_alert(event_type, username, event_detail, bits_amount) -> Optional[str]:
|
|
u = username or "Someone"
|
|
if event_type == "sub":
|
|
return f"{u} just subscribed!"
|
|
if event_type == "gift_sub":
|
|
return f"{u} gifted a sub!"
|
|
if event_type == "bits":
|
|
return f"{u} cheered {bits_amount} bits!"
|
|
if event_type == "channel_points":
|
|
return f"{u} redeemed channel points!"
|
|
if event_type == "follow":
|
|
return f"{u} just followed!"
|
|
if event_type == "raid":
|
|
viewers = f" with {event_detail} viewers" if event_detail else ""
|
|
return f"{u} is raiding{viewers}!"
|
|
return None
|