multichat-ts 0.0.92 → 0.0.94
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.js +1 -1
- package/dist/twitch.js +2 -2
- package/package.json +1 -1
- package/src/default/kick.ts +1 -1
- package/src/default/twitch.ts +547 -547
package/dist/kick.js
CHANGED
|
@@ -121,7 +121,7 @@ export class KickPusher {
|
|
|
121
121
|
onChatMessage(data) {
|
|
122
122
|
this.public_listeners.raw_message?.(data);
|
|
123
123
|
const text = data.content;
|
|
124
|
-
const emote_matches = [...data.content.matchAll(/\[emote:\d
|
|
124
|
+
const emote_matches = [...data.content.matchAll(/\[emote:\d+:.+\]/g)];
|
|
125
125
|
const body = [];
|
|
126
126
|
emote_matches.forEach((match) => {
|
|
127
127
|
const emote_string = match[0];
|
package/dist/twitch.js
CHANGED
|
@@ -120,7 +120,7 @@ export class TwitchIRC {
|
|
|
120
120
|
break;
|
|
121
121
|
}
|
|
122
122
|
case 'PRIVMSG': {
|
|
123
|
-
const { channel, params, tags } = message;
|
|
123
|
+
const { channel, params, tags, source } = message;
|
|
124
124
|
if (!tags || !tags['user-id'] || !tags['id'] || !tags['room-id'])
|
|
125
125
|
return;
|
|
126
126
|
const text = params[0];
|
|
@@ -215,7 +215,7 @@ export class TwitchIRC {
|
|
|
215
215
|
}) ?? [],
|
|
216
216
|
color: tags.color ?? '#FFFFFF',
|
|
217
217
|
id: tags['user-id'],
|
|
218
|
-
username:
|
|
218
|
+
username: source?.user ?? 'Unknown',
|
|
219
219
|
display_name: tags['display-name'] ?? 'Unknown',
|
|
220
220
|
roles: {
|
|
221
221
|
admin: tags['user-type'] === 'admin',
|
package/package.json
CHANGED
package/src/default/kick.ts
CHANGED
|
@@ -196,7 +196,7 @@ export class KickPusher {
|
|
|
196
196
|
private onChatMessage(data: ChatroomsV2Events['ChatMessageEvent']) {
|
|
197
197
|
this.public_listeners.raw_message?.(data);
|
|
198
198
|
const text = data.content;
|
|
199
|
-
const emote_matches = [...data.content.matchAll(/\[emote:\d
|
|
199
|
+
const emote_matches = [...data.content.matchAll(/\[emote:\d+:.+\]/g)];
|
|
200
200
|
|
|
201
201
|
const body: BodyComponent[] = [];
|
|
202
202
|
|
package/src/default/twitch.ts
CHANGED
|
@@ -1,547 +1,547 @@
|
|
|
1
|
-
import { WebSocket } from 'partysocket';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type BodyComponent,
|
|
5
|
-
type ClearMessages,
|
|
6
|
-
type DeleteMessage,
|
|
7
|
-
type Message,
|
|
8
|
-
type Event,
|
|
9
|
-
type EmoteURLsByName,
|
|
10
|
-
type BadgeURLsBySetIDOrSetIDAndVersion,
|
|
11
|
-
} from './index.js';
|
|
12
|
-
|
|
13
|
-
const ANONYMOUS_IRC_PASS = 'SCHMOOPIIE';
|
|
14
|
-
const ANONYMOUS_IRC_LOGIN = 'justinfan1234';
|
|
15
|
-
const PING_INTERVAL_MS = 30_000;
|
|
16
|
-
const PING_TIMEOUT_MS = 10_000;
|
|
17
|
-
|
|
18
|
-
type EventCallbackFunctions = {
|
|
19
|
-
message: (message: Message) => unknown;
|
|
20
|
-
clear_messages: (data: ClearMessages) => unknown;
|
|
21
|
-
delete_message: (data: DeleteMessage) => unknown;
|
|
22
|
-
event: (event: Event) => unknown;
|
|
23
|
-
raw_message: (message: IRC_Message) => unknown;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type EventNames = keyof EventCallbackFunctions;
|
|
27
|
-
|
|
28
|
-
export class TwitchIRC {
|
|
29
|
-
public channel_name?: string;
|
|
30
|
-
private assets: {
|
|
31
|
-
external_emotes: EmoteURLsByName;
|
|
32
|
-
badges: BadgeURLsBySetIDOrSetIDAndVersion;
|
|
33
|
-
} = {
|
|
34
|
-
external_emotes: {},
|
|
35
|
-
badges: {},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
public latency = 0;
|
|
39
|
-
private ping: {
|
|
40
|
-
interval?: ReturnType<typeof setInterval>;
|
|
41
|
-
lastSentTimestamp?: number | undefined;
|
|
42
|
-
timeout?: ReturnType<typeof setTimeout>;
|
|
43
|
-
} = {};
|
|
44
|
-
|
|
45
|
-
private public_listeners: Partial<EventCallbackFunctions> = {};
|
|
46
|
-
|
|
47
|
-
public socket?: WebSocket;
|
|
48
|
-
public ws?: WebSocket | undefined;
|
|
49
|
-
|
|
50
|
-
constructor(ws?: WebSocket) {
|
|
51
|
-
this.ws = ws;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
public setBadges(badges: BadgeURLsBySetIDOrSetIDAndVersion) {
|
|
55
|
-
this.assets.badges = { ...badges, ...this.assets.badges };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public setExternalEmotes(external_emotes: EmoteURLsByName) {
|
|
59
|
-
this.assets.external_emotes = { ...external_emotes, ...this.assets.external_emotes };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
public getStoredBadges() {
|
|
63
|
-
return this.assets.badges;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
public getStoredExternalEmotes() {
|
|
67
|
-
return this.assets.external_emotes;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public connect(channel?: { channelName?: string }) {
|
|
71
|
-
if (channel?.channelName) this.channel_name = channel.channelName;
|
|
72
|
-
if (!this.channel_name) return console.error('channel_name not specified');
|
|
73
|
-
|
|
74
|
-
console.log(`connecting to ${this.channel_name}...`);
|
|
75
|
-
|
|
76
|
-
this.socket?.close();
|
|
77
|
-
|
|
78
|
-
this.socket = new WebSocket('wss://irc-ws.chat.twitch.tv', null, {
|
|
79
|
-
WebSocket: this.ws,
|
|
80
|
-
});
|
|
81
|
-
this.socket.onopen = () => this.onOpen();
|
|
82
|
-
this.socket.onclose = () => this.onClose();
|
|
83
|
-
this.socket.onmessage = (event) => this.onMessage(event);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public disconnect() {
|
|
87
|
-
this.socket?.close();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public on<EventName extends EventNames>(
|
|
91
|
-
event_name: EventName,
|
|
92
|
-
callback_fn: EventCallbackFunctions[EventName],
|
|
93
|
-
) {
|
|
94
|
-
this.public_listeners[event_name] = callback_fn;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public isConnected(): this is { socket: { readyState: typeof WebSocket.OPEN } & WebSocket } {
|
|
98
|
-
return !!this.socket && this.socket.readyState === WebSocket.OPEN;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private onOpen() {
|
|
102
|
-
this.send('CAP REQ :twitch.tv/commands twitch.tv/tags');
|
|
103
|
-
this.send(`PASS ${ANONYMOUS_IRC_PASS}`);
|
|
104
|
-
this.send(`NICK ${ANONYMOUS_IRC_LOGIN}`);
|
|
105
|
-
this.send(`JOIN #${this.channel_name}`);
|
|
106
|
-
|
|
107
|
-
console.log(`Connected to Twitch IRC as Anonymous (${this.channel_name})`);
|
|
108
|
-
|
|
109
|
-
if (this.ping.interval) clearInterval(this.ping.interval);
|
|
110
|
-
this.sendPing();
|
|
111
|
-
this.ping.interval = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private onClose() {
|
|
115
|
-
clearInterval(this.ping.interval);
|
|
116
|
-
clearTimeout(this.ping.timeout);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private sendPing() {
|
|
120
|
-
this.send('PING');
|
|
121
|
-
this.ping.lastSentTimestamp = Date.now();
|
|
122
|
-
|
|
123
|
-
if (this.ping.timeout) clearTimeout(this.ping.timeout);
|
|
124
|
-
this.ping.timeout = setTimeout(() => {
|
|
125
|
-
console.error('PING Timeout, reconnecting...');
|
|
126
|
-
this.connect();
|
|
127
|
-
}, PING_TIMEOUT_MS);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private send(irc_message: string) {
|
|
131
|
-
if (!this.isConnected()) {
|
|
132
|
-
throw new Error('Not connected');
|
|
133
|
-
}
|
|
134
|
-
this.socket?.send(irc_message);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private onMessage(event: MessageEvent) {
|
|
138
|
-
if (!event.data) return;
|
|
139
|
-
const lines = (event.data as string).trim().split('\r\n');
|
|
140
|
-
const messages = lines.map(parseIRCLine);
|
|
141
|
-
messages.forEach((message) => {
|
|
142
|
-
this.public_listeners.raw_message?.(message);
|
|
143
|
-
switch (message.command) {
|
|
144
|
-
case 'PONG': {
|
|
145
|
-
if (!this.ping.lastSentTimestamp) return console.error('got PONG without sending PING');
|
|
146
|
-
clearInterval(this.ping.timeout);
|
|
147
|
-
this.latency = Date.now() - this.ping.lastSentTimestamp;
|
|
148
|
-
this.ping.lastSentTimestamp = undefined;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
case 'PING': {
|
|
152
|
-
this.send('PONG');
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
case 'CLEARCHAT': {
|
|
156
|
-
const { channel, tags } = message;
|
|
157
|
-
if (!tags) return;
|
|
158
|
-
|
|
159
|
-
this.public_listeners.clear_messages?.({
|
|
160
|
-
channel: {
|
|
161
|
-
name: channel,
|
|
162
|
-
room_id: tags['room-id'] ?? 'unknown',
|
|
163
|
-
},
|
|
164
|
-
timestamp_sent: Number(tags['tmi-sent-ts']),
|
|
165
|
-
timeout_duration_seconds: tags['ban-duration']
|
|
166
|
-
? Number(tags['ban-duration'])
|
|
167
|
-
: undefined,
|
|
168
|
-
});
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
case 'PRIVMSG': {
|
|
172
|
-
const { channel, params, tags } = message;
|
|
173
|
-
if (!tags || !tags['user-id'] || !tags['id'] || !tags['room-id']) return;
|
|
174
|
-
|
|
175
|
-
const text = params[0];
|
|
176
|
-
if (!text) return;
|
|
177
|
-
|
|
178
|
-
const body: BodyComponent[] = [];
|
|
179
|
-
|
|
180
|
-
tags.emotes?.split('/').forEach((raw_emote_string) => {
|
|
181
|
-
const [emote_id, raw_emote_positions_string] = raw_emote_string.split(':');
|
|
182
|
-
if (!emote_id || !raw_emote_positions_string) return;
|
|
183
|
-
|
|
184
|
-
const raw_emote_positions = raw_emote_positions_string.split(',');
|
|
185
|
-
raw_emote_positions.forEach((raw_emote_position) => {
|
|
186
|
-
const [emote_start, emote_end] = raw_emote_position.split('-');
|
|
187
|
-
if (!emote_start || !emote_end) return;
|
|
188
|
-
|
|
189
|
-
body.push({
|
|
190
|
-
type: 'emote',
|
|
191
|
-
start_inclusive: +emote_start,
|
|
192
|
-
end_exclusive: +emote_end + 1,
|
|
193
|
-
url: `https://static-cdn.jtvnw.net/emoticons/v2/${emote_id}/default/dark/1.0`,
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
body.sort((a, b) => a.start_inclusive - b.start_inclusive);
|
|
199
|
-
|
|
200
|
-
const old_body_length = body.length;
|
|
201
|
-
|
|
202
|
-
if (old_body_length > 0) {
|
|
203
|
-
body.forEach((segment, index) => {
|
|
204
|
-
const previous_segment = body[index - 1];
|
|
205
|
-
|
|
206
|
-
const text_start_inclusive =
|
|
207
|
-
previous_segment?.end_exclusive !== undefined
|
|
208
|
-
? previous_segment.end_exclusive + 1
|
|
209
|
-
: 0;
|
|
210
|
-
const text_end_exclusive = Math.max(0, segment.start_inclusive);
|
|
211
|
-
|
|
212
|
-
if (text_end_exclusive - text_start_inclusive > 0) {
|
|
213
|
-
body.push({
|
|
214
|
-
type: 'text',
|
|
215
|
-
text: text.slice(text_start_inclusive, text_end_exclusive),
|
|
216
|
-
start_inclusive: text_start_inclusive,
|
|
217
|
-
end_exclusive: text_end_exclusive,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
if (index === old_body_length - 1 && segment.end_exclusive < text.length - 1) {
|
|
221
|
-
body.push({
|
|
222
|
-
type: 'text',
|
|
223
|
-
text: text.slice(segment.end_exclusive),
|
|
224
|
-
start_inclusive: segment.end_exclusive,
|
|
225
|
-
end_exclusive: text.length,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
} else {
|
|
230
|
-
body.push({
|
|
231
|
-
type: 'text',
|
|
232
|
-
text,
|
|
233
|
-
start_inclusive: 0,
|
|
234
|
-
end_exclusive: text.length,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
body.sort((a, b) => a.start_inclusive - b.start_inclusive);
|
|
239
|
-
|
|
240
|
-
const badge_info: {
|
|
241
|
-
[set_id: string]: string;
|
|
242
|
-
} = {};
|
|
243
|
-
tags['badge-info']?.split(',').forEach((badge) => {
|
|
244
|
-
const [set_id, info] = badge.split('/');
|
|
245
|
-
if (!set_id || !info) return;
|
|
246
|
-
|
|
247
|
-
badge_info[set_id] = info;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
this.public_listeners.message?.({
|
|
251
|
-
body: body,
|
|
252
|
-
channel: {
|
|
253
|
-
room_id: tags['room-id'],
|
|
254
|
-
name: channel,
|
|
255
|
-
},
|
|
256
|
-
id: tags.id,
|
|
257
|
-
raw_text: text,
|
|
258
|
-
timestamp_sent: Number(tags['tmi-sent-ts']),
|
|
259
|
-
user: {
|
|
260
|
-
badges:
|
|
261
|
-
tags.badges?.split(',').flatMap((badge) => {
|
|
262
|
-
const [set_id, version] = badge.split('/');
|
|
263
|
-
if (!set_id || !version) return [];
|
|
264
|
-
|
|
265
|
-
const storedBadge = this.assets.badges[set_id];
|
|
266
|
-
if (!storedBadge) return [];
|
|
267
|
-
|
|
268
|
-
const url = typeof storedBadge === 'object' ? storedBadge[version] : storedBadge;
|
|
269
|
-
if (!url) return [];
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
info: badge_info[set_id],
|
|
273
|
-
set_id,
|
|
274
|
-
url,
|
|
275
|
-
};
|
|
276
|
-
}) ?? [],
|
|
277
|
-
color: tags.color ?? '#FFFFFF',
|
|
278
|
-
id: tags['user-id'],
|
|
279
|
-
username:
|
|
280
|
-
display_name: tags['display-name'] ?? 'Unknown',
|
|
281
|
-
roles: {
|
|
282
|
-
admin: tags['user-type'] === 'admin',
|
|
283
|
-
global_moderator: tags['user-type'] === 'global_mod',
|
|
284
|
-
staff: tags['user-type'] === 'staff',
|
|
285
|
-
turbo: tags.turbo === '1',
|
|
286
|
-
vip: tags.vip === '1',
|
|
287
|
-
moderator: tags.mod === '1',
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function parseIRCLine(line: string): IRC_Message {
|
|
299
|
-
const components = line.split(' ');
|
|
300
|
-
let componentIndex = 0;
|
|
301
|
-
|
|
302
|
-
let raw_tags_component: string | undefined;
|
|
303
|
-
let source: RawSource | undefined;
|
|
304
|
-
|
|
305
|
-
if (components[componentIndex]!.startsWith('@')) {
|
|
306
|
-
raw_tags_component = components[componentIndex]!.slice(1);
|
|
307
|
-
componentIndex++;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (components[componentIndex]!.startsWith(':')) {
|
|
311
|
-
source = parseSource(components[componentIndex]!.slice(1));
|
|
312
|
-
componentIndex++;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const command = components[componentIndex]! as CommandType;
|
|
316
|
-
componentIndex++;
|
|
317
|
-
|
|
318
|
-
let channel: string = '';
|
|
319
|
-
if (components[componentIndex]!.startsWith('#')) channel = components[componentIndex]!.slice(1);
|
|
320
|
-
componentIndex++;
|
|
321
|
-
|
|
322
|
-
const params: string[] = [];
|
|
323
|
-
while (components[componentIndex] !== undefined) {
|
|
324
|
-
const param = components[componentIndex]!;
|
|
325
|
-
if (param.startsWith(':')) {
|
|
326
|
-
params.push(components.slice(componentIndex).join(' ').slice(1));
|
|
327
|
-
componentIndex = -1;
|
|
328
|
-
} else {
|
|
329
|
-
params.push(param);
|
|
330
|
-
componentIndex++;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// TODO: handle message actions like "/me"
|
|
335
|
-
if (params[0])
|
|
336
|
-
params[0] = String.raw`${params[0]}`.replaceAll(
|
|
337
|
-
/.+ACTION (.*).+/g,
|
|
338
|
-
(_original, group) => group,
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const tags = raw_tags_component ? parseTags(raw_tags_component, command) : undefined;
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
channel,
|
|
345
|
-
command,
|
|
346
|
-
params,
|
|
347
|
-
source,
|
|
348
|
-
tags,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function parseTags(component: string, command: CommandType): AllRawTags {
|
|
353
|
-
// const tags = Object.fromEntries(RAW_TAGS[command].map((key) => [key, undefined])) as AllRawTags
|
|
354
|
-
const tags = {} as AllRawTags;
|
|
355
|
-
|
|
356
|
-
component.split(';').forEach((raw_tag) => {
|
|
357
|
-
const [key, value] = raw_tag.split('=') as [AllRawTagsKeys, string];
|
|
358
|
-
// console.log([key, value])
|
|
359
|
-
if (RAW_TAGS[command].findIndex((el) => el === key) === -1)
|
|
360
|
-
console.warn(`[${command}] Unknown Tag: ${raw_tag}`);
|
|
361
|
-
// if (value.length)
|
|
362
|
-
tags[key] = value;
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
return tags;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function parseSource(component: string): RawSource {
|
|
369
|
-
let user: string | undefined = undefined;
|
|
370
|
-
let host: string | undefined = component;
|
|
371
|
-
let nick: string | undefined = undefined;
|
|
372
|
-
|
|
373
|
-
if (component.includes('!')) [nick, host] = component.split('!');
|
|
374
|
-
if (host?.includes('@')) [user, host] = host.split('@');
|
|
375
|
-
|
|
376
|
-
return {
|
|
377
|
-
host: host ?? 'unknown',
|
|
378
|
-
nick,
|
|
379
|
-
user,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
type IRC_Message = {
|
|
384
|
-
[C in CommandType]: {
|
|
385
|
-
channel: string;
|
|
386
|
-
command: C;
|
|
387
|
-
params: string[];
|
|
388
|
-
source: RawSource | undefined;
|
|
389
|
-
tags: SpecificRawTags<C> | undefined;
|
|
390
|
-
};
|
|
391
|
-
}[CommandType];
|
|
392
|
-
|
|
393
|
-
type RAW_TAGS = typeof RAW_TAGS;
|
|
394
|
-
type CommandType = keyof RAW_TAGS;
|
|
395
|
-
type AllRawTagsKeys = RAW_TAGS[CommandType][number];
|
|
396
|
-
type AllRawTags = Record<AllRawTagsKeys, string | undefined>;
|
|
397
|
-
type SpecificRawTags<Command extends CommandType> = Record<
|
|
398
|
-
RAW_TAGS[Command][number],
|
|
399
|
-
string | undefined
|
|
400
|
-
>;
|
|
401
|
-
|
|
402
|
-
type RawSource = {
|
|
403
|
-
host: string;
|
|
404
|
-
nick?: string | undefined;
|
|
405
|
-
user?: string | undefined;
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const RAW_TAGS = {
|
|
409
|
-
CLEARCHAT: ['ban-duration', 'room-id', 'target-user-id', 'tmi-sent-ts'],
|
|
410
|
-
CLEARMSG: ['login', 'room-id', 'target-msg-id', 'tmi-sent-ts'],
|
|
411
|
-
|
|
412
|
-
GLOBALUSERSTATE: [
|
|
413
|
-
'badge-info',
|
|
414
|
-
'badges',
|
|
415
|
-
'color',
|
|
416
|
-
'display-name',
|
|
417
|
-
'emote-sets',
|
|
418
|
-
'turbo',
|
|
419
|
-
'user-id',
|
|
420
|
-
'user-type',
|
|
421
|
-
],
|
|
422
|
-
|
|
423
|
-
HOSTTARGET: [],
|
|
424
|
-
|
|
425
|
-
NOTICE: ['msg-id', 'target-user-id'],
|
|
426
|
-
|
|
427
|
-
PART: [],
|
|
428
|
-
|
|
429
|
-
PING: [],
|
|
430
|
-
|
|
431
|
-
PONG: [],
|
|
432
|
-
|
|
433
|
-
PRIVMSG: [
|
|
434
|
-
'badge-info',
|
|
435
|
-
'badges',
|
|
436
|
-
'bits',
|
|
437
|
-
'color',
|
|
438
|
-
'display-name',
|
|
439
|
-
'emotes',
|
|
440
|
-
'emote-only',
|
|
441
|
-
'id',
|
|
442
|
-
'mod',
|
|
443
|
-
'custom-reward-id',
|
|
444
|
-
'reply-thread-parent-display-name',
|
|
445
|
-
'reply-thread-parent-user-id',
|
|
446
|
-
'pinned-chat-paid-amount',
|
|
447
|
-
'pinned-chat-paid-currency',
|
|
448
|
-
'pinned-chat-paid-exponent',
|
|
449
|
-
'pinned-chat-paid-level',
|
|
450
|
-
'pinned-chat-paid-is-system-message',
|
|
451
|
-
'reply-parent-msg-id',
|
|
452
|
-
'reply-parent-user-id',
|
|
453
|
-
'reply-parent-user-login',
|
|
454
|
-
'reply-parent-display-name',
|
|
455
|
-
'reply-parent-msg-body',
|
|
456
|
-
'reply-thread-parent-msg-id',
|
|
457
|
-
'reply-thread-parent-user-login',
|
|
458
|
-
'room-id',
|
|
459
|
-
'subscriber',
|
|
460
|
-
'tmi-sent-ts',
|
|
461
|
-
'turbo',
|
|
462
|
-
'user-id',
|
|
463
|
-
'user-type',
|
|
464
|
-
'vip',
|
|
465
|
-
...[
|
|
466
|
-
// undocumented
|
|
467
|
-
'client-nonce',
|
|
468
|
-
'first-msg',
|
|
469
|
-
'flags',
|
|
470
|
-
'returning-chatter',
|
|
471
|
-
],
|
|
472
|
-
],
|
|
473
|
-
RECONNECT: [],
|
|
474
|
-
ROOMSTATE: ['emote-only', 'followers-only', 'r9k', 'room-id', 'slow', 'subs-only'],
|
|
475
|
-
USERNOTICE: [
|
|
476
|
-
'badge-info',
|
|
477
|
-
'badges',
|
|
478
|
-
'color',
|
|
479
|
-
'display-name',
|
|
480
|
-
'emotes',
|
|
481
|
-
'id',
|
|
482
|
-
'login',
|
|
483
|
-
'mod',
|
|
484
|
-
'msg-id',
|
|
485
|
-
'room-id',
|
|
486
|
-
'subscriber',
|
|
487
|
-
'system-msg',
|
|
488
|
-
'tmi-sent-ts',
|
|
489
|
-
'turbo',
|
|
490
|
-
'user-id',
|
|
491
|
-
'user-type',
|
|
492
|
-
'vip',
|
|
493
|
-
'flags',
|
|
494
|
-
...[
|
|
495
|
-
// Only subscription/raid related notices
|
|
496
|
-
'msg-param-cumulative-months',
|
|
497
|
-
'msg-param-displayName',
|
|
498
|
-
'msg-param-login',
|
|
499
|
-
'msg-param-multimonth-duration',
|
|
500
|
-
'msg-param-multimonth-tenure',
|
|
501
|
-
'msg-param-was-gifted=false',
|
|
502
|
-
'msg-param-months',
|
|
503
|
-
'msg-param-promo-gift-total',
|
|
504
|
-
'msg-param-promo-name',
|
|
505
|
-
'msg-param-recipient-display-name',
|
|
506
|
-
'msg-param-recipient-id',
|
|
507
|
-
'msg-param-recipient-user-name',
|
|
508
|
-
'msg-param-sender-login',
|
|
509
|
-
'msg-param-sender-name',
|
|
510
|
-
'msg-param-should-share-streak',
|
|
511
|
-
'msg-param-streak-months',
|
|
512
|
-
'msg-param-sub-plan',
|
|
513
|
-
'msg-param-sub-plan-name',
|
|
514
|
-
'msg-param-viewerCount',
|
|
515
|
-
'msg-param-ritual-name',
|
|
516
|
-
'msg-param-threshold',
|
|
517
|
-
'msg-param-gift-months',
|
|
518
|
-
'msg-param-was-gifted',
|
|
519
|
-
'msg-param-community-gift-id',
|
|
520
|
-
'msg-param-mass-gift-count',
|
|
521
|
-
'msg-param-origin-id',
|
|
522
|
-
],
|
|
523
|
-
],
|
|
524
|
-
USERSTATE: [
|
|
525
|
-
'badge-info',
|
|
526
|
-
'badges',
|
|
527
|
-
'color',
|
|
528
|
-
'display-name',
|
|
529
|
-
'emote-sets',
|
|
530
|
-
'id',
|
|
531
|
-
'mod',
|
|
532
|
-
'subscriber',
|
|
533
|
-
'turbo',
|
|
534
|
-
'user-type',
|
|
535
|
-
],
|
|
536
|
-
WHISPER: [
|
|
537
|
-
'badges',
|
|
538
|
-
'color',
|
|
539
|
-
'display-name',
|
|
540
|
-
'emotes',
|
|
541
|
-
'message-id',
|
|
542
|
-
'thread-id',
|
|
543
|
-
'turbo',
|
|
544
|
-
'user-id',
|
|
545
|
-
'user-type',
|
|
546
|
-
],
|
|
547
|
-
} as const;
|
|
1
|
+
import { WebSocket } from 'partysocket';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type BodyComponent,
|
|
5
|
+
type ClearMessages,
|
|
6
|
+
type DeleteMessage,
|
|
7
|
+
type Message,
|
|
8
|
+
type Event,
|
|
9
|
+
type EmoteURLsByName,
|
|
10
|
+
type BadgeURLsBySetIDOrSetIDAndVersion,
|
|
11
|
+
} from './index.js';
|
|
12
|
+
|
|
13
|
+
const ANONYMOUS_IRC_PASS = 'SCHMOOPIIE';
|
|
14
|
+
const ANONYMOUS_IRC_LOGIN = 'justinfan1234';
|
|
15
|
+
const PING_INTERVAL_MS = 30_000;
|
|
16
|
+
const PING_TIMEOUT_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
type EventCallbackFunctions = {
|
|
19
|
+
message: (message: Message) => unknown;
|
|
20
|
+
clear_messages: (data: ClearMessages) => unknown;
|
|
21
|
+
delete_message: (data: DeleteMessage) => unknown;
|
|
22
|
+
event: (event: Event) => unknown;
|
|
23
|
+
raw_message: (message: IRC_Message) => unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type EventNames = keyof EventCallbackFunctions;
|
|
27
|
+
|
|
28
|
+
export class TwitchIRC {
|
|
29
|
+
public channel_name?: string;
|
|
30
|
+
private assets: {
|
|
31
|
+
external_emotes: EmoteURLsByName;
|
|
32
|
+
badges: BadgeURLsBySetIDOrSetIDAndVersion;
|
|
33
|
+
} = {
|
|
34
|
+
external_emotes: {},
|
|
35
|
+
badges: {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
public latency = 0;
|
|
39
|
+
private ping: {
|
|
40
|
+
interval?: ReturnType<typeof setInterval>;
|
|
41
|
+
lastSentTimestamp?: number | undefined;
|
|
42
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
43
|
+
} = {};
|
|
44
|
+
|
|
45
|
+
private public_listeners: Partial<EventCallbackFunctions> = {};
|
|
46
|
+
|
|
47
|
+
public socket?: WebSocket;
|
|
48
|
+
public ws?: WebSocket | undefined;
|
|
49
|
+
|
|
50
|
+
constructor(ws?: WebSocket) {
|
|
51
|
+
this.ws = ws;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public setBadges(badges: BadgeURLsBySetIDOrSetIDAndVersion) {
|
|
55
|
+
this.assets.badges = { ...badges, ...this.assets.badges };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public setExternalEmotes(external_emotes: EmoteURLsByName) {
|
|
59
|
+
this.assets.external_emotes = { ...external_emotes, ...this.assets.external_emotes };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public getStoredBadges() {
|
|
63
|
+
return this.assets.badges;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getStoredExternalEmotes() {
|
|
67
|
+
return this.assets.external_emotes;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public connect(channel?: { channelName?: string }) {
|
|
71
|
+
if (channel?.channelName) this.channel_name = channel.channelName;
|
|
72
|
+
if (!this.channel_name) return console.error('channel_name not specified');
|
|
73
|
+
|
|
74
|
+
console.log(`connecting to ${this.channel_name}...`);
|
|
75
|
+
|
|
76
|
+
this.socket?.close();
|
|
77
|
+
|
|
78
|
+
this.socket = new WebSocket('wss://irc-ws.chat.twitch.tv', null, {
|
|
79
|
+
WebSocket: this.ws,
|
|
80
|
+
});
|
|
81
|
+
this.socket.onopen = () => this.onOpen();
|
|
82
|
+
this.socket.onclose = () => this.onClose();
|
|
83
|
+
this.socket.onmessage = (event) => this.onMessage(event);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public disconnect() {
|
|
87
|
+
this.socket?.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public on<EventName extends EventNames>(
|
|
91
|
+
event_name: EventName,
|
|
92
|
+
callback_fn: EventCallbackFunctions[EventName],
|
|
93
|
+
) {
|
|
94
|
+
this.public_listeners[event_name] = callback_fn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public isConnected(): this is { socket: { readyState: typeof WebSocket.OPEN } & WebSocket } {
|
|
98
|
+
return !!this.socket && this.socket.readyState === WebSocket.OPEN;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private onOpen() {
|
|
102
|
+
this.send('CAP REQ :twitch.tv/commands twitch.tv/tags');
|
|
103
|
+
this.send(`PASS ${ANONYMOUS_IRC_PASS}`);
|
|
104
|
+
this.send(`NICK ${ANONYMOUS_IRC_LOGIN}`);
|
|
105
|
+
this.send(`JOIN #${this.channel_name}`);
|
|
106
|
+
|
|
107
|
+
console.log(`Connected to Twitch IRC as Anonymous (${this.channel_name})`);
|
|
108
|
+
|
|
109
|
+
if (this.ping.interval) clearInterval(this.ping.interval);
|
|
110
|
+
this.sendPing();
|
|
111
|
+
this.ping.interval = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private onClose() {
|
|
115
|
+
clearInterval(this.ping.interval);
|
|
116
|
+
clearTimeout(this.ping.timeout);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private sendPing() {
|
|
120
|
+
this.send('PING');
|
|
121
|
+
this.ping.lastSentTimestamp = Date.now();
|
|
122
|
+
|
|
123
|
+
if (this.ping.timeout) clearTimeout(this.ping.timeout);
|
|
124
|
+
this.ping.timeout = setTimeout(() => {
|
|
125
|
+
console.error('PING Timeout, reconnecting...');
|
|
126
|
+
this.connect();
|
|
127
|
+
}, PING_TIMEOUT_MS);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private send(irc_message: string) {
|
|
131
|
+
if (!this.isConnected()) {
|
|
132
|
+
throw new Error('Not connected');
|
|
133
|
+
}
|
|
134
|
+
this.socket?.send(irc_message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private onMessage(event: MessageEvent) {
|
|
138
|
+
if (!event.data) return;
|
|
139
|
+
const lines = (event.data as string).trim().split('\r\n');
|
|
140
|
+
const messages = lines.map(parseIRCLine);
|
|
141
|
+
messages.forEach((message) => {
|
|
142
|
+
this.public_listeners.raw_message?.(message);
|
|
143
|
+
switch (message.command) {
|
|
144
|
+
case 'PONG': {
|
|
145
|
+
if (!this.ping.lastSentTimestamp) return console.error('got PONG without sending PING');
|
|
146
|
+
clearInterval(this.ping.timeout);
|
|
147
|
+
this.latency = Date.now() - this.ping.lastSentTimestamp;
|
|
148
|
+
this.ping.lastSentTimestamp = undefined;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'PING': {
|
|
152
|
+
this.send('PONG');
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'CLEARCHAT': {
|
|
156
|
+
const { channel, tags } = message;
|
|
157
|
+
if (!tags) return;
|
|
158
|
+
|
|
159
|
+
this.public_listeners.clear_messages?.({
|
|
160
|
+
channel: {
|
|
161
|
+
name: channel,
|
|
162
|
+
room_id: tags['room-id'] ?? 'unknown',
|
|
163
|
+
},
|
|
164
|
+
timestamp_sent: Number(tags['tmi-sent-ts']),
|
|
165
|
+
timeout_duration_seconds: tags['ban-duration']
|
|
166
|
+
? Number(tags['ban-duration'])
|
|
167
|
+
: undefined,
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'PRIVMSG': {
|
|
172
|
+
const { channel, params, tags, source } = message;
|
|
173
|
+
if (!tags || !tags['user-id'] || !tags['id'] || !tags['room-id']) return;
|
|
174
|
+
|
|
175
|
+
const text = params[0];
|
|
176
|
+
if (!text) return;
|
|
177
|
+
|
|
178
|
+
const body: BodyComponent[] = [];
|
|
179
|
+
|
|
180
|
+
tags.emotes?.split('/').forEach((raw_emote_string) => {
|
|
181
|
+
const [emote_id, raw_emote_positions_string] = raw_emote_string.split(':');
|
|
182
|
+
if (!emote_id || !raw_emote_positions_string) return;
|
|
183
|
+
|
|
184
|
+
const raw_emote_positions = raw_emote_positions_string.split(',');
|
|
185
|
+
raw_emote_positions.forEach((raw_emote_position) => {
|
|
186
|
+
const [emote_start, emote_end] = raw_emote_position.split('-');
|
|
187
|
+
if (!emote_start || !emote_end) return;
|
|
188
|
+
|
|
189
|
+
body.push({
|
|
190
|
+
type: 'emote',
|
|
191
|
+
start_inclusive: +emote_start,
|
|
192
|
+
end_exclusive: +emote_end + 1,
|
|
193
|
+
url: `https://static-cdn.jtvnw.net/emoticons/v2/${emote_id}/default/dark/1.0`,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
body.sort((a, b) => a.start_inclusive - b.start_inclusive);
|
|
199
|
+
|
|
200
|
+
const old_body_length = body.length;
|
|
201
|
+
|
|
202
|
+
if (old_body_length > 0) {
|
|
203
|
+
body.forEach((segment, index) => {
|
|
204
|
+
const previous_segment = body[index - 1];
|
|
205
|
+
|
|
206
|
+
const text_start_inclusive =
|
|
207
|
+
previous_segment?.end_exclusive !== undefined
|
|
208
|
+
? previous_segment.end_exclusive + 1
|
|
209
|
+
: 0;
|
|
210
|
+
const text_end_exclusive = Math.max(0, segment.start_inclusive);
|
|
211
|
+
|
|
212
|
+
if (text_end_exclusive - text_start_inclusive > 0) {
|
|
213
|
+
body.push({
|
|
214
|
+
type: 'text',
|
|
215
|
+
text: text.slice(text_start_inclusive, text_end_exclusive),
|
|
216
|
+
start_inclusive: text_start_inclusive,
|
|
217
|
+
end_exclusive: text_end_exclusive,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (index === old_body_length - 1 && segment.end_exclusive < text.length - 1) {
|
|
221
|
+
body.push({
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: text.slice(segment.end_exclusive),
|
|
224
|
+
start_inclusive: segment.end_exclusive,
|
|
225
|
+
end_exclusive: text.length,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
body.push({
|
|
231
|
+
type: 'text',
|
|
232
|
+
text,
|
|
233
|
+
start_inclusive: 0,
|
|
234
|
+
end_exclusive: text.length,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
body.sort((a, b) => a.start_inclusive - b.start_inclusive);
|
|
239
|
+
|
|
240
|
+
const badge_info: {
|
|
241
|
+
[set_id: string]: string;
|
|
242
|
+
} = {};
|
|
243
|
+
tags['badge-info']?.split(',').forEach((badge) => {
|
|
244
|
+
const [set_id, info] = badge.split('/');
|
|
245
|
+
if (!set_id || !info) return;
|
|
246
|
+
|
|
247
|
+
badge_info[set_id] = info;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this.public_listeners.message?.({
|
|
251
|
+
body: body,
|
|
252
|
+
channel: {
|
|
253
|
+
room_id: tags['room-id'],
|
|
254
|
+
name: channel,
|
|
255
|
+
},
|
|
256
|
+
id: tags.id,
|
|
257
|
+
raw_text: text,
|
|
258
|
+
timestamp_sent: Number(tags['tmi-sent-ts']),
|
|
259
|
+
user: {
|
|
260
|
+
badges:
|
|
261
|
+
tags.badges?.split(',').flatMap((badge) => {
|
|
262
|
+
const [set_id, version] = badge.split('/');
|
|
263
|
+
if (!set_id || !version) return [];
|
|
264
|
+
|
|
265
|
+
const storedBadge = this.assets.badges[set_id];
|
|
266
|
+
if (!storedBadge) return [];
|
|
267
|
+
|
|
268
|
+
const url = typeof storedBadge === 'object' ? storedBadge[version] : storedBadge;
|
|
269
|
+
if (!url) return [];
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
info: badge_info[set_id],
|
|
273
|
+
set_id,
|
|
274
|
+
url,
|
|
275
|
+
};
|
|
276
|
+
}) ?? [],
|
|
277
|
+
color: tags.color ?? '#FFFFFF',
|
|
278
|
+
id: tags['user-id'],
|
|
279
|
+
username: source?.user ?? 'Unknown',
|
|
280
|
+
display_name: tags['display-name'] ?? 'Unknown',
|
|
281
|
+
roles: {
|
|
282
|
+
admin: tags['user-type'] === 'admin',
|
|
283
|
+
global_moderator: tags['user-type'] === 'global_mod',
|
|
284
|
+
staff: tags['user-type'] === 'staff',
|
|
285
|
+
turbo: tags.turbo === '1',
|
|
286
|
+
vip: tags.vip === '1',
|
|
287
|
+
moderator: tags.mod === '1',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseIRCLine(line: string): IRC_Message {
|
|
299
|
+
const components = line.split(' ');
|
|
300
|
+
let componentIndex = 0;
|
|
301
|
+
|
|
302
|
+
let raw_tags_component: string | undefined;
|
|
303
|
+
let source: RawSource | undefined;
|
|
304
|
+
|
|
305
|
+
if (components[componentIndex]!.startsWith('@')) {
|
|
306
|
+
raw_tags_component = components[componentIndex]!.slice(1);
|
|
307
|
+
componentIndex++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (components[componentIndex]!.startsWith(':')) {
|
|
311
|
+
source = parseSource(components[componentIndex]!.slice(1));
|
|
312
|
+
componentIndex++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const command = components[componentIndex]! as CommandType;
|
|
316
|
+
componentIndex++;
|
|
317
|
+
|
|
318
|
+
let channel: string = '';
|
|
319
|
+
if (components[componentIndex]!.startsWith('#')) channel = components[componentIndex]!.slice(1);
|
|
320
|
+
componentIndex++;
|
|
321
|
+
|
|
322
|
+
const params: string[] = [];
|
|
323
|
+
while (components[componentIndex] !== undefined) {
|
|
324
|
+
const param = components[componentIndex]!;
|
|
325
|
+
if (param.startsWith(':')) {
|
|
326
|
+
params.push(components.slice(componentIndex).join(' ').slice(1));
|
|
327
|
+
componentIndex = -1;
|
|
328
|
+
} else {
|
|
329
|
+
params.push(param);
|
|
330
|
+
componentIndex++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// TODO: handle message actions like "/me"
|
|
335
|
+
if (params[0])
|
|
336
|
+
params[0] = String.raw`${params[0]}`.replaceAll(
|
|
337
|
+
/.+ACTION (.*).+/g,
|
|
338
|
+
(_original, group) => group,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const tags = raw_tags_component ? parseTags(raw_tags_component, command) : undefined;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
channel,
|
|
345
|
+
command,
|
|
346
|
+
params,
|
|
347
|
+
source,
|
|
348
|
+
tags,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseTags(component: string, command: CommandType): AllRawTags {
|
|
353
|
+
// const tags = Object.fromEntries(RAW_TAGS[command].map((key) => [key, undefined])) as AllRawTags
|
|
354
|
+
const tags = {} as AllRawTags;
|
|
355
|
+
|
|
356
|
+
component.split(';').forEach((raw_tag) => {
|
|
357
|
+
const [key, value] = raw_tag.split('=') as [AllRawTagsKeys, string];
|
|
358
|
+
// console.log([key, value])
|
|
359
|
+
if (RAW_TAGS[command].findIndex((el) => el === key) === -1)
|
|
360
|
+
console.warn(`[${command}] Unknown Tag: ${raw_tag}`);
|
|
361
|
+
// if (value.length)
|
|
362
|
+
tags[key] = value;
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return tags;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function parseSource(component: string): RawSource {
|
|
369
|
+
let user: string | undefined = undefined;
|
|
370
|
+
let host: string | undefined = component;
|
|
371
|
+
let nick: string | undefined = undefined;
|
|
372
|
+
|
|
373
|
+
if (component.includes('!')) [nick, host] = component.split('!');
|
|
374
|
+
if (host?.includes('@')) [user, host] = host.split('@');
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
host: host ?? 'unknown',
|
|
378
|
+
nick,
|
|
379
|
+
user,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
type IRC_Message = {
|
|
384
|
+
[C in CommandType]: {
|
|
385
|
+
channel: string;
|
|
386
|
+
command: C;
|
|
387
|
+
params: string[];
|
|
388
|
+
source: RawSource | undefined;
|
|
389
|
+
tags: SpecificRawTags<C> | undefined;
|
|
390
|
+
};
|
|
391
|
+
}[CommandType];
|
|
392
|
+
|
|
393
|
+
type RAW_TAGS = typeof RAW_TAGS;
|
|
394
|
+
type CommandType = keyof RAW_TAGS;
|
|
395
|
+
type AllRawTagsKeys = RAW_TAGS[CommandType][number];
|
|
396
|
+
type AllRawTags = Record<AllRawTagsKeys, string | undefined>;
|
|
397
|
+
type SpecificRawTags<Command extends CommandType> = Record<
|
|
398
|
+
RAW_TAGS[Command][number],
|
|
399
|
+
string | undefined
|
|
400
|
+
>;
|
|
401
|
+
|
|
402
|
+
type RawSource = {
|
|
403
|
+
host: string;
|
|
404
|
+
nick?: string | undefined;
|
|
405
|
+
user?: string | undefined;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const RAW_TAGS = {
|
|
409
|
+
CLEARCHAT: ['ban-duration', 'room-id', 'target-user-id', 'tmi-sent-ts'],
|
|
410
|
+
CLEARMSG: ['login', 'room-id', 'target-msg-id', 'tmi-sent-ts'],
|
|
411
|
+
|
|
412
|
+
GLOBALUSERSTATE: [
|
|
413
|
+
'badge-info',
|
|
414
|
+
'badges',
|
|
415
|
+
'color',
|
|
416
|
+
'display-name',
|
|
417
|
+
'emote-sets',
|
|
418
|
+
'turbo',
|
|
419
|
+
'user-id',
|
|
420
|
+
'user-type',
|
|
421
|
+
],
|
|
422
|
+
|
|
423
|
+
HOSTTARGET: [],
|
|
424
|
+
|
|
425
|
+
NOTICE: ['msg-id', 'target-user-id'],
|
|
426
|
+
|
|
427
|
+
PART: [],
|
|
428
|
+
|
|
429
|
+
PING: [],
|
|
430
|
+
|
|
431
|
+
PONG: [],
|
|
432
|
+
|
|
433
|
+
PRIVMSG: [
|
|
434
|
+
'badge-info',
|
|
435
|
+
'badges',
|
|
436
|
+
'bits',
|
|
437
|
+
'color',
|
|
438
|
+
'display-name',
|
|
439
|
+
'emotes',
|
|
440
|
+
'emote-only',
|
|
441
|
+
'id',
|
|
442
|
+
'mod',
|
|
443
|
+
'custom-reward-id',
|
|
444
|
+
'reply-thread-parent-display-name',
|
|
445
|
+
'reply-thread-parent-user-id',
|
|
446
|
+
'pinned-chat-paid-amount',
|
|
447
|
+
'pinned-chat-paid-currency',
|
|
448
|
+
'pinned-chat-paid-exponent',
|
|
449
|
+
'pinned-chat-paid-level',
|
|
450
|
+
'pinned-chat-paid-is-system-message',
|
|
451
|
+
'reply-parent-msg-id',
|
|
452
|
+
'reply-parent-user-id',
|
|
453
|
+
'reply-parent-user-login',
|
|
454
|
+
'reply-parent-display-name',
|
|
455
|
+
'reply-parent-msg-body',
|
|
456
|
+
'reply-thread-parent-msg-id',
|
|
457
|
+
'reply-thread-parent-user-login',
|
|
458
|
+
'room-id',
|
|
459
|
+
'subscriber',
|
|
460
|
+
'tmi-sent-ts',
|
|
461
|
+
'turbo',
|
|
462
|
+
'user-id',
|
|
463
|
+
'user-type',
|
|
464
|
+
'vip',
|
|
465
|
+
...[
|
|
466
|
+
// undocumented
|
|
467
|
+
'client-nonce',
|
|
468
|
+
'first-msg',
|
|
469
|
+
'flags',
|
|
470
|
+
'returning-chatter',
|
|
471
|
+
],
|
|
472
|
+
],
|
|
473
|
+
RECONNECT: [],
|
|
474
|
+
ROOMSTATE: ['emote-only', 'followers-only', 'r9k', 'room-id', 'slow', 'subs-only'],
|
|
475
|
+
USERNOTICE: [
|
|
476
|
+
'badge-info',
|
|
477
|
+
'badges',
|
|
478
|
+
'color',
|
|
479
|
+
'display-name',
|
|
480
|
+
'emotes',
|
|
481
|
+
'id',
|
|
482
|
+
'login',
|
|
483
|
+
'mod',
|
|
484
|
+
'msg-id',
|
|
485
|
+
'room-id',
|
|
486
|
+
'subscriber',
|
|
487
|
+
'system-msg',
|
|
488
|
+
'tmi-sent-ts',
|
|
489
|
+
'turbo',
|
|
490
|
+
'user-id',
|
|
491
|
+
'user-type',
|
|
492
|
+
'vip',
|
|
493
|
+
'flags',
|
|
494
|
+
...[
|
|
495
|
+
// Only subscription/raid related notices
|
|
496
|
+
'msg-param-cumulative-months',
|
|
497
|
+
'msg-param-displayName',
|
|
498
|
+
'msg-param-login',
|
|
499
|
+
'msg-param-multimonth-duration',
|
|
500
|
+
'msg-param-multimonth-tenure',
|
|
501
|
+
'msg-param-was-gifted=false',
|
|
502
|
+
'msg-param-months',
|
|
503
|
+
'msg-param-promo-gift-total',
|
|
504
|
+
'msg-param-promo-name',
|
|
505
|
+
'msg-param-recipient-display-name',
|
|
506
|
+
'msg-param-recipient-id',
|
|
507
|
+
'msg-param-recipient-user-name',
|
|
508
|
+
'msg-param-sender-login',
|
|
509
|
+
'msg-param-sender-name',
|
|
510
|
+
'msg-param-should-share-streak',
|
|
511
|
+
'msg-param-streak-months',
|
|
512
|
+
'msg-param-sub-plan',
|
|
513
|
+
'msg-param-sub-plan-name',
|
|
514
|
+
'msg-param-viewerCount',
|
|
515
|
+
'msg-param-ritual-name',
|
|
516
|
+
'msg-param-threshold',
|
|
517
|
+
'msg-param-gift-months',
|
|
518
|
+
'msg-param-was-gifted',
|
|
519
|
+
'msg-param-community-gift-id',
|
|
520
|
+
'msg-param-mass-gift-count',
|
|
521
|
+
'msg-param-origin-id',
|
|
522
|
+
],
|
|
523
|
+
],
|
|
524
|
+
USERSTATE: [
|
|
525
|
+
'badge-info',
|
|
526
|
+
'badges',
|
|
527
|
+
'color',
|
|
528
|
+
'display-name',
|
|
529
|
+
'emote-sets',
|
|
530
|
+
'id',
|
|
531
|
+
'mod',
|
|
532
|
+
'subscriber',
|
|
533
|
+
'turbo',
|
|
534
|
+
'user-type',
|
|
535
|
+
],
|
|
536
|
+
WHISPER: [
|
|
537
|
+
'badges',
|
|
538
|
+
'color',
|
|
539
|
+
'display-name',
|
|
540
|
+
'emotes',
|
|
541
|
+
'message-id',
|
|
542
|
+
'thread-id',
|
|
543
|
+
'turbo',
|
|
544
|
+
'user-id',
|
|
545
|
+
'user-type',
|
|
546
|
+
],
|
|
547
|
+
} as const;
|