multichat-ts 0.0.82 → 0.0.84

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.
@@ -0,0 +1,417 @@
1
+ import Pusher from 'pusher-js/worker';
2
+
3
+ import {
4
+ type BadgeURLsByNameOrCount,
5
+ type BodyComponent,
6
+ type EmoteURLsByName,
7
+ type Message,
8
+ } from './index.js';
9
+
10
+ type EventCallbackFunctions = {
11
+ message: (message: Message) => unknown;
12
+ subscription: (data: ChatSubscriptionEvent) => unknown;
13
+ gifted: (data: ChatGiftedEvent) => unknown;
14
+ raw_message: (message: ChatMessageEvent) => unknown;
15
+ };
16
+
17
+ type EventNames = keyof EventCallbackFunctions;
18
+
19
+ const DEFAULT_KICK_PUSHER_KEY = '32cbd69e4b950bf97679';
20
+
21
+ export class KickPusher {
22
+ public kick_pusher_key = DEFAULT_KICK_PUSHER_KEY;
23
+ public channel_name?: string;
24
+ private assets: {
25
+ external_emotes: EmoteURLsByName;
26
+ badges: BadgeURLsByNameOrCount;
27
+ } = {
28
+ external_emotes: {},
29
+ badges: {
30
+ founder: '/svgs/badges/default-kick/founder.svg',
31
+ moderator: '/svgs/badges/default-kick/moderator.svg',
32
+ og: '/svgs/badges/default-kick/og.svg',
33
+ sub_gifter: {
34
+ 1: '/svgs/badges/default-kick/sub-gifter-blue.svg',
35
+ 25: '/svgs/badges/default-kick/sub-gifter-purple.svg',
36
+ 50: '/svgs/badges/default-kick/sub-gifter-red.svg',
37
+ 100: '/svgs/badges/default-kick/sub-gifter-yellow.svg',
38
+ 200: '/svgs/badges/default-kick/sub-gifter-green.svg',
39
+ },
40
+ verified: '/svgs/badges/default-kick/verified.svg',
41
+ vip: '/svgs/badges/default-kick/vip.svg',
42
+ broadcaster: '/svgs/badges/default-kick/broadcaster.svg',
43
+ staff: '/svgs/badges/default-kick/staff.svg',
44
+ },
45
+ };
46
+
47
+ private public_listeners: Partial<EventCallbackFunctions> = {};
48
+
49
+ public socket?: Pusher;
50
+ public isConnected = false;
51
+
52
+ public setBadges(badges: BadgeURLsByNameOrCount) {
53
+ this.assets.badges = { ...this.assets.badges, ...badges };
54
+ }
55
+
56
+ public setExternalEmotes(external_emotes: EmoteURLsByName) {
57
+ this.assets.external_emotes = { ...this.assets.external_emotes, ...external_emotes };
58
+ }
59
+
60
+ public getStoredBadges() {
61
+ return this.assets.badges;
62
+ }
63
+
64
+ public getStoredExternalEmotes() {
65
+ return this.assets.external_emotes;
66
+ }
67
+
68
+ public async connect(channel?: { channelName?: string }) {
69
+ if (channel?.channelName) this.channel_name = channel.channelName;
70
+ if (!this.channel_name) return console.error('channel_name not specified');
71
+
72
+ console.log(`connecting to ${this.channel_name}...`);
73
+
74
+ const channel_response = await fetch(`https://kick.com/api/v2/channels/${this.channel_name}`, {
75
+ headers: {
76
+ accept: 'aplication/json',
77
+ 'user-agent':
78
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
79
+ },
80
+ })
81
+ .then((res) => res.json())
82
+ .then((json) => json as GetChannelResponse | undefined);
83
+
84
+ if (!channel_response) return console.error('Failed to connect to Kick.com chat');
85
+
86
+ channel_response.subscriber_badges.forEach((subscriber_badge) => {
87
+ this.assets.badges['subscriber'] = {
88
+ ...(this.assets.badges['subscriber'] ?? {}),
89
+ [subscriber_badge.months]: subscriber_badge.badge_image.src,
90
+ };
91
+ });
92
+
93
+ this.disconnect();
94
+
95
+ this.socket = new Pusher(this.kick_pusher_key, {
96
+ cluster: 'us2',
97
+ });
98
+
99
+ /*
100
+ | 'App\\Events\\ChatMessageEvent'
101
+ | 'App\\Events\\ChatroomClearEvent'
102
+ | 'App\\Events\\ChatroomUpdatedEvent'
103
+ | 'App\\Events\\GiftedSubscriptionsEvent'
104
+ | 'App\\Events\\MessageDeletedEvent'
105
+ | 'App\\Events\\PinnedMessageCreatedEvent'
106
+ | 'App\\Events\\PinnedMessageDeletedEvent'
107
+ | 'App\\Events\\PollDeleteEvent'
108
+ | 'App\\Events\\PollUpdateEvent'
109
+ | 'App\\Events\\StreamHostEvent'
110
+ | 'App\\Events\\SubscriptionEvent'
111
+ | 'App\\Events\\UserBannedEvent'
112
+ | 'App\\Events\\UserUnbannedEvent'
113
+ */
114
+ this.socket
115
+ .subscribe(`chatrooms.${channel_response.chatroom.id}.v2`)
116
+ .bind('pusher:subscription_succeeded', () => this.onSubscriptionSuccess())
117
+ .bind('App\\Events\\ChatMessageEvent', (data: ChatMessageEvent) => this.onChatMessage(data))
118
+ .bind('App\\Events\\SubscriptionEvent', (data: ChatSubscriptionEvent) =>
119
+ this.onChatSubscription(data),
120
+ )
121
+ .bind('App\\Events\\GiftedSubscriptionsEvent', (data: ChatGiftedEvent) =>
122
+ this.onChatGifted(data),
123
+ );
124
+ }
125
+
126
+ public disconnect() {
127
+ this.socket?.disconnect();
128
+ this.socket?.unbind_all();
129
+ }
130
+
131
+ public on<EventName extends EventNames>(
132
+ event_name: EventName,
133
+ callback_fn: EventCallbackFunctions[EventName],
134
+ ) {
135
+ this.public_listeners[event_name] = callback_fn;
136
+ }
137
+
138
+ private onSubscriptionSuccess() {
139
+ console.log(`Connected to Kick Pusher (${this.channel_name})`);
140
+ }
141
+
142
+ private onChatSubscription(data: ChatSubscriptionEvent) {
143
+ this.public_listeners.subscription?.(data);
144
+ }
145
+
146
+ private onChatGifted(data: ChatGiftedEvent) {
147
+ this.public_listeners.gifted?.(data);
148
+ }
149
+
150
+ private onChatMessage(data: ChatMessageEvent) {
151
+ this.public_listeners.raw_message?.(data);
152
+ const text = data.content;
153
+ const emote_matches = [...data.content.matchAll(/\[emote:\d+:[a-zA-Z0-9]*\]/g)];
154
+ console.log(emote_matches);
155
+
156
+ const body: BodyComponent[] = [];
157
+
158
+ emote_matches.forEach((match) => {
159
+ const emote_string = match[0];
160
+ const emote_parts = emote_string.slice(1, emote_string.length - 1).split(':');
161
+ const emote_id = emote_parts[1];
162
+ if (!emote_id) return;
163
+ console.log(emote_id);
164
+
165
+ body.push({
166
+ type: 'emote',
167
+ start_inclusive: match.index,
168
+ end_exclusive: match.index + emote_string.length,
169
+ url: `https://files.kick.com/emotes/${emote_id}/fullsize`,
170
+ });
171
+ });
172
+
173
+ body.sort((a, b) => a.start_inclusive - b.start_inclusive);
174
+
175
+ const old_body_length = body.length;
176
+
177
+ if (old_body_length > 0) {
178
+ body.forEach((segment, index) => {
179
+ const previous_segment = body[index - 1];
180
+
181
+ const text_start_inclusive =
182
+ previous_segment?.end_exclusive !== undefined ? previous_segment.end_exclusive + 1 : 0;
183
+ const text_end_exclusive = Math.max(0, segment.start_inclusive);
184
+
185
+ if (text_end_exclusive - text_start_inclusive > 0) {
186
+ body.push({
187
+ type: 'text',
188
+ text: text.slice(text_start_inclusive, text_end_exclusive),
189
+ start_inclusive: text_start_inclusive,
190
+ end_exclusive: text_end_exclusive,
191
+ });
192
+ }
193
+ if (index === old_body_length - 1 && segment.end_exclusive < text.length - 1) {
194
+ body.push({
195
+ type: 'text',
196
+ text: text.slice(segment.end_exclusive),
197
+ start_inclusive: segment.end_exclusive,
198
+ end_exclusive: text.length,
199
+ });
200
+ }
201
+ });
202
+ } else {
203
+ body.push({
204
+ type: 'text',
205
+ text,
206
+ start_inclusive: 0,
207
+ end_exclusive: text.length,
208
+ });
209
+ }
210
+
211
+ body.sort((a, b) => a.start_inclusive - b.start_inclusive);
212
+
213
+ this.public_listeners.message?.({
214
+ id: data.id,
215
+ user: {
216
+ id: `${data.sender.id}`,
217
+ username: data.sender.slug ?? data.sender.username.toLowerCase(),
218
+ display_name: data.sender.username,
219
+ roles: {},
220
+ color: data.sender.identity.color,
221
+ badges: data.sender.identity.badges.flatMap((badge) => {
222
+ let badge_url: string | undefined = undefined;
223
+
224
+ const badge_url_or_counts = this.assets.badges[badge.type];
225
+ const badge_count = badge.count;
226
+
227
+ if (typeof badge_url_or_counts === 'string') badge_url = badge_url_or_counts;
228
+ else if (typeof badge_url_or_counts === 'object' && badge_count !== undefined) {
229
+ const badge_entry_by_count = Object.entries(badge_url_or_counts)
230
+ .sort(([a_min_count], [b_min_count]) => Number(a_min_count) - Number(b_min_count))
231
+ .find(([min_count]) => badge_count >= Number(min_count));
232
+ if (badge_entry_by_count) badge_url = badge_entry_by_count[1];
233
+ }
234
+
235
+ if (!badge_url) return [];
236
+ return {
237
+ set_id: badge.type,
238
+ url: badge_url,
239
+ info: String(badge.count),
240
+ };
241
+ }),
242
+ },
243
+ body,
244
+ channel: {
245
+ room_id: String(data.chatroom_id),
246
+ name: this.channel_name ?? 'unknown',
247
+ },
248
+ raw_text: data.content,
249
+ timestamp_sent: Date.parse(data.created_at),
250
+ });
251
+ }
252
+ }
253
+
254
+ export interface GetChannelResponse {
255
+ id: number;
256
+ user_id: number;
257
+ slug: string;
258
+ is_banned: boolean;
259
+ playback_url?: string;
260
+ vod_enabled: boolean;
261
+ subscription_enabled: boolean;
262
+ followers_count: number;
263
+ following?: boolean;
264
+ subscription?: unknown;
265
+ subscriber_badges: Array<{
266
+ id: number;
267
+ channel_id: number;
268
+ months: number;
269
+ badge_image: {
270
+ srcset: string;
271
+ src: string;
272
+ };
273
+ }>;
274
+ banner_image?: {
275
+ url: string;
276
+ };
277
+ livestream?: ChannelLivestream;
278
+ role?: unknown;
279
+ muted: boolean;
280
+ follower_badges: unknown[];
281
+ offline_banner_image: unknown;
282
+ verified: boolean;
283
+ recent_categories: Array<{
284
+ id: number;
285
+ category_id: number;
286
+ name: string;
287
+ slug: string;
288
+ tags: string[];
289
+ description?: string;
290
+ deleted_at: unknown;
291
+ viewers: number;
292
+ banner: {
293
+ responsive: string;
294
+ url: string;
295
+ };
296
+ category: {
297
+ id: number;
298
+ name: string;
299
+ slug: string;
300
+ icon: string;
301
+ };
302
+ }>;
303
+ can_host: boolean;
304
+ user: {
305
+ id: number;
306
+ username: string;
307
+ agreed_to_terms: true;
308
+ email_verified_at: Date;
309
+ bio?: string;
310
+ country?: string;
311
+ state?: string;
312
+ city?: string;
313
+ instagram?: string;
314
+ twitter?: string;
315
+ youtube?: string;
316
+ discord?: string;
317
+ tiktok?: string;
318
+ facebook?: string;
319
+ profile_pic?: string;
320
+ };
321
+ chatroom: ChannelChatroom;
322
+ ascending_links?: Array<{
323
+ id: number;
324
+ channel_id: number;
325
+ description: string;
326
+ link: string;
327
+ created_at: Date;
328
+ updated_at: Date;
329
+ order: number;
330
+ title: string;
331
+ }>;
332
+ }
333
+
334
+ export interface ChannelLivestream {
335
+ id: number;
336
+ slug: string;
337
+ channel_id: number;
338
+ created_at: Date;
339
+ session_title: string;
340
+ is_live: boolean;
341
+ risk_level_id: unknown;
342
+ start_time: Date;
343
+ source: unknown;
344
+ twitch_channel: unknown;
345
+ duration: number;
346
+ language: string;
347
+ is_mature: boolean;
348
+ viewer_count: number;
349
+ thumbnail: {
350
+ url: string;
351
+ };
352
+ categories: Array<{
353
+ id: number;
354
+ category_id: number;
355
+ name: string;
356
+ slug: string;
357
+ tags: string[];
358
+ description?: string;
359
+ deleted_at: unknown;
360
+ viewers: number;
361
+ category: {
362
+ id: number;
363
+ name: string;
364
+ slug: string;
365
+ icon: string;
366
+ };
367
+ }>;
368
+ tags: unknown[];
369
+ }
370
+ export interface ChannelChatroom {
371
+ id: number;
372
+ chatable_type: string;
373
+ channel_id: string;
374
+ created_at: Date;
375
+ updated_at: Date;
376
+ chat_mode_old: string;
377
+ chat_mode: string;
378
+ slow_mode: boolean;
379
+ chatable_id: number;
380
+ followers_mode: boolean;
381
+ subscribers_mode: boolean;
382
+ emotes_mode: boolean;
383
+ message_interval: number;
384
+ following_min_duration: number;
385
+ }
386
+ export interface ChatMessageEvent {
387
+ id: string;
388
+ chatroom_id: number;
389
+ content: string;
390
+ type: string;
391
+ created_at: string;
392
+ sender: {
393
+ id: number;
394
+ username: string;
395
+ slug?: string;
396
+ identity: {
397
+ color: string;
398
+ badges: Array<{
399
+ type: string;
400
+ text: string;
401
+ count?: number;
402
+ }>;
403
+ };
404
+ };
405
+ }
406
+
407
+ export interface ChatSubscriptionEvent {
408
+ chatroom_id: string;
409
+ username: string;
410
+ months: number;
411
+ }
412
+
413
+ export interface ChatGiftedEvent {
414
+ chatroom_id: string;
415
+ gifted_usernames: string[];
416
+ gifter_username: string;
417
+ }