@blorkfield/twitch-integration 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,572 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ TwitchChat: () => TwitchChat
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/client.ts
38
+ var import_eventemitter3 = __toESM(require("eventemitter3"), 1);
39
+
40
+ // src/emotes/twitch.ts
41
+ var CDN = "https://static-cdn.jtvnw.net/emoticons/v2";
42
+ function buildTwitchEmote(id, name) {
43
+ return {
44
+ id,
45
+ name,
46
+ source: "twitch",
47
+ animated: false,
48
+ imageUrl1x: `${CDN}/${id}/default/dark/1.0`,
49
+ imageUrl2x: `${CDN}/${id}/default/dark/2.0`,
50
+ imageUrl3x: `${CDN}/${id}/default/dark/3.0`
51
+ };
52
+ }
53
+
54
+ // src/emotes/bttv.ts
55
+ var API = "https://api.betterttv.net/3";
56
+ var CDN2 = "https://cdn.betterttv.net/emote";
57
+ function parseEmote(e) {
58
+ return {
59
+ id: e.id,
60
+ name: e.code,
61
+ source: "bttv",
62
+ animated: e.animated,
63
+ imageUrl1x: `${CDN2}/${e.id}/1x`,
64
+ imageUrl2x: `${CDN2}/${e.id}/2x`,
65
+ imageUrl3x: `${CDN2}/${e.id}/3x`
66
+ };
67
+ }
68
+ async function fetchBttvGlobal() {
69
+ const res = await fetch(`${API}/cached/emotes/global`);
70
+ if (!res.ok) throw new Error(`BTTV global fetch failed: ${res.status}`);
71
+ const data = await res.json();
72
+ const map = /* @__PURE__ */ new Map();
73
+ for (const e of data) {
74
+ map.set(e.code, parseEmote(e));
75
+ }
76
+ return map;
77
+ }
78
+ async function fetchBttvChannel(channelId) {
79
+ const res = await fetch(`${API}/cached/users/twitch/${channelId}`);
80
+ if (!res.ok) throw new Error(`BTTV channel fetch failed: ${res.status}`);
81
+ const data = await res.json();
82
+ const map = /* @__PURE__ */ new Map();
83
+ for (const e of [...data.channelEmotes, ...data.sharedEmotes]) {
84
+ map.set(e.code, parseEmote(e));
85
+ }
86
+ return map;
87
+ }
88
+
89
+ // src/emotes/7tv.ts
90
+ var API2 = "https://7tv.io/v3";
91
+ var CDN3 = "https://cdn.7tv.app/emote";
92
+ function parseEmote2(e) {
93
+ return {
94
+ id: e.id,
95
+ name: e.name,
96
+ source: "7tv",
97
+ animated: e.data.animated,
98
+ imageUrl1x: `${CDN3}/${e.id}/1x.webp`,
99
+ imageUrl2x: `${CDN3}/${e.id}/2x.webp`,
100
+ imageUrl3x: `${CDN3}/${e.id}/3x.webp`
101
+ };
102
+ }
103
+ async function fetch7tvGlobal() {
104
+ const res = await fetch(`${API2}/emote-sets/global`);
105
+ if (!res.ok) throw new Error(`7TV global fetch failed: ${res.status}`);
106
+ const data = await res.json();
107
+ const map = /* @__PURE__ */ new Map();
108
+ for (const e of data.emotes) {
109
+ map.set(e.name, parseEmote2(e));
110
+ }
111
+ return map;
112
+ }
113
+ async function fetch7tvChannel(channelId) {
114
+ const res = await fetch(`${API2}/users/twitch/${channelId}`);
115
+ if (!res.ok) throw new Error(`7TV channel fetch failed: ${res.status}`);
116
+ const data = await res.json();
117
+ const map = /* @__PURE__ */ new Map();
118
+ for (const e of data.emote_set.emotes) {
119
+ map.set(e.name, parseEmote2(e));
120
+ }
121
+ return map;
122
+ }
123
+
124
+ // src/emotes/index.ts
125
+ var EmoteCache = class {
126
+ constructor(channelId) {
127
+ this.channelId = channelId;
128
+ this.bttvGlobal = /* @__PURE__ */ new Map();
129
+ this.bttvChannel = /* @__PURE__ */ new Map();
130
+ this.sevenTvGlobal = /* @__PURE__ */ new Map();
131
+ this.sevenTvChannel = /* @__PURE__ */ new Map();
132
+ }
133
+ async load() {
134
+ const results = await Promise.allSettled([
135
+ fetchBttvGlobal().then((m) => {
136
+ this.bttvGlobal = m;
137
+ }),
138
+ fetchBttvChannel(this.channelId).then((m) => {
139
+ this.bttvChannel = m;
140
+ }),
141
+ fetch7tvGlobal().then((m) => {
142
+ this.sevenTvGlobal = m;
143
+ }),
144
+ fetch7tvChannel(this.channelId).then((m) => {
145
+ this.sevenTvChannel = m;
146
+ })
147
+ ]);
148
+ for (const result of results) {
149
+ if (result.status === "rejected") {
150
+ console.warn("[twitch-integration] emote fetch error:", result.reason);
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Resolve a third-party emote by name.
156
+ * Priority: 7TV channel > BTTV channel > 7TV global > BTTV global
157
+ *
158
+ * Twitch emotes are resolved separately via resolveFromFragment(), since their
159
+ * IDs come directly from message fragments — no lookup table needed.
160
+ */
161
+ resolveByName(name) {
162
+ return this.sevenTvChannel.get(name) ?? this.bttvChannel.get(name) ?? this.sevenTvGlobal.get(name) ?? this.bttvGlobal.get(name);
163
+ }
164
+ /**
165
+ * Resolve a Twitch native emote from fragment data.
166
+ */
167
+ resolveTwitch(id, name) {
168
+ return buildTwitchEmote(id, name);
169
+ }
170
+ };
171
+
172
+ // src/users/index.ts
173
+ var HELIX_USERS = "https://api.twitch.tv/helix/users";
174
+ var TTL_MS = 5 * 60 * 1e3;
175
+ var UserCache = class {
176
+ constructor(getCredentials) {
177
+ this.getCredentials = getCredentials;
178
+ this.cache = /* @__PURE__ */ new Map();
179
+ }
180
+ async getProfilePictureUrl(userId) {
181
+ const results = await this.getProfilePictureUrls([userId]);
182
+ return results.get(userId) ?? null;
183
+ }
184
+ async getProfilePictureUrls(userIds) {
185
+ const now = Date.now();
186
+ const result = /* @__PURE__ */ new Map();
187
+ const toFetch = [];
188
+ for (const id of userIds) {
189
+ const entry = this.cache.get(id);
190
+ if (entry !== void 0 && entry.expiresAt > now) {
191
+ result.set(id, entry.url);
192
+ } else {
193
+ toFetch.push(id);
194
+ }
195
+ }
196
+ if (toFetch.length === 0) return result;
197
+ for (let i = 0; i < toFetch.length; i += 100) {
198
+ const chunk = toFetch.slice(i, i + 100);
199
+ const fetched = await this._fetchChunk(chunk, now);
200
+ for (const [id, url] of fetched) {
201
+ result.set(id, url);
202
+ }
203
+ }
204
+ return result;
205
+ }
206
+ async _fetchChunk(ids, now) {
207
+ const params = new URLSearchParams();
208
+ for (const id of ids) params.append("id", id);
209
+ const { accessToken, clientId } = this.getCredentials();
210
+ const res = await fetch(`${HELIX_USERS}?${params.toString()}`, {
211
+ headers: {
212
+ "Authorization": `Bearer ${accessToken}`,
213
+ "Client-Id": clientId
214
+ }
215
+ });
216
+ if (!res.ok) {
217
+ throw new Error(`Helix users fetch failed: ${res.status}`);
218
+ }
219
+ const body = await res.json();
220
+ const result = /* @__PURE__ */ new Map();
221
+ const expiresAt = now + TTL_MS;
222
+ for (const user of body.data) {
223
+ this.cache.set(user.id, { url: user.profile_image_url, expiresAt });
224
+ result.set(user.id, user.profile_image_url);
225
+ }
226
+ return result;
227
+ }
228
+ };
229
+
230
+ // src/normalizer.ts
231
+ function normalizeMessage(event, emoteCache) {
232
+ const emotes = [];
233
+ const fragments = [];
234
+ for (const frag of event.message.fragments) {
235
+ switch (frag.type) {
236
+ case "text": {
237
+ const tokens = frag.text.split(/(\s+)/);
238
+ let pendingText = "";
239
+ for (const token of tokens) {
240
+ if (/^\s+$/.test(token)) {
241
+ pendingText += token;
242
+ continue;
243
+ }
244
+ const resolved = emoteCache.resolveByName(token);
245
+ if (resolved) {
246
+ if (pendingText) {
247
+ fragments.push({ type: "text", text: pendingText });
248
+ pendingText = "";
249
+ }
250
+ fragments.push({ type: "emote", text: token, emote: resolved });
251
+ if (!emotes.some((e) => e.id === resolved.id)) {
252
+ emotes.push(resolved);
253
+ }
254
+ } else {
255
+ pendingText += token;
256
+ }
257
+ }
258
+ if (pendingText) {
259
+ fragments.push({ type: "text", text: pendingText });
260
+ }
261
+ break;
262
+ }
263
+ case "emote": {
264
+ const emoteData = frag.emote;
265
+ const resolved = emoteCache.resolveTwitch(emoteData.id, frag.text);
266
+ fragments.push({ type: "emote", text: frag.text, emote: resolved });
267
+ if (!emotes.some((e) => e.id === resolved.id)) {
268
+ emotes.push(resolved);
269
+ }
270
+ break;
271
+ }
272
+ case "cheermote": {
273
+ const cheer = frag.cheermote;
274
+ fragments.push({
275
+ type: "cheermote",
276
+ text: frag.text,
277
+ bits: cheer.bits,
278
+ tier: cheer.tier
279
+ });
280
+ break;
281
+ }
282
+ case "mention": {
283
+ const mention = frag.mention;
284
+ fragments.push({
285
+ type: "mention",
286
+ text: frag.text,
287
+ userId: mention.user_id,
288
+ userLogin: mention.user_login
289
+ });
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ const badges = event.badges.map((b) => ({
295
+ setId: b.set_id,
296
+ id: b.id,
297
+ info: b.info
298
+ }));
299
+ const badgeSetIds = new Set(badges.map((b) => b.setId));
300
+ const msg = {
301
+ id: event.message_id,
302
+ text: event.message.text,
303
+ user: {
304
+ id: event.chatter_user_id,
305
+ login: event.chatter_user_login,
306
+ displayName: event.chatter_user_name,
307
+ color: event.color,
308
+ badges,
309
+ isModerator: badgeSetIds.has("moderator"),
310
+ isSubscriber: badgeSetIds.has("subscriber"),
311
+ isBroadcaster: badgeSetIds.has("broadcaster"),
312
+ isVip: badgeSetIds.has("vip")
313
+ },
314
+ fragments,
315
+ emotes,
316
+ timestamp: event.timestamp
317
+ };
318
+ if (event.cheer != null) {
319
+ msg.cheer = { bits: event.cheer.bits };
320
+ }
321
+ if (event.reply != null) {
322
+ msg.reply = {
323
+ parentMessageId: event.reply.parent_message_id,
324
+ parentUserLogin: event.reply.parent_user_login,
325
+ parentUserDisplayName: event.reply.parent_user_display_name
326
+ };
327
+ }
328
+ if (event.channel_points_custom_reward_id != null) {
329
+ msg.channelPointsRewardId = event.channel_points_custom_reward_id;
330
+ }
331
+ return msg;
332
+ }
333
+
334
+ // src/client.ts
335
+ var EVENTSUB_URL = "wss://eventsub.wss.twitch.tv/ws";
336
+ var HELIX_SUBSCRIPTIONS = "https://api.twitch.tv/helix/eventsub/subscriptions";
337
+ function createWebSocket(url) {
338
+ if (typeof WebSocket !== "undefined") {
339
+ return new WebSocket(url);
340
+ }
341
+ const mod = require("ws");
342
+ const WsImpl = mod.default ?? mod;
343
+ return new WsImpl(url);
344
+ }
345
+ var TwitchChat = class extends import_eventemitter3.default {
346
+ constructor(options) {
347
+ super();
348
+ this.ws = null;
349
+ this.sessionId = null;
350
+ this.keepaliveTimeoutMs = 1e4;
351
+ this.keepaliveTimer = null;
352
+ // Holds the old ws during a session_reconnect handoff
353
+ this.oldWs = null;
354
+ this.stopped = false;
355
+ this.options = options;
356
+ this.emoteCache = new EmoteCache(options.channelId);
357
+ this.userCache = new UserCache(() => ({
358
+ accessToken: this.options.accessToken,
359
+ clientId: this.options.clientId
360
+ }));
361
+ }
362
+ // ---------------------------------------------------------------------------
363
+ // Public API
364
+ // ---------------------------------------------------------------------------
365
+ async connect() {
366
+ this.stopped = false;
367
+ await this._openConnection(EVENTSUB_URL, false);
368
+ }
369
+ disconnect() {
370
+ this.stopped = true;
371
+ this._clearKeepaliveTimer();
372
+ this._closeWs(this.ws, 1e3, "disconnect");
373
+ this.ws = null;
374
+ this.sessionId = null;
375
+ }
376
+ async preloadEmotes() {
377
+ await this.emoteCache.load();
378
+ }
379
+ async refreshEmotes() {
380
+ await this.emoteCache.load();
381
+ }
382
+ async getProfilePictureUrl(userId) {
383
+ return this.userCache.getProfilePictureUrl(userId);
384
+ }
385
+ async getProfilePictureUrls(userIds) {
386
+ const found = await this.userCache.getProfilePictureUrls(userIds);
387
+ const result = /* @__PURE__ */ new Map();
388
+ for (const id of userIds) {
389
+ result.set(id, found.get(id) ?? null);
390
+ }
391
+ return result;
392
+ }
393
+ // ---------------------------------------------------------------------------
394
+ // Connection
395
+ // ---------------------------------------------------------------------------
396
+ _openConnection(url, isReconnect) {
397
+ return new Promise((resolve, reject) => {
398
+ const ws = createWebSocket(url);
399
+ let settled = false;
400
+ const settle = (fn) => {
401
+ if (!settled) {
402
+ settled = true;
403
+ fn();
404
+ }
405
+ };
406
+ ws.addEventListener("message", (event) => {
407
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
408
+ let msg;
409
+ try {
410
+ msg = JSON.parse(raw);
411
+ } catch (e) {
412
+ this.emit("error", new Error(`Failed to parse WS message: ${String(e)}`));
413
+ return;
414
+ }
415
+ this._dispatch(msg, ws, isReconnect, settle, resolve, reject);
416
+ });
417
+ ws.addEventListener("close", (event) => {
418
+ const code = event.code;
419
+ const reason = typeof event.reason === "string" ? event.reason : event.reason.toString();
420
+ this._clearKeepaliveTimer();
421
+ if (!settled) {
422
+ settle(() => reject(new Error(`WebSocket closed before welcome: ${code} ${reason}`)));
423
+ return;
424
+ }
425
+ if (ws !== this.ws) return;
426
+ this.emit("disconnected", code, reason);
427
+ if (!this.stopped && code !== 1e3) {
428
+ setTimeout(() => {
429
+ if (!this.stopped) {
430
+ this._openConnection(EVENTSUB_URL, false).catch((err) => {
431
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
432
+ });
433
+ }
434
+ }, 2e3);
435
+ }
436
+ });
437
+ ws.addEventListener("error", (err) => {
438
+ const error = err instanceof Error ? err : new Error("WebSocket error");
439
+ if (!settled) {
440
+ settle(() => reject(error));
441
+ } else {
442
+ this.emit("error", error);
443
+ }
444
+ });
445
+ });
446
+ }
447
+ _dispatch(msg, ws, isReconnect, settle, resolve, reject) {
448
+ switch (msg.metadata.message_type) {
449
+ case "session_welcome": {
450
+ const payload = msg.payload;
451
+ this.sessionId = payload.session.id;
452
+ this.keepaliveTimeoutMs = payload.session.keepalive_timeout_seconds * 1e3;
453
+ this._resetKeepaliveTimer();
454
+ if (isReconnect) {
455
+ this._closeWs(this.oldWs, 1e3, "reconnected");
456
+ this.oldWs = null;
457
+ this.ws = ws;
458
+ settle(() => resolve());
459
+ break;
460
+ }
461
+ this.ws = ws;
462
+ this._subscribe().then(() => {
463
+ settle(() => resolve());
464
+ this.emit("connected");
465
+ }).catch((err) => {
466
+ settle(() => reject(err instanceof Error ? err : new Error(String(err))));
467
+ });
468
+ break;
469
+ }
470
+ case "session_keepalive": {
471
+ this._resetKeepaliveTimer();
472
+ break;
473
+ }
474
+ case "notification": {
475
+ this._resetKeepaliveTimer();
476
+ const payload = msg.payload;
477
+ if (payload.subscription.type === "channel.chat.message") {
478
+ try {
479
+ const normalized = normalizeMessage(payload.event, this.emoteCache);
480
+ this.emit("message", normalized);
481
+ } catch (e) {
482
+ this.emit("error", e instanceof Error ? e : new Error(String(e)));
483
+ }
484
+ }
485
+ break;
486
+ }
487
+ case "session_reconnect": {
488
+ const payload = msg.payload;
489
+ this.oldWs = this.ws;
490
+ this._openConnection(payload.session.reconnect_url, true).catch((err) => {
491
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
492
+ });
493
+ break;
494
+ }
495
+ case "revocation": {
496
+ const payload = msg.payload;
497
+ this.emit("revoked", payload.subscription.status);
498
+ break;
499
+ }
500
+ }
501
+ }
502
+ // ---------------------------------------------------------------------------
503
+ // Helix subscription
504
+ // ---------------------------------------------------------------------------
505
+ async _subscribe() {
506
+ if (!this.sessionId) throw new Error("No session ID");
507
+ const res = await fetch(HELIX_SUBSCRIPTIONS, {
508
+ method: "POST",
509
+ headers: {
510
+ "Authorization": `Bearer ${this.options.accessToken}`,
511
+ "Client-Id": this.options.clientId,
512
+ "Content-Type": "application/json"
513
+ },
514
+ body: JSON.stringify({
515
+ type: "channel.chat.message",
516
+ version: "1",
517
+ condition: {
518
+ broadcaster_user_id: this.options.channelId,
519
+ user_id: this.options.userId
520
+ },
521
+ transport: {
522
+ method: "websocket",
523
+ session_id: this.sessionId
524
+ }
525
+ })
526
+ });
527
+ if (res.status === 401) {
528
+ this.emit("auth_error");
529
+ throw new Error("Auth error subscribing to EventSub");
530
+ }
531
+ if (!res.ok) {
532
+ const body = await res.text();
533
+ throw new Error(`EventSub subscription failed: ${res.status} ${body}`);
534
+ }
535
+ }
536
+ // ---------------------------------------------------------------------------
537
+ // Keepalive timer
538
+ // ---------------------------------------------------------------------------
539
+ _resetKeepaliveTimer() {
540
+ this._clearKeepaliveTimer();
541
+ this.keepaliveTimer = setTimeout(() => {
542
+ this._closeWs(this.ws, 1001, "keepalive timeout");
543
+ this.ws = null;
544
+ if (!this.stopped) {
545
+ this._openConnection(EVENTSUB_URL, false).catch((err) => {
546
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
547
+ });
548
+ }
549
+ }, this.keepaliveTimeoutMs + 500);
550
+ }
551
+ _clearKeepaliveTimer() {
552
+ if (this.keepaliveTimer !== null) {
553
+ clearTimeout(this.keepaliveTimer);
554
+ this.keepaliveTimer = null;
555
+ }
556
+ }
557
+ // ---------------------------------------------------------------------------
558
+ // Helpers
559
+ // ---------------------------------------------------------------------------
560
+ _closeWs(ws, code, reason) {
561
+ if (!ws) return;
562
+ try {
563
+ ws.close(code, reason);
564
+ } catch {
565
+ }
566
+ }
567
+ };
568
+ // Annotate the CommonJS export names for ESM import in node:
569
+ 0 && (module.exports = {
570
+ TwitchChat
571
+ });
572
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}