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) => void): this; on(event: 'CLEARCHAT', listener: (channel: string, username: string, tags: Record) => void): this; on(event: 'CLEARMSG', listener: (channel: string, messageID: string, tags: Record) => void): this; on(event: 'GLOBALUSERSTATE', listener: () => void): this; on(event: 'HOSTTARGET', listener: (channel: string, viewers: number, targetChannel: string, tags: Record) => void): this; on(event: 'NOTICE', listener: (channel: string, message: string, messageType: MessageType, tags: Record) => void): this; on(event: 'RECONNECT', listener: () => void): this; on(event: 'ROOMSTATE', listener: (channel: string, isLive: boolean, tags: Record) => void): this; on(event: 'USERNOTICE', listener: (channel: string, messageType: MessageType, message: string, tags: Record) => void): this; on(event: 'USERSTATE', listener: (channel: string, tags: Record) => void): this; on(event: 'WHISPER', listener: (username: string, message: string, tags: Record) => 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, 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}`); } } }