Add twitch chat integration

This commit is contained in:
2026-01-13 17:55:08 +01:00
parent 9abb5e88dc
commit 4f1eb2fc82
6 changed files with 240 additions and 1 deletions

View File

@@ -0,0 +1,121 @@
const TWITCH_IRC_URL = "wss://irc-ws.chat.twitch.tv:443";
const ANON_PREFIX = "justinfan";
const ANON_PASSWORD = "SCHMOOPIIE";
const buildAnonymousNick = () => {
const suffix = Math.floor(Math.random() * 100000)
.toString()
.padStart(5, "0");
return `${ANON_PREFIX}${suffix}`;
};
const parseTags = (rawTags) => {
if (!rawTags) {
return {};
}
return rawTags.split(";").reduce((acc, entry) => {
const [key, value = ""] = entry.split("=");
acc[key] = value;
return acc;
}, {});
};
const parseIrcMessage = (rawLine) => {
let line = rawLine;
let tags = {};
let prefix = "";
if (line.startsWith("@")) {
const spaceIndex = line.indexOf(" ");
tags = parseTags(line.slice(1, spaceIndex));
line = line.slice(spaceIndex + 1);
}
if (line.startsWith(":")) {
const spaceIndex = line.indexOf(" ");
prefix = line.slice(1, spaceIndex);
line = line.slice(spaceIndex + 1);
}
const commandEnd = line.indexOf(" ");
const command = commandEnd === -1 ? line : line.slice(0, commandEnd);
const params = commandEnd === -1 ? "" : line.slice(commandEnd + 1);
return {
command,
params,
prefix,
tags,
};
};
const extractChatMessage = (ircMessage) => {
if (ircMessage.command !== "PRIVMSG") {
return null;
}
const messageSplit = ircMessage.params.split(" :");
const channel = messageSplit[0]?.trim() || "";
const message = messageSplit.slice(1).join(" :");
const displayName = ircMessage.tags["display-name"] || ircMessage.prefix.split("!")[0];
return {
channel,
displayName,
message,
};
};
export const connectTwitchChat = (channelName, onMessage = console.log) => {
if (!channelName) {
console.warn("Twitch chat connection skipped: missing channel name");
return () => {};
}
const normalizedChannel = channelName.toLowerCase();
const nick = buildAnonymousNick();
const socket = new WebSocket(TWITCH_IRC_URL);
socket.addEventListener("open", () => {
socket.send(`PASS ${ANON_PASSWORD}`);
socket.send(`NICK ${nick}`);
socket.send("CAP REQ :twitch.tv/tags twitch.tv/commands");
socket.send(`JOIN #${normalizedChannel}`);
});
socket.addEventListener("message", (event) => {
const lines = String(event.data).split("\r\n").filter(Boolean);
lines.forEach((line) => {
if (line.startsWith("PING")) {
const payload = line.split(" ")[1] || ":tmi.twitch.tv";
socket.send(`PONG ${payload}`);
return;
}
const parsed = parseIrcMessage(line);
const chatMessage = extractChatMessage(parsed);
if (chatMessage) {
onMessage({
channel: chatMessage.channel,
displayName: chatMessage.displayName,
message: chatMessage.message,
});
}
});
});
socket.addEventListener("close", () => {
console.info(`Twitch chat connection closed for #${normalizedChannel}`);
});
socket.addEventListener("error", (event) => {
console.error("Twitch chat connection error", event);
});
return () => {
socket.close();
};
};