@blorkfield/twitch-integration 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # @blorkfield/twitch-integration
2
+
3
+ Manages a Twitch EventSub WebSocket connection and normalizes chat messages into a typed event stream with emotes resolved across Twitch, BTTV, and 7TV.
4
+
5
+ No knowledge of overlays, physics, or effects — just "what did chat say and what emotes were in it."
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm add @blorkfield/twitch-integration
13
+ # ws is required in Node.js environments
14
+ pnpm add ws
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Prerequisites: credentials
20
+
21
+ You need four things before constructing `TwitchChat`:
22
+
23
+ | Option | What it is | How to get it |
24
+ |---|---|---|
25
+ | `clientId` | Your Twitch app's client ID | [Twitch Developer Console](https://dev.twitch.tv/console/apps) → your app |
26
+ | `accessToken` | A **user** access token for the bot/reading account | OAuth flow with `user:read:chat` scope — **not** an app access token |
27
+ | `userId` | Twitch numeric user ID of the account that owns the token | Call `GET /helix/users` with the token |
28
+ | `channelId` | Twitch numeric user ID of the broadcaster whose chat you're reading | Call `GET /helix/users?login=channelname` |
29
+
30
+ The library does not handle OAuth. Obtain the token yourself and pass it in. Use `onTokenRefresh` to persist refreshed tokens.
31
+
32
+ > **Why a user token?** Twitch's EventSub WebSocket transport does not accept app access tokens — this is a hard Twitch protocol requirement.
33
+
34
+ ---
35
+
36
+ ## APIs called at runtime
37
+
38
+ ### On `connect()`
39
+
40
+ 1. Opens a WebSocket to `wss://eventsub.wss.twitch.tv/ws`
41
+ 2. Receives `session_welcome` from Twitch containing a `session_id`
42
+ 3. `POST https://api.twitch.tv/helix/eventsub/subscriptions` — registers a `channel.chat.message` subscription tied to that session, using your `clientId` + `accessToken`
43
+
44
+ That's it. Twitch then pushes `channel.chat.message` notifications over the same socket.
45
+
46
+ ### On `preloadEmotes()` / `refreshEmotes()`
47
+
48
+ Four parallel fetches, all unauthenticated:
49
+
50
+ | Source | Endpoint |
51
+ |---|---|
52
+ | BTTV global | `GET https://api.betterttv.net/3/cached/emotes/global` |
53
+ | BTTV channel | `GET https://api.betterttv.net/3/cached/users/twitch/{channelId}` |
54
+ | 7TV global | `GET https://7tv.io/v3/emote-sets/global` |
55
+ | 7TV channel | `GET https://7tv.io/v3/users/twitch/{channelId}` |
56
+
57
+ Twitch emotes don't need a fetch — their IDs come in the message fragments directly and URLs are constructed from the CDN pattern.
58
+
59
+ ---
60
+
61
+ ## Usage
62
+
63
+ ```typescript
64
+ import { TwitchChat } from '@blorkfield/twitch-integration'
65
+
66
+ const chat = new TwitchChat({
67
+ channelId: '123456789', // broadcaster's numeric Twitch user ID
68
+ userId: '987654321', // bot/reading account's numeric Twitch user ID
69
+ clientId: 'abc123...', // your Twitch app client ID
70
+ accessToken: 'oauth:...', // user access token with user:read:chat scope
71
+ onTokenRefresh: (token) => saveTokenSomewhere(token),
72
+ })
73
+
74
+ await chat.preloadEmotes() // fetch BTTV + 7TV emote maps before connecting
75
+ await chat.connect() // resolves once subscribed and receiving messages
76
+
77
+ chat.on('message', (msg) => {
78
+ console.log(msg.user.displayName, msg.text)
79
+ console.log(msg.emotes) // all resolved emotes in this message
80
+ console.log(msg.fragments) // text/emote/cheermote/mention breakdown
81
+ })
82
+
83
+ chat.on('auth_error', () => {
84
+ // token is bad or expired — re-authenticate and call chat.connect() again
85
+ })
86
+
87
+ chat.on('revoked', (reason) => {
88
+ // Twitch revoked the subscription — do not auto-reconnect
89
+ console.error('Subscription revoked:', reason)
90
+ })
91
+
92
+ chat.on('disconnected', (code, reason) => {
93
+ // library auto-reconnects on unexpected disconnects (non-1000 codes)
94
+ })
95
+
96
+ // Later:
97
+ chat.disconnect()
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Event reference
103
+
104
+ | Event | Arguments | When |
105
+ |---|---|---|
106
+ | `connected` | — | Subscribed and receiving messages |
107
+ | `disconnected` | `code: number, reason: string` | WebSocket closed |
108
+ | `message` | `msg: NormalizedMessage` | Chat message received |
109
+ | `revoked` | `reason: string` | Twitch revoked the subscription |
110
+ | `auth_error` | — | 401 response on subscription POST |
111
+ | `error` | `err: Error` | Unexpected error |
112
+
113
+ ---
114
+
115
+ ## NormalizedMessage shape
116
+
117
+ ```typescript
118
+ interface NormalizedMessage {
119
+ id: string
120
+ text: string // full raw message text
121
+ user: ChatUser
122
+ fragments: MessageFragment[] // per-token breakdown
123
+ emotes: ResolvedEmote[] // deduplicated list of all emotes in message
124
+ timestamp: string // RFC3339
125
+ cheer?: { bits: number }
126
+ reply?: {
127
+ parentMessageId: string
128
+ parentUserLogin: string
129
+ parentUserDisplayName: string
130
+ }
131
+ channelPointsRewardId?: string
132
+ }
133
+
134
+ type MessageFragment =
135
+ | { type: 'text'; text: string }
136
+ | { type: 'emote'; text: string; emote: ResolvedEmote }
137
+ | { type: 'cheermote'; text: string; bits: number; tier: number }
138
+ | { type: 'mention'; text: string; userId: string; userLogin: string }
139
+
140
+ interface ResolvedEmote {
141
+ id: string
142
+ name: string
143
+ source: 'twitch' | 'bttv' | '7tv'
144
+ animated: boolean
145
+ imageUrl1x: string
146
+ imageUrl2x?: string
147
+ imageUrl3x?: string
148
+ }
149
+ ```
150
+
151
+ ### Emote resolution
152
+
153
+ Third-party emote name collisions are resolved in priority order:
154
+
155
+ 1. 7TV channel
156
+ 2. BTTV channel
157
+ 3. 7TV global
158
+ 4. BTTV global
159
+ 5. Twitch (authoritative for native emotes — resolved from fragment data, not name lookup)
160
+
161
+ ---
162
+
163
+ ## Lifecycle notes
164
+
165
+ - **Reconnect:** handled automatically on unexpected disconnects with a 2s backoff
166
+ - **`session_reconnect`:** library connects to the new URL, waits for `session_welcome`, then closes the old connection — subscriptions carry over automatically, no re-POST
167
+ - **Keepalive:** Twitch sends keepalives; if one doesn't arrive within `keepalive_timeout_seconds + 0.5s`, the library reconnects
168
+ - **Max 3 active WebSocket connections** per Twitch user account — `disconnect()` cleanly closes before reconnecting elsewhere
169
+
170
+ ---
171
+
172
+ ## Build
173
+
174
+ ```bash
175
+ pnpm build # tsup → dist/ (ESM + CJS + .d.ts)
176
+ pnpm typecheck # tsc --noEmit
177
+ ```