multichat-ts 0.0.95 → 0.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kick.d.ts +5 -3
- package/dist/kick.js +202 -123
- package/dist/safety.d.ts +5 -0
- package/dist/safety.js +56 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +24 -0
- package/dist/twitch.d.ts +17 -35
- package/dist/twitch.js +334 -374
- package/package.json +1 -1
- package/src/default/kick.ts +277 -159
- package/src/default/safety.ts +72 -0
- package/src/default/test.ts +32 -0
- package/src/default/twitch.ts +411 -428
package/package.json
CHANGED
package/src/default/kick.ts
CHANGED
|
@@ -6,8 +6,15 @@ import {
|
|
|
6
6
|
type EmoteURLsByName,
|
|
7
7
|
type Message,
|
|
8
8
|
} from './index.js';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { buildMessageBody, isFiniteNumber, isRecord, runSafely } from './safety.js';
|
|
10
|
+
|
|
11
|
+
type ConnectionState =
|
|
12
|
+
| 'initialized'
|
|
13
|
+
| 'connecting'
|
|
14
|
+
| 'connected'
|
|
15
|
+
| 'unavailable'
|
|
16
|
+
| 'failed'
|
|
17
|
+
| 'disconnected';
|
|
11
18
|
type ConnectionStateEvent = { previous: ConnectionState; current: ConnectionState };
|
|
12
19
|
|
|
13
20
|
type EventCallbackFunctions = {
|
|
@@ -50,7 +57,7 @@ export class KickPusher {
|
|
|
50
57
|
|
|
51
58
|
private public_listeners: Partial<EventCallbackFunctions> = {};
|
|
52
59
|
|
|
53
|
-
public socket?: Pusher;
|
|
60
|
+
public socket?: Pusher | undefined;
|
|
54
61
|
public isConnected = false;
|
|
55
62
|
|
|
56
63
|
public setBadges(badges: BadgeURLsByNameOrCount) {
|
|
@@ -69,109 +76,143 @@ export class KickPusher {
|
|
|
69
76
|
return this.assets.external_emotes;
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
private connectionVersion = 0;
|
|
80
|
+
|
|
81
|
+
private closeSocket() {
|
|
82
|
+
const socket = this.socket;
|
|
83
|
+
|
|
84
|
+
this.socket = undefined;
|
|
85
|
+
this.isConnected = false;
|
|
86
|
+
|
|
87
|
+
if (!socket) return;
|
|
88
|
+
|
|
89
|
+
runSafely('kick.cleanup.connection', () => socket.connection.unbind_all());
|
|
90
|
+
|
|
91
|
+
runSafely('kick.cleanup.listeners', () => socket.unbind_all());
|
|
92
|
+
|
|
93
|
+
runSafely('kick.cleanup.disconnect', () => socket.disconnect());
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
public async connect(
|
|
73
97
|
channel?: { channelName?: string },
|
|
74
|
-
|
|
75
|
-
channelName,
|
|
76
|
-
) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
const json = await res.json();
|
|
85
|
-
return json as GetChannelResponse | undefined;
|
|
86
|
-
},
|
|
87
|
-
) {
|
|
88
|
-
if (channel?.channelName) this.channel_name = channel.channelName;
|
|
89
|
-
if (!this.channel_name) return console.error('channel_name not specified');
|
|
98
|
+
getChannel: (
|
|
99
|
+
channelName: string,
|
|
100
|
+
) => Promise<GetChannelResponse | undefined> = defaultGetChannel,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
if (channel?.channelName) {
|
|
103
|
+
this.channel_name = channel.channelName;
|
|
104
|
+
}
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
const channelName = this.channel_name;
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
if (!channelName) {
|
|
109
|
+
throw new Error('Kick channel_name not specified');
|
|
110
|
+
}
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
const version = ++this.connectionVersion;
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
this.assets.badges['subscriber'] = {
|
|
99
|
-
...(this.assets.badges['subscriber'] ?? {}),
|
|
100
|
-
[subscriber_badge.months]: subscriber_badge.badge_image.src,
|
|
101
|
-
};
|
|
102
|
-
});
|
|
114
|
+
this.closeSocket();
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
const response = await getChannel(channelName);
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| 'App\\Events\\StreamHostEvent'
|
|
120
|
-
| 'App\\Events\\SubscriptionEvent'
|
|
121
|
-
| 'App\\Events\\UserBannedEvent'
|
|
122
|
-
| 'App\\Events\\UserUnbannedEvent'
|
|
123
|
-
*/
|
|
124
|
-
this.socket.subscribe(`chatroom_${channel_response.chatroom.id}`);
|
|
125
|
-
this.socket.subscribe(`chatrooms.${channel_response.chatroom.id}.v2`);
|
|
126
|
-
|
|
127
|
-
this.socket
|
|
128
|
-
.bind('pusher:subscription_succeeded', () =>
|
|
129
|
-
this.onSubscriptionSuccess('pusher:subscription_succeeded'),
|
|
130
|
-
)
|
|
131
|
-
.bind('App\\Events\\ChatMessageEvent', (data: ChatroomsV2Events['ChatMessageEvent']) =>
|
|
132
|
-
this.onChatMessage(data),
|
|
133
|
-
)
|
|
134
|
-
.bind('App\\Events\\SubscriptionEvent', (data: ChatroomsV2Events['SubscriptionEvent']) =>
|
|
135
|
-
this.onChatSubscription(data),
|
|
136
|
-
)
|
|
137
|
-
.bind('GiftedSubscriptionsEvent', (data: ChatroomV1Events['GiftedSubscriptionsEvent']) =>
|
|
138
|
-
this.onChatGifted(data),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
this.socket.connection.bind('state_change', (state: ConnectionStateEvent) => {
|
|
142
|
-
switch (state.current) {
|
|
143
|
-
case 'connected': {
|
|
144
|
-
this.isConnected = true;
|
|
145
|
-
console.log(`Connected to Kick Pusher (${this.channel_name})!`);
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case 'connecting': {
|
|
149
|
-
this.isConnected = false;
|
|
150
|
-
console.log(`Connecting to Kick Pusher (${this.channel_name})...`);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case 'failed': {
|
|
154
|
-
this.isConnected = false;
|
|
155
|
-
console.log(`Failed to connect to Kick Pusher (${this.channel_name})`);
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
case 'unavailable': {
|
|
159
|
-
this.isConnected = false;
|
|
160
|
-
console.log(`Disconnected from Kick Pusher (${this.channel_name})`);
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
default: {
|
|
164
|
-
this.isConnected = false;
|
|
165
|
-
break;
|
|
118
|
+
// ignore a lookup superseded by connect() or disconnect().
|
|
119
|
+
if (version !== this.connectionVersion) return;
|
|
120
|
+
|
|
121
|
+
if (!response || !isFiniteNumber(response.chatroom?.id) || response.chatroom.id <= 0) {
|
|
122
|
+
throw new Error(`Invalid Kick channel response: ${channelName}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const subscriberBadges: Record<string, string> = {};
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(response.subscriber_badges)) {
|
|
128
|
+
for (const badge of response.subscriber_badges) {
|
|
129
|
+
if (badge && isFiniteNumber(badge.months) && typeof badge.badge_image?.src === 'string') {
|
|
130
|
+
subscriberBadges[badge.months] = badge.badge_image.src;
|
|
166
131
|
}
|
|
167
132
|
}
|
|
168
|
-
|
|
169
|
-
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Replace channel-specific badges rather than retaining old ones.
|
|
136
|
+
this.assets.badges['subscriber'] = subscriberBadges;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const socket = new Pusher(this.kick_pusher_key, {
|
|
140
|
+
cluster: 'us2',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.socket = socket;
|
|
144
|
+
|
|
145
|
+
const bind = (event: string, handler: (data: unknown) => unknown) => {
|
|
146
|
+
socket.bind(event, (data: unknown) => {
|
|
147
|
+
if (this.socket !== socket) return;
|
|
148
|
+
|
|
149
|
+
runSafely(`kick.${event}`, () => handler(data));
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
bind('App\\Events\\ChatMessageEvent', (data) => this.onChatMessage(data));
|
|
154
|
+
|
|
155
|
+
bind('App\\Events\\SubscriptionEvent', (data) => this.onChatSubscription(data));
|
|
156
|
+
|
|
157
|
+
bind('GiftedSubscriptionsEvent', (data) => this.onChatGifted(data));
|
|
158
|
+
|
|
159
|
+
socket.connection.bind('state_change', (state: ConnectionStateEvent) => {
|
|
160
|
+
if (this.socket !== socket) return;
|
|
161
|
+
|
|
162
|
+
runSafely('kick.state_change', () => {
|
|
163
|
+
if (!state || typeof state.current !== 'string') {
|
|
164
|
+
throw new Error('Invalid Pusher state-change payload');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.isConnected = state.current === 'connected';
|
|
168
|
+
|
|
169
|
+
runSafely('kick.connection_state_changed', () =>
|
|
170
|
+
this.public_listeners.connection_state_changed?.(state),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
socket.connection.bind('error', (error: unknown) => {
|
|
176
|
+
if (this.socket !== socket) return;
|
|
177
|
+
|
|
178
|
+
console.error('Kick Pusher connection error', {
|
|
179
|
+
channelName,
|
|
180
|
+
error,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const names = [`chatroom_${response.chatroom.id}`, `chatrooms.${response.chatroom.id}.v2`];
|
|
185
|
+
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
const subscription = socket.subscribe(name);
|
|
188
|
+
|
|
189
|
+
subscription.bind('pusher:subscription_succeeded', () => {
|
|
190
|
+
if (this.socket !== socket) return;
|
|
191
|
+
|
|
192
|
+
runSafely('kick.subscription_succeeded', () => this.onSubscriptionSuccess(name));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
subscription.bind('pusher:subscription_error', (error: unknown) => {
|
|
196
|
+
if (this.socket !== socket) return;
|
|
197
|
+
|
|
198
|
+
console.error('Kick Pusher subscription error', {
|
|
199
|
+
channelName,
|
|
200
|
+
subscription: name,
|
|
201
|
+
error,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.isConnected = socket.connection.state === 'connected';
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.closeSocket();
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
170
211
|
}
|
|
171
212
|
|
|
172
|
-
public disconnect() {
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
213
|
+
public disconnect(): void {
|
|
214
|
+
++this.connectionVersion;
|
|
215
|
+
this.closeSocket();
|
|
175
216
|
}
|
|
176
217
|
|
|
177
218
|
public on<EventName extends EventNames>(
|
|
@@ -185,74 +226,56 @@ export class KickPusher {
|
|
|
185
226
|
console.log(`Subscribed to Channel on Kick Pusher (${channel})`);
|
|
186
227
|
}
|
|
187
228
|
|
|
188
|
-
private onChatSubscription(data:
|
|
189
|
-
|
|
190
|
-
|
|
229
|
+
private onChatSubscription(data: unknown) {
|
|
230
|
+
if (!isSubscription(data)) {
|
|
231
|
+
console.warn('Ignoring invalid Kick subscription payload', JSON.stringify(data));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
191
234
|
|
|
192
|
-
|
|
193
|
-
|
|
235
|
+
runSafely('kick.subscription', () => {
|
|
236
|
+
this.public_listeners.subscription?.(data);
|
|
237
|
+
});
|
|
194
238
|
}
|
|
195
239
|
|
|
196
|
-
private
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
240
|
+
private onChatGifted(data: unknown) {
|
|
241
|
+
if (!isGifted(data)) {
|
|
242
|
+
console.warn('Ignoring invalid Kick gifted payload', JSON.stringify(data));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
runSafely('kick.gifted', () => {
|
|
246
|
+
this.public_listeners.gifted?.(data);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
202
249
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
250
|
+
private onChatMessage(data: unknown) {
|
|
251
|
+
if (!isChatMessage(data)) {
|
|
252
|
+
console.warn('Ignoring invalid Kick chat payload: ', JSON.stringify(data));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
208
255
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
start_inclusive: match.index,
|
|
212
|
-
end_exclusive: match.index + emote_string.length,
|
|
213
|
-
url: `https://files.kick.com/emotes/${emote_id}/fullsize`,
|
|
214
|
-
});
|
|
256
|
+
runSafely('kick.raw_message', () => {
|
|
257
|
+
this.public_listeners.raw_message?.(data);
|
|
215
258
|
});
|
|
216
259
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const old_body_length = body.length;
|
|
260
|
+
const text = data.content;
|
|
261
|
+
const emotes: BodyComponent[] = [];
|
|
220
262
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
263
|
+
for (const match of text.matchAll(/\[emote:(\d+):[^\]]+\]/g)) {
|
|
264
|
+
const id = match[1];
|
|
265
|
+
const start = match.index;
|
|
224
266
|
|
|
225
|
-
|
|
226
|
-
previous_segment?.end_exclusive !== undefined ? previous_segment.end_exclusive + 1 : 0;
|
|
227
|
-
const text_end_exclusive = Math.max(0, segment.start_inclusive);
|
|
267
|
+
if (!id || start === undefined) continue;
|
|
228
268
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
end_exclusive: text_end_exclusive,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
if (index === old_body_length - 1 && segment.end_exclusive < text.length - 1) {
|
|
238
|
-
body.push({
|
|
239
|
-
type: 'text',
|
|
240
|
-
text: text.slice(segment.end_exclusive),
|
|
241
|
-
start_inclusive: segment.end_exclusive,
|
|
242
|
-
end_exclusive: text.length,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
} else {
|
|
247
|
-
body.push({
|
|
248
|
-
type: 'text',
|
|
249
|
-
text,
|
|
250
|
-
start_inclusive: 0,
|
|
251
|
-
end_exclusive: text.length,
|
|
269
|
+
emotes.push({
|
|
270
|
+
type: 'emote',
|
|
271
|
+
start_inclusive: start,
|
|
272
|
+
end_exclusive: start + match[0].length,
|
|
273
|
+
url: `https://files.kick.com/emotes/${id}/fullsize`,
|
|
252
274
|
});
|
|
253
275
|
}
|
|
254
276
|
|
|
255
|
-
body
|
|
277
|
+
const body = buildMessageBody(text, emotes);
|
|
278
|
+
|
|
256
279
|
const message: Message = {
|
|
257
280
|
id: data.id,
|
|
258
281
|
user: {
|
|
@@ -268,11 +291,19 @@ export class KickPusher {
|
|
|
268
291
|
const badge_count = badge.count;
|
|
269
292
|
|
|
270
293
|
if (typeof badge_url_or_counts === 'string') badge_url = badge_url_or_counts;
|
|
271
|
-
else if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
294
|
+
else if (
|
|
295
|
+
badge_url_or_counts !== null &&
|
|
296
|
+
typeof badge_url_or_counts === 'object' &&
|
|
297
|
+
isFiniteNumber(badge_count)
|
|
298
|
+
) {
|
|
299
|
+
const entry = Object.entries(badge_url_or_counts)
|
|
300
|
+
.filter(
|
|
301
|
+
([minimum, url]) => Number.isFinite(Number(minimum)) && typeof url === 'string',
|
|
302
|
+
)
|
|
303
|
+
.sort(([a], [b]) => Number(b) - Number(a))
|
|
304
|
+
.find(([minimum]) => badge_count >= Number(minimum));
|
|
305
|
+
|
|
306
|
+
badge_url = entry?.[1];
|
|
276
307
|
}
|
|
277
308
|
|
|
278
309
|
if (!badge_url) return [];
|
|
@@ -292,17 +323,104 @@ export class KickPusher {
|
|
|
292
323
|
timestamp_sent: Date.parse(data.created_at),
|
|
293
324
|
};
|
|
294
325
|
|
|
295
|
-
|
|
326
|
+
const metadata: unknown = data.metadata;
|
|
327
|
+
const celebration = isRecord(metadata) ? metadata['celebration'] : undefined;
|
|
328
|
+
|
|
329
|
+
if (
|
|
330
|
+
data.type === 'celebration' &&
|
|
331
|
+
isRecord(celebration) &&
|
|
332
|
+
typeof celebration['id'] === 'string' &&
|
|
333
|
+
isFiniteNumber(celebration['total_months']) &&
|
|
334
|
+
celebration['total_months'] > 0 &&
|
|
335
|
+
typeof celebration['created_at'] === 'string' &&
|
|
336
|
+
Number.isFinite(Date.parse(celebration['created_at']))
|
|
337
|
+
) {
|
|
296
338
|
message.resubscription = {
|
|
297
|
-
id:
|
|
298
|
-
months:
|
|
299
|
-
subscribed_since_timestamp:
|
|
339
|
+
id: celebration['id'],
|
|
340
|
+
months: celebration['total_months'],
|
|
341
|
+
subscribed_since_timestamp: celebration['created_at'],
|
|
300
342
|
};
|
|
301
343
|
}
|
|
302
|
-
|
|
344
|
+
runSafely('kick.message', () => {
|
|
345
|
+
this.public_listeners.message?.(message);
|
|
346
|
+
});
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
349
|
|
|
350
|
+
async function defaultGetChannel(channelName: string): Promise<GetChannelResponse | undefined> {
|
|
351
|
+
const url = 'https://kick.com/api/v2/channels/' + encodeURIComponent(channelName);
|
|
352
|
+
|
|
353
|
+
const response = await fetch(url, {
|
|
354
|
+
headers: {
|
|
355
|
+
accept: 'application/json',
|
|
356
|
+
},
|
|
357
|
+
signal: AbortSignal.timeout(15_000),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
throw new Error(`Kick channel lookup HTTP ${response.status}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return (await response.json()) as GetChannelResponse;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isChatMessage(value: unknown): value is ChatMessageEvent {
|
|
368
|
+
if (!isRecord(value)) return false;
|
|
369
|
+
|
|
370
|
+
const sender = value['sender'];
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
typeof value['id'] !== 'string' ||
|
|
374
|
+
!isFiniteNumber(value['chatroom_id']) ||
|
|
375
|
+
typeof value['content'] !== 'string' ||
|
|
376
|
+
typeof value['created_at'] !== 'string' ||
|
|
377
|
+
!Number.isFinite(Date.parse(value['created_at'])) ||
|
|
378
|
+
!['message', 'celebration', 'reply'].includes(String(value['type'])) ||
|
|
379
|
+
!isRecord(sender) ||
|
|
380
|
+
!isFiniteNumber(sender['id']) ||
|
|
381
|
+
typeof sender['username'] !== 'string' ||
|
|
382
|
+
(sender['slug'] !== undefined && typeof sender['slug'] !== 'string')
|
|
383
|
+
) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const identity = sender['identity'];
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
isRecord(identity) &&
|
|
391
|
+
typeof identity['color'] === 'string' &&
|
|
392
|
+
Array.isArray(identity['badges']) &&
|
|
393
|
+
identity['badges'].every(
|
|
394
|
+
(badge) =>
|
|
395
|
+
isRecord(badge) &&
|
|
396
|
+
typeof badge['type'] === 'string' &&
|
|
397
|
+
typeof badge['text'] === 'string' &&
|
|
398
|
+
(badge['count'] === undefined || isFiniteNumber(badge['count'])),
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isSubscription(value: unknown): value is ChatroomsV2Events['SubscriptionEvent'] {
|
|
404
|
+
return (
|
|
405
|
+
isRecord(value) &&
|
|
406
|
+
isFiniteNumber(value['chatroom_id']) &&
|
|
407
|
+
typeof value['username'] === 'string' &&
|
|
408
|
+
isFiniteNumber(value['months']) &&
|
|
409
|
+
value['months'] > 0
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isGifted(value: unknown): value is ChatroomV1Events['GiftedSubscriptionsEvent'] {
|
|
414
|
+
return (
|
|
415
|
+
isRecord(value) &&
|
|
416
|
+
isFiniteNumber(value['chatroom_id']) &&
|
|
417
|
+
typeof value['gifter_username'] === 'string' &&
|
|
418
|
+
isFiniteNumber(value['gifter_total']) &&
|
|
419
|
+
Array.isArray(value['gifted_usernames']) &&
|
|
420
|
+
value['gifted_usernames'].every((name) => typeof name === 'string')
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
306
424
|
export interface GetChannelResponse {
|
|
307
425
|
id: number;
|
|
308
426
|
user_id: number;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BodyComponent } from '.';
|
|
2
|
+
|
|
3
|
+
export function runSafely(context: string, callback: () => unknown) {
|
|
4
|
+
try {
|
|
5
|
+
const result = callback();
|
|
6
|
+
|
|
7
|
+
if (
|
|
8
|
+
result !== null &&
|
|
9
|
+
result !== undefined &&
|
|
10
|
+
typeof (result as PromiseLike<unknown>).then === 'function'
|
|
11
|
+
) {
|
|
12
|
+
void Promise.resolve(result).catch((error) => {
|
|
13
|
+
console.error(`[${context}] Async failure`, error);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(`[${context}] Failure`, error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildMessageBody(text: string, emotes: BodyComponent[]): BodyComponent[] {
|
|
30
|
+
const sorted = [...emotes].sort((a, b) => a.start_inclusive - b.start_inclusive);
|
|
31
|
+
|
|
32
|
+
const body: BodyComponent[] = [];
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
|
|
35
|
+
for (const emote of sorted) {
|
|
36
|
+
const start = emote.start_inclusive;
|
|
37
|
+
const end = emote.end_exclusive;
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
!Number.isInteger(start) ||
|
|
41
|
+
!Number.isInteger(end) ||
|
|
42
|
+
start < cursor ||
|
|
43
|
+
end <= start ||
|
|
44
|
+
end > text.length
|
|
45
|
+
) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (start > cursor) {
|
|
50
|
+
body.push({
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: text.slice(cursor, start),
|
|
53
|
+
start_inclusive: cursor,
|
|
54
|
+
end_exclusive: start,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body.push(emote);
|
|
59
|
+
cursor = end;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (cursor < text.length || body.length === 0) {
|
|
63
|
+
body.push({
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: text.slice(cursor),
|
|
66
|
+
start_inclusive: cursor,
|
|
67
|
+
end_exclusive: text.length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return body;
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { format } from 'util';
|
|
2
|
+
import { KickPusher } from './kick.js';
|
|
3
|
+
|
|
4
|
+
const originalError = console.error.bind(console);
|
|
5
|
+
const originalWarn = console.warn.bind(console);
|
|
6
|
+
|
|
7
|
+
console.warn = (...args) => {
|
|
8
|
+
originalWarn(`\x1b[33m${format(...args)}\x1b[0m`);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
console.error = (...args) => {
|
|
12
|
+
originalError(`\x1b[31m${format(...args)}\x1b[0m`);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const client = new KickPusher();
|
|
16
|
+
client.on('subscription', (data) => {
|
|
17
|
+
console.log(JSON.stringify(data));
|
|
18
|
+
});
|
|
19
|
+
client.on('connection_state_changed', (data) => {
|
|
20
|
+
console.log(JSON.stringify(data));
|
|
21
|
+
});
|
|
22
|
+
client.on('gifted', (data) => {
|
|
23
|
+
console.log(JSON.stringify(data));
|
|
24
|
+
});
|
|
25
|
+
client.on('message', (data) => {
|
|
26
|
+
console.log(JSON.stringify(data));
|
|
27
|
+
});
|
|
28
|
+
// client.on('raw_message', data => {
|
|
29
|
+
// console.log(JSON.stringify(data))
|
|
30
|
+
// })
|
|
31
|
+
|
|
32
|
+
client.connect({ channelName: 'drb7h' });
|