@blorkfield/twitch-integration 0.2.1 → 0.3.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 +56 -0
- package/dist/index.cjs +106 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -5
- package/dist/index.d.ts +22 -5
- package/dist/index.js +106 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -169,6 +169,62 @@ Third-party emote name collisions are resolved in priority order:
|
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
|
172
|
+
## User lookup
|
|
173
|
+
|
|
174
|
+
`TwitchChat` exposes a cached user lookup backed by the Helix API. Requires an active connection (uses the same credentials).
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Full user info
|
|
178
|
+
const user: UserInfo | null = await chat.getUser('123456789')
|
|
179
|
+
// { id, login, displayName, profileImageUrl, broadcasterType, description, createdAt }
|
|
180
|
+
|
|
181
|
+
// Batch
|
|
182
|
+
const users: Map<string, UserInfo | null> = await chat.getUsers(['123456789', '987654321'])
|
|
183
|
+
|
|
184
|
+
// Profile picture shorthand (delegates to getUser, same cache)
|
|
185
|
+
const url: string | null = await chat.getProfilePictureUrl('123456789')
|
|
186
|
+
const urls: Map<string, string | null> = await chat.getProfilePictureUrls(['123456789', '987654321'])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`broadcasterType` is `'partner'`, `'affiliate'`, or `''`. Results are cached for 5 minutes — repeated calls for the same user ID don't hit the API again.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Badges
|
|
194
|
+
|
|
195
|
+
### Preloading
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await chat.preloadBadges()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Fetches global and channel badge sets from Helix in two parallel calls. Call this once before connecting, alongside `preloadEmotes()`.
|
|
202
|
+
|
|
203
|
+
### Auto-resolution on messages
|
|
204
|
+
|
|
205
|
+
If badges are preloaded, each `Badge` in `msg.user.badges` will have a `resolved` field populated automatically:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
chat.on('message', (msg) => {
|
|
209
|
+
for (const badge of msg.user.badges) {
|
|
210
|
+
console.log(badge.setId, badge.id) // e.g. 'subscriber', '6'
|
|
211
|
+
console.log(badge.resolved?.title) // e.g. 'Subscriber'
|
|
212
|
+
console.log(badge.resolved?.imageUrl2x) // CDN image URL
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`badge.resolved` is `undefined` if badges weren't preloaded or the badge isn't in the fetched sets.
|
|
218
|
+
|
|
219
|
+
### Manual resolution
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const badge: ResolvedBadge | undefined = chat.resolveBadge('subscriber', '6')
|
|
223
|
+
// { title, imageUrl1x, imageUrl2x, imageUrl4x }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
172
228
|
## Build
|
|
173
229
|
|
|
174
230
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -177,32 +177,44 @@ var UserCache = class {
|
|
|
177
177
|
this.getCredentials = getCredentials;
|
|
178
178
|
this.cache = /* @__PURE__ */ new Map();
|
|
179
179
|
}
|
|
180
|
-
async
|
|
181
|
-
const results = await this.
|
|
180
|
+
async getUser(userId) {
|
|
181
|
+
const results = await this.getUsers([userId]);
|
|
182
182
|
return results.get(userId) ?? null;
|
|
183
183
|
}
|
|
184
|
-
async
|
|
184
|
+
async getUsers(userIds) {
|
|
185
185
|
const now = Date.now();
|
|
186
186
|
const result = /* @__PURE__ */ new Map();
|
|
187
187
|
const toFetch = [];
|
|
188
188
|
for (const id of userIds) {
|
|
189
189
|
const entry = this.cache.get(id);
|
|
190
190
|
if (entry !== void 0 && entry.expiresAt > now) {
|
|
191
|
-
result.set(id, entry.
|
|
191
|
+
result.set(id, entry.user);
|
|
192
192
|
} else {
|
|
193
|
+
result.set(id, null);
|
|
193
194
|
toFetch.push(id);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
|
-
if (toFetch.length === 0) return result;
|
|
197
197
|
for (let i = 0; i < toFetch.length; i += 100) {
|
|
198
198
|
const chunk = toFetch.slice(i, i + 100);
|
|
199
199
|
const fetched = await this._fetchChunk(chunk, now);
|
|
200
|
-
for (const [id,
|
|
201
|
-
result.set(id,
|
|
200
|
+
for (const [id, user] of fetched) {
|
|
201
|
+
result.set(id, user);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
return result;
|
|
205
205
|
}
|
|
206
|
+
async getProfilePictureUrl(userId) {
|
|
207
|
+
const user = await this.getUser(userId);
|
|
208
|
+
return user?.profileImageUrl ?? null;
|
|
209
|
+
}
|
|
210
|
+
async getProfilePictureUrls(userIds) {
|
|
211
|
+
const users = await this.getUsers(userIds);
|
|
212
|
+
const result = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const [id, user] of users) {
|
|
214
|
+
if (user !== null) result.set(id, user.profileImageUrl);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
206
218
|
async _fetchChunk(ids, now) {
|
|
207
219
|
const params = new URLSearchParams();
|
|
208
220
|
for (const id of ids) params.append("id", id);
|
|
@@ -219,16 +231,69 @@ var UserCache = class {
|
|
|
219
231
|
const body = await res.json();
|
|
220
232
|
const result = /* @__PURE__ */ new Map();
|
|
221
233
|
const expiresAt = now + TTL_MS;
|
|
222
|
-
for (const
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
for (const u of body.data) {
|
|
235
|
+
const user = {
|
|
236
|
+
id: u.id,
|
|
237
|
+
login: u.login,
|
|
238
|
+
displayName: u.display_name,
|
|
239
|
+
profileImageUrl: u.profile_image_url,
|
|
240
|
+
broadcasterType: u.broadcaster_type,
|
|
241
|
+
description: u.description,
|
|
242
|
+
createdAt: u.created_at
|
|
243
|
+
};
|
|
244
|
+
this.cache.set(u.id, { user, expiresAt });
|
|
245
|
+
result.set(u.id, user);
|
|
225
246
|
}
|
|
226
247
|
return result;
|
|
227
248
|
}
|
|
228
249
|
};
|
|
229
250
|
|
|
251
|
+
// src/badges/index.ts
|
|
252
|
+
var HELIX_BADGES_GLOBAL = "https://api.twitch.tv/helix/chat/badges/global";
|
|
253
|
+
var HELIX_BADGES_CHANNEL = "https://api.twitch.tv/helix/chat/badges";
|
|
254
|
+
var BadgeCache = class {
|
|
255
|
+
constructor(channelId, getCredentials) {
|
|
256
|
+
this.channelId = channelId;
|
|
257
|
+
this.getCredentials = getCredentials;
|
|
258
|
+
// setId → version → ResolvedBadge
|
|
259
|
+
this.sets = /* @__PURE__ */ new Map();
|
|
260
|
+
}
|
|
261
|
+
async load() {
|
|
262
|
+
const { accessToken, clientId } = this.getCredentials();
|
|
263
|
+
const headers = {
|
|
264
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
265
|
+
"Client-Id": clientId
|
|
266
|
+
};
|
|
267
|
+
const [globalRes, channelRes] = await Promise.all([
|
|
268
|
+
fetch(HELIX_BADGES_GLOBAL, { headers }),
|
|
269
|
+
fetch(`${HELIX_BADGES_CHANNEL}?broadcaster_id=${this.channelId}`, { headers })
|
|
270
|
+
]);
|
|
271
|
+
if (!globalRes.ok) throw new Error(`Global badges fetch failed: ${globalRes.status}`);
|
|
272
|
+
if (!channelRes.ok) throw new Error(`Channel badges fetch failed: ${channelRes.status}`);
|
|
273
|
+
const [globalBody, channelBody] = await Promise.all([
|
|
274
|
+
globalRes.json(),
|
|
275
|
+
channelRes.json()
|
|
276
|
+
]);
|
|
277
|
+
for (const set of [...globalBody.data, ...channelBody.data]) {
|
|
278
|
+
const versionMap = this.sets.get(set.set_id) ?? /* @__PURE__ */ new Map();
|
|
279
|
+
for (const v of set.versions) {
|
|
280
|
+
versionMap.set(v.id, {
|
|
281
|
+
title: v.title,
|
|
282
|
+
imageUrl1x: v.image_url_1x,
|
|
283
|
+
imageUrl2x: v.image_url_2x,
|
|
284
|
+
imageUrl4x: v.image_url_4x
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
this.sets.set(set.set_id, versionMap);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
resolve(setId, version) {
|
|
291
|
+
return this.sets.get(setId)?.get(version);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
230
295
|
// src/normalizer.ts
|
|
231
|
-
function normalizeMessage(event, emoteCache) {
|
|
296
|
+
function normalizeMessage(event, emoteCache, resolveBadge) {
|
|
232
297
|
const emotes = [];
|
|
233
298
|
const fragments = [];
|
|
234
299
|
for (const frag of event.message.fragments) {
|
|
@@ -291,11 +356,15 @@ function normalizeMessage(event, emoteCache) {
|
|
|
291
356
|
}
|
|
292
357
|
}
|
|
293
358
|
}
|
|
294
|
-
const badges = event.badges.map((b) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
359
|
+
const badges = event.badges.map((b) => {
|
|
360
|
+
const resolved = resolveBadge?.(b.set_id, b.id);
|
|
361
|
+
return {
|
|
362
|
+
setId: b.set_id,
|
|
363
|
+
id: b.id,
|
|
364
|
+
info: b.info,
|
|
365
|
+
...resolved !== void 0 && { resolved }
|
|
366
|
+
};
|
|
367
|
+
});
|
|
299
368
|
const badgeSetIds = new Set(badges.map((b) => b.setId));
|
|
300
369
|
const msg = {
|
|
301
370
|
id: event.message_id,
|
|
@@ -358,6 +427,10 @@ var TwitchChat = class extends import_eventemitter3.default {
|
|
|
358
427
|
accessToken: this.options.accessToken,
|
|
359
428
|
clientId: this.options.clientId
|
|
360
429
|
}));
|
|
430
|
+
this.badgeCache = new BadgeCache(options.channelId, () => ({
|
|
431
|
+
accessToken: this.options.accessToken,
|
|
432
|
+
clientId: this.options.clientId
|
|
433
|
+
}));
|
|
361
434
|
}
|
|
362
435
|
// ---------------------------------------------------------------------------
|
|
363
436
|
// Public API
|
|
@@ -379,6 +452,15 @@ var TwitchChat = class extends import_eventemitter3.default {
|
|
|
379
452
|
async refreshEmotes() {
|
|
380
453
|
await this.emoteCache.load();
|
|
381
454
|
}
|
|
455
|
+
async preloadBadges() {
|
|
456
|
+
await this.badgeCache.load();
|
|
457
|
+
}
|
|
458
|
+
async getUser(userId) {
|
|
459
|
+
return this.userCache.getUser(userId);
|
|
460
|
+
}
|
|
461
|
+
async getUsers(userIds) {
|
|
462
|
+
return this.userCache.getUsers(userIds);
|
|
463
|
+
}
|
|
382
464
|
async getProfilePictureUrl(userId) {
|
|
383
465
|
return this.userCache.getProfilePictureUrl(userId);
|
|
384
466
|
}
|
|
@@ -390,6 +472,9 @@ var TwitchChat = class extends import_eventemitter3.default {
|
|
|
390
472
|
}
|
|
391
473
|
return result;
|
|
392
474
|
}
|
|
475
|
+
resolveBadge(setId, version) {
|
|
476
|
+
return this.badgeCache.resolve(setId, version);
|
|
477
|
+
}
|
|
393
478
|
// ---------------------------------------------------------------------------
|
|
394
479
|
// Connection
|
|
395
480
|
// ---------------------------------------------------------------------------
|
|
@@ -476,7 +561,11 @@ var TwitchChat = class extends import_eventemitter3.default {
|
|
|
476
561
|
const payload = msg.payload;
|
|
477
562
|
if (payload.subscription.type === "channel.chat.message") {
|
|
478
563
|
try {
|
|
479
|
-
const normalized = normalizeMessage(
|
|
564
|
+
const normalized = normalizeMessage(
|
|
565
|
+
payload.event,
|
|
566
|
+
this.emoteCache,
|
|
567
|
+
(setId, version) => this.badgeCache.resolve(setId, version)
|
|
568
|
+
);
|
|
480
569
|
this.emit("message", normalized);
|
|
481
570
|
} catch (e) {
|
|
482
571
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/emotes/twitch.ts","../src/emotes/bttv.ts","../src/emotes/7tv.ts","../src/emotes/index.ts","../src/users/index.ts","../src/normalizer.ts"],"sourcesContent":["export { TwitchChat } from './client.js'\nexport type {\n TwitchChatOptions,\n NormalizedMessage,\n ChatUser,\n Badge,\n MessageFragment,\n ResolvedEmote,\n UserProfile,\n} from './types.js'\n","import EventEmitter from 'eventemitter3'\nimport type {\n TwitchChatOptions,\n NormalizedMessage,\n TwitchEventSubMessage,\n TwitchWelcomePayload,\n TwitchNotificationPayload,\n TwitchReconnectPayload,\n TwitchRevocationPayload,\n} from './types.js'\nimport { EmoteCache } from './emotes/index.js'\nimport { UserCache } from './users/index.js'\nimport { normalizeMessage } from './normalizer.js'\n\nconst EVENTSUB_URL = 'wss://eventsub.wss.twitch.tv/ws'\nconst HELIX_SUBSCRIPTIONS = 'https://api.twitch.tv/helix/eventsub/subscriptions'\n\n// Minimal interface covering both `ws` WebSocket and browser WebSocket.\ninterface WSLike {\n close(code?: number, reason?: string): void\n addEventListener(type: 'open', listener: () => void): void\n addEventListener(type: 'message', listener: (event: { data: string }) => void): void\n addEventListener(type: 'close', listener: (event: { code: number; reason: string | Buffer }) => void): void\n addEventListener(type: 'error', listener: (event: unknown) => void): void\n}\n\nfunction createWebSocket(url: string): WSLike {\n if (typeof WebSocket !== 'undefined') {\n return new WebSocket(url) as unknown as WSLike\n }\n // Node.js — require ws at runtime (peer dep)\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('ws') as { default?: new (url: string) => WSLike } & (new (url: string) => WSLike)\n const WsImpl = mod.default ?? mod\n return new WsImpl(url)\n}\n\ninterface TwitchChatEvents {\n connected: []\n disconnected: [code: number, reason: string]\n message: [msg: NormalizedMessage]\n revoked: [reason: string]\n auth_error: []\n error: [err: Error]\n}\n\nexport class TwitchChat extends EventEmitter<TwitchChatEvents> {\n private options: TwitchChatOptions\n private emoteCache: EmoteCache\n private userCache: UserCache\n\n private ws: WSLike | null = null\n private sessionId: string | null = null\n private keepaliveTimeoutMs = 10_000\n private keepaliveTimer: ReturnType<typeof setTimeout> | null = null\n\n // Holds the old ws during a session_reconnect handoff\n private oldWs: WSLike | null = null\n\n private stopped = false\n\n constructor(options: TwitchChatOptions) {\n super()\n this.options = options\n this.emoteCache = new EmoteCache(options.channelId)\n this.userCache = new UserCache(() => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n async connect(): Promise<void> {\n this.stopped = false\n await this._openConnection(EVENTSUB_URL, false)\n }\n\n disconnect(): void {\n this.stopped = true\n this._clearKeepaliveTimer()\n this._closeWs(this.ws, 1000, 'disconnect')\n this.ws = null\n this.sessionId = null\n }\n\n async preloadEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async refreshEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n return this.userCache.getProfilePictureUrl(userId)\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>> {\n const found = await this.userCache.getProfilePictureUrls(userIds)\n const result = new Map<string, string | null>()\n for (const id of userIds) {\n result.set(id, found.get(id) ?? null)\n }\n return result\n }\n\n // ---------------------------------------------------------------------------\n // Connection\n // ---------------------------------------------------------------------------\n\n private _openConnection(url: string, isReconnect: boolean): Promise<void> {\n return new Promise((resolve, reject) => {\n const ws = createWebSocket(url)\n let settled = false\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true\n fn()\n }\n }\n\n ws.addEventListener('message', (event) => {\n const raw = typeof event.data === 'string' ? event.data : String(event.data)\n let msg: TwitchEventSubMessage\n try {\n msg = JSON.parse(raw) as TwitchEventSubMessage\n } catch (e) {\n this.emit('error', new Error(`Failed to parse WS message: ${String(e)}`))\n return\n }\n this._dispatch(msg, ws, isReconnect, settle, resolve, reject)\n })\n\n ws.addEventListener('close', (event) => {\n const code = event.code\n const reason = typeof event.reason === 'string' ? event.reason : event.reason.toString()\n\n this._clearKeepaliveTimer()\n\n if (!settled) {\n settle(() => reject(new Error(`WebSocket closed before welcome: ${code} ${reason}`)))\n return\n }\n\n if (ws !== this.ws) return // this was an old ws that got closed; ignore\n\n this.emit('disconnected', code, reason)\n\n if (!this.stopped && code !== 1000) {\n setTimeout(() => {\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, 2_000)\n }\n })\n\n ws.addEventListener('error', (err) => {\n const error = err instanceof Error ? err : new Error('WebSocket error')\n if (!settled) {\n settle(() => reject(error))\n } else {\n this.emit('error', error)\n }\n })\n })\n }\n\n private _dispatch(\n msg: TwitchEventSubMessage,\n ws: WSLike,\n isReconnect: boolean,\n settle: (fn: () => void) => void,\n resolve: () => void,\n reject: (err: Error) => void,\n ): void {\n switch (msg.metadata.message_type) {\n case 'session_welcome': {\n const payload = msg.payload as TwitchWelcomePayload\n this.sessionId = payload.session.id\n this.keepaliveTimeoutMs = payload.session.keepalive_timeout_seconds * 1_000\n this._resetKeepaliveTimer()\n\n if (isReconnect) {\n // Subscriptions carry over — no need to re-POST.\n // Close the old connection now that the new one is ready.\n this._closeWs(this.oldWs, 1000, 'reconnected')\n this.oldWs = null\n this.ws = ws\n settle(() => resolve())\n break\n }\n\n this.ws = ws\n this._subscribe()\n .then(() => {\n settle(() => resolve())\n this.emit('connected')\n })\n .catch(err => {\n settle(() => reject(err instanceof Error ? err : new Error(String(err))))\n })\n break\n }\n\n case 'session_keepalive': {\n this._resetKeepaliveTimer()\n break\n }\n\n case 'notification': {\n this._resetKeepaliveTimer()\n const payload = msg.payload as TwitchNotificationPayload\n if (payload.subscription.type === 'channel.chat.message') {\n try {\n const normalized = normalizeMessage(payload.event, this.emoteCache)\n this.emit('message', normalized)\n } catch (e) {\n this.emit('error', e instanceof Error ? e : new Error(String(e)))\n }\n }\n break\n }\n\n case 'session_reconnect': {\n const payload = msg.payload as TwitchReconnectPayload\n // Keep current ws open until new one sends session_welcome\n this.oldWs = this.ws\n this._openConnection(payload.session.reconnect_url, true).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n break\n }\n\n case 'revocation': {\n const payload = msg.payload as TwitchRevocationPayload\n this.emit('revoked', payload.subscription.status)\n break\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helix subscription\n // ---------------------------------------------------------------------------\n\n private async _subscribe(): Promise<void> {\n if (!this.sessionId) throw new Error('No session ID')\n\n const res = await fetch(HELIX_SUBSCRIPTIONS, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.options.accessToken}`,\n 'Client-Id': this.options.clientId,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: 'channel.chat.message',\n version: '1',\n condition: {\n broadcaster_user_id: this.options.channelId,\n user_id: this.options.userId,\n },\n transport: {\n method: 'websocket',\n session_id: this.sessionId,\n },\n }),\n })\n\n if (res.status === 401) {\n this.emit('auth_error')\n throw new Error('Auth error subscribing to EventSub')\n }\n\n if (!res.ok) {\n const body = await res.text()\n throw new Error(`EventSub subscription failed: ${res.status} ${body}`)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Keepalive timer\n // ---------------------------------------------------------------------------\n\n private _resetKeepaliveTimer(): void {\n this._clearKeepaliveTimer()\n this.keepaliveTimer = setTimeout(() => {\n this._closeWs(this.ws, 1001, 'keepalive timeout')\n this.ws = null\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, this.keepaliveTimeoutMs + 500)\n }\n\n private _clearKeepaliveTimer(): void {\n if (this.keepaliveTimer !== null) {\n clearTimeout(this.keepaliveTimer)\n this.keepaliveTimer = null\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private _closeWs(ws: WSLike | null, code: number, reason: string): void {\n if (!ws) return\n try {\n ws.close(code, reason)\n } catch {\n // ignore\n }\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst CDN = 'https://static-cdn.jtvnw.net/emoticons/v2'\n\nexport function buildTwitchEmote(id: string, name: string): ResolvedEmote {\n return {\n id,\n name,\n source: 'twitch',\n animated: false,\n imageUrl1x: `${CDN}/${id}/default/dark/1.0`,\n imageUrl2x: `${CDN}/${id}/default/dark/2.0`,\n imageUrl3x: `${CDN}/${id}/default/dark/3.0`,\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://api.betterttv.net/3'\nconst CDN = 'https://cdn.betterttv.net/emote'\n\ninterface BttvEmote {\n id: string\n code: string\n imageType: string\n animated: boolean\n}\n\ninterface BttvChannelResponse {\n channelEmotes: BttvEmote[]\n sharedEmotes: BttvEmote[]\n}\n\nfunction parseEmote(e: BttvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.code,\n source: 'bttv',\n animated: e.animated,\n imageUrl1x: `${CDN}/${e.id}/1x`,\n imageUrl2x: `${CDN}/${e.id}/2x`,\n imageUrl3x: `${CDN}/${e.id}/3x`,\n }\n}\n\nexport async function fetchBttvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/emotes/global`)\n if (!res.ok) throw new Error(`BTTV global fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvEmote[]\n const map = new Map<string, ResolvedEmote>()\n for (const e of data) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n\nexport async function fetchBttvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`BTTV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of [...data.channelEmotes, ...data.sharedEmotes]) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://7tv.io/v3'\nconst CDN = 'https://cdn.7tv.app/emote'\n\ninterface SevenTvFile {\n name: string\n static_name: string\n width: number\n height: number\n frame_count: number\n size: number\n format: string\n}\n\ninterface SevenTvEmoteData {\n host: {\n url: string\n files: SevenTvFile[]\n }\n animated: boolean\n}\n\ninterface SevenTvEmote {\n id: string\n name: string\n data: SevenTvEmoteData\n}\n\ninterface SevenTvEmoteSet {\n emotes: SevenTvEmote[]\n}\n\ninterface SevenTvChannelResponse {\n emote_set: SevenTvEmoteSet\n}\n\nfunction parseEmote(e: SevenTvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.name,\n source: '7tv',\n animated: e.data.animated,\n imageUrl1x: `${CDN}/${e.id}/1x.webp`,\n imageUrl2x: `${CDN}/${e.id}/2x.webp`,\n imageUrl3x: `${CDN}/${e.id}/3x.webp`,\n }\n}\n\nexport async function fetch7tvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/emote-sets/global`)\n if (!res.ok) throw new Error(`7TV global fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvEmoteSet\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n\nexport async function fetch7tvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`7TV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emote_set.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\nimport { buildTwitchEmote } from './twitch.js'\nimport { fetchBttvGlobal, fetchBttvChannel } from './bttv.js'\nimport { fetch7tvGlobal, fetch7tvChannel } from './7tv.js'\n\nexport class EmoteCache {\n private bttvGlobal = new Map<string, ResolvedEmote>()\n private bttvChannel = new Map<string, ResolvedEmote>()\n private sevenTvGlobal = new Map<string, ResolvedEmote>()\n private sevenTvChannel = new Map<string, ResolvedEmote>()\n\n constructor(private readonly channelId: string) {}\n\n async load(): Promise<void> {\n const results = await Promise.allSettled([\n fetchBttvGlobal().then(m => { this.bttvGlobal = m }),\n fetchBttvChannel(this.channelId).then(m => { this.bttvChannel = m }),\n fetch7tvGlobal().then(m => { this.sevenTvGlobal = m }),\n fetch7tvChannel(this.channelId).then(m => { this.sevenTvChannel = m }),\n ])\n\n for (const result of results) {\n if (result.status === 'rejected') {\n console.warn('[twitch-integration] emote fetch error:', result.reason)\n }\n }\n }\n\n /**\n * Resolve a third-party emote by name.\n * Priority: 7TV channel > BTTV channel > 7TV global > BTTV global\n *\n * Twitch emotes are resolved separately via resolveFromFragment(), since their\n * IDs come directly from message fragments — no lookup table needed.\n */\n resolveByName(name: string): ResolvedEmote | undefined {\n return (\n this.sevenTvChannel.get(name) ??\n this.bttvChannel.get(name) ??\n this.sevenTvGlobal.get(name) ??\n this.bttvGlobal.get(name)\n )\n }\n\n /**\n * Resolve a Twitch native emote from fragment data.\n */\n resolveTwitch(id: string, name: string): ResolvedEmote {\n return buildTwitchEmote(id, name)\n }\n}\n","interface HelixUser {\n id: string\n login: string\n display_name: string\n profile_image_url: string\n}\n\ninterface CacheEntry {\n url: string\n expiresAt: number\n}\n\nconst HELIX_USERS = 'https://api.twitch.tv/helix/users'\nconst TTL_MS = 5 * 60 * 1_000\n\nexport class UserCache {\n private cache = new Map<string, CacheEntry>()\n\n constructor(\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n const results = await this.getProfilePictureUrls([userId])\n return results.get(userId) ?? null\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string>> {\n const now = Date.now()\n const result = new Map<string, string>()\n const toFetch: string[] = []\n\n for (const id of userIds) {\n const entry = this.cache.get(id)\n if (entry !== undefined && entry.expiresAt > now) {\n result.set(id, entry.url)\n } else {\n toFetch.push(id)\n }\n }\n\n if (toFetch.length === 0) return result\n\n for (let i = 0; i < toFetch.length; i += 100) {\n const chunk = toFetch.slice(i, i + 100)\n const fetched = await this._fetchChunk(chunk, now)\n for (const [id, url] of fetched) {\n result.set(id, url)\n }\n }\n\n return result\n }\n\n private async _fetchChunk(ids: string[], now: number): Promise<Map<string, string>> {\n const params = new URLSearchParams()\n for (const id of ids) params.append('id', id)\n\n const { accessToken, clientId } = this.getCredentials()\n const res = await fetch(`${HELIX_USERS}?${params.toString()}`, {\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n },\n })\n\n if (!res.ok) {\n throw new Error(`Helix users fetch failed: ${res.status}`)\n }\n\n const body = (await res.json()) as { data: HelixUser[] }\n const result = new Map<string, string>()\n const expiresAt = now + TTL_MS\n\n for (const user of body.data) {\n this.cache.set(user.id, { url: user.profile_image_url, expiresAt })\n result.set(user.id, user.profile_image_url)\n }\n\n return result\n }\n}\n","import type {\n TwitchChatMessageEvent,\n NormalizedMessage,\n MessageFragment,\n ResolvedEmote,\n} from './types.js'\nimport type { EmoteCache } from './emotes/index.js'\n\nexport function normalizeMessage(\n event: TwitchChatMessageEvent,\n emoteCache: EmoteCache,\n): NormalizedMessage {\n const emotes: ResolvedEmote[] = []\n const fragments: MessageFragment[] = []\n\n for (const frag of event.message.fragments) {\n switch (frag.type) {\n case 'text': {\n // A \"text\" fragment may contain third-party emote names.\n // Split on whitespace and check each token against the emote cache.\n const tokens = frag.text.split(/(\\s+)/)\n let pendingText = ''\n\n for (const token of tokens) {\n if (/^\\s+$/.test(token)) {\n pendingText += token\n continue\n }\n const resolved = emoteCache.resolveByName(token)\n if (resolved) {\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n pendingText = ''\n }\n fragments.push({ type: 'emote', text: token, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n } else {\n pendingText += token\n }\n }\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n }\n break\n }\n\n case 'emote': {\n const emoteData = frag.emote!\n const resolved = emoteCache.resolveTwitch(emoteData.id, frag.text)\n fragments.push({ type: 'emote', text: frag.text, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n break\n }\n\n case 'cheermote': {\n const cheer = frag.cheermote!\n fragments.push({\n type: 'cheermote',\n text: frag.text,\n bits: cheer.bits,\n tier: cheer.tier,\n })\n break\n }\n\n case 'mention': {\n const mention = frag.mention!\n fragments.push({\n type: 'mention',\n text: frag.text,\n userId: mention.user_id,\n userLogin: mention.user_login,\n })\n break\n }\n }\n }\n\n const badges = event.badges.map(b => ({\n setId: b.set_id,\n id: b.id,\n info: b.info,\n }))\n\n const badgeSetIds = new Set(badges.map(b => b.setId))\n\n const msg: NormalizedMessage = {\n id: event.message_id,\n text: event.message.text,\n user: {\n id: event.chatter_user_id,\n login: event.chatter_user_login,\n displayName: event.chatter_user_name,\n color: event.color,\n badges,\n isModerator: badgeSetIds.has('moderator'),\n isSubscriber: badgeSetIds.has('subscriber'),\n isBroadcaster: badgeSetIds.has('broadcaster'),\n isVip: badgeSetIds.has('vip'),\n },\n fragments,\n emotes,\n timestamp: event.timestamp,\n }\n\n if (event.cheer != null) {\n msg.cheer = { bits: event.cheer.bits }\n }\n\n if (event.reply != null) {\n msg.reply = {\n parentMessageId: event.reply.parent_message_id,\n parentUserLogin: event.reply.parent_user_login,\n parentUserDisplayName: event.reply.parent_user_display_name,\n }\n }\n\n if (event.channel_points_custom_reward_id != null) {\n msg.channelPointsRewardId = event.channel_points_custom_reward_id\n }\n\n return msg\n}\n"],"mappings":"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","names":["CDN","API","CDN","parseEmote","EventEmitter"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/emotes/twitch.ts","../src/emotes/bttv.ts","../src/emotes/7tv.ts","../src/emotes/index.ts","../src/users/index.ts","../src/badges/index.ts","../src/normalizer.ts"],"sourcesContent":["export { TwitchChat } from './client.js'\nexport type {\n TwitchChatOptions,\n NormalizedMessage,\n ChatUser,\n Badge,\n ResolvedBadge,\n MessageFragment,\n ResolvedEmote,\n UserInfo,\n} from './types.js'\n","import EventEmitter from 'eventemitter3'\nimport type {\n TwitchChatOptions,\n NormalizedMessage,\n TwitchEventSubMessage,\n TwitchWelcomePayload,\n TwitchNotificationPayload,\n TwitchReconnectPayload,\n TwitchRevocationPayload,\n UserInfo,\n ResolvedBadge,\n} from './types.js'\nimport { EmoteCache } from './emotes/index.js'\nimport { UserCache } from './users/index.js'\nimport { BadgeCache } from './badges/index.js'\nimport { normalizeMessage } from './normalizer.js'\n\nconst EVENTSUB_URL = 'wss://eventsub.wss.twitch.tv/ws'\nconst HELIX_SUBSCRIPTIONS = 'https://api.twitch.tv/helix/eventsub/subscriptions'\n\n// Minimal interface covering both `ws` WebSocket and browser WebSocket.\ninterface WSLike {\n close(code?: number, reason?: string): void\n addEventListener(type: 'open', listener: () => void): void\n addEventListener(type: 'message', listener: (event: { data: string }) => void): void\n addEventListener(type: 'close', listener: (event: { code: number; reason: string | Buffer }) => void): void\n addEventListener(type: 'error', listener: (event: unknown) => void): void\n}\n\nfunction createWebSocket(url: string): WSLike {\n if (typeof WebSocket !== 'undefined') {\n return new WebSocket(url) as unknown as WSLike\n }\n // Node.js — require ws at runtime (peer dep)\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('ws') as { default?: new (url: string) => WSLike } & (new (url: string) => WSLike)\n const WsImpl = mod.default ?? mod\n return new WsImpl(url)\n}\n\ninterface TwitchChatEvents {\n connected: []\n disconnected: [code: number, reason: string]\n message: [msg: NormalizedMessage]\n revoked: [reason: string]\n auth_error: []\n error: [err: Error]\n}\n\nexport class TwitchChat extends EventEmitter<TwitchChatEvents> {\n private options: TwitchChatOptions\n private emoteCache: EmoteCache\n private userCache: UserCache\n private badgeCache: BadgeCache\n\n private ws: WSLike | null = null\n private sessionId: string | null = null\n private keepaliveTimeoutMs = 10_000\n private keepaliveTimer: ReturnType<typeof setTimeout> | null = null\n\n // Holds the old ws during a session_reconnect handoff\n private oldWs: WSLike | null = null\n\n private stopped = false\n\n constructor(options: TwitchChatOptions) {\n super()\n this.options = options\n this.emoteCache = new EmoteCache(options.channelId)\n this.userCache = new UserCache(() => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n this.badgeCache = new BadgeCache(options.channelId, () => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n async connect(): Promise<void> {\n this.stopped = false\n await this._openConnection(EVENTSUB_URL, false)\n }\n\n disconnect(): void {\n this.stopped = true\n this._clearKeepaliveTimer()\n this._closeWs(this.ws, 1000, 'disconnect')\n this.ws = null\n this.sessionId = null\n }\n\n async preloadEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async refreshEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async preloadBadges(): Promise<void> {\n await this.badgeCache.load()\n }\n\n async getUser(userId: string): Promise<UserInfo | null> {\n return this.userCache.getUser(userId)\n }\n\n async getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>> {\n return this.userCache.getUsers(userIds)\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n return this.userCache.getProfilePictureUrl(userId)\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>> {\n const found = await this.userCache.getProfilePictureUrls(userIds)\n const result = new Map<string, string | null>()\n for (const id of userIds) {\n result.set(id, found.get(id) ?? null)\n }\n return result\n }\n\n resolveBadge(setId: string, version: string): ResolvedBadge | undefined {\n return this.badgeCache.resolve(setId, version)\n }\n\n // ---------------------------------------------------------------------------\n // Connection\n // ---------------------------------------------------------------------------\n\n private _openConnection(url: string, isReconnect: boolean): Promise<void> {\n return new Promise((resolve, reject) => {\n const ws = createWebSocket(url)\n let settled = false\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true\n fn()\n }\n }\n\n ws.addEventListener('message', (event) => {\n const raw = typeof event.data === 'string' ? event.data : String(event.data)\n let msg: TwitchEventSubMessage\n try {\n msg = JSON.parse(raw) as TwitchEventSubMessage\n } catch (e) {\n this.emit('error', new Error(`Failed to parse WS message: ${String(e)}`))\n return\n }\n this._dispatch(msg, ws, isReconnect, settle, resolve, reject)\n })\n\n ws.addEventListener('close', (event) => {\n const code = event.code\n const reason = typeof event.reason === 'string' ? event.reason : event.reason.toString()\n\n this._clearKeepaliveTimer()\n\n if (!settled) {\n settle(() => reject(new Error(`WebSocket closed before welcome: ${code} ${reason}`)))\n return\n }\n\n if (ws !== this.ws) return // this was an old ws that got closed; ignore\n\n this.emit('disconnected', code, reason)\n\n if (!this.stopped && code !== 1000) {\n setTimeout(() => {\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, 2_000)\n }\n })\n\n ws.addEventListener('error', (err) => {\n const error = err instanceof Error ? err : new Error('WebSocket error')\n if (!settled) {\n settle(() => reject(error))\n } else {\n this.emit('error', error)\n }\n })\n })\n }\n\n private _dispatch(\n msg: TwitchEventSubMessage,\n ws: WSLike,\n isReconnect: boolean,\n settle: (fn: () => void) => void,\n resolve: () => void,\n reject: (err: Error) => void,\n ): void {\n switch (msg.metadata.message_type) {\n case 'session_welcome': {\n const payload = msg.payload as TwitchWelcomePayload\n this.sessionId = payload.session.id\n this.keepaliveTimeoutMs = payload.session.keepalive_timeout_seconds * 1_000\n this._resetKeepaliveTimer()\n\n if (isReconnect) {\n // Subscriptions carry over — no need to re-POST.\n // Close the old connection now that the new one is ready.\n this._closeWs(this.oldWs, 1000, 'reconnected')\n this.oldWs = null\n this.ws = ws\n settle(() => resolve())\n break\n }\n\n this.ws = ws\n this._subscribe()\n .then(() => {\n settle(() => resolve())\n this.emit('connected')\n })\n .catch(err => {\n settle(() => reject(err instanceof Error ? err : new Error(String(err))))\n })\n break\n }\n\n case 'session_keepalive': {\n this._resetKeepaliveTimer()\n break\n }\n\n case 'notification': {\n this._resetKeepaliveTimer()\n const payload = msg.payload as TwitchNotificationPayload\n if (payload.subscription.type === 'channel.chat.message') {\n try {\n const normalized = normalizeMessage(\n payload.event,\n this.emoteCache,\n (setId, version) => this.badgeCache.resolve(setId, version),\n )\n this.emit('message', normalized)\n } catch (e) {\n this.emit('error', e instanceof Error ? e : new Error(String(e)))\n }\n }\n break\n }\n\n case 'session_reconnect': {\n const payload = msg.payload as TwitchReconnectPayload\n // Keep current ws open until new one sends session_welcome\n this.oldWs = this.ws\n this._openConnection(payload.session.reconnect_url, true).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n break\n }\n\n case 'revocation': {\n const payload = msg.payload as TwitchRevocationPayload\n this.emit('revoked', payload.subscription.status)\n break\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helix subscription\n // ---------------------------------------------------------------------------\n\n private async _subscribe(): Promise<void> {\n if (!this.sessionId) throw new Error('No session ID')\n\n const res = await fetch(HELIX_SUBSCRIPTIONS, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.options.accessToken}`,\n 'Client-Id': this.options.clientId,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: 'channel.chat.message',\n version: '1',\n condition: {\n broadcaster_user_id: this.options.channelId,\n user_id: this.options.userId,\n },\n transport: {\n method: 'websocket',\n session_id: this.sessionId,\n },\n }),\n })\n\n if (res.status === 401) {\n this.emit('auth_error')\n throw new Error('Auth error subscribing to EventSub')\n }\n\n if (!res.ok) {\n const body = await res.text()\n throw new Error(`EventSub subscription failed: ${res.status} ${body}`)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Keepalive timer\n // ---------------------------------------------------------------------------\n\n private _resetKeepaliveTimer(): void {\n this._clearKeepaliveTimer()\n this.keepaliveTimer = setTimeout(() => {\n this._closeWs(this.ws, 1001, 'keepalive timeout')\n this.ws = null\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, this.keepaliveTimeoutMs + 500)\n }\n\n private _clearKeepaliveTimer(): void {\n if (this.keepaliveTimer !== null) {\n clearTimeout(this.keepaliveTimer)\n this.keepaliveTimer = null\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private _closeWs(ws: WSLike | null, code: number, reason: string): void {\n if (!ws) return\n try {\n ws.close(code, reason)\n } catch {\n // ignore\n }\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst CDN = 'https://static-cdn.jtvnw.net/emoticons/v2'\n\nexport function buildTwitchEmote(id: string, name: string): ResolvedEmote {\n return {\n id,\n name,\n source: 'twitch',\n animated: false,\n imageUrl1x: `${CDN}/${id}/default/dark/1.0`,\n imageUrl2x: `${CDN}/${id}/default/dark/2.0`,\n imageUrl3x: `${CDN}/${id}/default/dark/3.0`,\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://api.betterttv.net/3'\nconst CDN = 'https://cdn.betterttv.net/emote'\n\ninterface BttvEmote {\n id: string\n code: string\n imageType: string\n animated: boolean\n}\n\ninterface BttvChannelResponse {\n channelEmotes: BttvEmote[]\n sharedEmotes: BttvEmote[]\n}\n\nfunction parseEmote(e: BttvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.code,\n source: 'bttv',\n animated: e.animated,\n imageUrl1x: `${CDN}/${e.id}/1x`,\n imageUrl2x: `${CDN}/${e.id}/2x`,\n imageUrl3x: `${CDN}/${e.id}/3x`,\n }\n}\n\nexport async function fetchBttvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/emotes/global`)\n if (!res.ok) throw new Error(`BTTV global fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvEmote[]\n const map = new Map<string, ResolvedEmote>()\n for (const e of data) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n\nexport async function fetchBttvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`BTTV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of [...data.channelEmotes, ...data.sharedEmotes]) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://7tv.io/v3'\nconst CDN = 'https://cdn.7tv.app/emote'\n\ninterface SevenTvFile {\n name: string\n static_name: string\n width: number\n height: number\n frame_count: number\n size: number\n format: string\n}\n\ninterface SevenTvEmoteData {\n host: {\n url: string\n files: SevenTvFile[]\n }\n animated: boolean\n}\n\ninterface SevenTvEmote {\n id: string\n name: string\n data: SevenTvEmoteData\n}\n\ninterface SevenTvEmoteSet {\n emotes: SevenTvEmote[]\n}\n\ninterface SevenTvChannelResponse {\n emote_set: SevenTvEmoteSet\n}\n\nfunction parseEmote(e: SevenTvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.name,\n source: '7tv',\n animated: e.data.animated,\n imageUrl1x: `${CDN}/${e.id}/1x.webp`,\n imageUrl2x: `${CDN}/${e.id}/2x.webp`,\n imageUrl3x: `${CDN}/${e.id}/3x.webp`,\n }\n}\n\nexport async function fetch7tvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/emote-sets/global`)\n if (!res.ok) throw new Error(`7TV global fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvEmoteSet\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n\nexport async function fetch7tvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`7TV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emote_set.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\nimport { buildTwitchEmote } from './twitch.js'\nimport { fetchBttvGlobal, fetchBttvChannel } from './bttv.js'\nimport { fetch7tvGlobal, fetch7tvChannel } from './7tv.js'\n\nexport class EmoteCache {\n private bttvGlobal = new Map<string, ResolvedEmote>()\n private bttvChannel = new Map<string, ResolvedEmote>()\n private sevenTvGlobal = new Map<string, ResolvedEmote>()\n private sevenTvChannel = new Map<string, ResolvedEmote>()\n\n constructor(private readonly channelId: string) {}\n\n async load(): Promise<void> {\n const results = await Promise.allSettled([\n fetchBttvGlobal().then(m => { this.bttvGlobal = m }),\n fetchBttvChannel(this.channelId).then(m => { this.bttvChannel = m }),\n fetch7tvGlobal().then(m => { this.sevenTvGlobal = m }),\n fetch7tvChannel(this.channelId).then(m => { this.sevenTvChannel = m }),\n ])\n\n for (const result of results) {\n if (result.status === 'rejected') {\n console.warn('[twitch-integration] emote fetch error:', result.reason)\n }\n }\n }\n\n /**\n * Resolve a third-party emote by name.\n * Priority: 7TV channel > BTTV channel > 7TV global > BTTV global\n *\n * Twitch emotes are resolved separately via resolveFromFragment(), since their\n * IDs come directly from message fragments — no lookup table needed.\n */\n resolveByName(name: string): ResolvedEmote | undefined {\n return (\n this.sevenTvChannel.get(name) ??\n this.bttvChannel.get(name) ??\n this.sevenTvGlobal.get(name) ??\n this.bttvGlobal.get(name)\n )\n }\n\n /**\n * Resolve a Twitch native emote from fragment data.\n */\n resolveTwitch(id: string, name: string): ResolvedEmote {\n return buildTwitchEmote(id, name)\n }\n}\n","import type { UserInfo } from '../types.js'\n\ninterface HelixUser {\n id: string\n login: string\n display_name: string\n profile_image_url: string\n broadcaster_type: 'partner' | 'affiliate' | ''\n description: string\n created_at: string\n}\n\ninterface CacheEntry {\n user: UserInfo\n expiresAt: number\n}\n\nconst HELIX_USERS = 'https://api.twitch.tv/helix/users'\nconst TTL_MS = 5 * 60 * 1_000\n\nexport class UserCache {\n private cache = new Map<string, CacheEntry>()\n\n constructor(\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async getUser(userId: string): Promise<UserInfo | null> {\n const results = await this.getUsers([userId])\n return results.get(userId) ?? null\n }\n\n async getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>> {\n const now = Date.now()\n const result = new Map<string, UserInfo | null>()\n const toFetch: string[] = []\n\n for (const id of userIds) {\n const entry = this.cache.get(id)\n if (entry !== undefined && entry.expiresAt > now) {\n result.set(id, entry.user)\n } else {\n result.set(id, null)\n toFetch.push(id)\n }\n }\n\n for (let i = 0; i < toFetch.length; i += 100) {\n const chunk = toFetch.slice(i, i + 100)\n const fetched = await this._fetchChunk(chunk, now)\n for (const [id, user] of fetched) {\n result.set(id, user)\n }\n }\n\n return result\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n const user = await this.getUser(userId)\n return user?.profileImageUrl ?? null\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string>> {\n const users = await this.getUsers(userIds)\n const result = new Map<string, string>()\n for (const [id, user] of users) {\n if (user !== null) result.set(id, user.profileImageUrl)\n }\n return result\n }\n\n private async _fetchChunk(ids: string[], now: number): Promise<Map<string, UserInfo>> {\n const params = new URLSearchParams()\n for (const id of ids) params.append('id', id)\n\n const { accessToken, clientId } = this.getCredentials()\n const res = await fetch(`${HELIX_USERS}?${params.toString()}`, {\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n },\n })\n\n if (!res.ok) {\n throw new Error(`Helix users fetch failed: ${res.status}`)\n }\n\n const body = (await res.json()) as { data: HelixUser[] }\n const result = new Map<string, UserInfo>()\n const expiresAt = now + TTL_MS\n\n for (const u of body.data) {\n const user: UserInfo = {\n id: u.id,\n login: u.login,\n displayName: u.display_name,\n profileImageUrl: u.profile_image_url,\n broadcasterType: u.broadcaster_type,\n description: u.description,\n createdAt: u.created_at,\n }\n this.cache.set(u.id, { user, expiresAt })\n result.set(u.id, user)\n }\n\n return result\n }\n}\n","import type { ResolvedBadge } from '../types.js'\n\ninterface HelixBadgeVersion {\n id: string\n image_url_1x: string\n image_url_2x: string\n image_url_4x: string\n title: string\n}\n\ninterface HelixBadgeSet {\n set_id: string\n versions: HelixBadgeVersion[]\n}\n\nconst HELIX_BADGES_GLOBAL = 'https://api.twitch.tv/helix/chat/badges/global'\nconst HELIX_BADGES_CHANNEL = 'https://api.twitch.tv/helix/chat/badges'\n\nexport class BadgeCache {\n // setId → version → ResolvedBadge\n private sets = new Map<string, Map<string, ResolvedBadge>>()\n\n constructor(\n private readonly channelId: string,\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async load(): Promise<void> {\n const { accessToken, clientId } = this.getCredentials()\n const headers = {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n }\n\n const [globalRes, channelRes] = await Promise.all([\n fetch(HELIX_BADGES_GLOBAL, { headers }),\n fetch(`${HELIX_BADGES_CHANNEL}?broadcaster_id=${this.channelId}`, { headers }),\n ])\n\n if (!globalRes.ok) throw new Error(`Global badges fetch failed: ${globalRes.status}`)\n if (!channelRes.ok) throw new Error(`Channel badges fetch failed: ${channelRes.status}`)\n\n const [globalBody, channelBody] = await Promise.all([\n globalRes.json() as Promise<{ data: HelixBadgeSet[] }>,\n channelRes.json() as Promise<{ data: HelixBadgeSet[] }>,\n ])\n\n // Load global first, channel second so channel versions override global\n for (const set of [...globalBody.data, ...channelBody.data]) {\n const versionMap = this.sets.get(set.set_id) ?? new Map<string, ResolvedBadge>()\n for (const v of set.versions) {\n versionMap.set(v.id, {\n title: v.title,\n imageUrl1x: v.image_url_1x,\n imageUrl2x: v.image_url_2x,\n imageUrl4x: v.image_url_4x,\n })\n }\n this.sets.set(set.set_id, versionMap)\n }\n }\n\n resolve(setId: string, version: string): ResolvedBadge | undefined {\n return this.sets.get(setId)?.get(version)\n }\n}\n","import type {\n TwitchChatMessageEvent,\n NormalizedMessage,\n MessageFragment,\n ResolvedEmote,\n ResolvedBadge,\n} from './types.js'\nimport type { EmoteCache } from './emotes/index.js'\n\nexport function normalizeMessage(\n event: TwitchChatMessageEvent,\n emoteCache: EmoteCache,\n resolveBadge?: (setId: string, version: string) => ResolvedBadge | undefined,\n): NormalizedMessage {\n const emotes: ResolvedEmote[] = []\n const fragments: MessageFragment[] = []\n\n for (const frag of event.message.fragments) {\n switch (frag.type) {\n case 'text': {\n // A \"text\" fragment may contain third-party emote names.\n // Split on whitespace and check each token against the emote cache.\n const tokens = frag.text.split(/(\\s+)/)\n let pendingText = ''\n\n for (const token of tokens) {\n if (/^\\s+$/.test(token)) {\n pendingText += token\n continue\n }\n const resolved = emoteCache.resolveByName(token)\n if (resolved) {\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n pendingText = ''\n }\n fragments.push({ type: 'emote', text: token, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n } else {\n pendingText += token\n }\n }\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n }\n break\n }\n\n case 'emote': {\n const emoteData = frag.emote!\n const resolved = emoteCache.resolveTwitch(emoteData.id, frag.text)\n fragments.push({ type: 'emote', text: frag.text, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n break\n }\n\n case 'cheermote': {\n const cheer = frag.cheermote!\n fragments.push({\n type: 'cheermote',\n text: frag.text,\n bits: cheer.bits,\n tier: cheer.tier,\n })\n break\n }\n\n case 'mention': {\n const mention = frag.mention!\n fragments.push({\n type: 'mention',\n text: frag.text,\n userId: mention.user_id,\n userLogin: mention.user_login,\n })\n break\n }\n }\n }\n\n const badges = event.badges.map(b => {\n const resolved = resolveBadge?.(b.set_id, b.id)\n return {\n setId: b.set_id,\n id: b.id,\n info: b.info,\n ...(resolved !== undefined && { resolved }),\n }\n })\n\n const badgeSetIds = new Set(badges.map(b => b.setId))\n\n const msg: NormalizedMessage = {\n id: event.message_id,\n text: event.message.text,\n user: {\n id: event.chatter_user_id,\n login: event.chatter_user_login,\n displayName: event.chatter_user_name,\n color: event.color,\n badges,\n isModerator: badgeSetIds.has('moderator'),\n isSubscriber: badgeSetIds.has('subscriber'),\n isBroadcaster: badgeSetIds.has('broadcaster'),\n isVip: badgeSetIds.has('vip'),\n },\n fragments,\n emotes,\n timestamp: event.timestamp,\n }\n\n if (event.cheer != null) {\n msg.cheer = { bits: event.cheer.bits }\n }\n\n if (event.reply != null) {\n msg.reply = {\n parentMessageId: event.reply.parent_message_id,\n parentUserLogin: event.reply.parent_user_login,\n parentUserDisplayName: event.reply.parent_user_display_name,\n }\n }\n\n if (event.channel_points_custom_reward_id != null) {\n msg.channelPointsRewardId = event.channel_points_custom_reward_id\n }\n\n return msg\n}\n"],"mappings":"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","names":["CDN","API","CDN","parseEmote","EventEmitter"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,10 +7,26 @@ interface TwitchChatOptions {
|
|
|
7
7
|
accessToken: string;
|
|
8
8
|
onTokenRefresh?: (newToken: string) => void;
|
|
9
9
|
}
|
|
10
|
+
interface UserInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
login: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
profileImageUrl: string;
|
|
15
|
+
broadcasterType: 'partner' | 'affiliate' | '';
|
|
16
|
+
description: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
interface ResolvedBadge {
|
|
20
|
+
title: string;
|
|
21
|
+
imageUrl1x: string;
|
|
22
|
+
imageUrl2x: string;
|
|
23
|
+
imageUrl4x: string;
|
|
24
|
+
}
|
|
10
25
|
interface Badge {
|
|
11
26
|
setId: string;
|
|
12
27
|
id: string;
|
|
13
28
|
info: string;
|
|
29
|
+
resolved?: ResolvedBadge;
|
|
14
30
|
}
|
|
15
31
|
interface ChatUser {
|
|
16
32
|
id: string;
|
|
@@ -67,10 +83,6 @@ interface NormalizedMessage {
|
|
|
67
83
|
};
|
|
68
84
|
channelPointsRewardId?: string;
|
|
69
85
|
}
|
|
70
|
-
interface UserProfile {
|
|
71
|
-
id: string;
|
|
72
|
-
profileImageUrl: string;
|
|
73
|
-
}
|
|
74
86
|
|
|
75
87
|
interface TwitchChatEvents {
|
|
76
88
|
connected: [];
|
|
@@ -84,6 +96,7 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
84
96
|
private options;
|
|
85
97
|
private emoteCache;
|
|
86
98
|
private userCache;
|
|
99
|
+
private badgeCache;
|
|
87
100
|
private ws;
|
|
88
101
|
private sessionId;
|
|
89
102
|
private keepaliveTimeoutMs;
|
|
@@ -95,8 +108,12 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
95
108
|
disconnect(): void;
|
|
96
109
|
preloadEmotes(): Promise<void>;
|
|
97
110
|
refreshEmotes(): Promise<void>;
|
|
111
|
+
preloadBadges(): Promise<void>;
|
|
112
|
+
getUser(userId: string): Promise<UserInfo | null>;
|
|
113
|
+
getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>>;
|
|
98
114
|
getProfilePictureUrl(userId: string): Promise<string | null>;
|
|
99
115
|
getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>>;
|
|
116
|
+
resolveBadge(setId: string, version: string): ResolvedBadge | undefined;
|
|
100
117
|
private _openConnection;
|
|
101
118
|
private _dispatch;
|
|
102
119
|
private _subscribe;
|
|
@@ -105,4 +122,4 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
105
122
|
private _closeWs;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
export { type Badge, type ChatUser, type MessageFragment, type NormalizedMessage, type ResolvedEmote, TwitchChat, type TwitchChatOptions, type
|
|
125
|
+
export { type Badge, type ChatUser, type MessageFragment, type NormalizedMessage, type ResolvedBadge, type ResolvedEmote, TwitchChat, type TwitchChatOptions, type UserInfo };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,10 +7,26 @@ interface TwitchChatOptions {
|
|
|
7
7
|
accessToken: string;
|
|
8
8
|
onTokenRefresh?: (newToken: string) => void;
|
|
9
9
|
}
|
|
10
|
+
interface UserInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
login: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
profileImageUrl: string;
|
|
15
|
+
broadcasterType: 'partner' | 'affiliate' | '';
|
|
16
|
+
description: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
interface ResolvedBadge {
|
|
20
|
+
title: string;
|
|
21
|
+
imageUrl1x: string;
|
|
22
|
+
imageUrl2x: string;
|
|
23
|
+
imageUrl4x: string;
|
|
24
|
+
}
|
|
10
25
|
interface Badge {
|
|
11
26
|
setId: string;
|
|
12
27
|
id: string;
|
|
13
28
|
info: string;
|
|
29
|
+
resolved?: ResolvedBadge;
|
|
14
30
|
}
|
|
15
31
|
interface ChatUser {
|
|
16
32
|
id: string;
|
|
@@ -67,10 +83,6 @@ interface NormalizedMessage {
|
|
|
67
83
|
};
|
|
68
84
|
channelPointsRewardId?: string;
|
|
69
85
|
}
|
|
70
|
-
interface UserProfile {
|
|
71
|
-
id: string;
|
|
72
|
-
profileImageUrl: string;
|
|
73
|
-
}
|
|
74
86
|
|
|
75
87
|
interface TwitchChatEvents {
|
|
76
88
|
connected: [];
|
|
@@ -84,6 +96,7 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
84
96
|
private options;
|
|
85
97
|
private emoteCache;
|
|
86
98
|
private userCache;
|
|
99
|
+
private badgeCache;
|
|
87
100
|
private ws;
|
|
88
101
|
private sessionId;
|
|
89
102
|
private keepaliveTimeoutMs;
|
|
@@ -95,8 +108,12 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
95
108
|
disconnect(): void;
|
|
96
109
|
preloadEmotes(): Promise<void>;
|
|
97
110
|
refreshEmotes(): Promise<void>;
|
|
111
|
+
preloadBadges(): Promise<void>;
|
|
112
|
+
getUser(userId: string): Promise<UserInfo | null>;
|
|
113
|
+
getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>>;
|
|
98
114
|
getProfilePictureUrl(userId: string): Promise<string | null>;
|
|
99
115
|
getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>>;
|
|
116
|
+
resolveBadge(setId: string, version: string): ResolvedBadge | undefined;
|
|
100
117
|
private _openConnection;
|
|
101
118
|
private _dispatch;
|
|
102
119
|
private _subscribe;
|
|
@@ -105,4 +122,4 @@ declare class TwitchChat extends EventEmitter<TwitchChatEvents> {
|
|
|
105
122
|
private _closeWs;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
export { type Badge, type ChatUser, type MessageFragment, type NormalizedMessage, type ResolvedEmote, TwitchChat, type TwitchChatOptions, type
|
|
125
|
+
export { type Badge, type ChatUser, type MessageFragment, type NormalizedMessage, type ResolvedBadge, type ResolvedEmote, TwitchChat, type TwitchChatOptions, type UserInfo };
|
package/dist/index.js
CHANGED
|
@@ -148,32 +148,44 @@ var UserCache = class {
|
|
|
148
148
|
this.getCredentials = getCredentials;
|
|
149
149
|
this.cache = /* @__PURE__ */ new Map();
|
|
150
150
|
}
|
|
151
|
-
async
|
|
152
|
-
const results = await this.
|
|
151
|
+
async getUser(userId) {
|
|
152
|
+
const results = await this.getUsers([userId]);
|
|
153
153
|
return results.get(userId) ?? null;
|
|
154
154
|
}
|
|
155
|
-
async
|
|
155
|
+
async getUsers(userIds) {
|
|
156
156
|
const now = Date.now();
|
|
157
157
|
const result = /* @__PURE__ */ new Map();
|
|
158
158
|
const toFetch = [];
|
|
159
159
|
for (const id of userIds) {
|
|
160
160
|
const entry = this.cache.get(id);
|
|
161
161
|
if (entry !== void 0 && entry.expiresAt > now) {
|
|
162
|
-
result.set(id, entry.
|
|
162
|
+
result.set(id, entry.user);
|
|
163
163
|
} else {
|
|
164
|
+
result.set(id, null);
|
|
164
165
|
toFetch.push(id);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
if (toFetch.length === 0) return result;
|
|
168
168
|
for (let i = 0; i < toFetch.length; i += 100) {
|
|
169
169
|
const chunk = toFetch.slice(i, i + 100);
|
|
170
170
|
const fetched = await this._fetchChunk(chunk, now);
|
|
171
|
-
for (const [id,
|
|
172
|
-
result.set(id,
|
|
171
|
+
for (const [id, user] of fetched) {
|
|
172
|
+
result.set(id, user);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
return result;
|
|
176
176
|
}
|
|
177
|
+
async getProfilePictureUrl(userId) {
|
|
178
|
+
const user = await this.getUser(userId);
|
|
179
|
+
return user?.profileImageUrl ?? null;
|
|
180
|
+
}
|
|
181
|
+
async getProfilePictureUrls(userIds) {
|
|
182
|
+
const users = await this.getUsers(userIds);
|
|
183
|
+
const result = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const [id, user] of users) {
|
|
185
|
+
if (user !== null) result.set(id, user.profileImageUrl);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
177
189
|
async _fetchChunk(ids, now) {
|
|
178
190
|
const params = new URLSearchParams();
|
|
179
191
|
for (const id of ids) params.append("id", id);
|
|
@@ -190,16 +202,69 @@ var UserCache = class {
|
|
|
190
202
|
const body = await res.json();
|
|
191
203
|
const result = /* @__PURE__ */ new Map();
|
|
192
204
|
const expiresAt = now + TTL_MS;
|
|
193
|
-
for (const
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
for (const u of body.data) {
|
|
206
|
+
const user = {
|
|
207
|
+
id: u.id,
|
|
208
|
+
login: u.login,
|
|
209
|
+
displayName: u.display_name,
|
|
210
|
+
profileImageUrl: u.profile_image_url,
|
|
211
|
+
broadcasterType: u.broadcaster_type,
|
|
212
|
+
description: u.description,
|
|
213
|
+
createdAt: u.created_at
|
|
214
|
+
};
|
|
215
|
+
this.cache.set(u.id, { user, expiresAt });
|
|
216
|
+
result.set(u.id, user);
|
|
196
217
|
}
|
|
197
218
|
return result;
|
|
198
219
|
}
|
|
199
220
|
};
|
|
200
221
|
|
|
222
|
+
// src/badges/index.ts
|
|
223
|
+
var HELIX_BADGES_GLOBAL = "https://api.twitch.tv/helix/chat/badges/global";
|
|
224
|
+
var HELIX_BADGES_CHANNEL = "https://api.twitch.tv/helix/chat/badges";
|
|
225
|
+
var BadgeCache = class {
|
|
226
|
+
constructor(channelId, getCredentials) {
|
|
227
|
+
this.channelId = channelId;
|
|
228
|
+
this.getCredentials = getCredentials;
|
|
229
|
+
// setId → version → ResolvedBadge
|
|
230
|
+
this.sets = /* @__PURE__ */ new Map();
|
|
231
|
+
}
|
|
232
|
+
async load() {
|
|
233
|
+
const { accessToken, clientId } = this.getCredentials();
|
|
234
|
+
const headers = {
|
|
235
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
236
|
+
"Client-Id": clientId
|
|
237
|
+
};
|
|
238
|
+
const [globalRes, channelRes] = await Promise.all([
|
|
239
|
+
fetch(HELIX_BADGES_GLOBAL, { headers }),
|
|
240
|
+
fetch(`${HELIX_BADGES_CHANNEL}?broadcaster_id=${this.channelId}`, { headers })
|
|
241
|
+
]);
|
|
242
|
+
if (!globalRes.ok) throw new Error(`Global badges fetch failed: ${globalRes.status}`);
|
|
243
|
+
if (!channelRes.ok) throw new Error(`Channel badges fetch failed: ${channelRes.status}`);
|
|
244
|
+
const [globalBody, channelBody] = await Promise.all([
|
|
245
|
+
globalRes.json(),
|
|
246
|
+
channelRes.json()
|
|
247
|
+
]);
|
|
248
|
+
for (const set of [...globalBody.data, ...channelBody.data]) {
|
|
249
|
+
const versionMap = this.sets.get(set.set_id) ?? /* @__PURE__ */ new Map();
|
|
250
|
+
for (const v of set.versions) {
|
|
251
|
+
versionMap.set(v.id, {
|
|
252
|
+
title: v.title,
|
|
253
|
+
imageUrl1x: v.image_url_1x,
|
|
254
|
+
imageUrl2x: v.image_url_2x,
|
|
255
|
+
imageUrl4x: v.image_url_4x
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
this.sets.set(set.set_id, versionMap);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
resolve(setId, version) {
|
|
262
|
+
return this.sets.get(setId)?.get(version);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
201
266
|
// src/normalizer.ts
|
|
202
|
-
function normalizeMessage(event, emoteCache) {
|
|
267
|
+
function normalizeMessage(event, emoteCache, resolveBadge) {
|
|
203
268
|
const emotes = [];
|
|
204
269
|
const fragments = [];
|
|
205
270
|
for (const frag of event.message.fragments) {
|
|
@@ -262,11 +327,15 @@ function normalizeMessage(event, emoteCache) {
|
|
|
262
327
|
}
|
|
263
328
|
}
|
|
264
329
|
}
|
|
265
|
-
const badges = event.badges.map((b) =>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
330
|
+
const badges = event.badges.map((b) => {
|
|
331
|
+
const resolved = resolveBadge?.(b.set_id, b.id);
|
|
332
|
+
return {
|
|
333
|
+
setId: b.set_id,
|
|
334
|
+
id: b.id,
|
|
335
|
+
info: b.info,
|
|
336
|
+
...resolved !== void 0 && { resolved }
|
|
337
|
+
};
|
|
338
|
+
});
|
|
270
339
|
const badgeSetIds = new Set(badges.map((b) => b.setId));
|
|
271
340
|
const msg = {
|
|
272
341
|
id: event.message_id,
|
|
@@ -329,6 +398,10 @@ var TwitchChat = class extends EventEmitter {
|
|
|
329
398
|
accessToken: this.options.accessToken,
|
|
330
399
|
clientId: this.options.clientId
|
|
331
400
|
}));
|
|
401
|
+
this.badgeCache = new BadgeCache(options.channelId, () => ({
|
|
402
|
+
accessToken: this.options.accessToken,
|
|
403
|
+
clientId: this.options.clientId
|
|
404
|
+
}));
|
|
332
405
|
}
|
|
333
406
|
// ---------------------------------------------------------------------------
|
|
334
407
|
// Public API
|
|
@@ -350,6 +423,15 @@ var TwitchChat = class extends EventEmitter {
|
|
|
350
423
|
async refreshEmotes() {
|
|
351
424
|
await this.emoteCache.load();
|
|
352
425
|
}
|
|
426
|
+
async preloadBadges() {
|
|
427
|
+
await this.badgeCache.load();
|
|
428
|
+
}
|
|
429
|
+
async getUser(userId) {
|
|
430
|
+
return this.userCache.getUser(userId);
|
|
431
|
+
}
|
|
432
|
+
async getUsers(userIds) {
|
|
433
|
+
return this.userCache.getUsers(userIds);
|
|
434
|
+
}
|
|
353
435
|
async getProfilePictureUrl(userId) {
|
|
354
436
|
return this.userCache.getProfilePictureUrl(userId);
|
|
355
437
|
}
|
|
@@ -361,6 +443,9 @@ var TwitchChat = class extends EventEmitter {
|
|
|
361
443
|
}
|
|
362
444
|
return result;
|
|
363
445
|
}
|
|
446
|
+
resolveBadge(setId, version) {
|
|
447
|
+
return this.badgeCache.resolve(setId, version);
|
|
448
|
+
}
|
|
364
449
|
// ---------------------------------------------------------------------------
|
|
365
450
|
// Connection
|
|
366
451
|
// ---------------------------------------------------------------------------
|
|
@@ -447,7 +532,11 @@ var TwitchChat = class extends EventEmitter {
|
|
|
447
532
|
const payload = msg.payload;
|
|
448
533
|
if (payload.subscription.type === "channel.chat.message") {
|
|
449
534
|
try {
|
|
450
|
-
const normalized = normalizeMessage(
|
|
535
|
+
const normalized = normalizeMessage(
|
|
536
|
+
payload.event,
|
|
537
|
+
this.emoteCache,
|
|
538
|
+
(setId, version) => this.badgeCache.resolve(setId, version)
|
|
539
|
+
);
|
|
451
540
|
this.emit("message", normalized);
|
|
452
541
|
} catch (e) {
|
|
453
542
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/emotes/twitch.ts","../src/emotes/bttv.ts","../src/emotes/7tv.ts","../src/emotes/index.ts","../src/users/index.ts","../src/normalizer.ts"],"sourcesContent":["import EventEmitter from 'eventemitter3'\nimport type {\n TwitchChatOptions,\n NormalizedMessage,\n TwitchEventSubMessage,\n TwitchWelcomePayload,\n TwitchNotificationPayload,\n TwitchReconnectPayload,\n TwitchRevocationPayload,\n} from './types.js'\nimport { EmoteCache } from './emotes/index.js'\nimport { UserCache } from './users/index.js'\nimport { normalizeMessage } from './normalizer.js'\n\nconst EVENTSUB_URL = 'wss://eventsub.wss.twitch.tv/ws'\nconst HELIX_SUBSCRIPTIONS = 'https://api.twitch.tv/helix/eventsub/subscriptions'\n\n// Minimal interface covering both `ws` WebSocket and browser WebSocket.\ninterface WSLike {\n close(code?: number, reason?: string): void\n addEventListener(type: 'open', listener: () => void): void\n addEventListener(type: 'message', listener: (event: { data: string }) => void): void\n addEventListener(type: 'close', listener: (event: { code: number; reason: string | Buffer }) => void): void\n addEventListener(type: 'error', listener: (event: unknown) => void): void\n}\n\nfunction createWebSocket(url: string): WSLike {\n if (typeof WebSocket !== 'undefined') {\n return new WebSocket(url) as unknown as WSLike\n }\n // Node.js — require ws at runtime (peer dep)\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('ws') as { default?: new (url: string) => WSLike } & (new (url: string) => WSLike)\n const WsImpl = mod.default ?? mod\n return new WsImpl(url)\n}\n\ninterface TwitchChatEvents {\n connected: []\n disconnected: [code: number, reason: string]\n message: [msg: NormalizedMessage]\n revoked: [reason: string]\n auth_error: []\n error: [err: Error]\n}\n\nexport class TwitchChat extends EventEmitter<TwitchChatEvents> {\n private options: TwitchChatOptions\n private emoteCache: EmoteCache\n private userCache: UserCache\n\n private ws: WSLike | null = null\n private sessionId: string | null = null\n private keepaliveTimeoutMs = 10_000\n private keepaliveTimer: ReturnType<typeof setTimeout> | null = null\n\n // Holds the old ws during a session_reconnect handoff\n private oldWs: WSLike | null = null\n\n private stopped = false\n\n constructor(options: TwitchChatOptions) {\n super()\n this.options = options\n this.emoteCache = new EmoteCache(options.channelId)\n this.userCache = new UserCache(() => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n async connect(): Promise<void> {\n this.stopped = false\n await this._openConnection(EVENTSUB_URL, false)\n }\n\n disconnect(): void {\n this.stopped = true\n this._clearKeepaliveTimer()\n this._closeWs(this.ws, 1000, 'disconnect')\n this.ws = null\n this.sessionId = null\n }\n\n async preloadEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async refreshEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n return this.userCache.getProfilePictureUrl(userId)\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>> {\n const found = await this.userCache.getProfilePictureUrls(userIds)\n const result = new Map<string, string | null>()\n for (const id of userIds) {\n result.set(id, found.get(id) ?? null)\n }\n return result\n }\n\n // ---------------------------------------------------------------------------\n // Connection\n // ---------------------------------------------------------------------------\n\n private _openConnection(url: string, isReconnect: boolean): Promise<void> {\n return new Promise((resolve, reject) => {\n const ws = createWebSocket(url)\n let settled = false\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true\n fn()\n }\n }\n\n ws.addEventListener('message', (event) => {\n const raw = typeof event.data === 'string' ? event.data : String(event.data)\n let msg: TwitchEventSubMessage\n try {\n msg = JSON.parse(raw) as TwitchEventSubMessage\n } catch (e) {\n this.emit('error', new Error(`Failed to parse WS message: ${String(e)}`))\n return\n }\n this._dispatch(msg, ws, isReconnect, settle, resolve, reject)\n })\n\n ws.addEventListener('close', (event) => {\n const code = event.code\n const reason = typeof event.reason === 'string' ? event.reason : event.reason.toString()\n\n this._clearKeepaliveTimer()\n\n if (!settled) {\n settle(() => reject(new Error(`WebSocket closed before welcome: ${code} ${reason}`)))\n return\n }\n\n if (ws !== this.ws) return // this was an old ws that got closed; ignore\n\n this.emit('disconnected', code, reason)\n\n if (!this.stopped && code !== 1000) {\n setTimeout(() => {\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, 2_000)\n }\n })\n\n ws.addEventListener('error', (err) => {\n const error = err instanceof Error ? err : new Error('WebSocket error')\n if (!settled) {\n settle(() => reject(error))\n } else {\n this.emit('error', error)\n }\n })\n })\n }\n\n private _dispatch(\n msg: TwitchEventSubMessage,\n ws: WSLike,\n isReconnect: boolean,\n settle: (fn: () => void) => void,\n resolve: () => void,\n reject: (err: Error) => void,\n ): void {\n switch (msg.metadata.message_type) {\n case 'session_welcome': {\n const payload = msg.payload as TwitchWelcomePayload\n this.sessionId = payload.session.id\n this.keepaliveTimeoutMs = payload.session.keepalive_timeout_seconds * 1_000\n this._resetKeepaliveTimer()\n\n if (isReconnect) {\n // Subscriptions carry over — no need to re-POST.\n // Close the old connection now that the new one is ready.\n this._closeWs(this.oldWs, 1000, 'reconnected')\n this.oldWs = null\n this.ws = ws\n settle(() => resolve())\n break\n }\n\n this.ws = ws\n this._subscribe()\n .then(() => {\n settle(() => resolve())\n this.emit('connected')\n })\n .catch(err => {\n settle(() => reject(err instanceof Error ? err : new Error(String(err))))\n })\n break\n }\n\n case 'session_keepalive': {\n this._resetKeepaliveTimer()\n break\n }\n\n case 'notification': {\n this._resetKeepaliveTimer()\n const payload = msg.payload as TwitchNotificationPayload\n if (payload.subscription.type === 'channel.chat.message') {\n try {\n const normalized = normalizeMessage(payload.event, this.emoteCache)\n this.emit('message', normalized)\n } catch (e) {\n this.emit('error', e instanceof Error ? e : new Error(String(e)))\n }\n }\n break\n }\n\n case 'session_reconnect': {\n const payload = msg.payload as TwitchReconnectPayload\n // Keep current ws open until new one sends session_welcome\n this.oldWs = this.ws\n this._openConnection(payload.session.reconnect_url, true).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n break\n }\n\n case 'revocation': {\n const payload = msg.payload as TwitchRevocationPayload\n this.emit('revoked', payload.subscription.status)\n break\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helix subscription\n // ---------------------------------------------------------------------------\n\n private async _subscribe(): Promise<void> {\n if (!this.sessionId) throw new Error('No session ID')\n\n const res = await fetch(HELIX_SUBSCRIPTIONS, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.options.accessToken}`,\n 'Client-Id': this.options.clientId,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: 'channel.chat.message',\n version: '1',\n condition: {\n broadcaster_user_id: this.options.channelId,\n user_id: this.options.userId,\n },\n transport: {\n method: 'websocket',\n session_id: this.sessionId,\n },\n }),\n })\n\n if (res.status === 401) {\n this.emit('auth_error')\n throw new Error('Auth error subscribing to EventSub')\n }\n\n if (!res.ok) {\n const body = await res.text()\n throw new Error(`EventSub subscription failed: ${res.status} ${body}`)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Keepalive timer\n // ---------------------------------------------------------------------------\n\n private _resetKeepaliveTimer(): void {\n this._clearKeepaliveTimer()\n this.keepaliveTimer = setTimeout(() => {\n this._closeWs(this.ws, 1001, 'keepalive timeout')\n this.ws = null\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, this.keepaliveTimeoutMs + 500)\n }\n\n private _clearKeepaliveTimer(): void {\n if (this.keepaliveTimer !== null) {\n clearTimeout(this.keepaliveTimer)\n this.keepaliveTimer = null\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private _closeWs(ws: WSLike | null, code: number, reason: string): void {\n if (!ws) return\n try {\n ws.close(code, reason)\n } catch {\n // ignore\n }\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst CDN = 'https://static-cdn.jtvnw.net/emoticons/v2'\n\nexport function buildTwitchEmote(id: string, name: string): ResolvedEmote {\n return {\n id,\n name,\n source: 'twitch',\n animated: false,\n imageUrl1x: `${CDN}/${id}/default/dark/1.0`,\n imageUrl2x: `${CDN}/${id}/default/dark/2.0`,\n imageUrl3x: `${CDN}/${id}/default/dark/3.0`,\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://api.betterttv.net/3'\nconst CDN = 'https://cdn.betterttv.net/emote'\n\ninterface BttvEmote {\n id: string\n code: string\n imageType: string\n animated: boolean\n}\n\ninterface BttvChannelResponse {\n channelEmotes: BttvEmote[]\n sharedEmotes: BttvEmote[]\n}\n\nfunction parseEmote(e: BttvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.code,\n source: 'bttv',\n animated: e.animated,\n imageUrl1x: `${CDN}/${e.id}/1x`,\n imageUrl2x: `${CDN}/${e.id}/2x`,\n imageUrl3x: `${CDN}/${e.id}/3x`,\n }\n}\n\nexport async function fetchBttvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/emotes/global`)\n if (!res.ok) throw new Error(`BTTV global fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvEmote[]\n const map = new Map<string, ResolvedEmote>()\n for (const e of data) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n\nexport async function fetchBttvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`BTTV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of [...data.channelEmotes, ...data.sharedEmotes]) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://7tv.io/v3'\nconst CDN = 'https://cdn.7tv.app/emote'\n\ninterface SevenTvFile {\n name: string\n static_name: string\n width: number\n height: number\n frame_count: number\n size: number\n format: string\n}\n\ninterface SevenTvEmoteData {\n host: {\n url: string\n files: SevenTvFile[]\n }\n animated: boolean\n}\n\ninterface SevenTvEmote {\n id: string\n name: string\n data: SevenTvEmoteData\n}\n\ninterface SevenTvEmoteSet {\n emotes: SevenTvEmote[]\n}\n\ninterface SevenTvChannelResponse {\n emote_set: SevenTvEmoteSet\n}\n\nfunction parseEmote(e: SevenTvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.name,\n source: '7tv',\n animated: e.data.animated,\n imageUrl1x: `${CDN}/${e.id}/1x.webp`,\n imageUrl2x: `${CDN}/${e.id}/2x.webp`,\n imageUrl3x: `${CDN}/${e.id}/3x.webp`,\n }\n}\n\nexport async function fetch7tvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/emote-sets/global`)\n if (!res.ok) throw new Error(`7TV global fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvEmoteSet\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n\nexport async function fetch7tvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`7TV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emote_set.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\nimport { buildTwitchEmote } from './twitch.js'\nimport { fetchBttvGlobal, fetchBttvChannel } from './bttv.js'\nimport { fetch7tvGlobal, fetch7tvChannel } from './7tv.js'\n\nexport class EmoteCache {\n private bttvGlobal = new Map<string, ResolvedEmote>()\n private bttvChannel = new Map<string, ResolvedEmote>()\n private sevenTvGlobal = new Map<string, ResolvedEmote>()\n private sevenTvChannel = new Map<string, ResolvedEmote>()\n\n constructor(private readonly channelId: string) {}\n\n async load(): Promise<void> {\n const results = await Promise.allSettled([\n fetchBttvGlobal().then(m => { this.bttvGlobal = m }),\n fetchBttvChannel(this.channelId).then(m => { this.bttvChannel = m }),\n fetch7tvGlobal().then(m => { this.sevenTvGlobal = m }),\n fetch7tvChannel(this.channelId).then(m => { this.sevenTvChannel = m }),\n ])\n\n for (const result of results) {\n if (result.status === 'rejected') {\n console.warn('[twitch-integration] emote fetch error:', result.reason)\n }\n }\n }\n\n /**\n * Resolve a third-party emote by name.\n * Priority: 7TV channel > BTTV channel > 7TV global > BTTV global\n *\n * Twitch emotes are resolved separately via resolveFromFragment(), since their\n * IDs come directly from message fragments — no lookup table needed.\n */\n resolveByName(name: string): ResolvedEmote | undefined {\n return (\n this.sevenTvChannel.get(name) ??\n this.bttvChannel.get(name) ??\n this.sevenTvGlobal.get(name) ??\n this.bttvGlobal.get(name)\n )\n }\n\n /**\n * Resolve a Twitch native emote from fragment data.\n */\n resolveTwitch(id: string, name: string): ResolvedEmote {\n return buildTwitchEmote(id, name)\n }\n}\n","interface HelixUser {\n id: string\n login: string\n display_name: string\n profile_image_url: string\n}\n\ninterface CacheEntry {\n url: string\n expiresAt: number\n}\n\nconst HELIX_USERS = 'https://api.twitch.tv/helix/users'\nconst TTL_MS = 5 * 60 * 1_000\n\nexport class UserCache {\n private cache = new Map<string, CacheEntry>()\n\n constructor(\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n const results = await this.getProfilePictureUrls([userId])\n return results.get(userId) ?? null\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string>> {\n const now = Date.now()\n const result = new Map<string, string>()\n const toFetch: string[] = []\n\n for (const id of userIds) {\n const entry = this.cache.get(id)\n if (entry !== undefined && entry.expiresAt > now) {\n result.set(id, entry.url)\n } else {\n toFetch.push(id)\n }\n }\n\n if (toFetch.length === 0) return result\n\n for (let i = 0; i < toFetch.length; i += 100) {\n const chunk = toFetch.slice(i, i + 100)\n const fetched = await this._fetchChunk(chunk, now)\n for (const [id, url] of fetched) {\n result.set(id, url)\n }\n }\n\n return result\n }\n\n private async _fetchChunk(ids: string[], now: number): Promise<Map<string, string>> {\n const params = new URLSearchParams()\n for (const id of ids) params.append('id', id)\n\n const { accessToken, clientId } = this.getCredentials()\n const res = await fetch(`${HELIX_USERS}?${params.toString()}`, {\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n },\n })\n\n if (!res.ok) {\n throw new Error(`Helix users fetch failed: ${res.status}`)\n }\n\n const body = (await res.json()) as { data: HelixUser[] }\n const result = new Map<string, string>()\n const expiresAt = now + TTL_MS\n\n for (const user of body.data) {\n this.cache.set(user.id, { url: user.profile_image_url, expiresAt })\n result.set(user.id, user.profile_image_url)\n }\n\n return result\n }\n}\n","import type {\n TwitchChatMessageEvent,\n NormalizedMessage,\n MessageFragment,\n ResolvedEmote,\n} from './types.js'\nimport type { EmoteCache } from './emotes/index.js'\n\nexport function normalizeMessage(\n event: TwitchChatMessageEvent,\n emoteCache: EmoteCache,\n): NormalizedMessage {\n const emotes: ResolvedEmote[] = []\n const fragments: MessageFragment[] = []\n\n for (const frag of event.message.fragments) {\n switch (frag.type) {\n case 'text': {\n // A \"text\" fragment may contain third-party emote names.\n // Split on whitespace and check each token against the emote cache.\n const tokens = frag.text.split(/(\\s+)/)\n let pendingText = ''\n\n for (const token of tokens) {\n if (/^\\s+$/.test(token)) {\n pendingText += token\n continue\n }\n const resolved = emoteCache.resolveByName(token)\n if (resolved) {\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n pendingText = ''\n }\n fragments.push({ type: 'emote', text: token, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n } else {\n pendingText += token\n }\n }\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n }\n break\n }\n\n case 'emote': {\n const emoteData = frag.emote!\n const resolved = emoteCache.resolveTwitch(emoteData.id, frag.text)\n fragments.push({ type: 'emote', text: frag.text, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n break\n }\n\n case 'cheermote': {\n const cheer = frag.cheermote!\n fragments.push({\n type: 'cheermote',\n text: frag.text,\n bits: cheer.bits,\n tier: cheer.tier,\n })\n break\n }\n\n case 'mention': {\n const mention = frag.mention!\n fragments.push({\n type: 'mention',\n text: frag.text,\n userId: mention.user_id,\n userLogin: mention.user_login,\n })\n break\n }\n }\n }\n\n const badges = event.badges.map(b => ({\n setId: b.set_id,\n id: b.id,\n info: b.info,\n }))\n\n const badgeSetIds = new Set(badges.map(b => b.setId))\n\n const msg: NormalizedMessage = {\n id: event.message_id,\n text: event.message.text,\n user: {\n id: event.chatter_user_id,\n login: event.chatter_user_login,\n displayName: event.chatter_user_name,\n color: event.color,\n badges,\n isModerator: badgeSetIds.has('moderator'),\n isSubscriber: badgeSetIds.has('subscriber'),\n isBroadcaster: badgeSetIds.has('broadcaster'),\n isVip: badgeSetIds.has('vip'),\n },\n fragments,\n emotes,\n timestamp: event.timestamp,\n }\n\n if (event.cheer != null) {\n msg.cheer = { bits: event.cheer.bits }\n }\n\n if (event.reply != null) {\n msg.reply = {\n parentMessageId: event.reply.parent_message_id,\n parentUserLogin: event.reply.parent_user_login,\n parentUserDisplayName: event.reply.parent_user_display_name,\n }\n }\n\n if (event.channel_points_custom_reward_id != null) {\n msg.channelPointsRewardId = event.channel_points_custom_reward_id\n }\n\n return msg\n}\n"],"mappings":"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","names":["CDN","API","CDN","parseEmote"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/emotes/twitch.ts","../src/emotes/bttv.ts","../src/emotes/7tv.ts","../src/emotes/index.ts","../src/users/index.ts","../src/badges/index.ts","../src/normalizer.ts"],"sourcesContent":["import EventEmitter from 'eventemitter3'\nimport type {\n TwitchChatOptions,\n NormalizedMessage,\n TwitchEventSubMessage,\n TwitchWelcomePayload,\n TwitchNotificationPayload,\n TwitchReconnectPayload,\n TwitchRevocationPayload,\n UserInfo,\n ResolvedBadge,\n} from './types.js'\nimport { EmoteCache } from './emotes/index.js'\nimport { UserCache } from './users/index.js'\nimport { BadgeCache } from './badges/index.js'\nimport { normalizeMessage } from './normalizer.js'\n\nconst EVENTSUB_URL = 'wss://eventsub.wss.twitch.tv/ws'\nconst HELIX_SUBSCRIPTIONS = 'https://api.twitch.tv/helix/eventsub/subscriptions'\n\n// Minimal interface covering both `ws` WebSocket and browser WebSocket.\ninterface WSLike {\n close(code?: number, reason?: string): void\n addEventListener(type: 'open', listener: () => void): void\n addEventListener(type: 'message', listener: (event: { data: string }) => void): void\n addEventListener(type: 'close', listener: (event: { code: number; reason: string | Buffer }) => void): void\n addEventListener(type: 'error', listener: (event: unknown) => void): void\n}\n\nfunction createWebSocket(url: string): WSLike {\n if (typeof WebSocket !== 'undefined') {\n return new WebSocket(url) as unknown as WSLike\n }\n // Node.js — require ws at runtime (peer dep)\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require('ws') as { default?: new (url: string) => WSLike } & (new (url: string) => WSLike)\n const WsImpl = mod.default ?? mod\n return new WsImpl(url)\n}\n\ninterface TwitchChatEvents {\n connected: []\n disconnected: [code: number, reason: string]\n message: [msg: NormalizedMessage]\n revoked: [reason: string]\n auth_error: []\n error: [err: Error]\n}\n\nexport class TwitchChat extends EventEmitter<TwitchChatEvents> {\n private options: TwitchChatOptions\n private emoteCache: EmoteCache\n private userCache: UserCache\n private badgeCache: BadgeCache\n\n private ws: WSLike | null = null\n private sessionId: string | null = null\n private keepaliveTimeoutMs = 10_000\n private keepaliveTimer: ReturnType<typeof setTimeout> | null = null\n\n // Holds the old ws during a session_reconnect handoff\n private oldWs: WSLike | null = null\n\n private stopped = false\n\n constructor(options: TwitchChatOptions) {\n super()\n this.options = options\n this.emoteCache = new EmoteCache(options.channelId)\n this.userCache = new UserCache(() => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n this.badgeCache = new BadgeCache(options.channelId, () => ({\n accessToken: this.options.accessToken,\n clientId: this.options.clientId,\n }))\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n async connect(): Promise<void> {\n this.stopped = false\n await this._openConnection(EVENTSUB_URL, false)\n }\n\n disconnect(): void {\n this.stopped = true\n this._clearKeepaliveTimer()\n this._closeWs(this.ws, 1000, 'disconnect')\n this.ws = null\n this.sessionId = null\n }\n\n async preloadEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async refreshEmotes(): Promise<void> {\n await this.emoteCache.load()\n }\n\n async preloadBadges(): Promise<void> {\n await this.badgeCache.load()\n }\n\n async getUser(userId: string): Promise<UserInfo | null> {\n return this.userCache.getUser(userId)\n }\n\n async getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>> {\n return this.userCache.getUsers(userIds)\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n return this.userCache.getProfilePictureUrl(userId)\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string | null>> {\n const found = await this.userCache.getProfilePictureUrls(userIds)\n const result = new Map<string, string | null>()\n for (const id of userIds) {\n result.set(id, found.get(id) ?? null)\n }\n return result\n }\n\n resolveBadge(setId: string, version: string): ResolvedBadge | undefined {\n return this.badgeCache.resolve(setId, version)\n }\n\n // ---------------------------------------------------------------------------\n // Connection\n // ---------------------------------------------------------------------------\n\n private _openConnection(url: string, isReconnect: boolean): Promise<void> {\n return new Promise((resolve, reject) => {\n const ws = createWebSocket(url)\n let settled = false\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true\n fn()\n }\n }\n\n ws.addEventListener('message', (event) => {\n const raw = typeof event.data === 'string' ? event.data : String(event.data)\n let msg: TwitchEventSubMessage\n try {\n msg = JSON.parse(raw) as TwitchEventSubMessage\n } catch (e) {\n this.emit('error', new Error(`Failed to parse WS message: ${String(e)}`))\n return\n }\n this._dispatch(msg, ws, isReconnect, settle, resolve, reject)\n })\n\n ws.addEventListener('close', (event) => {\n const code = event.code\n const reason = typeof event.reason === 'string' ? event.reason : event.reason.toString()\n\n this._clearKeepaliveTimer()\n\n if (!settled) {\n settle(() => reject(new Error(`WebSocket closed before welcome: ${code} ${reason}`)))\n return\n }\n\n if (ws !== this.ws) return // this was an old ws that got closed; ignore\n\n this.emit('disconnected', code, reason)\n\n if (!this.stopped && code !== 1000) {\n setTimeout(() => {\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, 2_000)\n }\n })\n\n ws.addEventListener('error', (err) => {\n const error = err instanceof Error ? err : new Error('WebSocket error')\n if (!settled) {\n settle(() => reject(error))\n } else {\n this.emit('error', error)\n }\n })\n })\n }\n\n private _dispatch(\n msg: TwitchEventSubMessage,\n ws: WSLike,\n isReconnect: boolean,\n settle: (fn: () => void) => void,\n resolve: () => void,\n reject: (err: Error) => void,\n ): void {\n switch (msg.metadata.message_type) {\n case 'session_welcome': {\n const payload = msg.payload as TwitchWelcomePayload\n this.sessionId = payload.session.id\n this.keepaliveTimeoutMs = payload.session.keepalive_timeout_seconds * 1_000\n this._resetKeepaliveTimer()\n\n if (isReconnect) {\n // Subscriptions carry over — no need to re-POST.\n // Close the old connection now that the new one is ready.\n this._closeWs(this.oldWs, 1000, 'reconnected')\n this.oldWs = null\n this.ws = ws\n settle(() => resolve())\n break\n }\n\n this.ws = ws\n this._subscribe()\n .then(() => {\n settle(() => resolve())\n this.emit('connected')\n })\n .catch(err => {\n settle(() => reject(err instanceof Error ? err : new Error(String(err))))\n })\n break\n }\n\n case 'session_keepalive': {\n this._resetKeepaliveTimer()\n break\n }\n\n case 'notification': {\n this._resetKeepaliveTimer()\n const payload = msg.payload as TwitchNotificationPayload\n if (payload.subscription.type === 'channel.chat.message') {\n try {\n const normalized = normalizeMessage(\n payload.event,\n this.emoteCache,\n (setId, version) => this.badgeCache.resolve(setId, version),\n )\n this.emit('message', normalized)\n } catch (e) {\n this.emit('error', e instanceof Error ? e : new Error(String(e)))\n }\n }\n break\n }\n\n case 'session_reconnect': {\n const payload = msg.payload as TwitchReconnectPayload\n // Keep current ws open until new one sends session_welcome\n this.oldWs = this.ws\n this._openConnection(payload.session.reconnect_url, true).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n break\n }\n\n case 'revocation': {\n const payload = msg.payload as TwitchRevocationPayload\n this.emit('revoked', payload.subscription.status)\n break\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helix subscription\n // ---------------------------------------------------------------------------\n\n private async _subscribe(): Promise<void> {\n if (!this.sessionId) throw new Error('No session ID')\n\n const res = await fetch(HELIX_SUBSCRIPTIONS, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.options.accessToken}`,\n 'Client-Id': this.options.clientId,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: 'channel.chat.message',\n version: '1',\n condition: {\n broadcaster_user_id: this.options.channelId,\n user_id: this.options.userId,\n },\n transport: {\n method: 'websocket',\n session_id: this.sessionId,\n },\n }),\n })\n\n if (res.status === 401) {\n this.emit('auth_error')\n throw new Error('Auth error subscribing to EventSub')\n }\n\n if (!res.ok) {\n const body = await res.text()\n throw new Error(`EventSub subscription failed: ${res.status} ${body}`)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Keepalive timer\n // ---------------------------------------------------------------------------\n\n private _resetKeepaliveTimer(): void {\n this._clearKeepaliveTimer()\n this.keepaliveTimer = setTimeout(() => {\n this._closeWs(this.ws, 1001, 'keepalive timeout')\n this.ws = null\n if (!this.stopped) {\n this._openConnection(EVENTSUB_URL, false).catch(err => {\n this.emit('error', err instanceof Error ? err : new Error(String(err)))\n })\n }\n }, this.keepaliveTimeoutMs + 500)\n }\n\n private _clearKeepaliveTimer(): void {\n if (this.keepaliveTimer !== null) {\n clearTimeout(this.keepaliveTimer)\n this.keepaliveTimer = null\n }\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private _closeWs(ws: WSLike | null, code: number, reason: string): void {\n if (!ws) return\n try {\n ws.close(code, reason)\n } catch {\n // ignore\n }\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst CDN = 'https://static-cdn.jtvnw.net/emoticons/v2'\n\nexport function buildTwitchEmote(id: string, name: string): ResolvedEmote {\n return {\n id,\n name,\n source: 'twitch',\n animated: false,\n imageUrl1x: `${CDN}/${id}/default/dark/1.0`,\n imageUrl2x: `${CDN}/${id}/default/dark/2.0`,\n imageUrl3x: `${CDN}/${id}/default/dark/3.0`,\n }\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://api.betterttv.net/3'\nconst CDN = 'https://cdn.betterttv.net/emote'\n\ninterface BttvEmote {\n id: string\n code: string\n imageType: string\n animated: boolean\n}\n\ninterface BttvChannelResponse {\n channelEmotes: BttvEmote[]\n sharedEmotes: BttvEmote[]\n}\n\nfunction parseEmote(e: BttvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.code,\n source: 'bttv',\n animated: e.animated,\n imageUrl1x: `${CDN}/${e.id}/1x`,\n imageUrl2x: `${CDN}/${e.id}/2x`,\n imageUrl3x: `${CDN}/${e.id}/3x`,\n }\n}\n\nexport async function fetchBttvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/emotes/global`)\n if (!res.ok) throw new Error(`BTTV global fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvEmote[]\n const map = new Map<string, ResolvedEmote>()\n for (const e of data) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n\nexport async function fetchBttvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/cached/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`BTTV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as BttvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of [...data.channelEmotes, ...data.sharedEmotes]) {\n map.set(e.code, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\n\nconst API = 'https://7tv.io/v3'\nconst CDN = 'https://cdn.7tv.app/emote'\n\ninterface SevenTvFile {\n name: string\n static_name: string\n width: number\n height: number\n frame_count: number\n size: number\n format: string\n}\n\ninterface SevenTvEmoteData {\n host: {\n url: string\n files: SevenTvFile[]\n }\n animated: boolean\n}\n\ninterface SevenTvEmote {\n id: string\n name: string\n data: SevenTvEmoteData\n}\n\ninterface SevenTvEmoteSet {\n emotes: SevenTvEmote[]\n}\n\ninterface SevenTvChannelResponse {\n emote_set: SevenTvEmoteSet\n}\n\nfunction parseEmote(e: SevenTvEmote): ResolvedEmote {\n return {\n id: e.id,\n name: e.name,\n source: '7tv',\n animated: e.data.animated,\n imageUrl1x: `${CDN}/${e.id}/1x.webp`,\n imageUrl2x: `${CDN}/${e.id}/2x.webp`,\n imageUrl3x: `${CDN}/${e.id}/3x.webp`,\n }\n}\n\nexport async function fetch7tvGlobal(): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/emote-sets/global`)\n if (!res.ok) throw new Error(`7TV global fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvEmoteSet\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n\nexport async function fetch7tvChannel(channelId: string): Promise<Map<string, ResolvedEmote>> {\n const res = await fetch(`${API}/users/twitch/${channelId}`)\n if (!res.ok) throw new Error(`7TV channel fetch failed: ${res.status}`)\n const data = (await res.json()) as SevenTvChannelResponse\n const map = new Map<string, ResolvedEmote>()\n for (const e of data.emote_set.emotes) {\n map.set(e.name, parseEmote(e))\n }\n return map\n}\n","import type { ResolvedEmote } from '../types.js'\nimport { buildTwitchEmote } from './twitch.js'\nimport { fetchBttvGlobal, fetchBttvChannel } from './bttv.js'\nimport { fetch7tvGlobal, fetch7tvChannel } from './7tv.js'\n\nexport class EmoteCache {\n private bttvGlobal = new Map<string, ResolvedEmote>()\n private bttvChannel = new Map<string, ResolvedEmote>()\n private sevenTvGlobal = new Map<string, ResolvedEmote>()\n private sevenTvChannel = new Map<string, ResolvedEmote>()\n\n constructor(private readonly channelId: string) {}\n\n async load(): Promise<void> {\n const results = await Promise.allSettled([\n fetchBttvGlobal().then(m => { this.bttvGlobal = m }),\n fetchBttvChannel(this.channelId).then(m => { this.bttvChannel = m }),\n fetch7tvGlobal().then(m => { this.sevenTvGlobal = m }),\n fetch7tvChannel(this.channelId).then(m => { this.sevenTvChannel = m }),\n ])\n\n for (const result of results) {\n if (result.status === 'rejected') {\n console.warn('[twitch-integration] emote fetch error:', result.reason)\n }\n }\n }\n\n /**\n * Resolve a third-party emote by name.\n * Priority: 7TV channel > BTTV channel > 7TV global > BTTV global\n *\n * Twitch emotes are resolved separately via resolveFromFragment(), since their\n * IDs come directly from message fragments — no lookup table needed.\n */\n resolveByName(name: string): ResolvedEmote | undefined {\n return (\n this.sevenTvChannel.get(name) ??\n this.bttvChannel.get(name) ??\n this.sevenTvGlobal.get(name) ??\n this.bttvGlobal.get(name)\n )\n }\n\n /**\n * Resolve a Twitch native emote from fragment data.\n */\n resolveTwitch(id: string, name: string): ResolvedEmote {\n return buildTwitchEmote(id, name)\n }\n}\n","import type { UserInfo } from '../types.js'\n\ninterface HelixUser {\n id: string\n login: string\n display_name: string\n profile_image_url: string\n broadcaster_type: 'partner' | 'affiliate' | ''\n description: string\n created_at: string\n}\n\ninterface CacheEntry {\n user: UserInfo\n expiresAt: number\n}\n\nconst HELIX_USERS = 'https://api.twitch.tv/helix/users'\nconst TTL_MS = 5 * 60 * 1_000\n\nexport class UserCache {\n private cache = new Map<string, CacheEntry>()\n\n constructor(\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async getUser(userId: string): Promise<UserInfo | null> {\n const results = await this.getUsers([userId])\n return results.get(userId) ?? null\n }\n\n async getUsers(userIds: string[]): Promise<Map<string, UserInfo | null>> {\n const now = Date.now()\n const result = new Map<string, UserInfo | null>()\n const toFetch: string[] = []\n\n for (const id of userIds) {\n const entry = this.cache.get(id)\n if (entry !== undefined && entry.expiresAt > now) {\n result.set(id, entry.user)\n } else {\n result.set(id, null)\n toFetch.push(id)\n }\n }\n\n for (let i = 0; i < toFetch.length; i += 100) {\n const chunk = toFetch.slice(i, i + 100)\n const fetched = await this._fetchChunk(chunk, now)\n for (const [id, user] of fetched) {\n result.set(id, user)\n }\n }\n\n return result\n }\n\n async getProfilePictureUrl(userId: string): Promise<string | null> {\n const user = await this.getUser(userId)\n return user?.profileImageUrl ?? null\n }\n\n async getProfilePictureUrls(userIds: string[]): Promise<Map<string, string>> {\n const users = await this.getUsers(userIds)\n const result = new Map<string, string>()\n for (const [id, user] of users) {\n if (user !== null) result.set(id, user.profileImageUrl)\n }\n return result\n }\n\n private async _fetchChunk(ids: string[], now: number): Promise<Map<string, UserInfo>> {\n const params = new URLSearchParams()\n for (const id of ids) params.append('id', id)\n\n const { accessToken, clientId } = this.getCredentials()\n const res = await fetch(`${HELIX_USERS}?${params.toString()}`, {\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n },\n })\n\n if (!res.ok) {\n throw new Error(`Helix users fetch failed: ${res.status}`)\n }\n\n const body = (await res.json()) as { data: HelixUser[] }\n const result = new Map<string, UserInfo>()\n const expiresAt = now + TTL_MS\n\n for (const u of body.data) {\n const user: UserInfo = {\n id: u.id,\n login: u.login,\n displayName: u.display_name,\n profileImageUrl: u.profile_image_url,\n broadcasterType: u.broadcaster_type,\n description: u.description,\n createdAt: u.created_at,\n }\n this.cache.set(u.id, { user, expiresAt })\n result.set(u.id, user)\n }\n\n return result\n }\n}\n","import type { ResolvedBadge } from '../types.js'\n\ninterface HelixBadgeVersion {\n id: string\n image_url_1x: string\n image_url_2x: string\n image_url_4x: string\n title: string\n}\n\ninterface HelixBadgeSet {\n set_id: string\n versions: HelixBadgeVersion[]\n}\n\nconst HELIX_BADGES_GLOBAL = 'https://api.twitch.tv/helix/chat/badges/global'\nconst HELIX_BADGES_CHANNEL = 'https://api.twitch.tv/helix/chat/badges'\n\nexport class BadgeCache {\n // setId → version → ResolvedBadge\n private sets = new Map<string, Map<string, ResolvedBadge>>()\n\n constructor(\n private readonly channelId: string,\n private readonly getCredentials: () => { accessToken: string; clientId: string },\n ) {}\n\n async load(): Promise<void> {\n const { accessToken, clientId } = this.getCredentials()\n const headers = {\n 'Authorization': `Bearer ${accessToken}`,\n 'Client-Id': clientId,\n }\n\n const [globalRes, channelRes] = await Promise.all([\n fetch(HELIX_BADGES_GLOBAL, { headers }),\n fetch(`${HELIX_BADGES_CHANNEL}?broadcaster_id=${this.channelId}`, { headers }),\n ])\n\n if (!globalRes.ok) throw new Error(`Global badges fetch failed: ${globalRes.status}`)\n if (!channelRes.ok) throw new Error(`Channel badges fetch failed: ${channelRes.status}`)\n\n const [globalBody, channelBody] = await Promise.all([\n globalRes.json() as Promise<{ data: HelixBadgeSet[] }>,\n channelRes.json() as Promise<{ data: HelixBadgeSet[] }>,\n ])\n\n // Load global first, channel second so channel versions override global\n for (const set of [...globalBody.data, ...channelBody.data]) {\n const versionMap = this.sets.get(set.set_id) ?? new Map<string, ResolvedBadge>()\n for (const v of set.versions) {\n versionMap.set(v.id, {\n title: v.title,\n imageUrl1x: v.image_url_1x,\n imageUrl2x: v.image_url_2x,\n imageUrl4x: v.image_url_4x,\n })\n }\n this.sets.set(set.set_id, versionMap)\n }\n }\n\n resolve(setId: string, version: string): ResolvedBadge | undefined {\n return this.sets.get(setId)?.get(version)\n }\n}\n","import type {\n TwitchChatMessageEvent,\n NormalizedMessage,\n MessageFragment,\n ResolvedEmote,\n ResolvedBadge,\n} from './types.js'\nimport type { EmoteCache } from './emotes/index.js'\n\nexport function normalizeMessage(\n event: TwitchChatMessageEvent,\n emoteCache: EmoteCache,\n resolveBadge?: (setId: string, version: string) => ResolvedBadge | undefined,\n): NormalizedMessage {\n const emotes: ResolvedEmote[] = []\n const fragments: MessageFragment[] = []\n\n for (const frag of event.message.fragments) {\n switch (frag.type) {\n case 'text': {\n // A \"text\" fragment may contain third-party emote names.\n // Split on whitespace and check each token against the emote cache.\n const tokens = frag.text.split(/(\\s+)/)\n let pendingText = ''\n\n for (const token of tokens) {\n if (/^\\s+$/.test(token)) {\n pendingText += token\n continue\n }\n const resolved = emoteCache.resolveByName(token)\n if (resolved) {\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n pendingText = ''\n }\n fragments.push({ type: 'emote', text: token, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n } else {\n pendingText += token\n }\n }\n if (pendingText) {\n fragments.push({ type: 'text', text: pendingText })\n }\n break\n }\n\n case 'emote': {\n const emoteData = frag.emote!\n const resolved = emoteCache.resolveTwitch(emoteData.id, frag.text)\n fragments.push({ type: 'emote', text: frag.text, emote: resolved })\n if (!emotes.some(e => e.id === resolved.id)) {\n emotes.push(resolved)\n }\n break\n }\n\n case 'cheermote': {\n const cheer = frag.cheermote!\n fragments.push({\n type: 'cheermote',\n text: frag.text,\n bits: cheer.bits,\n tier: cheer.tier,\n })\n break\n }\n\n case 'mention': {\n const mention = frag.mention!\n fragments.push({\n type: 'mention',\n text: frag.text,\n userId: mention.user_id,\n userLogin: mention.user_login,\n })\n break\n }\n }\n }\n\n const badges = event.badges.map(b => {\n const resolved = resolveBadge?.(b.set_id, b.id)\n return {\n setId: b.set_id,\n id: b.id,\n info: b.info,\n ...(resolved !== undefined && { resolved }),\n }\n })\n\n const badgeSetIds = new Set(badges.map(b => b.setId))\n\n const msg: NormalizedMessage = {\n id: event.message_id,\n text: event.message.text,\n user: {\n id: event.chatter_user_id,\n login: event.chatter_user_login,\n displayName: event.chatter_user_name,\n color: event.color,\n badges,\n isModerator: badgeSetIds.has('moderator'),\n isSubscriber: badgeSetIds.has('subscriber'),\n isBroadcaster: badgeSetIds.has('broadcaster'),\n isVip: badgeSetIds.has('vip'),\n },\n fragments,\n emotes,\n timestamp: event.timestamp,\n }\n\n if (event.cheer != null) {\n msg.cheer = { bits: event.cheer.bits }\n }\n\n if (event.reply != null) {\n msg.reply = {\n parentMessageId: event.reply.parent_message_id,\n parentUserLogin: event.reply.parent_user_login,\n parentUserDisplayName: event.reply.parent_user_display_name,\n }\n }\n\n if (event.channel_points_custom_reward_id != null) {\n msg.channelPointsRewardId = event.channel_points_custom_reward_id\n }\n\n return msg\n}\n"],"mappings":"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","names":["CDN","API","CDN","parseEmote"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blorkfield/twitch-integration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Twitch EventSub WebSocket client with normalized chat message stream",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"build": "tsup",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"changeset": "changeset",
|
|
34
|
-
"publish": "pnpm build && pnpm publish --access public",
|
|
35
34
|
"prepare": "[ -d testbed ] && pnpm -C testbed install || true"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|