function parseIRCMessage(line) { line = line.replace(/\r?\n$/, ''); let tags = {}; let prefix = null; let nick = null; let command = null; const params = []; // tags if (line[0] === '@') { const end = line.indexOf(' '); const rawTags = line.slice(1, end); for (const tag of rawTags.split(';')) { const [k, v = true] = tag.split('='); tags[k] = v === true ? true : v; } line = line.slice(end + 1); } // prefix if (line[0] === ':') { const end = line.indexOf(' '); prefix = line.slice(1, end); const bang = prefix.indexOf('!'); if (bang !== -1) nick = prefix.slice(0, bang); line = line.slice(end + 1); } // command and params const parts = line.split(' '); command = parts.shift(); for (let i = 0; i < parts.length; i++) { if (parts[i][0] === ':') { params.push(parts.slice(i).join(' ').slice(1)); break; } if (parts[i].length) params.push(parts[i]); } return { tags, prefix, nick, command, params }; } function displayMessage(data) { const messages = document.getElementById("window-messages"); const message = document.createElement("p"); message.classList.add("message"); if ("nick" in data && data.nick.length > 0) { const nickElement = document.createElement("span"); nickElement.classList.add("nick"); nickElement.textContent = `${data.nick}: `; if ("color" in data.tags) { nickElement.style.color = data.tags["color"]; } message.append(nickElement); } const messageContent = document.createElement("span"); messageContent.classList.add("content"); messageContent.textContent = data.params[1]; message.append(messageContent); messages.append(message); } function displaySystemMessage(channelName, message, nick = "System") { displayMessage({ "nick": nick, "prefix": nick, "command": "PRIVMSG", "params": [channelName, message], "tags": {} }); } class TwitchIRCClient { constructor (host, nick, pass) { this.joinedChannels = []; this.socket = null; this.host = host; this.nick = nick; this.pass = pass; } connect() { this.socket = new WebSocket(this.host); this.socket.addEventListener("open", () => { this.socket.send(`NICK ${this.nick}`); this.socket.send(`PASS ${this.pass}`); this.socket.send(`CAP REQ :twitch.tv/tags twitch.tv/membership`); }); this.socket.addEventListener("message", (e) => { const lines = e.data.split("\r\n"); for (const line of lines) { if (line.trim().length == 0) { continue; } const msg = parseIRCMessage(line); switch (msg.command) { case "001": { for (const c of this.joinedChannels) { this.join(c); } displaySystemMessage("*", "Connected!"); break; } case "PRIVMSG": { displayMessage(msg); } default: break; } } }); this.socket.addEventListener("error", (e) => { console.error(e); }); this.socket.addEventListener("close", (e) => { for (const c of this.joinedChannels) { displaySystemMessage(c, `Disconnected! Reason: ${e.reason}`); displaySystemMessage(c, "Reconnecting in 10 seconds..."); } setTimeout(() => this.connect(), 10000); }); } say(channelName, message) { this.socket.send(`PRIVMSG #${channelName} :${message}`); } join(channelName) { if (this.socket != null && this.socket.readyState == this.socket.OPEN) { this.socket.send(`JOIN #${channelName}`); } if (!this.joinedChannels.includes(channelName)) { this.joinedChannels.push(channelName); } } }