0
Fork 0
mirror of https://codeberg.org/SafeTwitch/safetwitch-backend.git synced 2025-02-12 14:48:14 -05:00
safetwitch-backend/util/scraping/chat/chat.ts
2023-04-09 12:38:29 -04:00

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}`);
}
}
}