mirror of
https://codeberg.org/SafeTwitch/safetwitch-backend.git
synced 2025-02-12 14:48:14 -05:00
169 lines
5.5 KiB
TypeScript
169 lines
5.5 KiB
TypeScript
import { EventEmitter } from 'stream';
|
|
import WebSocket from 'ws';
|
|
import { TwitchChatOptions, Metadata, MessageType, MessageTypes } from '../../../types/scraping/Chat';
|
|
import { parseUsername } from './utils';
|
|
|
|
export declare interface TwitchChat {
|
|
on(event: 'PRIVMSG', listener: (username: string, messageType: MessageType, channel: string, message: string, tags: Record<string,string>) => void): this;
|
|
on(event: 'CLEARCHAT', listener: (channel: string, username: string, tags: Record<string,string>) => void): this;
|
|
on(event: 'CLEARMSG', listener: (channel: string, messageID: string, tags: Record<string,string>) => void): this;
|
|
on(event: 'GLOBALUSERSTATE', listener: () => void): this;
|
|
on(event: 'HOSTTARGET', listener: (channel: string, viewers: number, targetChannel: string, tags: Record<string,string>) => void): this;
|
|
on(event: 'NOTICE', listener: (channel: string, message: string, messageType: MessageType, tags: Record<string,string>) => void): this;
|
|
on(event: 'RECONNECT', listener: () => void): this;
|
|
on(event: 'ROOMSTATE', listener: (channel: string, isLive: boolean, tags: Record<string, string>) => void): this;
|
|
on(event: 'USERNOTICE', listener: (channel: string, messageType: MessageType, message: string, tags: Record<string, string>) => void): this;
|
|
on(event: 'USERSTATE', listener: (channel: string, tags: Record<string, string>) => void): this;
|
|
on(event: 'WHISPER', listener: (username: string, message: string, tags: Record<string, string>) => void): this;
|
|
}
|
|
|
|
export class TwitchChat extends EventEmitter {
|
|
private channels: string[];
|
|
private readonly url = 'wss://irc-ws.chat.twitch.tv:443';
|
|
private ws: WebSocket | null = null;
|
|
private isConnected = false;
|
|
private manualDisconnect = false;
|
|
|
|
constructor(options: TwitchChatOptions) {
|
|
super();
|
|
this.channels = options.channels;
|
|
}
|
|
|
|
private parser(message: string) {
|
|
|
|
/*
|
|
EXAMPLE MESSAGE
|
|
|
|
*/
|
|
|
|
const splitted = message.split(' ');
|
|
let unparsedTags = splitted[0]
|
|
let messageData = splitted.slice(1)
|
|
|
|
const metadata: Metadata = {
|
|
username: parseUsername(messageData[0]),
|
|
messageType: messageData[1],
|
|
channel: messageData[2]?.replace('#', '') || '',
|
|
message: messageData.slice(3, messageData.length).join(" ").replace(':', ''),
|
|
tags: {},
|
|
};
|
|
|
|
const tags = unparsedTags.split(';').reduce((prev: Record<string, string>, curr: string) => {
|
|
const [key, value] = curr.split('=');
|
|
prev[key] = value;
|
|
return prev;
|
|
}, {});
|
|
metadata.tags = tags;
|
|
|
|
switch (metadata.messageType) {
|
|
case 'PRIVMSG':
|
|
this.emit('PRIVMSG', metadata.username, metadata.messageType, metadata.channel, metadata.message, tags);
|
|
break;
|
|
case 'CLEARCHAT':
|
|
this.emit('CLEARCHAT', metadata.channel, metadata.username, tags);
|
|
break;
|
|
case 'CLEARMSG':
|
|
const messageID = metadata.tags['target-msg-id'] || '';
|
|
this.emit('CLEARMSG', metadata.channel, messageID, tags);
|
|
break;
|
|
case 'GLOBALUSERSTATE':
|
|
this.emit('GLOBALUSERSTATE');
|
|
break;
|
|
case 'HOSTTARGET':
|
|
const [targetChannel, viewersStr] = metadata.message.split(' ');
|
|
const viewers = parseInt(viewersStr);
|
|
this.emit('HOSTTARGET', metadata.channel, viewers, targetChannel, tags);
|
|
break;
|
|
case 'NOTICE':
|
|
this.emit('NOTICE', metadata.channel, metadata.message, metadata.tags['msg-id'] || '', tags);
|
|
break;
|
|
case 'RECONNECT':
|
|
this.emit('RECONNECT');
|
|
break;
|
|
case 'ROOMSTATE':
|
|
const isLive = metadata.tags['room-id'] ? true : false;
|
|
this.emit('ROOMSTATE', metadata.channel, isLive, metadata.tags, tags);
|
|
break;
|
|
case 'USERNOTICE':
|
|
this.emit('USERNOTICE', metadata.channel, metadata.tags['msg-id'] || '', metadata.message, metadata.tags);
|
|
break;
|
|
case 'USERSTATE':
|
|
this.emit('USERSTATE', metadata.channel, metadata.tags);
|
|
break;
|
|
case 'WHISPER':
|
|
this.emit('WHISPER', metadata.username, metadata.message, metadata.tags);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private connectToTwitch() {
|
|
this.ws = new WebSocket(this.url);
|
|
|
|
this.ws.onclose = () => {
|
|
|
|
if (this.manualDisconnect) {
|
|
return;
|
|
}
|
|
|
|
const disconnectMessage = {
|
|
type: 'SERVERMSG',
|
|
message: 'Disconnected',
|
|
};
|
|
|
|
this.emit(JSON.stringify(disconnectMessage));
|
|
|
|
this.ws = null;
|
|
this.isConnected = false;
|
|
this.connectToTwitch();
|
|
};
|
|
|
|
this.ws.onopen = () => {
|
|
if (!this.ws) {
|
|
return;
|
|
}
|
|
|
|
this.ws.send('CAP REQ :twitch.tv/membership');
|
|
this.ws.send('CAP REQ :twitch.tv/tags');
|
|
this.ws.send('CAP REQ :twitch.tv/commands');
|
|
this.ws.send('PASS none');
|
|
this.ws.send('NICK justinfan333333333333');
|
|
|
|
for (const channel of this.channels) {
|
|
this.ws.send(`JOIN #${channel}`);
|
|
}
|
|
|
|
this.ws.on('message', (data) => {
|
|
const message = data.toString();
|
|
this.parser(message);
|
|
});
|
|
|
|
this.isConnected = true;
|
|
};
|
|
}
|
|
|
|
public async connect() {
|
|
this.connectToTwitch();
|
|
}
|
|
|
|
public addStreamer(streamerName: string): void {
|
|
if (!this.isConnected || this.channels.includes(streamerName)) {
|
|
return;
|
|
}
|
|
|
|
this.channels.push(streamerName);
|
|
this.ws?.send(`JOIN #${streamerName}`);
|
|
}
|
|
|
|
public removeStreamer(streamerName: string): void {
|
|
if (!this.isConnected) {
|
|
return;
|
|
}
|
|
|
|
const index = this.channels.indexOf(streamerName);
|
|
|
|
if (index >= 0) {
|
|
this.channels.splice(index, 1);
|
|
this.ws?.send(`PART #${streamerName}`);
|
|
}
|
|
}
|
|
}
|