heystreamer/app/dispatch.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

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