import { randomUUID } from 'crypto'; import ws from 'ws'; import { TwitchChat } from '../util/scraping/chat/chat'; interface ExtWebSocket extends ws { id?: string; } export class TwitchChatServer { private chat: TwitchChat; private clients: { [k:string]: ExtWebSocket[] }; constructor() { this.clients = {}; this.chat = new TwitchChat({ login: { username: 'justinfan23423', password: 'none' }, channels: [] }); this.chat.connect() this.chat.on('PRIVMSG', this.handlePrivateMessage.bind(this)); this.chat.on('CLEARCHAT', this.handleClearChat.bind(this)); this.chat.on('CLEARMSG', this.handleClearMessage.bind(this)); this.chat.on('GLOBALUSERSTATE', this.handleGlobalUserState.bind(this)); this.chat.on('HOSTTARGET', this.handleHostTarget.bind(this)); this.chat.on('NOTICE', this.handleNotice.bind(this)); this.chat.on('RECONNECT', this.handleReconnect.bind(this)); this.chat.on('ROOMSTATE', this.handleRoomState.bind(this)); this.chat.on('USERNOTICE', this.handleUserNotice.bind(this)); this.chat.on('USERSTATE', this.handleUserState.bind(this)); this.chat.on('WHISPER', this.handleWhisper.bind(this)); } public async startWebSocketServer(server: ws.Server): Promise { server.on('connection', (ws: ws) => { const socket = ws as ExtWebSocket socket.on('message', this.handleWebSocketMessage.bind(this, socket)); socket.on('close', this.handleWebSocketClose.bind(this, socket)); }); } private handlePrivateMessage = async(username: string, type: string, channel: string, message: string, tags: Record) => { const socketsToSend = await this.findClientsForStreamer(channel) for(let socket of socketsToSend) { let payload = { type: 'PRIVMSG', username, channel, message, tags } socket.send(JSON.stringify(payload)) } } private handleClearChat = async(channel: string, username: string, tags: Record) => { const socketsToSend = await this.findClientsForStreamer(channel) for(let socket of socketsToSend) { let payload = { type: 'CLEARCHAT', username, channel, tags } socket.send(JSON.stringify(payload)) } } private handleClearMessage = async(channel: string, messageId: string, tags: Record) => { const socketsToSend = await this.findClientsForStreamer(channel) for(let socket of socketsToSend) { let payload = { type: 'CLEARMSG', channel, tags } socket.send(JSON.stringify(payload)) } } private handleGlobalUserState = async() => { // Do nothing for now } private handleHostTarget = async(channel: string, viewers: number, target: string, tags: Record) => { const socketsToSend = await this.findClientsForStreamer(channel) for(let socket of socketsToSend) { let payload = { type: 'HOSTTARGET', channel, target, viewers, tags } socket.send(JSON.stringify(payload)) } } private handleNotice = async(channel: string, message: string) => { // Do nothing for now } private handleReconnect = async() => { // Do nothing for now } private handleRoomState = async(channel: string, isLive: boolean, tags: Record) => { // Do nothing for now } private handleUserNotice = async(channel: string, type: string, message: string, tags: { [id: string]: string }) => { const socketsToSend = await this.findClientsForStreamer(channel) for(let socket of socketsToSend) { let payload = { type: 'USERNOTICE', channel, message, tags } socket.send(JSON.stringify(payload)) } } private handleUserState = async(channel: string, tags: { [id: string]: string }) => { // Do nothing for now } private handleWhisper = async(username: string, message: string, tags: { [id: string]: string }) => { // Do nothing for now } private async findClientsForStreamer(streamerName: string): Promise { if(!this.clients[streamerName]) return Promise.reject('No clients following streamer') return this.clients[streamerName]; } private async handleWebSocketMessage(socket: ExtWebSocket, message: ws.Data) { const data = message.toString() const splitted = data.split(' ') if (splitted.length < 2 || splitted[0] !== 'JOIN') { socket.close(); return; } const streamersToJoin = splitted[1].split(',') if (streamersToJoin.length > 1) { socket.close(); return; } const id = randomUUID() for (let streamer of streamersToJoin) { this.chat.addStreamer(streamer) if (this.clients[streamer]) { this.clients[streamer].push(socket) } else { this.clients[streamer] = [socket] } } socket.id = id socket.send('OK') } private handleWebSocketClose(socket: ExtWebSocket) { if (socket.id) { // Remove the socket from the list of clients following each streamer Object.entries(this.clients).forEach(([streamer, sockets]) => { this.clients[streamer] = sockets.filter(s => s !== socket); if (this.clients[streamer].length === 0) { // No more clients following this streamer, remove it from the chat this.chat.removeStreamer(streamer); delete this.clients[streamer]; } }); } } }