mirror of
https://codeberg.org/SafeTwitch/safetwitch-backend.git
synced 2025-01-05 04:10:06 -05:00
184 lines
5.4 KiB
TypeScript
184 lines
5.4 KiB
TypeScript
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<void> {
|
|
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<string,string>) => {
|
|
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<string,string>) => {
|
|
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<string,string>) => {
|
|
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<string,string>) => {
|
|
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<string, string>) => {
|
|
// 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<ExtWebSocket[]> {
|
|
if(!this.clients[streamerName]) return []
|
|
|
|
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];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|