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 addMessage(message) { const messages = document.getElementById("messages"); if (!messages || message.command != "PRIVMSG") { return; } // msg base const elem = document.createElement("p"); elem.classList.add("message"); // badges if ("badges" in message.tags && message.tags["badges"].length > 0) { for (const b of message.tags["badges"].split(",")) { if (b in badges) { const badgeImg = document.createElement("img"); badgeImg.src = badges[b]; badgeImg.loading = 'lazy'; badgeImg.classList.add("badge"); elem.append(badgeImg); } } } // parsing twitch emotes if ("emotes" in message.tags && message.tags["emotes"].length > 0) { const parts = message.tags["emotes"].split("/"); for (const part of parts) { const p = part.split(":"); const id = p[0]; const positions = p[1].split(","); for (const pos of positions) { const p = pos.split("-"); const start = p[0]; const end = p[1]; const emotename = message.params[1].substring(parseInt(start), parseInt(end) + 1); emotes[emotename] = `https://static-cdn.jtvnw.net/emoticons/v2/${id}/default/dark/1.0`; } } } // username if (message.nick != null && message.nick.length > 0) { const usernameElem = document.createElement("span"); elem.append(usernameElem); if ("color" in message.tags) { usernameElem.style.color = message.tags["color"]; } usernameElem.classList.add("author"); usernameElem.textContent = `${message.nick}:`; } // message text let msgWords = message.params[1].split(" "); for (let i = 0; i < msgWords.length; i++) { if (msgWords[i] in emotes) { msgWords[i] = `${msgWords[i]}`; } } elem.innerHTML += ' ' + msgWords.join(" "); messages.append(elem); window.scrollTo(0, document.body.scrollHeight); // remove old messages if (messages.childElementCount > 100) { for (let i = 0; i < messages.childElementCount - 100; i++) { messages.childNodes[i].remove(); } } } function addSystemMessage(message, nick = "System") { addMessage({ "params": ["#", message], "nick": nick, "prefix": "system", "tags": {}, "command": "PRIVMSG", }); } function connectToChat(host, nick, password, room) { const socket = new WebSocket(host); const membership = { "joined": [], "parted": [] }; const membershipInterval = setInterval(() => { if (membership.joined.length > 0) { addSystemMessage(`Users joined: ${membership.joined.join(", ")}`, null); membership.joined = []; } if (membership.parted.length > 0) { addSystemMessage(`Users parted: ${membership.parted.join(", ")}`, null); membership.parted = []; } }, 10000); socket.addEventListener("open", () => { socket.send(`NICK ${nick}`); socket.send(`PASS ${password}`); socket.send("CAP REQ :twitch.tv/tags"); if (params["membership"] == "1") { socket.send("CAP REQ :twitch.tv/membership"); } socket.send(`JOIN #${room}`); addSystemMessage("Connected to the chat!"); }); socket.addEventListener("message", (e) => { const lines = e.data.split("\r\n"); for (const line of lines) { const l = line.trim(); if (l.length == 0) { continue; } let m = parseIRCMessage(l); console.log(m); addMessage(m); if (m.command == "JOIN" && m.nick != "justinfan12345") { membership.joined.push(m.nick); } else if (m.command == "PART" && m.nick != "justinfan12345") { membership.parted.push(m.nick); } else if (m.command == "PING") { socket.send(`PONG :${m.params[0]}`); } } }); socket.addEventListener("close", (e) => { addMessage("Chat", `Disconnected from the chat: ${e.reason}`); addMessage("Chat", "Reconnecting to the chat in 5 seconds..."); clearInterval(membershipInterval); setTimeout(() => connectToChat(host, nick, password, room), 5000); }); } function getRecentMessages(room) { fetch(`https://recent-messages.robotty.de/api/v2/recent-messages/${room}`) .then((r) => r.json()) .then((json) => { if (json["error"] != null) { addSystemMessage(`${json["error"]} (Recent messages)`); return; } for (const message of json["messages"]) { addMessage(parseIRCMessage(message)); } }); }