multichat-ts 0.0.94 → 0.0.95

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/twitch.d.ts CHANGED
@@ -6,24 +6,40 @@ type EventCallbackFunctions = {
6
6
  delete_message: (data: DeleteMessage) => unknown;
7
7
  event: (event: Event) => unknown;
8
8
  raw_message: (message: IRC_Message) => unknown;
9
+ connected: () => unknown;
10
+ auth_error: () => unknown;
9
11
  };
10
12
  type EventNames = keyof EventCallbackFunctions;
11
13
  export declare class TwitchIRC {
12
14
  channel_name?: string;
15
+ auth?: {
16
+ username: string;
17
+ token: string;
18
+ } | undefined;
19
+ bot_name?: string;
20
+ bot_token?: string;
13
21
  private assets;
14
22
  latency: number;
15
23
  private ping;
16
24
  private public_listeners;
17
25
  socket?: WebSocket;
18
- ws?: WebSocket | undefined;
19
- constructor(ws?: WebSocket);
26
+ wsCustom?: WebSocket | undefined;
27
+ constructor(options: {
28
+ ws?: WebSocket;
29
+ auth?: {
30
+ username: string;
31
+ token: string;
32
+ };
33
+ });
20
34
  setBadges(badges: BadgeURLsBySetIDOrSetIDAndVersion): void;
21
35
  setExternalEmotes(external_emotes: EmoteURLsByName): void;
22
36
  getStoredBadges(): BadgeURLsBySetIDOrSetIDAndVersion;
23
37
  getStoredExternalEmotes(): EmoteURLsByName;
38
+ authenticate(username: string, token: string): void;
24
39
  connect(channel?: {
25
40
  channelName?: string;
26
41
  }): void;
42
+ send(message: string, replyParentMessageId?: string): void;
27
43
  disconnect(): void;
28
44
  on<EventName extends EventNames>(event_name: EventName, callback_fn: EventCallbackFunctions[EventName]): void;
29
45
  isConnected(): this is {
@@ -34,7 +50,7 @@ export declare class TwitchIRC {
34
50
  private onOpen;
35
51
  private onClose;
36
52
  private sendPing;
37
- private send;
53
+ private sendIRC;
38
54
  private onMessage;
39
55
  }
40
56
  type IRC_Message = {
@@ -63,6 +79,7 @@ declare const RAW_TAGS: {
63
79
  readonly PART: readonly [];
64
80
  readonly PING: readonly [];
65
81
  readonly PONG: readonly [];
82
+ readonly '001': readonly [];
66
83
  readonly PRIVMSG: readonly ["badge-info", "badges", "bits", "color", "display-name", "emotes", "emote-only", "id", "mod", "custom-reward-id", "reply-thread-parent-display-name", "reply-thread-parent-user-id", "pinned-chat-paid-amount", "pinned-chat-paid-currency", "pinned-chat-paid-exponent", "pinned-chat-paid-level", "pinned-chat-paid-is-system-message", "reply-parent-msg-id", "reply-parent-user-id", "reply-parent-user-login", "reply-parent-display-name", "reply-parent-msg-body", "reply-thread-parent-msg-id", "reply-thread-parent-user-login", "room-id", "subscriber", "tmi-sent-ts", "turbo", "user-id", "user-type", "vip", "client-nonce", "first-msg", "flags", "returning-chatter"];
67
84
  readonly RECONNECT: readonly [];
68
85
  readonly ROOMSTATE: readonly ["emote-only", "followers-only", "r9k", "room-id", "slow", "subs-only"];
package/dist/twitch.js CHANGED
@@ -5,6 +5,9 @@ const PING_INTERVAL_MS = 30_000;
5
5
  const PING_TIMEOUT_MS = 10_000;
6
6
  export class TwitchIRC {
7
7
  channel_name;
8
+ auth;
9
+ bot_name;
10
+ bot_token;
8
11
  assets = {
9
12
  external_emotes: {},
10
13
  badges: {},
@@ -13,9 +16,10 @@ export class TwitchIRC {
13
16
  ping = {};
14
17
  public_listeners = {};
15
18
  socket;
16
- ws;
17
- constructor(ws) {
18
- this.ws = ws;
19
+ wsCustom;
20
+ constructor(options) {
21
+ this.wsCustom = options.ws;
22
+ this.auth = options.auth;
19
23
  }
20
24
  setBadges(badges) {
21
25
  this.assets.badges = { ...badges, ...this.assets.badges };
@@ -29,6 +33,10 @@ export class TwitchIRC {
29
33
  getStoredExternalEmotes() {
30
34
  return this.assets.external_emotes;
31
35
  }
36
+ authenticate(username, token) {
37
+ this.bot_name = username;
38
+ this.bot_token = token;
39
+ }
32
40
  connect(channel) {
33
41
  if (channel?.channelName)
34
42
  this.channel_name = channel.channelName;
@@ -37,13 +45,21 @@ export class TwitchIRC {
37
45
  console.log(`connecting to ${this.channel_name}...`);
38
46
  this.socket?.close();
39
47
  this.socket = new WebSocket('wss://irc-ws.chat.twitch.tv', null, {
40
- WebSocket: this.ws,
48
+ WebSocket: this.wsCustom,
41
49
  });
42
50
  this.socket.onopen = () => this.onOpen();
43
51
  this.socket.onclose = () => this.onClose();
44
52
  this.socket.onmessage = (event) => this.onMessage(event);
45
53
  }
54
+ send(message, replyParentMessageId) {
55
+ if (this.auth === undefined)
56
+ return console.error('No Auth Information');
57
+ if (!this.isConnected())
58
+ return console.error('Not Connected');
59
+ this.socket.send(`${replyParentMessageId ? `@reply-parent-msg-id=${replyParentMessageId} ` : ''}PRIVMSG #${this.channel_name} :${message}`);
60
+ }
46
61
  disconnect() {
62
+ this.socket?.send(`Part #${this.channel_name}`);
47
63
  this.socket?.close();
48
64
  }
49
65
  on(event_name, callback_fn) {
@@ -53,11 +69,18 @@ export class TwitchIRC {
53
69
  return !!this.socket && this.socket.readyState === WebSocket.OPEN;
54
70
  }
55
71
  onOpen() {
56
- this.send('CAP REQ :twitch.tv/commands twitch.tv/tags');
57
- this.send(`PASS ${ANONYMOUS_IRC_PASS}`);
58
- this.send(`NICK ${ANONYMOUS_IRC_LOGIN}`);
59
- this.send(`JOIN #${this.channel_name}`);
60
- console.log(`Connected to Twitch IRC as Anonymous (${this.channel_name})`);
72
+ this.sendIRC('CAP REQ :twitch.tv/commands twitch.tv/tags');
73
+ if (this.auth !== undefined) {
74
+ this.sendIRC(`PASS oauth:${this.auth.token}`);
75
+ this.sendIRC(`NICK ${this.auth.username}`);
76
+ this.sendIRC(`JOIN #${this.channel_name}`);
77
+ }
78
+ else {
79
+ this.sendIRC(`PASS ${ANONYMOUS_IRC_PASS}`);
80
+ this.sendIRC(`NICK ${ANONYMOUS_IRC_LOGIN}`);
81
+ this.sendIRC(`JOIN #${this.channel_name}`);
82
+ }
83
+ this.public_listeners.connected?.();
61
84
  if (this.ping.interval)
62
85
  clearInterval(this.ping.interval);
63
86
  this.sendPing();
@@ -68,7 +91,7 @@ export class TwitchIRC {
68
91
  clearTimeout(this.ping.timeout);
69
92
  }
70
93
  sendPing() {
71
- this.send('PING');
94
+ this.sendIRC('PING');
72
95
  this.ping.lastSentTimestamp = Date.now();
73
96
  if (this.ping.timeout)
74
97
  clearTimeout(this.ping.timeout);
@@ -77,10 +100,9 @@ export class TwitchIRC {
77
100
  this.connect();
78
101
  }, PING_TIMEOUT_MS);
79
102
  }
80
- send(irc_message) {
81
- if (!this.isConnected()) {
82
- throw new Error('Not connected');
83
- }
103
+ sendIRC(irc_message) {
104
+ if (!this.isConnected())
105
+ return console.error('Not Connected');
84
106
  this.socket?.send(irc_message);
85
107
  }
86
108
  onMessage(event) {
@@ -100,7 +122,21 @@ export class TwitchIRC {
100
122
  break;
101
123
  }
102
124
  case 'PING': {
103
- this.send('PONG');
125
+ this.sendIRC('PONG');
126
+ break;
127
+ }
128
+ case '001': {
129
+ console.log(`Connected to Twitch IRC as ${this.auth?.username ?? 'Anonymous'} (${this.channel_name})`);
130
+ this.public_listeners.connected?.();
131
+ break;
132
+ }
133
+ case 'NOTICE': {
134
+ if (message.params[0] === 'Login authentication failed' ||
135
+ message.params[0] === 'Improperly formatted auth') {
136
+ clearTimeout(this.ping.timeout);
137
+ this.disconnect();
138
+ this.public_listeners.auth_error?.();
139
+ }
104
140
  break;
105
141
  }
106
142
  case 'CLEARCHAT': {
@@ -249,7 +285,7 @@ function parseIRCLine(line) {
249
285
  const command = components[componentIndex];
250
286
  componentIndex++;
251
287
  let channel = '';
252
- if (components[componentIndex].startsWith('#'))
288
+ if (components[componentIndex]?.startsWith('#'))
253
289
  channel = components[componentIndex].slice(1);
254
290
  componentIndex++;
255
291
  const params = [];
@@ -317,6 +353,7 @@ const RAW_TAGS = {
317
353
  PART: [],
318
354
  PING: [],
319
355
  PONG: [],
356
+ '001': [],
320
357
  PRIVMSG: [
321
358
  'badge-info',
322
359
  'badges',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multichat-ts",
3
- "version": "0.0.94",
3
+ "version": "0.0.95",
4
4
  "type": "module",
5
5
  "description": "Receive type-safe realtime events for chat-related messages on multiple platforms (Twitch, Kick)",
6
6
  "repository": {
@@ -21,12 +21,18 @@ type EventCallbackFunctions = {
21
21
  delete_message: (data: DeleteMessage) => unknown;
22
22
  event: (event: Event) => unknown;
23
23
  raw_message: (message: IRC_Message) => unknown;
24
+ connected: () => unknown;
25
+ auth_error: () => unknown;
24
26
  };
25
27
 
26
28
  type EventNames = keyof EventCallbackFunctions;
27
29
 
28
30
  export class TwitchIRC {
29
31
  public channel_name?: string;
32
+ public auth?: { username: string; token: string } | undefined;
33
+ public bot_name?: string;
34
+ public bot_token?: string;
35
+
30
36
  private assets: {
31
37
  external_emotes: EmoteURLsByName;
32
38
  badges: BadgeURLsBySetIDOrSetIDAndVersion;
@@ -45,10 +51,11 @@ export class TwitchIRC {
45
51
  private public_listeners: Partial<EventCallbackFunctions> = {};
46
52
 
47
53
  public socket?: WebSocket;
48
- public ws?: WebSocket | undefined;
54
+ public wsCustom?: WebSocket | undefined;
49
55
 
50
- constructor(ws?: WebSocket) {
51
- this.ws = ws;
56
+ constructor(options: { ws?: WebSocket; auth?: { username: string; token: string } }) {
57
+ this.wsCustom = options.ws;
58
+ this.auth = options.auth;
52
59
  }
53
60
 
54
61
  public setBadges(badges: BadgeURLsBySetIDOrSetIDAndVersion) {
@@ -67,6 +74,11 @@ export class TwitchIRC {
67
74
  return this.assets.external_emotes;
68
75
  }
69
76
 
77
+ public authenticate(username: string, token: string) {
78
+ this.bot_name = username;
79
+ this.bot_token = token;
80
+ }
81
+
70
82
  public connect(channel?: { channelName?: string }) {
71
83
  if (channel?.channelName) this.channel_name = channel.channelName;
72
84
  if (!this.channel_name) return console.error('channel_name not specified');
@@ -76,14 +88,24 @@ export class TwitchIRC {
76
88
  this.socket?.close();
77
89
 
78
90
  this.socket = new WebSocket('wss://irc-ws.chat.twitch.tv', null, {
79
- WebSocket: this.ws,
91
+ WebSocket: this.wsCustom,
80
92
  });
81
93
  this.socket.onopen = () => this.onOpen();
82
94
  this.socket.onclose = () => this.onClose();
83
95
  this.socket.onmessage = (event) => this.onMessage(event);
84
96
  }
85
97
 
98
+ public send(message: string, replyParentMessageId?: string) {
99
+ if (this.auth === undefined) return console.error('No Auth Information');
100
+ if (!this.isConnected()) return console.error('Not Connected');
101
+
102
+ this.socket.send(
103
+ `${replyParentMessageId ? `@reply-parent-msg-id=${replyParentMessageId} ` : ''}PRIVMSG #${this.channel_name} :${message}`,
104
+ );
105
+ }
106
+
86
107
  public disconnect() {
108
+ this.socket?.send(`Part #${this.channel_name}`);
87
109
  this.socket?.close();
88
110
  }
89
111
 
@@ -99,12 +121,18 @@ export class TwitchIRC {
99
121
  }
100
122
 
101
123
  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}`);
124
+ this.sendIRC('CAP REQ :twitch.tv/commands twitch.tv/tags');
125
+ if (this.auth !== undefined) {
126
+ this.sendIRC(`PASS oauth:${this.auth.token}`);
127
+ this.sendIRC(`NICK ${this.auth.username}`);
128
+ this.sendIRC(`JOIN #${this.channel_name}`);
129
+ } else {
130
+ this.sendIRC(`PASS ${ANONYMOUS_IRC_PASS}`);
131
+ this.sendIRC(`NICK ${ANONYMOUS_IRC_LOGIN}`);
132
+ this.sendIRC(`JOIN #${this.channel_name}`);
133
+ }
106
134
 
107
- console.log(`Connected to Twitch IRC as Anonymous (${this.channel_name})`);
135
+ this.public_listeners.connected?.();
108
136
 
109
137
  if (this.ping.interval) clearInterval(this.ping.interval);
110
138
  this.sendPing();
@@ -117,7 +145,7 @@ export class TwitchIRC {
117
145
  }
118
146
 
119
147
  private sendPing() {
120
- this.send('PING');
148
+ this.sendIRC('PING');
121
149
  this.ping.lastSentTimestamp = Date.now();
122
150
 
123
151
  if (this.ping.timeout) clearTimeout(this.ping.timeout);
@@ -127,10 +155,9 @@ export class TwitchIRC {
127
155
  }, PING_TIMEOUT_MS);
128
156
  }
129
157
 
130
- private send(irc_message: string) {
131
- if (!this.isConnected()) {
132
- throw new Error('Not connected');
133
- }
158
+ private sendIRC(irc_message: string) {
159
+ if (!this.isConnected()) return console.error('Not Connected');
160
+
134
161
  this.socket?.send(irc_message);
135
162
  }
136
163
 
@@ -149,7 +176,26 @@ export class TwitchIRC {
149
176
  break;
150
177
  }
151
178
  case 'PING': {
152
- this.send('PONG');
179
+ this.sendIRC('PONG');
180
+ break;
181
+ }
182
+ case '001': {
183
+ console.log(
184
+ `Connected to Twitch IRC as ${this.auth?.username ?? 'Anonymous'} (${this.channel_name})`,
185
+ );
186
+
187
+ this.public_listeners.connected?.();
188
+ break;
189
+ }
190
+ case 'NOTICE': {
191
+ if (
192
+ message.params[0] === 'Login authentication failed' ||
193
+ message.params[0] === 'Improperly formatted auth'
194
+ ) {
195
+ clearTimeout(this.ping.timeout);
196
+ this.disconnect();
197
+ this.public_listeners.auth_error?.();
198
+ }
153
199
  break;
154
200
  }
155
201
  case 'CLEARCHAT': {
@@ -316,7 +362,7 @@ function parseIRCLine(line: string): IRC_Message {
316
362
  componentIndex++;
317
363
 
318
364
  let channel: string = '';
319
- if (components[componentIndex]!.startsWith('#')) channel = components[componentIndex]!.slice(1);
365
+ if (components[componentIndex]?.startsWith('#')) channel = components[componentIndex]!.slice(1);
320
366
  componentIndex++;
321
367
 
322
368
  const params: string[] = [];
@@ -430,6 +476,8 @@ const RAW_TAGS = {
430
476
 
431
477
  PONG: [],
432
478
 
479
+ '001': [],
480
+
433
481
  PRIVMSG: [
434
482
  'badge-info',
435
483
  'badges',