////////////////////// // GLOBAL VARIABLES // ////////////////////// const avatarMap = new Map(); const pronounMap = new Map(); ////////////////////// // HELPER FUNCTIONS // ////////////////////// function GetBooleanParam(paramName, defaultValue) { const urlParams = new URLSearchParams(window.location.search); const paramValue = urlParams.get(paramName); if (paramValue === null) { return defaultValue; // Parameter not found } const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity if (lowercaseValue === 'true') { return true; } else if (lowercaseValue === 'false') { return false; } else { return paramValue; // Return original string if not 'true' or 'false' } } function GetIntParam(paramName, defaultValue) { const urlParams = new URLSearchParams(window.location.search); const paramValue = urlParams.get(paramName); if (paramValue === null) { return defaultValue; // or undefined, or a default value, depending on your needs } const intValue = parseInt(paramValue, 10); // Parse as base 10 integer if (isNaN(intValue)) { return null; // or handle the error in another way, e.g., throw an error } return intValue; } async function GetKickIds(username) { // First attempt with the original username let url = `https://kick.com/api/v2/channels/${username}`; try { let response = await fetch(url); if (!response.ok) { // Retry with underscores replaced by dashes const altUsername = username.replace(/_/g, "-"); url = `https://kick.com/api/v2/channels/${altUsername}`; response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } } const data = await response.json(); if (data.chatroom && data.chatroom.id) { return { chatroomId: data.chatroom.id, channelId: data.chatroom.channel_id }; } else { throw new Error("Chatroom ID not found in response."); } } catch (error) { console.error("Failed to fetch chatroom ID:", error.message); return null; } } async function GetKickSubBadges(username) { let url = `https://kick.com/api/v2/channels/${username}`; try { let response = await fetch(url); if (!response.ok) { // Retry with underscores replaced by dashes const altUsername = username.replace(/_/g, "-"); url = `https://kick.com/api/v2/channels/${altUsername}`; response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } } const data = await response.json(); return data.subscriber_badges || []; } catch (error) { console.error("Failed to fetch subscriber badges:", error.message); return []; } } async function GetAvatar(username, platform) { // First, check if the username is hashed already if (avatarMap.has(`${username}-${platform}`)) { console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`); return avatarMap.get(`${username}-${platform}`); } // If code reaches this point, the username hasn't been hashed, so retrieve avatar switch (platform) { case 'twitch': { console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`); let response = await fetch('https://decapi.me/twitch/avatar/' + username); let data = await response.text(); avatarMap.set(`${username}-${platform}`, data); return data; } case 'kick': { console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`); let url = `https://kick.com/api/v2/channels/${username}`; try { let response = await fetch(url); if (!response.ok) { const altUsername = username.replace(/_/g, "-"); url = `https://kick.com/api/v2/channels/${altUsername}`; response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } } let data = await response.json(); const defaultAvatars = [ 'https://kick.com/img/default-profile-pictures/default-avatar-1.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-2.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-3.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-4.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-5.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-6.webp', ]; const avatarURL = data.user?.profile_pic || defaultAvatars[hashString(`${username}-${platform}`) % defaultAvatars.length]; avatarMap.set(`${username}-${platform}`, avatarURL); return avatarURL; } catch (error) { console.error("Failed to fetch avatar:", error.message); const defaultAvatars = [ 'https://kick.com/img/default-profile-pictures/default-avatar-1.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-2.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-3.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-4.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-5.webp', 'https://kick.com/img/default-profile-pictures/default-avatar-6.webp', ]; return defaultAvatars[hashString(`${username}-${platform}`) % defaultAvatars.length]; } function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // convert to 32-bit int } return Math.abs(hash); } } } } async function GetPronouns(platform, username) { if (pronounMap.has(username)) { console.debug(`Pronouns found for ${username}. Retrieving from hash map.`) return pronounMap.get(username); } else { console.debug(`No pronouns found for ${username}. Retrieving from alejo.io.`) const response = await client.getUserPronouns(platform, username); const userFound = response.pronoun.userFound; const pronouns = userFound ? `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}` : ''; pronounMap.set(username, pronouns); return pronouns; } } function RenderKickEmotes(message) { const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; return message.replace(emoteRegex, (_, id, name) => { const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; return `${name}`; }); } function GetCurrentTimeFormatted(timeFormat = '12-hour') { const now = new Date(); let hours = now.getHours(); const minutes = String(now.getMinutes()).padStart(2, '0'); if (timeFormat === '24-hour') { const paddedHours = String(hours).padStart(2, '0'); return `${paddedHours}:${minutes}`; } const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; hours = hours ? hours : 12; // the hour '0' should be '12' return `${hours}:${minutes} ${ampm}`; } function DecodeHTMLString(html) { var txt = document.createElement("textarea"); txt.innerHTML = html; return txt.value; } // Simple HTML escape function to prevent XSS attacks function EscapeHTML(str) { return str.replace(/[&<>"']/g, match => { const escape = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return escape[match]; }); } function TranslateToFurry(sentence) { // Split on tags, keeping them in the result const parts = sentence.split(/(]*>)/gi); const furryParts = parts.map(part => { // If this part is an , leave it unchanged if (/^]*>$/.test(part)) { return part; } // Otherwise, apply furry translation const words = part.toLowerCase().split(/\b/); return words.map(word => { if (/\w+/.test(word)) { let newWord = word; // Common substitutions newWord = newWord.replace(/l/g, 'w'); newWord = newWord.replace(/r/g, 'w'); newWord = newWord.replace(/th/g, 'f'); newWord = newWord.replace(/you/g, 'yous'); newWord = newWord.replace(/my/g, 'mah'); newWord = newWord.replace(/me/g, 'meh'); newWord = newWord.replace(/am/g, 'am'); newWord = newWord.replace(/is/g, 'is'); newWord = newWord.replace(/are/g, 'are'); newWord = newWord.replace(/very/g, 'vewy'); newWord = newWord.replace(/pretty/g, 'pwetty'); newWord = newWord.replace(/little/g, 'wittle'); newWord = newWord.replace(/nice/g, 'nyce'); // Random additions if (Math.random() < 0.15) { newWord += ' nya~'; } else if (Math.random() < 0.1) { newWord += ' >w<'; } else if (Math.random() < 0.05) { newWord += ' owo'; } return newWord; } return word; }).join(''); }); return furryParts.join(''); } // Given a string, return a random hex code. The same input always results in the same output function RandomHex(str) { // Simple hash to convert string to a number let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); hash |= 0; } // Use the hash to generate a hue (0–360) const hue = Math.abs(hash) % 360; // Force super saturation + readable brightness const saturation = 100; // maximum saturation const lightness = 55; // tweak 50–60 depending on your background // Convert HSL → RGB → hex function hslToHex(h, s, l) { s /= 100; l /= 100; const k = n => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); const r = Math.round(255 * f(0)); const g = Math.round(255 * f(8)); const b = Math.round(255 * f(4)); return ( "#" + r.toString(16).padStart(2, "0") + g.toString(16).padStart(2, "0") + b.toString(16).padStart(2, "0") ); } return hslToHex(hue, saturation, lightness); } // Used to construct a message from "parts" variable commonly found in Streamer.bot chat messages function ConstructMessageFromParts(parts) { return parts.map(part => { if (part.emoji) return ` ${EscapeHTML(part.text)} `; if (!part.type) return EscapeHTML(part.text); if (part.source == 'Twemoji') return EscapeHTML(part.text); switch (part.type) { case "text": return EscapeHTML(part.text); case "cheer": // TODO: It seems like Streamer.bot v1.0.5-alpha3 doesn't include the imageUrl for bits in the message parts, only the bits count. We may need to hardcode the image URL format for Twitch bits, or find another way to retrieve it. let imageUrl = ''; if (!part.imageUrl) { const tiers = [100000, 10000, 5000, 1000, 100, 10, 1]; const activeTier = tiers.find(tier => part.bits >= tier) || 1; part.imageUrl = `https://d3aqoihi2n8ty8.cloudfront.net/actions/cheer/dark/animated/${activeTier}/4.gif`; } // Render the cheer emote image const emoteImg = `${EscapeHTML(part.text)}`; // Render the bits count const bitLabel = `${EscapeHTML(part.bits.toString())}`; return emoteImg + bitLabel; case "mention": return part.text; case "gif": return ``; default: return `${EscapeHTML(part.text)}`; } }).join(''); } // Used to construct a message from "emotes" variable commonly found in Streamer.bot chat messages function RenderMessageWithEmotesHTML(originalMessage, emotes) { if (!emotes || emotes.length === 0) return originalMessage; // Sort emotes by startIndex emotes.sort((a, b) => a.startIndex - b.startIndex); let html = ''; let cursor = 0; emotes.forEach(emote => { // Add text before the emote if (emote.startIndex > cursor) { html += EscapeHTML(originalMessage.slice(cursor, emote.startIndex)); } // Add emote image html += `${EscapeHTML(emote.name)}`; cursor = emote.endIndex + 1; }); // Add remaining text after last emote if (cursor < originalMessage.length) { html += EscapeHTML(originalMessage.slice(cursor)); } return html; } async function LoadHTMLTemplate(name) { const response = await fetch(name); if (!response.ok) { throw new Error(`Failed to load template: ${name}`); } const html = await response.text(); const tempDiv = document.createElement("div"); tempDiv.innerHTML = html; const template = tempDiv.querySelector("template"); if (!template) { throw new Error(`No