@blorkfield/twitch-integration 0.2.2 → 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 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 getProfilePictureUrl(userId) {
181
- const results = await this.getProfilePictureUrls([userId]);
180
+ async getUser(userId) {
181
+ const results = await this.getUsers([userId]);
182
182
  return results.get(userId) ?? null;
183
183
  }
184
- async getProfilePictureUrls(userIds) {
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.url);
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, url] of fetched) {
201
- result.set(id, url);
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 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);
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
- setId: b.set_id,
296
- id: b.id,
297
- info: b.info
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(payload.event, this.emoteCache);
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)));
@@ -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 UserProfile };
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 UserProfile };
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 getProfilePictureUrl(userId) {
152
- const results = await this.getProfilePictureUrls([userId]);
151
+ async getUser(userId) {
152
+ const results = await this.getUsers([userId]);
153
153
  return results.get(userId) ?? null;
154
154
  }
155
- async getProfilePictureUrls(userIds) {
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.url);
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, url] of fetched) {
172
- result.set(id, url);
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 user of body.data) {
194
- this.cache.set(user.id, { url: user.profile_image_url, expiresAt });
195
- result.set(user.id, user.profile_image_url);
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
- setId: b.set_id,
267
- id: b.id,
268
- info: b.info
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(payload.event, this.emoteCache);
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":";;;;;;;;AAAA,OAAO,kBAAkB;;;ACEzB,IAAM,MAAM;AAEL,SAAS,iBAAiB,IAAY,MAA6B;AACxE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,YAAY,GAAG,GAAG,IAAI,EAAE;AAAA,IACxB,YAAY,GAAG,GAAG,IAAI,EAAE;AAAA,IACxB,YAAY,GAAG,GAAG,IAAI,EAAE;AAAA,EAC1B;AACF;;;ACZA,IAAM,MAAM;AACZ,IAAMA,OAAM;AAcZ,SAAS,WAAW,GAA6B;AAC/C,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IACN,MAAM,EAAE;AAAA,IACR,QAAQ;AAAA,IACR,UAAU,EAAE;AAAA,IACZ,YAAY,GAAGA,IAAG,IAAI,EAAE,EAAE;AAAA,IAC1B,YAAY,GAAGA,IAAG,IAAI,EAAE,EAAE;AAAA,IAC1B,YAAY,GAAGA,IAAG,IAAI,EAAE,EAAE;AAAA,EAC5B;AACF;AAEA,eAAsB,kBAAuD;AAC3E,QAAM,MAAM,MAAM,MAAM,GAAG,GAAG,uBAAuB;AACrD,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,EAAE;AACtE,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,MAAM,oBAAI,IAA2B;AAC3C,aAAW,KAAK,MAAM;AACpB,QAAI,IAAI,EAAE,MAAM,WAAW,CAAC,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,eAAsB,iBAAiB,WAAwD;AAC7F,QAAM,MAAM,MAAM,MAAM,GAAG,GAAG,wBAAwB,SAAS,EAAE;AACjE,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,EAAE;AACvE,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,MAAM,oBAAI,IAA2B;AAC3C,aAAW,KAAK,CAAC,GAAG,KAAK,eAAe,GAAG,KAAK,YAAY,GAAG;AAC7D,QAAI,IAAI,EAAE,MAAM,WAAW,CAAC,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;;;AC/CA,IAAMC,OAAM;AACZ,IAAMC,OAAM;AAkCZ,SAASC,YAAW,GAAgC;AAClD,SAAO;AAAA,IACL,IAAI,EAAE;AAAA,IACN,MAAM,EAAE;AAAA,IACR,QAAQ;AAAA,IACR,UAAU,EAAE,KAAK;AAAA,IACjB,YAAY,GAAGD,IAAG,IAAI,EAAE,EAAE;AAAA,IAC1B,YAAY,GAAGA,IAAG,IAAI,EAAE,EAAE;AAAA,IAC1B,YAAY,GAAGA,IAAG,IAAI,EAAE,EAAE;AAAA,EAC5B;AACF;AAEA,eAAsB,iBAAsD;AAC1E,QAAM,MAAM,MAAM,MAAM,GAAGD,IAAG,oBAAoB;AAClD,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,EAAE;AACrE,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,MAAM,oBAAI,IAA2B;AAC3C,aAAW,KAAK,KAAK,QAAQ;AAC3B,QAAI,IAAI,EAAE,MAAME,YAAW,CAAC,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,eAAsB,gBAAgB,WAAwD;AAC5F,QAAM,MAAM,MAAM,MAAM,GAAGF,IAAG,iBAAiB,SAAS,EAAE;AAC1D,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,EAAE;AACtE,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,MAAM,oBAAI,IAA2B;AAC3C,aAAW,KAAK,KAAK,UAAU,QAAQ;AACrC,QAAI,IAAI,EAAE,MAAME,YAAW,CAAC,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;;;AChEO,IAAM,aAAN,MAAiB;AAAA,EAMtB,YAA6B,WAAmB;AAAnB;AAL7B,SAAQ,aAAa,oBAAI,IAA2B;AACpD,SAAQ,cAAc,oBAAI,IAA2B;AACrD,SAAQ,gBAAgB,oBAAI,IAA2B;AACvD,SAAQ,iBAAiB,oBAAI,IAA2B;AAAA,EAEP;AAAA,EAEjD,MAAM,OAAsB;AAC1B,UAAM,UAAU,MAAM,QAAQ,WAAW;AAAA,MACvC,gBAAgB,EAAE,KAAK,OAAK;AAAE,aAAK,aAAa;AAAA,MAAE,CAAC;AAAA,MACnD,iBAAiB,KAAK,SAAS,EAAE,KAAK,OAAK;AAAE,aAAK,cAAc;AAAA,MAAE,CAAC;AAAA,MACnE,eAAe,EAAE,KAAK,OAAK;AAAE,aAAK,gBAAgB;AAAA,MAAE,CAAC;AAAA,MACrD,gBAAgB,KAAK,SAAS,EAAE,KAAK,OAAK;AAAE,aAAK,iBAAiB;AAAA,MAAE,CAAC;AAAA,IACvE,CAAC;AAED,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,WAAW,YAAY;AAChC,gBAAQ,KAAK,2CAA2C,OAAO,MAAM;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAc,MAAyC;AACrD,WACE,KAAK,eAAe,IAAI,IAAI,KAC5B,KAAK,YAAY,IAAI,IAAI,KACzB,KAAK,cAAc,IAAI,IAAI,KAC3B,KAAK,WAAW,IAAI,IAAI;AAAA,EAE5B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,IAAY,MAA6B;AACrD,WAAO,iBAAiB,IAAI,IAAI;AAAA,EAClC;AACF;;;ACjCA,IAAM,cAAc;AACpB,IAAM,SAAS,IAAI,KAAK;AAEjB,IAAM,YAAN,MAAgB;AAAA,EAGrB,YACmB,gBACjB;AADiB;AAHnB,SAAQ,QAAQ,oBAAI,IAAwB;AAAA,EAIzC;AAAA,EAEH,MAAM,QAAQ,QAA0C;AACtD,UAAM,UAAU,MAAM,KAAK,SAAS,CAAC,MAAM,CAAC;AAC5C,WAAO,QAAQ,IAAI,MAAM,KAAK;AAAA,EAChC;AAAA,EAEA,MAAM,SAAS,SAA0D;AACvE,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,oBAAI,IAA6B;AAChD,UAAM,UAAoB,CAAC;AAE3B,eAAW,MAAM,SAAS;AACxB,YAAM,QAAQ,KAAK,MAAM,IAAI,EAAE;AAC/B,UAAI,UAAU,UAAa,MAAM,YAAY,KAAK;AAChD,eAAO,IAAI,IAAI,MAAM,IAAI;AAAA,MAC3B,OAAO;AACL,eAAO,IAAI,IAAI,IAAI;AACnB,gBAAQ,KAAK,EAAE;AAAA,MACjB;AAAA,IACF;AAEA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAC5C,YAAM,QAAQ,QAAQ,MAAM,GAAG,IAAI,GAAG;AACtC,YAAM,UAAU,MAAM,KAAK,YAAY,OAAO,GAAG;AACjD,iBAAW,CAAC,IAAI,IAAI,KAAK,SAAS;AAChC,eAAO,IAAI,IAAI,IAAI;AAAA,MACrB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,qBAAqB,QAAwC;AACjE,UAAM,OAAO,MAAM,KAAK,QAAQ,MAAM;AACtC,WAAO,MAAM,mBAAmB;AAAA,EAClC;AAAA,EAEA,MAAM,sBAAsB,SAAiD;AAC3E,UAAM,QAAQ,MAAM,KAAK,SAAS,OAAO;AACzC,UAAM,SAAS,oBAAI,IAAoB;AACvC,eAAW,CAAC,IAAI,IAAI,KAAK,OAAO;AAC9B,UAAI,SAAS,KAAM,QAAO,IAAI,IAAI,KAAK,eAAe;AAAA,IACxD;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,YAAY,KAAe,KAA6C;AACpF,UAAM,SAAS,IAAI,gBAAgB;AACnC,eAAW,MAAM,IAAK,QAAO,OAAO,MAAM,EAAE;AAE5C,UAAM,EAAE,aAAa,SAAS,IAAI,KAAK,eAAe;AACtD,UAAM,MAAM,MAAM,MAAM,GAAG,WAAW,IAAI,OAAO,SAAS,CAAC,IAAI;AAAA,MAC7D,SAAS;AAAA,QACP,iBAAiB,UAAU,WAAW;AAAA,QACtC,aAAa;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,EAAE;AAAA,IAC3D;AAEA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAM,SAAS,oBAAI,IAAsB;AACzC,UAAM,YAAY,MAAM;AAExB,eAAW,KAAK,KAAK,MAAM;AACzB,YAAM,OAAiB;AAAA,QACrB,IAAI,EAAE;AAAA,QACN,OAAO,EAAE;AAAA,QACT,aAAa,EAAE;AAAA,QACf,iBAAiB,EAAE;AAAA,QACnB,iBAAiB,EAAE;AAAA,QACnB,aAAa,EAAE;AAAA,QACf,WAAW,EAAE;AAAA,MACf;AACA,WAAK,MAAM,IAAI,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACxC,aAAO,IAAI,EAAE,IAAI,IAAI;AAAA,IACvB;AAEA,WAAO;AAAA,EACT;AACF;;;AC7FA,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAEtB,IAAM,aAAN,MAAiB;AAAA,EAItB,YACmB,WACA,gBACjB;AAFiB;AACA;AAJnB;AAAA,SAAQ,OAAO,oBAAI,IAAwC;AAAA,EAKxD;AAAA,EAEH,MAAM,OAAsB;AAC1B,UAAM,EAAE,aAAa,SAAS,IAAI,KAAK,eAAe;AACtD,UAAM,UAAU;AAAA,MACd,iBAAiB,UAAU,WAAW;AAAA,MACtC,aAAa;AAAA,IACf;AAEA,UAAM,CAAC,WAAW,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,MAAM,qBAAqB,EAAE,QAAQ,CAAC;AAAA,MACtC,MAAM,GAAG,oBAAoB,mBAAmB,KAAK,SAAS,IAAI,EAAE,QAAQ,CAAC;AAAA,IAC/E,CAAC;AAED,QAAI,CAAC,UAAU,GAAI,OAAM,IAAI,MAAM,+BAA+B,UAAU,MAAM,EAAE;AACpF,QAAI,CAAC,WAAW,GAAI,OAAM,IAAI,MAAM,gCAAgC,WAAW,MAAM,EAAE;AAEvF,UAAM,CAAC,YAAY,WAAW,IAAI,MAAM,QAAQ,IAAI;AAAA,MAClD,UAAU,KAAK;AAAA,MACf,WAAW,KAAK;AAAA,IAClB,CAAC;AAGD,eAAW,OAAO,CAAC,GAAG,WAAW,MAAM,GAAG,YAAY,IAAI,GAAG;AAC3D,YAAM,aAAa,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,oBAAI,IAA2B;AAC/E,iBAAW,KAAK,IAAI,UAAU;AAC5B,mBAAW,IAAI,EAAE,IAAI;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,YAAY,EAAE;AAAA,UACd,YAAY,EAAE;AAAA,QAChB,CAAC;AAAA,MACH;AACA,WAAK,KAAK,IAAI,IAAI,QAAQ,UAAU;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,QAAQ,OAAe,SAA4C;AACjE,WAAO,KAAK,KAAK,IAAI,KAAK,GAAG,IAAI,OAAO;AAAA,EAC1C;AACF;;;ACxDO,SAAS,iBACd,OACA,YACA,cACmB;AACnB,QAAM,SAA0B,CAAC;AACjC,QAAM,YAA+B,CAAC;AAEtC,aAAW,QAAQ,MAAM,QAAQ,WAAW;AAC1C,YAAQ,KAAK,MAAM;AAAA,MACjB,KAAK,QAAQ;AAGX,cAAM,SAAS,KAAK,KAAK,MAAM,OAAO;AACtC,YAAI,cAAc;AAElB,mBAAW,SAAS,QAAQ;AAC1B,cAAI,QAAQ,KAAK,KAAK,GAAG;AACvB,2BAAe;AACf;AAAA,UACF;AACA,gBAAM,WAAW,WAAW,cAAc,KAAK;AAC/C,cAAI,UAAU;AACZ,gBAAI,aAAa;AACf,wBAAU,KAAK,EAAE,MAAM,QAAQ,MAAM,YAAY,CAAC;AAClD,4BAAc;AAAA,YAChB;AACA,sBAAU,KAAK,EAAE,MAAM,SAAS,MAAM,OAAO,OAAO,SAAS,CAAC;AAC9D,gBAAI,CAAC,OAAO,KAAK,OAAK,EAAE,OAAO,SAAS,EAAE,GAAG;AAC3C,qBAAO,KAAK,QAAQ;AAAA,YACtB;AAAA,UACF,OAAO;AACL,2BAAe;AAAA,UACjB;AAAA,QACF;AACA,YAAI,aAAa;AACf,oBAAU,KAAK,EAAE,MAAM,QAAQ,MAAM,YAAY,CAAC;AAAA,QACpD;AACA;AAAA,MACF;AAAA,MAEA,KAAK,SAAS;AACZ,cAAM,YAAY,KAAK;AACvB,cAAM,WAAW,WAAW,cAAc,UAAU,IAAI,KAAK,IAAI;AACjE,kBAAU,KAAK,EAAE,MAAM,SAAS,MAAM,KAAK,MAAM,OAAO,SAAS,CAAC;AAClE,YAAI,CAAC,OAAO,KAAK,OAAK,EAAE,OAAO,SAAS,EAAE,GAAG;AAC3C,iBAAO,KAAK,QAAQ;AAAA,QACtB;AACA;AAAA,MACF;AAAA,MAEA,KAAK,aAAa;AAChB,cAAM,QAAQ,KAAK;AACnB,kBAAU,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,MAAM,MAAM;AAAA,UACZ,MAAM,MAAM;AAAA,QACd,CAAC;AACD;AAAA,MACF;AAAA,MAEA,KAAK,WAAW;AACd,cAAM,UAAU,KAAK;AACrB,kBAAU,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,QAAQ,QAAQ;AAAA,UAChB,WAAW,QAAQ;AAAA,QACrB,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,OAAO,IAAI,OAAK;AACnC,UAAM,WAAW,eAAe,EAAE,QAAQ,EAAE,EAAE;AAC9C,WAAO;AAAA,MACL,OAAO,EAAE;AAAA,MACT,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,GAAI,aAAa,UAAa,EAAE,SAAS;AAAA,IAC3C;AAAA,EACF,CAAC;AAED,QAAM,cAAc,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,KAAK,CAAC;AAEpD,QAAM,MAAyB;AAAA,IAC7B,IAAI,MAAM;AAAA,IACV,MAAM,MAAM,QAAQ;AAAA,IACpB,MAAM;AAAA,MACJ,IAAI,MAAM;AAAA,MACV,OAAO,MAAM;AAAA,MACb,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,MACb;AAAA,MACA,aAAa,YAAY,IAAI,WAAW;AAAA,MACxC,cAAc,YAAY,IAAI,YAAY;AAAA,MAC1C,eAAe,YAAY,IAAI,aAAa;AAAA,MAC5C,OAAO,YAAY,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM;AAAA,EACnB;AAEA,MAAI,MAAM,SAAS,MAAM;AACvB,QAAI,QAAQ,EAAE,MAAM,MAAM,MAAM,KAAK;AAAA,EACvC;AAEA,MAAI,MAAM,SAAS,MAAM;AACvB,QAAI,QAAQ;AAAA,MACV,iBAAiB,MAAM,MAAM;AAAA,MAC7B,iBAAiB,MAAM,MAAM;AAAA,MAC7B,uBAAuB,MAAM,MAAM;AAAA,IACrC;AAAA,EACF;AAEA,MAAI,MAAM,mCAAmC,MAAM;AACjD,QAAI,wBAAwB,MAAM;AAAA,EACpC;AAEA,SAAO;AACT;;;APnHA,IAAM,eAAe;AACrB,IAAM,sBAAsB;AAW5B,SAAS,gBAAgB,KAAqB;AAC5C,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO,IAAI,UAAU,GAAG;AAAA,EAC1B;AAGA,QAAM,MAAM,UAAQ,IAAI;AACxB,QAAM,SAAS,IAAI,WAAW;AAC9B,SAAO,IAAI,OAAO,GAAG;AACvB;AAWO,IAAM,aAAN,cAAyB,aAA+B;AAAA,EAgB7D,YAAY,SAA4B;AACtC,UAAM;AAXR,SAAQ,KAAoB;AAC5B,SAAQ,YAA2B;AACnC,SAAQ,qBAAqB;AAC7B,SAAQ,iBAAuD;AAG/D;AAAA,SAAQ,QAAuB;AAE/B,SAAQ,UAAU;AAIhB,SAAK,UAAU;AACf,SAAK,aAAa,IAAI,WAAW,QAAQ,SAAS;AAClD,SAAK,YAAY,IAAI,UAAU,OAAO;AAAA,MACpC,aAAa,KAAK,QAAQ;AAAA,MAC1B,UAAU,KAAK,QAAQ;AAAA,IACzB,EAAE;AACF,SAAK,aAAa,IAAI,WAAW,QAAQ,WAAW,OAAO;AAAA,MACzD,aAAa,KAAK,QAAQ;AAAA,MAC1B,UAAU,KAAK,QAAQ;AAAA,IACzB,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAyB;AAC7B,SAAK,UAAU;AACf,UAAM,KAAK,gBAAgB,cAAc,KAAK;AAAA,EAChD;AAAA,EAEA,aAAmB;AACjB,SAAK,UAAU;AACf,SAAK,qBAAqB;AAC1B,SAAK,SAAS,KAAK,IAAI,KAAM,YAAY;AACzC,SAAK,KAAK;AACV,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,gBAA+B;AACnC,UAAM,KAAK,WAAW,KAAK;AAAA,EAC7B;AAAA,EAEA,MAAM,gBAA+B;AACnC,UAAM,KAAK,WAAW,KAAK;AAAA,EAC7B;AAAA,EAEA,MAAM,gBAA+B;AACnC,UAAM,KAAK,WAAW,KAAK;AAAA,EAC7B;AAAA,EAEA,MAAM,QAAQ,QAA0C;AACtD,WAAO,KAAK,UAAU,QAAQ,MAAM;AAAA,EACtC;AAAA,EAEA,MAAM,SAAS,SAA0D;AACvE,WAAO,KAAK,UAAU,SAAS,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,qBAAqB,QAAwC;AACjE,WAAO,KAAK,UAAU,qBAAqB,MAAM;AAAA,EACnD;AAAA,EAEA,MAAM,sBAAsB,SAAwD;AAClF,UAAM,QAAQ,MAAM,KAAK,UAAU,sBAAsB,OAAO;AAChE,UAAM,SAAS,oBAAI,IAA2B;AAC9C,eAAW,MAAM,SAAS;AACxB,aAAO,IAAI,IAAI,MAAM,IAAI,EAAE,KAAK,IAAI;AAAA,IACtC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,aAAa,OAAe,SAA4C;AACtE,WAAO,KAAK,WAAW,QAAQ,OAAO,OAAO;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAMQ,gBAAgB,KAAa,aAAqC;AACxE,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,gBAAgB,GAAG;AAC9B,UAAI,UAAU;AAEd,YAAM,SAAS,CAAC,OAAmB;AACjC,YAAI,CAAC,SAAS;AACZ,oBAAU;AACV,aAAG;AAAA,QACL;AAAA,MACF;AAEA,SAAG,iBAAiB,WAAW,CAAC,UAAU;AACxC,cAAM,MAAM,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,OAAO,MAAM,IAAI;AAC3E,YAAI;AACJ,YAAI;AACF,gBAAM,KAAK,MAAM,GAAG;AAAA,QACtB,SAAS,GAAG;AACV,eAAK,KAAK,SAAS,IAAI,MAAM,+BAA+B,OAAO,CAAC,CAAC,EAAE,CAAC;AACxE;AAAA,QACF;AACA,aAAK,UAAU,KAAK,IAAI,aAAa,QAAQ,SAAS,MAAM;AAAA,MAC9D,CAAC;AAED,SAAG,iBAAiB,SAAS,CAAC,UAAU;AACtC,cAAM,OAAO,MAAM;AACnB,cAAM,SAAS,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS,MAAM,OAAO,SAAS;AAEvF,aAAK,qBAAqB;AAE1B,YAAI,CAAC,SAAS;AACZ,iBAAO,MAAM,OAAO,IAAI,MAAM,oCAAoC,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC;AACpF;AAAA,QACF;AAEA,YAAI,OAAO,KAAK,GAAI;AAEpB,aAAK,KAAK,gBAAgB,MAAM,MAAM;AAEtC,YAAI,CAAC,KAAK,WAAW,SAAS,KAAM;AAClC,qBAAW,MAAM;AACf,gBAAI,CAAC,KAAK,SAAS;AACjB,mBAAK,gBAAgB,cAAc,KAAK,EAAE,MAAM,SAAO;AACrD,qBAAK,KAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,cACxE,CAAC;AAAA,YACH;AAAA,UACF,GAAG,GAAK;AAAA,QACV;AAAA,MACF,CAAC;AAED,SAAG,iBAAiB,SAAS,CAAC,QAAQ;AACpC,cAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,iBAAiB;AACtE,YAAI,CAAC,SAAS;AACZ,iBAAO,MAAM,OAAO,KAAK,CAAC;AAAA,QAC5B,OAAO;AACL,eAAK,KAAK,SAAS,KAAK;AAAA,QAC1B;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,UACN,KACA,IACA,aACA,QACA,SACA,QACM;AACN,YAAQ,IAAI,SAAS,cAAc;AAAA,MACjC,KAAK,mBAAmB;AACtB,cAAM,UAAU,IAAI;AACpB,aAAK,YAAY,QAAQ,QAAQ;AACjC,aAAK,qBAAqB,QAAQ,QAAQ,4BAA4B;AACtE,aAAK,qBAAqB;AAE1B,YAAI,aAAa;AAGf,eAAK,SAAS,KAAK,OAAO,KAAM,aAAa;AAC7C,eAAK,QAAQ;AACb,eAAK,KAAK;AACV,iBAAO,MAAM,QAAQ,CAAC;AACtB;AAAA,QACF;AAEA,aAAK,KAAK;AACV,aAAK,WAAW,EACb,KAAK,MAAM;AACV,iBAAO,MAAM,QAAQ,CAAC;AACtB,eAAK,KAAK,WAAW;AAAA,QACvB,CAAC,EACA,MAAM,SAAO;AACZ,iBAAO,MAAM,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC;AAAA,QAC1E,CAAC;AACH;AAAA,MACF;AAAA,MAEA,KAAK,qBAAqB;AACxB,aAAK,qBAAqB;AAC1B;AAAA,MACF;AAAA,MAEA,KAAK,gBAAgB;AACnB,aAAK,qBAAqB;AAC1B,cAAM,UAAU,IAAI;AACpB,YAAI,QAAQ,aAAa,SAAS,wBAAwB;AACxD,cAAI;AACF,kBAAM,aAAa;AAAA,cACnB,QAAQ;AAAA,cACR,KAAK;AAAA,cACL,CAAC,OAAO,YAAY,KAAK,WAAW,QAAQ,OAAO,OAAO;AAAA,YAC5D;AACE,iBAAK,KAAK,WAAW,UAAU;AAAA,UACjC,SAAS,GAAG;AACV,iBAAK,KAAK,SAAS,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC;AAAA,UAClE;AAAA,QACF;AACA;AAAA,MACF;AAAA,MAEA,KAAK,qBAAqB;AACxB,cAAM,UAAU,IAAI;AAEpB,aAAK,QAAQ,KAAK;AAClB,aAAK,gBAAgB,QAAQ,QAAQ,eAAe,IAAI,EAAE,MAAM,SAAO;AACrE,eAAK,KAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,QACxE,CAAC;AACD;AAAA,MACF;AAAA,MAEA,KAAK,cAAc;AACjB,cAAM,UAAU,IAAI;AACpB,aAAK,KAAK,WAAW,QAAQ,aAAa,MAAM;AAChD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,aAA4B;AACxC,QAAI,CAAC,KAAK,UAAW,OAAM,IAAI,MAAM,eAAe;AAEpD,UAAM,MAAM,MAAM,MAAM,qBAAqB;AAAA,MAC3C,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,UAAU,KAAK,QAAQ,WAAW;AAAA,QACnD,aAAa,KAAK,QAAQ;AAAA,QAC1B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,MAAM;AAAA,QACN,SAAS;AAAA,QACT,WAAW;AAAA,UACT,qBAAqB,KAAK,QAAQ;AAAA,UAClC,SAAS,KAAK,QAAQ;AAAA,QACxB;AAAA,QACA,WAAW;AAAA,UACT,QAAQ;AAAA,UACR,YAAY,KAAK;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,QAAI,IAAI,WAAW,KAAK;AACtB,WAAK,KAAK,YAAY;AACtB,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,IAAI,MAAM,iCAAiC,IAAI,MAAM,IAAI,IAAI,EAAE;AAAA,IACvE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,uBAA6B;AACnC,SAAK,qBAAqB;AAC1B,SAAK,iBAAiB,WAAW,MAAM;AACrC,WAAK,SAAS,KAAK,IAAI,MAAM,mBAAmB;AAChD,WAAK,KAAK;AACV,UAAI,CAAC,KAAK,SAAS;AACjB,aAAK,gBAAgB,cAAc,KAAK,EAAE,MAAM,SAAO;AACrD,eAAK,KAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,QACxE,CAAC;AAAA,MACH;AAAA,IACF,GAAG,KAAK,qBAAqB,GAAG;AAAA,EAClC;AAAA,EAEQ,uBAA6B;AACnC,QAAI,KAAK,mBAAmB,MAAM;AAChC,mBAAa,KAAK,cAAc;AAChC,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,SAAS,IAAmB,MAAc,QAAsB;AACtE,QAAI,CAAC,GAAI;AACT,QAAI;AACF,SAAG,MAAM,MAAM,MAAM;AAAA,IACvB,QAAQ;AAAA,IAER;AAAA,EACF;AACF;","names":["CDN","API","CDN","parseEmote"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blorkfield/twitch-integration",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Twitch EventSub WebSocket client with normalized chat message stream",
5
5
  "repository": {
6
6
  "type": "git",