/* ----------------------- */ /* OPTIONS */ /* ----------------------- */ const showPlatform = getURLParam("showPlatform", true); const showPlatformDot = getURLParam("showPlatformDot", false); const showAvatar = getURLParam("showAvatar", true); const showTimestamps = getURLParam("showTimestamps", true); const ampmTimeStamps = getURLParam("ampmTimeStamps", false); const showBadges = getURLParam("showBadges", true); const showPlatformStatistics = getURLParam("showPlatformStatistics", true); const chatThreshold = 50; const chatOneLine = getURLParam("chatOneLine", false); const chatHorizontal = getURLParam("chatHorizontal", false); const chatFontSize = getURLParam("chatFontSize", 1); const chatBackground = getURLParam("chatBackground", "#121212"); const chatBackgroundOpacity = getURLParam("chatBackgroundOpacity", 1); const chatScrollBar = getURLParam("chatScrollBar", false); const chatField = getURLParam("chatField", false); const chatModeration = getURLParam("chatModeration", false); const excludeCommands = getURLParam("excludeCommands", true); const ignoreChatters = getURLParam("ignoreChatters", ""); const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || []; const hideAfter = getURLParam("hideAfter", 0); const chatContainer = document.querySelector('#chat'); const chatTemplate = document.querySelector('#chat-message'); const eventTemplate = document.querySelector('#event-message'); const userColors = new Map(); if (showPlatformStatistics == true) { document.querySelector('#statistics').style.display = ''; } if (chatScrollBar == false) { chatContainer.classList.add('noscrollbar'); } if (chatOneLine == true && !chatHorizontal) { chatContainer.classList.add('oneline'); } if (chatHorizontal == true) { chatContainer.classList.remove('oneline'); chatContainer.classList.add('horizontal'); } let backgroundColor = hexToRGBA(chatBackground,chatBackgroundOpacity); chatContainer.parentElement.style.backgroundColor = backgroundColor; chatContainer.style.zoom = chatFontSize; if (chatField) { const chatfieldelement = document.getElementById("chat-input"); chatfieldelement.style.display = ''; } function addMessageItem(platform, clone, classes, userid, messageid) { removeExtraChatMessages(); let chatmodtwitch = `
`; let chatmodyoutube = `
`; let chatmodkick = `
`; if (showSpeakerbot == true && speakerBotChatRead == true) { speakerBotTTSRead(clone, 'chat'); } const root = clone.firstElementChild; root.classList.add(...classes); root.dataset.user = userid; root.id = messageid; root.style.opacity = '0'; const platformElement = clone.querySelector('.platform'); if (showPlatform == true) { platformElement.innerHTML = ``; } if (showPlatformDot == true) { platformElement.innerHTML = ``; } if (showPlatform == false && showPlatformDot == false) { platformElement.remove(); } const timestamp = clone.querySelector('.timestamp'); if (timestamp) { if (showTimestamps) { timestamp.textContent = whatTimeIsIt(); } else { timestamp.remove(); } } const dimensionProp = chatHorizontal ? 'Width' : 'Height'; // Starts it collapsed root.style[dimensionProp.toLowerCase()] = '0px'; if (chatModeration == true) { switch (platform) { case "twitch": root.insertAdjacentHTML("beforeend", chatmodtwitch); break; case "youtube": root.insertAdjacentHTML("beforeend", chatmodyoutube); break; case "kick": root.insertAdjacentHTML("beforeend", chatmodkick); break; default: console.warn(`Plataforma desconhecida: ${platform}`); } } chatContainer.prepend(clone); const item = document.getElementById(messageid); const itemDimension = item.querySelector('.message')?.[`offset${dimensionProp}`] || 0; // Animates the item requestAnimationFrame(() => { item.style[dimensionProp.toLowerCase()] = itemDimension + 'px'; item.style.opacity = '1'; }); item.addEventListener('transitionend', () => { item.style[dimensionProp.toLowerCase()] = ''; item.style.opacity = ''; }, { once: true }); // Hides it after a while if (hideAfter > 0) { setTimeout(() => { item.style.opacity = '0'; setTimeout(() => { item.remove(); }, 1000); }, Math.floor(hideAfter * 1000)); } } function addEventItem(platform, clone, classes, userid, messageid) { removeExtraChatMessages(); if (showSpeakerbot == true && speakerBotEventRead == true) { speakerBotTTSRead(clone, 'event'); } const root = clone.firstElementChild; root.classList.add(...classes); root.dataset.user = userid; root.id = messageid; root.style.opacity = '0'; const platformElement = clone.querySelector('.platform'); if (showPlatform == true) { platformElement.innerHTML = ``; } if (showPlatformDot == true) { platformElement.innerHTML = ``; } if (showPlatform == false && showPlatformDot == false) { platformElement.remove(); } const timestamp = clone.querySelector('.timestamp'); if (timestamp) { if (showTimestamps) { timestamp.textContent = whatTimeIsIt(); } else { timestamp.remove(); } } const dimensionProp = chatHorizontal ? 'Width' : 'Height'; // Starts it collapsed root.style[dimensionProp.toLowerCase()] = '0px'; chatContainer.prepend(clone); const item = document.getElementById(messageid); const itemDimension = item.querySelector('.message')?.[`offset${dimensionProp}`] || 0; // Animates the item requestAnimationFrame(() => { item.style[dimensionProp.toLowerCase()] = itemDimension + 'px'; item.style.opacity = '1'; }); item.addEventListener('transitionend', () => { item.style[dimensionProp.toLowerCase()] = ''; item.style.opacity = ''; }, { once: true }); // Hides it after a while if (hideAfter > 0) { setTimeout(() => { item.style.opacity = '0'; setTimeout(() => { item.remove(); }, 1000); }, Math.floor(hideAfter * 1000)); } } function removeItem(element) { element.remove(); } function removeExtraChatMessages() { const chatMessages = chatContainer.querySelectorAll(':scope > div'); const total = chatMessages.length; if (total >= chatThreshold) { const toRemove = Math.floor(total * 0.25); // 25% do total for (let i = 0; i < toRemove; i++) { const last = chatContainer.lastElementChild; if (last) chatContainer.removeChild(last); } } } function whatTimeIsIt() { const now = new Date(); const hours24 = now.getHours(); const minutes = now.getMinutes().toString().padStart(2, '0'); const ampm = hours24 >= 12 ? 'PM' : 'AM'; const hours12 = (hours24 % 12) || 12; if (ampmTimeStamps === true) { return `${hours12}:${minutes} ${ampm}`; } else { return `${hours24}:${minutes}`; } } // Function to format large numbers (e.g., 1000 => '1K') function formatNumber(num) { if (num >= 1000000) { let numStr = (num / 1000000).toFixed(1); if (numStr.endsWith('.0')) { numStr = numStr.slice(0, -2); } return numStr + 'M'; } else if (num >= 1000) { let numStr = (num / 1000).toFixed(1); if (numStr.endsWith('.0')) { numStr = numStr.slice(0, -2); } return numStr + 'K'; } return num.toString(); } function formatCurrency(amount, currencyCode) { if (!currencyCode) { currencyCode = 'USD'; } return new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode, minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(amount); } function createRandomString(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } function createRandomColor(platform, username) { if (userColors.get(platform).has(username)) { return userColors.get(platform).get(username); } else { const randomColor = "hsl(" + Math.random() * 360 + ", 100%, 75%)"; userColors.get(platform).set(username, randomColor); return randomColor; } } function hexToRGBA(hexadecimal,opacity) { const hex = hexadecimal; const alpha = parseFloat(opacity); // Converter hex para RGB const r = parseInt(hex.substr(1, 2), 16); const g = parseInt(hex.substr(3, 2), 16); const b = parseInt(hex.substr(5, 2), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function stripStringFromHtml(html) { let doc = new DOMParser().parseFromString(html, 'text/html'); return doc.body.textContent || ""; } async function cleanStringOfHTMLButEmotes(string) { // Cria um elemento DOM temporário const container = document.createElement('div'); container.innerHTML = string; // Substitui ... por texto do alt const emotes = container.querySelectorAll('img.emote[alt]'); emotes.forEach(img => { const altText = img.getAttribute('alt'); const textNode = document.createTextNode(altText); img.replaceWith(textNode); }); // Remove todo o restante do HTML return container.textContent || ""; } const pushNotify = (data) => { const SimpleNotify = { effect: 'fade', speed: 500, customClass: 'toasty', customIcon: '', showIcon: true, showCloseButton: true, autoclose: true, autotimeout: 5000, notificationsGap: null, notificationsPadding: null, type: 'outline', position: 'x-center bottom', customWrapper: '', }; const mergedData = { ...SimpleNotify, ...data } new Notify (mergedData); } const notifyError = (err) => { err.status = 'error'; pushNotify(err); } const notifyInfo = (info) => { info.status = 'info'; pushNotify(info); } const notifyWarning = (warn) => { warn.status = 'warning'; pushNotify(warn); } const notifySuccess = (success) => { success.status = 'success'; pushNotify(success); } /* -------------------------- */ /* ---- CHAT INPUT UTILS ---- */ /* -------------------------- */ const chatcommandslist = document.getElementById('chat-autocomplete-list'); let chatcurrentFocus = -1; const chatInputSend = document.getElementById("chat-input-send"); const chatInputForm = document.querySelector("#chat-input form"); const chatInput = chatInputForm.querySelector("input[type=text]") let chatcommands = { "Twitch" : [ { "name" : "/me", "usage" : "Creates a colored message. Usage: /me [message]" }, { "name" : "/clip", "usage" : "Creates a 30s clip. Usage: /clip" }, { "name" : "/announce", "usage" : "Sends an announcement. Usage: /announce [message]" }, { "name" : "/announceblue", "usage" : "Sends an announcement in blue. Usage: /announceblue [message]" }, { "name" : "/announcegreen", "usage" : "Sends an announcement in green. Usage: /announcegreen [message]" }, { "name" : "/announceorange", "usage" : "Sends an announcement in orange. Usage: /announceorange [message]" }, { "name" : "/announcepurple", "usage" : "Sends an announcement in purple. Usage: /announcepurple [message]" }, { "name" : "/clear", "usage" : "Clear Chat Messages. Usage: /clear" }, { "name" : "/slow", "usage" : "Activates slow mode. Usage: /slow [duration]" }, { "name" : "/slowoff", "usage" : "Deactivates slow mode. Usage: /slowoff" }, { "name" : "/subscribers", "usage" : "Activates subscribers only mode. Usage: /subscribers" }, { "name" : "/subscribersoff", "usage" : "Deactivates subscribers only mode. Usage: /subscribersoff" }, { "name" : "/emoteonly", "usage" : "Activates emote only mode. Usage: /emoteonly" }, { "name" : "/emoteonlyoff", "usage" : "Deactivates emote only mode. Usage: /emoteonlyoff" }, { "name" : "/commercial", "usage" : "Add a commercial break. Usage: /commercial [duration]" }, { "name" : "/timeout", "usage" : "Timeouts a user. Usage: /timeout [user] [duration] [reason]" }, { "name" : "/untimeout", "usage" : "Removes timeout from a user. Usage: /untimeout [user]" }, { "name" : "/ban", "usage" : "Bans a user. Usage: /ban [user] [reason]" }, { "name" : "/unban", "usage" : "Unbans a user. Usage: /unban [user]" }, { "name" : "/mod", "usage" : "Mod a user. Usage: /mod [user]" }, { "name" : "/unmod", "usage" : "Removes mod from a user. Usage: /unmod [user]" }, { "name" : "/vip", "usage" : "Adds user to VIP. Usage: /vip [user]" }, { "name" : "/unvip", "usage" : "Removes user from VIP. Usage: /unvip [user]" }, { "name" : "/shoutout", "usage" : "Shoutouts a user. Usage: /shoutout [user]" }, { "name" : "/raid", "usage" : "Raids a user. Usage: /raid [user]" }, { "name" : "/unraid", "usage" : "Removes the outcoming raid. Usage: /unraid" }, { "name" : "/settitle", "usage" : "Sets the stream title. Usage: /settitle [title]" }, { "name" : "/setgame", "usage" : "Sets the stream game. Usage: /setgame [game]" }, ], "YouTube" : [ { "name" : "/yt/title", "usage" : "Sets the stream title. Usage: /yt/title [title]" }, { "name" : "/yt/timeout", "usage" : "Times out a user. Usage: /yt/timeout [user] [duration]" }, { "name" : "/yt/ban", "usage" : "Bans a user. Usage: /yt/ban [user]" } ], "Kick" : [ { "name" : "/kick/title", "usage" : "Sets the stream title. Usage: /kick/title [title]" }, { "name" : "/kick/category", "usage" : "Sets the stream category. Usage: /kick/category [category]" }, { "name" : "/kick/timeout", "usage" : "Times out a user. Usage: /kick/timeout [user] [duration]" }, { "name" : "/kick/untimeout", "usage" : "Removes timeout from a user. Usage: /kick/untimeout [user]" }, { "name" : "/kick/ban", "usage" : "Bans a user. Usage: /kick/ban [user]" }, { "name" : "/kick/unban", "usage" : "Unbans a user. Usage: /kick/unban [user]" } ] }; function highlightItem(items) { if (!items) return; items.forEach(item => item.classList.remove('active')); if (chatcurrentFocus >= items.length) chatcurrentFocus = 0; if (chatcurrentFocus < 0) chatcurrentFocus = items.length - 1; items[chatcurrentFocus].classList.add('active'); items[chatcurrentFocus].scrollIntoView({ block: "nearest" }); } chatInput.addEventListener('input', function () { const value = this.value.trim(); chatcommandslist.innerHTML = ''; chatcurrentFocus = -1; if (!value.startsWith('/')) return; Object.entries(chatcommands).forEach(([groupName, commands]) => { const filtered = commands.filter(cmd => cmd.name.startsWith(value)); if (filtered.length === 0) return; const groupTitle = document.createElement('div'); groupTitle.textContent = groupName; chatcommandslist.appendChild(groupTitle); filtered.forEach(cmd => { const item = document.createElement('div'); item.classList.add('autocomplete-item'); item.innerHTML = `${cmd.name} ${cmd.usage}`; item.addEventListener('click', () => { chatInput.value = cmd.name + ' '; chatcommandslist.innerHTML = ''; }); chatcommandslist.appendChild(item); }); }); }); chatInput.addEventListener('keydown', function (e) { const items = chatcommandslist.querySelectorAll('.autocomplete-item'); if (items.length === 0) return; if (e.key === 'ArrowDown') { chatcurrentFocus++; highlightItem(items); } else if (e.key === 'ArrowUp') { chatcurrentFocus--; highlightItem(items); } else if (e.key === 'Enter') { e.preventDefault(); if (chatcurrentFocus > -1 && items[chatcurrentFocus]) { items[chatcurrentFocus].click(); } } }); chatInputForm.addEventListener("submit", function(event) { event.preventDefault(); var chatSendPlatforms = []; if (showTwitch == true && showTwitchMessages == true) { chatSendPlatforms.push('twitch'); } if (showYoutube == true && showYouTubeMessages == true) { chatSendPlatforms.push('youtube'); } if (showTiktok == true && showTikTokMessages == true) { chatSendPlatforms.push('tiktok'); } if (showKick == true && showKickMessages == true) { chatSendPlatforms.push('kick'); } chatSendPlatforms = chatSendPlatforms.join(',') const chatInput = chatInputForm.querySelector("input[type=text]") const chatInputText = chatInput.value; // Sends Message to Twitch and YouTube streamerBotClient.doAction( { name : "[Twitch][YouTube][Kick] Msgs/Cmds" }, { "type": "chat", "platforms": chatSendPlatforms, "message": chatInputText, } ).then( (sendchatstuff) => { console.debug('ChatRD] Sending Chat to Streamer.Bot', sendchatstuff); }); // Sends Message to TikTok that are not commands if (chatSendPlatforms.includes('tiktok')) { if (!chatInputText.startsWith('/')) { streamerBotClient.doAction( { name : "[TikTok] Msgs" }, { "ttkmessage": chatInputText, } ).then( (sendchatstuff) => { console.debug('[ChatRD] Sending TikTok Chat to Streamer.Bot', sendchatstuff); }); } } chatInput.value = ''; }); chatInputSend.addEventListener("click", function () { chatInputForm.requestSubmit(); }); document.addEventListener('click', function (e) { if (e.target !== chatcommandslist) { chatcommandslist.innerHTML = ''; } }); async function speakerBotTTSRead(clone,type) { var TTSMessage = ""; const { header, user, action, value, 'actual-message': message } = Object.fromEntries( [...clone.querySelectorAll('[class]')] .map(el => [el.className, el]) ); if (type == "chat") { var cleanmessage = ""; if (message == null) { cleanmessage = "
"; } else { cleanmessage = message.innerHTML; } var strippedmessage = await cleanStringOfHTMLButEmotes(cleanmessage); const tts = { user: user.textContent, message: strippedmessage } TTSMessage = renderTemplate(speakerBotChatTemplate, tts); } if (type == "event") { var cleanvalue = ""; if (value == null) { cleanvalue = ""; } else { cleanvalue = value.innerHTML; } var cleanmessage = ""; if (message == null) { cleanmessage = "
"; } else { cleanmessage = message.innerHTML; } var strippedmessage = await cleanStringOfHTMLButEmotes(cleanmessage); var strippedaction = await cleanStringOfHTMLButEmotes(action.innerHTML); var strippedvalue = await cleanStringOfHTMLButEmotes(cleanvalue); TTSMessage = user.textContent + strippedaction + strippedvalue + ". " + strippedmessage; } speakerBotClient.speak(TTSMessage); /*streamerBotClient.doAction({ name : "[Speakerbot] TTS" }, { "message": TTSMessage, "alias" : speakerBotVoiceAlias } ).then( (response) => { console.debug('[ChatRD][Streamer.bot -> Speaker.bot] Sending TTS...', response); });*/ } function renderTemplate(template, data) { return template.replace(/\{(\w+)\}/g, (match, key) => { return key in data ? data[key] : match; }); } async function cleanStringOfHTMLButEmotes(string) { // Cria um elemento DOM temporário const container = document.createElement('div'); container.innerHTML = string; // Substitui ... por texto do alt const emotes = container.querySelectorAll('img.emote[alt]'); emotes.forEach(img => { const altText = img.getAttribute('alt'); const textNode = document.createTextNode(altText); img.replaceWith(textNode); }); // Remove todo o restante do HTML return container.textContent || ""; } async function executeModCommand(event, command) { event.preventDefault(); chatInput.value = command; chatInputForm.requestSubmit(); } document.addEventListener("DOMContentLoaded", function () { });