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 receive
4;;; real-time push notifications for stream and chat events.
5;;;
6;;; Lifecycle:
7;;; 1. Connect to wss://eventsub.wss.twitch.tv/ws
8;;; 2. Receive session_welcome with session ID
9;;; 3. Subscribe to event types using the session ID via REST API
10;;; 4. Receive notification messages with event data
11;;; 5. Handle reconnect messages when server requests migration
12;;;
13;;; Gotchas:
14;;; - Do NOT send any messages on the WebSocket (except pong)
15;;; - Must subscribe within the keepalive timeout or connection closes
16;;; - Max 3 connections per user token, 300 subscriptions per connection
18(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 ;; Records
29 twitch-eventsub-session
30 twitch-eventsub-session?
31 twitch-eventsub-session-id
32 twitch-eventsub-session-status
33 twitch-eventsub-session-keepalive-timeout
34 twitch-eventsub-session-reconnect-url
36 ;; Message parsing
37 parse-eventsub-message
38 eventsub-message-type
39 eventsub-session-from-welcome
40 eventsub-notification-event
41 eventsub-notification-subscription-type
43 ;; Subscription management (REST API)
44 twitch-eventsub-subscribe
45 twitch-eventsub-subscriptions
46 twitch-eventsub-delete-subscription
48 ;; Common subscription helpers
49 twitch-subscribe-stream-online
50 twitch-subscribe-stream-offline
51 twitch-subscribe-channel-update
52 twitch-subscribe-chat-message
53 twitch-subscribe-follow)
55 (begin
57 ;; ---------------------------------------------------------------
58 ;; Records
59 ;; ---------------------------------------------------------------
61 (define-struct twitch-eventsub-session
62 (id)
63 (status default: "connected")
64 (keepalive-timeout default: 10)
65 (reconnect-url default: #f))
67 ;; ---------------------------------------------------------------
68 ;; Message parsing
69 ;; ---------------------------------------------------------------
71 ;;; Parse a raw EventSub WebSocket message (JSON string) into
72 ;;; 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-session
89 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 management
110 ;; ---------------------------------------------------------------
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 parameters
116 ;;; session-id: WebSocket session ID from welcome message
117 (define (twitch-eventsub-subscribe client type version condition session-id)
118 (let* ((body #{ type: type
119 version: version
120 condition: condition
121 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 status
129 ;;; type: filter by subscription type
130 ;;; after: pagination cursor
131 (define (twitch-eventsub-subscriptions client . rest)
132 (let ((opts (if (null? rest) #{} (car rest))))
133 (let* ((params (list
134 (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-append
141 (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-append
148 (twitch-api-url client "eventsub" "subscriptions")
149 (build-query-string
150 (list (cons "id" subscription-id))))))
151 (twitch-delete/json client url)))
153 ;; ---------------------------------------------------------------
154 ;; Convenience subscription helpers
155 ;; ---------------------------------------------------------------
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-id
178 user-id)
179 (twitch-eventsub-subscribe client "channel.chat.message" "1"
180 #{ broadcaster_user_id: broadcaster-id
181 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-id
187 moderator-id)
188 (twitch-eventsub-subscribe client "channel.follow" "2"
189 #{ broadcaster_user_id: broadcaster-id
190 moderator_user_id: moderator-id }
191 session-id))
193 ))