@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 +177 -0
- package/dist/index.cjs +572 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +542 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
|
+
```
|