Skip to content

Instantly share code, notes, and snippets.

@MrPandir
Last active June 2, 2025 16:12
Show Gist options
  • Save MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917 to your computer and use it in GitHub Desktop.
Save MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917 to your computer and use it in GitHub Desktop.
Inserts single words from Twitch chat into specified websites
// ==UserScript==
// @name Twitch Chat Answer Inserter
// @namespace http://tampermonkey.net/
// @author MrPandir
// @version 1.8.2
// @description Inserts single words from Twitch chat into specified websites
// @match https://slovo.win/*
// @homepage https://gist.github.com/MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917
// @updateURL https://gist.githubusercontent.com/MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917/raw/chat-to-input.js
// @downloadURL https://gist.githubusercontent.com/MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917/raw/chat-to-input.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
"use strict";
let socket;
let channelNickname = GM_getValue("channelNickname", null);
let winnerNickname = null;
const selector = "input";
const gamesMenuSelector = "#menu-list-\\:r1\\:";
const currentGameSelector = "#menu-button-\\:r1\\: > span";
const resultSelector =
"#root > div > div > div:nth-child(2) > div:nth-child(3) > div";
const textSelector =
"#root > div > div > div:nth-child(2) > div > div > div > div > div > p";
// ===== Basic logic =====
function processMessage(user, message, tags) {
if (message.startsWith("!")) {
const [command, ...args] = message.slice(1).toLowerCase().split(" ");
return processCommand(user, command, args, tags);
}
const words = message.split(" ");
if (words.length !== 1) return;
const answer = words[0];
const inputElement = document.querySelector(selector);
if (!inputElement) {
console.error("Input element not found");
return;
}
const value = inputElement.value;
setNativeValue(inputElement, answer);
triggerEnterKey(inputElement);
// Check if the game has ended after submitting the answer
const currentUserDisplayName = tags["display-name"];
setTimeout(() => {
const textsNodes = document.querySelectorAll(textSelector);
const regex = /Поздравляем!/;
const congratulationTextNode = searchNodeWithRegex(textsNodes, regex);
if (congratulationTextNode && winnerNickname === null) {
winnerNickname = currentUserDisplayName;
const congratulationNode = congratulationTextNode.parentNode.parentNode;
displayWinner(winnerNickname, congratulationNode);
}
}, 1000);
setTimeout(() => {
inputElement.value = value;
}, 500);
}
function displayWinner(nickname, congratulationNode) {
const winnerDiv = congratulationNode.cloneNode(true);
const p = winnerDiv.querySelector("p");
if (!p) console.error("No paragraph element found");
p.textContent = `Победитель: ${nickname}`;
const parentBox = congratulationNode.parentNode;
parentBox.insertBefore(winnerDiv, congratulationNode.nextSibling);
}
function processCommand(user, command, args, tags) {
console.debug(`Processing command: ${command} Args: `, args);
const is_privileged_user = tags.is_broadcaster || tags.is_mod;
if (command === "next" && is_privileged_user) {
console.log("Starting next game");
const currentGameNumber = getCurrentGame();
if (!currentGameNumber) return console.error("Current game not found");
selectGame(currentGameNumber - 1);
} else if (command === "start" && is_privileged_user) {
const gameNumber = args[0].replace("#", "");
console.log(`Starting game ${gameNumber}`);
selectGame(gameNumber);
}
}
function getCurrentGame() {
const currentGameSpan = document.querySelector(currentGameSelector);
if (currentGameSpan) {
const currentGameNumber = currentGameSpan.textContent.match(/#(\d+)/);
if (currentGameNumber) return currentGameNumber[1];
}
const textsNodes = document.querySelectorAll(textSelector);
const regex = /Игра #(\d+)/;
const currentGameSpanInResult = searchNodeWithRegex(textsNodes, regex);
if (!currentGameSpanInResult) return null;
const currentGameNumberInResult =
currentGameSpanInResult.textContent.match(/#(\d+)/);
if (currentGameNumberInResult) return currentGameNumberInResult[1];
return null;
}
function selectGame(gameNumber) {
const gamesMenu = document.querySelector(gamesMenuSelector);
const targetItem = Array.from(gamesMenu.children).find((item) =>
item.textContent.startsWith(`#${gameNumber}`),
);
if (targetItem) {
targetItem.click();
}
}
// ===== Twitch WebSocket connection =====
function startTwitchBot(channel, selector) {
socket = new WebSocket("wss://irc-ws.chat.twitch.tv:443");
socket.onopen = function () {
socket.send("NICK justinfan12345");
socket.send("CAP REQ :twitch.tv/tags");
setChannelTwitchBot(channel);
};
socket.onmessage = function (event) {
const messageData = event.data;
if (messageData.startsWith("PING")) {
socket.send("PONG :tmi.twitch.tv");
return;
}
if (!messageData.includes("PRIVMSG")) return;
const [raw_tags, _, command, user, ...msg] = messageData.split(" ");
const tags = parseTwitchTags(raw_tags);
const message = msg.join(" ").slice(1).trim();
console.debug("Received message:", { command, user, message, tags });
processMessage(user, message, tags);
};
socket.onerror = function (error) {
console.error("WebSocket error:", error);
};
socket.onclose = function () {
console.log("Disconnected from Twitch chat");
setTimeout(reconnectTwitchBot, 5000); // reconnect
};
}
function setChannelTwitchBot(newChannel) {
if (!socket) {
channelNickname = newChannel;
return;
}
if (channelNickname !== newChannel) socket.send(`PART #${channelNickname}`);
socket.send(`JOIN #${newChannel}`);
channelNickname = newChannel;
}
function reconnectTwitchBot() {
console.log("Reconnecting...");
startTwitchBot(channelNickname, selector);
}
function stopTwitchBot() {
if (socket) socket.close();
}
function configureTwitchChannel() {
let channel = prompt("Enter Twitch channel name:", channelNickname || "");
if (!channelNickname || !channel) configureTwitchChannel();
channel = channel.trim().toLowerCase();
console.log(`Configuring channel: ${channel}`);
if (!channel) configureTwitchChannel();
GM_setValue("channelNickname", channel);
setChannelTwitchBot(channel);
}
// ===== Main =====
GM_registerMenuCommand("Set Twitch Channel", configureTwitchChannel);
if (!channelNickname) {
console.info("Twitch channel not set");
configureTwitchChannel();
} else {
console.info(`Current channel: ${channelNickname}`);
}
startTwitchBot(channelNickname, selector);
window.addEventListener("unload", stopTwitchBot);
// ===== Utils =====
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
const inputEvent = new Event("input", { bubbles: true });
if (valueSetter) {
valueSetter.call(element, value);
element.dispatchEvent(inputEvent);
} else {
element.value = value;
element.dispatchEvent(inputEvent);
}
}
function triggerEnterKey(element) {
const enterEvent = new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter",
});
element.dispatchEvent(enterEvent);
}
function parseTwitchTags(raw_tags) {
// Parsing IRC tags (@tag1=val1;tag2=val2 ...)
const result = {};
const tags = raw_tags.slice(1).split(";");
tags.forEach((tag) => {
const [key, value] = tag.split("=");
result[key] = value || null;
});
result.is_broadcaster = result.badges?.includes("broadcaster");
result.is_mod = result.badges?.includes("moderator");
return result;
}
function searchNodeWithRegex(nodes, regex) {
if (!nodes.length || !regex) return null;
return Array.from(nodes).find((node) => regex.test(node.textContent));
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment