AtlatestRepositorysigil-twitch
sigil-twitch / tree / src / twitcheventsub.sgl
1
;;; (twitch eventsub) - Twitch EventSub WebSocket client.2
;;;3
;;; Connects to the Twitch EventSub WebSocket endpoint to receive4
;;; real-time push notifications for stream and chat events.5
;;;6
;;; Lifecycle:7
;;; 1. Connect to wss://eventsub.wss.twitch.tv/ws8
;;; 2. Receive session_welcome with session ID9
;;; 3. Subscribe to event types using the session ID via REST API10
;;; 4. Receive notification messages with event data11
;;; 5. Handle reconnect messages when server requests migration12
;;;13
;;; Gotchas:14
;;; - Do NOT send any messages on the WebSocket (except pong)15
;;; - Must subscribe within the keepalive timeout or connection closes16
;;; - Max 3 connections per user token, 300 subscriptions per connection18
(define-library (twitch eventsub)19
(import (sigil core)20
(sigil dict)21
(sigil string)22
(sigil struct)23
(sigil json)24
(only (sigil http) build-query-string)25
(sigil http client)26
(twitch))28
(export ;; Records29
twitch-eventsub-session30
twitch-eventsub-session?31
twitch-eventsub-session-id32
twitch-eventsub-session-status33
twitch-eventsub-session-keepalive-timeout34
twitch-eventsub-session-reconnect-url36
;; Message parsing37
parse-eventsub-message38
eventsub-message-type39
eventsub-session-from-welcome40
eventsub-notification-event41
eventsub-notification-subscription-type43
;; Subscription management (REST API)44
twitch-eventsub-subscribe45
twitch-eventsub-subscriptions46
twitch-eventsub-delete-subscription48
;; Common subscription helpers49
twitch-subscribe-stream-online50
twitch-subscribe-stream-offline51
twitch-subscribe-channel-update52
twitch-subscribe-chat-message53
twitch-subscribe-follow)55
(begin57
;; ---------------------------------------------------------------58
;; Records59
;; ---------------------------------------------------------------61
(define-struct twitch-eventsub-session62
(id)63
(status default: "connected")64
(keepalive-timeout default: 10)65
(reconnect-url default: #f))67
;; ---------------------------------------------------------------68
;; Message parsing69
;; ---------------------------------------------------------------71
;;; Parse a raw EventSub WebSocket message (JSON string) into72
;;; a structured dict.73
(define (parse-eventsub-message json-str)74
(json-decode json-str))76
;;; Get the message type from an EventSub message.77
;;; Returns: "session_welcome", "notification", "session_keepalive",78
;;; "session_reconnect", or "revocation".79
(define (eventsub-message-type msg)80
(let ((metadata (dict-ref msg metadata: #{})))81
(dict-ref metadata message_type: #f)))83
;;; Extract session info from a session_welcome message.84
;;; Returns a twitch-eventsub-session record.85
(define (eventsub-session-from-welcome msg)86
(let* ((payload (dict-ref msg payload: #{}))87
(session (dict-ref payload session: #{})))88
(twitch-eventsub-session89
id: (dict-ref session id:)90
status: (dict-ref session status: "connected")91
keepalive-timeout: (dict-ref session keepalive_timeout_seconds: 10)92
reconnect-url: (let ((url (dict-ref session reconnect_url: #f)))93
(if (or (not url) (eq? url 'null)) #f url)))))95
;;; Extract the event data from a notification message.96
;;; Returns the event dict with type-specific fields.97
(define (eventsub-notification-event msg)98
(let ((payload (dict-ref msg payload: #{})))99
(dict-ref payload event: #{})))101
;;; Get the subscription type from a notification message.102
;;; e.g., "stream.online", "channel.chat.message"103
(define (eventsub-notification-subscription-type msg)104
(let* ((payload (dict-ref msg payload: #{}))105
(sub (dict-ref payload subscription: #{})))106
(dict-ref sub type: #f)))108
;; ---------------------------------------------------------------109
;; REST API — Subscription management110
;; ---------------------------------------------------------------112
;;; Create an EventSub subscription.113
;;; type: event type string (e.g., "stream.online")114
;;; version: API version string (e.g., "1")115
;;; condition: dict of condition parameters116
;;; session-id: WebSocket session ID from welcome message117
(define (twitch-eventsub-subscribe client type version condition session-id)118
(let* ((body #{ type: type119
version: version120
condition: condition121
transport: #{ method: "websocket"122
session_id: session-id } })123
(url (twitch-api-url client "eventsub" "subscriptions")))124
(twitch-post/json client url body)))126
;;; List current EventSub subscriptions.127
;;; Optional opts dict:128
;;; status: filter by status129
;;; type: filter by subscription type130
;;; after: pagination cursor131
(define (twitch-eventsub-subscriptions client . rest)132
(let ((opts (if (null? rest) #{} (car rest))))133
(let* ((params (list134
(cons "status"135
(dict-ref opts status: #f))136
(cons "type"137
(dict-ref opts type: #f))138
(cons "after"139
(dict-ref opts after: #f))))140
(url (string-append141
(twitch-api-url client "eventsub" "subscriptions")142
(build-query-string params))))143
(twitch-get/json client url))))145
;;; Delete an EventSub subscription by ID.146
(define (twitch-eventsub-delete-subscription client subscription-id)147
(let ((url (string-append148
(twitch-api-url client "eventsub" "subscriptions")149
(build-query-string150
(list (cons "id" subscription-id))))))151
(twitch-delete/json client url)))153
;; ---------------------------------------------------------------154
;; Convenience subscription helpers155
;; ---------------------------------------------------------------157
;;; Subscribe to stream.online events.158
(define (twitch-subscribe-stream-online client session-id broadcaster-id)159
(twitch-eventsub-subscribe client "stream.online" "1"160
#{ broadcaster_user_id: broadcaster-id }161
session-id))163
;;; Subscribe to stream.offline events.164
(define (twitch-subscribe-stream-offline client session-id broadcaster-id)165
(twitch-eventsub-subscribe client "stream.offline" "1"166
#{ broadcaster_user_id: broadcaster-id }167
session-id))169
;;; Subscribe to channel.update events (title/category changes).170
(define (twitch-subscribe-channel-update client session-id broadcaster-id)171
(twitch-eventsub-subscribe client "channel.update" "2"172
#{ broadcaster_user_id: broadcaster-id }173
session-id))175
;;; Subscribe to channel.chat.message events.176
;;; Requires user:read:chat scope.177
(define (twitch-subscribe-chat-message client session-id broadcaster-id178
user-id)179
(twitch-eventsub-subscribe client "channel.chat.message" "1"180
#{ broadcaster_user_id: broadcaster-id181
user_id: user-id }182
session-id))184
;;; Subscribe to channel.follow events (v2).185
;;; Requires moderator:read:followers scope.186
(define (twitch-subscribe-follow client session-id broadcaster-id187
moderator-id)188
(twitch-eventsub-subscribe client "channel.follow" "2"189
#{ broadcaster_user_id: broadcaster-id190
moderator_user_id: moderator-id }191
session-id))193
))