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