/* ----------------------- */
/* 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 () {
});