Last active
June 2, 2025 16:12
-
-
Save MrPandir/f3d2b2c3c2fca98ac0fd4052288c2917 to your computer and use it in GitHub Desktop.
Inserts single words from Twitch chat into specified websites
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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