From 022b296016a22b560550ec3cbcb7099f92fa89ab Mon Sep 17 00:00:00 2001 From: nutty Date: Tue, 24 Feb 2026 16:00:31 +1100 Subject: [PATCH] =?UTF-8?q?=E2=80=A2=20Kick=20username=20will=20now=20be?= =?UTF-8?q?=20fetched=20(option=20hidden)=20=E2=80=A2=20Added=20Kick=20Fol?= =?UTF-8?q?low=20support=20=E2=80=A2=20Improved=20emote=20support=20(needs?= =?UTF-8?q?=20testing)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .common/utils/helpers.js | 226 ++++++++----- horizontal-chat/script.js | 255 ++++++--------- horizontal-chat/settings/settings.json | 16 +- multichat-overlay/script.js | 366 ++++++++++------------ multichat-overlay/settings/settings.json | 16 +- multistream-alerts/script.js | 92 +++--- multistream-alerts/settings/settings.json | 15 +- printer-bot/contents/script.js | 36 +-- 8 files changed, 476 insertions(+), 546 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 298a2bf..02293b4 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -12,39 +12,39 @@ const pronounMap = new Map(); ////////////////////// function GetBooleanParam(paramName, defaultValue) { - const urlParams = new URLSearchParams(window.location.search); - const paramValue = urlParams.get(paramName); + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); - if (paramValue === null) { - return defaultValue; // Parameter not found - } + if (paramValue === null) { + return defaultValue; // Parameter not found + } - const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity + 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' - } + 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); + 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 - } + 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 + 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 - } + if (isNaN(intValue)) { + return null; // or handle the error in another way, e.g., throw an error + } - return intValue; + return intValue; } async function GetKickIds(username) { @@ -143,82 +143,89 @@ async function GetAvatar(username, platform) { } 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}` : ''; + 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); + pronounMap.set(username, pronouns); - return pronouns; - } + return pronouns; + } } function GetCurrentTimeFormatted() { - const now = new Date(); - let hours = now.getHours(); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const ampm = hours >= 12 ? 'PM' : 'AM'; + const now = new Date(); + let hours = now.getHours(); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' - const formattedTime = `${hours}:${minutes} ${ampm}`; - return formattedTime; + const formattedTime = `${hours}:${minutes} ${ampm}`; + return formattedTime; } function DecodeHTMLString(html) { - var txt = document.createElement("textarea"); - txt.innerHTML = html; - return txt.value; + var txt = document.createElement("textarea"); + txt.innerHTML = html; + return txt.value; } function TranslateToFurry(sentence) { - const words = sentence.toLowerCase().split(/\b/); + // Split on tags, keeping them in the result + const parts = sentence.split(/(]*>)/gi); - const furryWords = words.map(word => { - if (/\w+/.test(word)) { - let newWord = word; + const furryParts = parts.map(part => { + // If this part is an , leave it unchanged + if (/^]*>$/.test(part)) { + return part; + } - // 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'); + // Otherwise, apply furry translation + const words = part.toLowerCase().split(/\b/); - // 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 words.map(word => { + if (/\w+/.test(word)) { + let newWord = word; - return newWord; - } - return 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'); - return furryWords.join(''); -} + // 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'; + } -function EscapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + 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 @@ -260,4 +267,63 @@ function RandomHex(str) { } 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 => { + switch (part.type) + { + case "text": + return part.text; + case "cheer": + // Render the cheer emote image + const emoteImg = `${part.text}`; + + // Render the bits count + const bitLabel = `${part.bits}`; + + return emoteImg + bitLabel; + default: + return `${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)); + } + + // Simple HTML escape function + function escapeHTML(str) { + return str.replace(/[&<>"']/g, match => { + const escape = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; + return escape[match]; + }); + } + + return html; } \ No newline at end of file diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 93d4008..08d4186 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -50,9 +50,8 @@ const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true); -const kickUsername = urlParams.get("kickUsername") || ""; const showKickMessages = GetBooleanParam("showKickMessages", true); -// const showKickFollows = GetBooleanParam("showKickFollows", false); +const showKickFollows = GetBooleanParam("showKickFollows", false); const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickHosts = GetBooleanParam("showKickHosts", true); @@ -86,6 +85,7 @@ const animationSpeed = GetIntParam("animationSpeed", 0.5); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; +const kickUsername = urlParams.get("kickUsername") || ""; @@ -233,6 +233,37 @@ client.on('YouTube.GiftMembershipReceived', (response) => { YouTubeGiftMembershipReceived(); }) +client.on('Kick.ChatMessage', (response) => { + console.debug(response.data); + KickChatMessage(response.data); +}) + +client.on('Kick.Follow', (response) => { + console.debug(response.data); + KickFollow(response.data); +}) + +// // TODO: Seems to be missing data, using Pusher until this is fixed +// client.on('Kick.Subscription', (response) => { +// console.debug(response.data); +// KickSubscription(response.data); +// }) + +// client.on('Kick.Resubscription', (response) => { +// console.debug(response.data); +// KickSubscription(response.data); +// }) + +// client.on('Kick.GiftSubscription', (response) => { +// console.debug(response.data); +// KickGiftedSubscriptions(response.data); +// }) + +// client.on('Kick.RewardRedemption', (response) => { +// console.debug(response.data); +// KickRewardRedeemed(response.data); +// }) + client.on('Streamlabs.Donation', (response) => { console.debug(response.data); StreamlabsDonation(response.data); @@ -311,8 +342,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) - return; + { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + if (broadcasterInfo.platforms.kick) + kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; + else + return; + } // Channel to subscribe to (you'll need the correct channel name here) const kickIds = await GetKickIds(kickUsername); @@ -327,7 +367,7 @@ async function KickConnect() { // Reconnect websocket.onclose = function () { console.log(`Reconnecting to ${kickUsername}...`); - setTimeout(connectPusher, 5000); + setTimeout(KickConnect, 5000); }; websocket.onopen = function () { @@ -358,9 +398,9 @@ async function KickConnect() { const eventArgs = JSON.parse(data.data); const event = data.event.split('\\').pop(); switch (event) { - case 'ChatMessageEvent': - KickChatMessage(eventArgs); - break; + // case 'ChatMessageEvent': + // KickChatMessage(eventArgs); + // break; //// 'Follows' unsupported by pusher // case 'FollowEvent': // break; @@ -397,7 +437,6 @@ async function KickConnect() { window.addEventListener('load', KickConnect); - ////////////////////// // TIKFINITY CLIENT // ////////////////////// @@ -522,7 +561,7 @@ async function TwitchChatMessage(data) { } // Set the message data - let message = data.message.message; + let message = ConstructMessageFromParts(data.parts); const messageColor = data.message.color; const role = data.message.role; @@ -532,7 +571,7 @@ async function TwitchChatMessage(data) { // Set message text if (showMessage) { - messageDiv.innerText = message; + messageDiv.innerHTML = message; } // Set the "action" color @@ -545,7 +584,6 @@ async function TwitchChatMessage(data) { platformDiv.innerHTML = platformElements; } - // Render badges if (showBadges) { badgeListDiv.innerHTML = ""; @@ -557,38 +595,6 @@ async function TwitchChatMessage(data) { } } - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - messageDiv.innerHTML = messageDiv.innerHTML.replace(regex, emoteElement); - } - - // Render cheermotes - for (i in data.cheerEmotes) { - // const cheerEmoteElement = ``; - // messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.cheerEmotes[i].name}\\b`), cheerEmoteElement); - const bits = data.cheerEmotes[i].bits; - const imageUrl = data.cheerEmotes[i].imageUrl; - const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; - const bitsElements = `${bits}` - messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); - } - // Render avatars if (showAvatar) { const username = data.message.username; @@ -660,29 +666,7 @@ async function TwitchAnnouncement(data) { break; } - let message = data.text; - - // Render emotes - for (i in data.parts) { - if (data.parts[i].type == `emote`) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.parts[i].text); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - message = message.replace(regex, emoteElement); - } - } + let message = ConstructMessageFromParts(data.parts); ShowAlert(message, background); } @@ -918,13 +902,17 @@ function YouTubeMessage(data) { usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red } - if (showMessage) { - messageDiv.innerText = data.message; - } - + // Set the message data + let message = RenderMessageWithEmotesHTML(data.message, data.emotes); + // Set furry mode if (furryMode) - messageDiv.innerText = TranslateToFurry(data.message); + message = TranslateToFurry(message); + + // Set message text + if (showMessage) { + messageDiv.innerHTML = message; + } // Render platform if (showPlatform) { @@ -972,12 +960,6 @@ function YouTubeMessage(data) { badgeListDiv.appendChild(badge); } - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement); - } - // Render avatars if (showAvatar) { const avatar = new Image(); @@ -1305,11 +1287,11 @@ async function KickChatMessage(data) { return; // Don't post messages starting with "!" - if (data.content.startsWith("!") && excludeCommands) + if (data.text.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.sender.username.toLowerCase())) + if (ignoreUserList.includes(data.user.name.toLowerCase())) return; // Get a reference to the template @@ -1336,12 +1318,12 @@ async function KickChatMessage(data) { // Set the username info if (showUsername) { - usernameDiv.innerText = data.sender.username; - usernameDiv.style.color = data.sender.identity.color; + usernameDiv.innerText = data.user.name; + usernameDiv.style.color = data.user.color; } // Set the message data - let message = data.content; + let message = ConstructMessageFromParts(data.parts); // Set furry mode if (furryMode) @@ -1349,7 +1331,7 @@ async function KickChatMessage(data) { // Set message text if (showMessage) { - messageDiv.innerText = message; + messageDiv.innerHTML = message; } // Render platform @@ -1361,28 +1343,38 @@ async function KickChatMessage(data) { // Render badges if (showBadges) { badgeListDiv.innerHTML = ""; - for (i in data.sender.identity.badges) { + for (i in data.user.badges) { const badge = new Image(); - badge.src = GetKickBadgeURL(data.sender.identity.badges[i]); + if (data.user.badges[i].imageUrl) + badge.src = data.user.badges[i].imageUrl; + else + badge.src = GetKickBadgeURL(data.user.badges[i]); badge.classList.add("badge"); badgeListDiv.appendChild(badge); } } + function GetKickBadgeURL(data) { + switch (data.id) { + case 'subscriber': + return CalculateKickSubBadge(data.count); + default: + return `icons/badges/kick-${data.id}.svg`; + } + function CalculateKickSubBadge(months) { + if (!Array.isArray(kickSubBadges)) return null; - // Render emotes - function replaceEmotes(message) { - const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; + // Filter for eligible badges, then get the one with the highest 'months' + const badge = kickSubBadges + .filter(b => b.months <= months) + .sort((a, b) => b.months - a.months)[0]; - return message.replace(emoteRegex, (_, id, name) => { - const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; - return `${name}`; - }); + return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`; + } } - messageDiv.innerHTML = replaceEmotes(message); // Render avatars if (showAvatar) { - const username = data.sender.slug; + const username = data.user.login; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; @@ -1399,7 +1391,21 @@ async function KickChatMessage(data) { userInfoDiv.style.display = "none"; } - AddMessageItem(instance, data.id, 'kick', data.sender.id); + AddMessageItem(instance, data.messageId, 'kick', data.user.id); +} + +function KickFollow(data) { + if (!showKickFollows) + return; + + // Set the text + let username = data.user.name; + if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) + username = `${data.user.name} (${data.user.login})`; + + const message = `${username} followed`; + + ShowAlert(message, 'kick'); } function KickSubscription(data) { @@ -1408,7 +1414,7 @@ function KickSubscription(data) { // Set the text const username = data.username; - const months = data.months; + const months = data.months let message = ''; if (months <= 1) @@ -1710,43 +1716,6 @@ function AddMessageItem(element, elementID, platform, userId) { }, 200); } -// I used Gemini for this shit so if it doesn't work, blame Google -function FindFirstImageUrl(jsonObject) { - if (typeof jsonObject !== 'object' || jsonObject === null) { - return null; // Handle invalid input - } - - function iterate(obj) { - if (Array.isArray(obj)) { - for (const item of obj) { - const result = iterate(item); - if (result) { - return result; - } - } - return null; - } - - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - if (key === 'imageUrl') { - return obj[key]; // Found it! Return the value. - } - - if (typeof obj[key] === 'object' && obj[key] !== null) { - const result = iterate(obj[key]); // Recursive call for nested objects - if (result) { - return result; // Propagate the found value - } - } - } - } - return null; // Key not found in this level - } - - return iterate(jsonObject); -} - function ShowAlert(message, background = null, duration = animationDuration) { // Check if the widget is in the middle of an animation @@ -1810,26 +1779,6 @@ function GetWinnersList(gifts) { } } -function GetKickBadgeURL(data) { - switch (data.type) { - case 'subscriber': - return CalculateKickSubBadge(data.count); - default: - return `icons/badges/kick-${data.type}.svg`; - } -} - -function CalculateKickSubBadge(months) { - if (!Array.isArray(kickSubBadges)) return null; - - // Filter for eligible badges, then get the one with the highest 'months' - const badge = kickSubBadges - .filter(b => b.months <= months) - .sort((a, b) => b.months - a.months)[0]; - - return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`; -} - /////////////////////////////////// diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json index 7a21efc..bf48dea 100644 --- a/horizontal-chat/settings/settings.json +++ b/horizontal-chat/settings/settings.json @@ -231,14 +231,6 @@ "defaultValue": true, "group": "Which YouTube messages do you want to see?" }, - { - "id": "kickUsername", - "label": "Kick Username", - "description": "", - "type": "text", - "defaultValue": "", - "group": "Which Kick messages do you want to see?" - }, { "id": "showKickMessages", "label": "Chat Messages", @@ -247,6 +239,14 @@ "defaultValue": true, "group": "Which Kick messages do you want to see?" }, + { + "id": "showKickFollows", + "label": "New Followers", + "description": "", + "type": "checkbox", + "defaultValue": false, + "group": "Which Kick messages do you want to see?" + }, { "id": "showKickSubs", "label": "New Subscribers", diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 73e794a..bcaf0b8 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -55,9 +55,8 @@ const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); -const kickUsername = urlParams.get("kickUsername") || ""; const showKickMessages = GetBooleanParam("showKickMessages", true); -// const showKickFollows = GetBooleanParam("showKickFollows", false); +const showKickFollows = GetBooleanParam("showKickFollows", false); const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickHosts = GetBooleanParam("showKickHosts", true); @@ -92,6 +91,7 @@ const animationSpeed = GetIntParam("animationSpeed", 0.1); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; +let kickUsername = urlParams.get("kickUsername") || ""; @@ -268,6 +268,37 @@ client.on('YouTube.GiftMembershipReceived', (response) => { YouTubeGiftMembershipReceived(response.data); }) +client.on('Kick.ChatMessage', (response) => { + console.debug(response.data); + KickChatMessage(response.data); +}) + +client.on('Kick.Follow', (response) => { + console.debug(response.data); + KickFollow(response.data); +}) + +// // TODO: Seems to be missing data, using Pusher until this is fixed +// client.on('Kick.Subscription', (response) => { +// console.debug(response.data); +// KickSubscription(response.data); +// }) + +// client.on('Kick.Resubscription', (response) => { +// console.debug(response.data); +// KickSubscription(response.data); +// }) + +// client.on('Kick.GiftSubscription', (response) => { +// console.debug(response.data); +// KickGiftedSubscriptions(response.data); +// }) + +// client.on('Kick.RewardRedemption', (response) => { +// console.debug(response.data); +// KickRewardRedeemed(response.data); +// }) + client.on('Streamlabs.Donation', (response) => { console.debug(response.data); StreamlabsDonation(response.data); @@ -351,8 +382,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) - return; + { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + if (broadcasterInfo.platforms.kick) + kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; + else + return; + } // Channel to subscribe to (you'll need the correct channel name here) const kickIds = await GetKickIds(kickUsername); @@ -367,7 +407,7 @@ async function KickConnect() { // Reconnect websocket.onclose = function () { console.log(`Reconnecting to ${kickUsername}...`); - setTimeout(connectPusher, 5000); + setTimeout(KickConnect, 5000); }; websocket.onopen = function () { @@ -398,9 +438,9 @@ async function KickConnect() { const eventArgs = JSON.parse(data.data); const event = data.event.split('\\').pop(); switch (event) { - case 'ChatMessageEvent': - KickChatMessage(eventArgs); - break; + // case 'ChatMessageEvent': + // KickChatMessage(eventArgs); + // break; //// 'Follows' unsupported by pusher // case 'FollowEvent': // break; @@ -617,7 +657,7 @@ async function TwitchChatMessage(data) { } // Set the message data - let message = data.message.message; + let message = ConstructMessageFromParts(data.parts); const messageColor = data.message.color; const role = data.message.role; @@ -627,7 +667,7 @@ async function TwitchChatMessage(data) { // Set message text if (showMessage) { - messageDiv.innerText = message; + messageDiv.innerHTML = message; } // Set the "action" color @@ -658,36 +698,6 @@ async function TwitchChatMessage(data) { } } - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - messageDiv.innerHTML = messageDiv.innerHTML.replace(regex, emoteElement); - } - - // Render cheermotes - for (i in data.cheerEmotes) { - const bits = data.cheerEmotes[i].bits; - const imageUrl = data.cheerEmotes[i].imageUrl; - const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; - const bitsElements = `${bits}` - messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); - } - // Render avatars if (showAvatar) { const username = data.message.username; @@ -893,7 +903,7 @@ async function TwitchAnnouncement(data) { else content.querySelector("#username").innerText = `${data.user.name} (${data.user.login})`; content.querySelector("#username").style.color = data.user.color; - content.querySelector("#message").innerText = data.text; + content.querySelector("#message").innerHTML = ConstructMessageFromParts(data.parts); // Remove the line break content.querySelector("#colon-separator").style.display = `inline`; @@ -921,28 +931,6 @@ async function TwitchAnnouncement(data) { content.querySelector("#pronouns").innerText = pronouns; } - // Render emotes - for (i in data.parts) { - if (data.parts[i].type == `emote`) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.parts[i].text); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - content.querySelector("#message").innerHTML = content.querySelector("#message").innerHTML.replace(regex, emoteElement); - } - } - // Insert the modified template instance into the DOM instance.querySelector("#content").appendChild(content); @@ -1244,30 +1232,10 @@ async function TwitchWatchStreak(data) { const displayName = data.displayName; const watchStreak = data.watchStreak; - const message = data.message; + const message = RenderMessageWithEmotesHTML(data.message, data.emotes); titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; - contentDiv.innerText = `${message}`; - - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - contentDiv.innerHTML = contentDiv.innerHTML.replace(regex, emoteElement); - } + contentDiv.innerHTML = message; AddMessageItem(instance, data.messageId); } @@ -1381,14 +1349,18 @@ async function YouTubeMessage(data) { else usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red } - - if (showMessage) { - messageDiv.innerText = data.message; - } - + + // Set the message data + let message = RenderMessageWithEmotesHTML(data.message, data.emotes); + // Set furry mode if (furryMode) - messageDiv.innerText = TranslateToFurry(data.message); + message = TranslateToFurry(message); + + // Set message text + if (showMessage) { + messageDiv.innerHTML = message; + } // Remove the line break if (inlineChat) { @@ -1443,13 +1415,6 @@ async function YouTubeMessage(data) { badgeListDiv.appendChild(badge); } - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - // messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement); - messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement); - } - // Render avatars if (showAvatar) { const avatar = new Image(); @@ -1483,7 +1448,6 @@ async function YouTubeMessage(data) { } // Embed image - const message = data.message; if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'youtube') && IsImageUrl(message)) { const image = new Image(); @@ -1573,6 +1537,43 @@ function YouTubeSuperSticker(data) { const stickerURL = FindFirstImageUrl(data); const stickerImage = ``; + // I used Gemini for this shit so if it doesn't work, blame Google + function FindFirstImageUrl(jsonObject) { + if (typeof jsonObject !== 'object' || jsonObject === null) { + return null; // Handle invalid input + } + + function iterate(obj) { + if (Array.isArray(obj)) { + for (const item of obj) { + const result = iterate(item); + if (result) { + return result; + } + } + return null; + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === 'imageUrl') { + return obj[key]; // Found it! Return the value. + } + + if (typeof obj[key] === 'object' && obj[key] !== null) { + const result = iterate(obj[key]); // Recursive call for nested objects + if (result) { + return result; // Propagate the found value + } + } + } + } + return null; // Key not found in this level + } + + return iterate(jsonObject); + } + avatarDiv.innerHTML = stickerImage; titleDiv.innerHTML = `${user} sent a Super Sticker (${amount})`; @@ -2249,11 +2250,11 @@ async function KickChatMessage(data) { return; // Don't post messages starting with "!" - if (data.content.startsWith("!") && excludeCommands) + if (data.text.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.sender.username.toLowerCase())) + if (ignoreUserList.includes(data.user.name.toLowerCase())) return; // Get a reference to the template @@ -2298,10 +2299,10 @@ async function KickChatMessage(data) { // } // Set Reply Message - const isReply = data.type == 'reply'; + const isReply = data.isReply; if (isReply && showMessage) { - const replyUser = data.metadata.original_sender.username; - const replyMsg = data.metadata.original_message.content; + const replyUser = data.reply.sender.name; + const replyMsg = data.reply.sender.content; replyDiv.style.display = 'block'; replyUserDiv.innerText = replyUser; @@ -2316,13 +2317,13 @@ async function KickChatMessage(data) { // Set the username info if (showUsername) { - usernameDiv.innerText = data.sender.username; - usernameDiv.style.color = data.sender.identity.color; + usernameDiv.innerText = data.user.name; + usernameDiv.style.color = data.user.color; } // Set the message data - let message = data.content; - + let message = ConstructMessageFromParts(data.parts); + // Highlight mentions const mentionRgx = new RegExp(`(^|\\s)@${kickUsername}(\\s|$)`, 'i'); const mention = mentionRgx.test(message); @@ -2335,7 +2336,7 @@ async function KickChatMessage(data) { // Set message text if (showMessage) { - messageDiv.innerText = message; + messageDiv.innerHTML = message; } // Remove the line break @@ -2354,28 +2355,38 @@ async function KickChatMessage(data) { // Render badges if (showBadges) { badgeListDiv.innerHTML = ""; - for (i in data.sender.identity.badges) { + for (i in data.user.badges) { const badge = new Image(); - badge.src = GetKickBadgeURL(data.sender.identity.badges[i]); + if (data.user.badges[i].imageUrl) + badge.src = data.user.badges[i].imageUrl; + else + badge.src = GetKickBadgeURL(data.user.badges[i]); badge.classList.add("badge"); badgeListDiv.appendChild(badge); } } + function GetKickBadgeURL(data) { + switch (data.id) { + case 'subscriber': + return CalculateKickSubBadge(data.count); + default: + return `icons/badges/kick-${data.id}.svg`; + } + function CalculateKickSubBadge(months) { + if (!Array.isArray(kickSubBadges)) return null; - // Render emotes - function replaceEmotes(message) { - const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; + // Filter for eligible badges, then get the one with the highest 'months' + const badge = kickSubBadges + .filter(b => b.months <= months) + .sort((a, b) => b.months - a.months)[0]; - return message.replace(emoteRegex, (_, id, name) => { - const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; - return `${name}`; - }); + return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`; + } } - messageDiv.innerHTML = replaceEmotes(message); // Render avatars if (showAvatar) { - const username = data.sender.slug; + const username = data.user.login; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; @@ -2389,7 +2400,7 @@ async function KickChatMessage(data) { if (groupConsecutiveMessages && messageList.children.length > 0 && scrollDirection != 2) { const lastPlatform = messageList.lastChild.dataset.platform; const lastUserId = messageList.lastChild.dataset.userId; - if (lastPlatform == "kick" && lastUserId == data.sender.id) { + if (lastPlatform == "kick" && lastUserId == data.user.id) { userInfoDiv.style.display = "none"; avatarDiv.style.visibility = "hidden"; avatarDiv.style.height = "0px"; @@ -2406,7 +2417,7 @@ async function KickChatMessage(data) { messageDiv.innerHTML = ''; messageDiv.appendChild(image); - AddMessageItem(instance, data.id, 'kick', data.sender.id); + AddMessageItem(instance, data.messageId, 'kick', data.user.id); }; const urlObj = new URL(message); @@ -2416,7 +2427,7 @@ async function KickChatMessage(data) { image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString(); } else { - AddMessageItem(instance, data.id, 'kick', data.sender.id); + AddMessageItem(instance, data.messageId, 'kick', data.user.id); } // Render YouTube links @@ -2428,33 +2439,33 @@ async function KickChatMessage(data) { } } -// async function KickFollow(data) { -// if (!showKickFollows) -// return; +async function KickFollow(data) { + if (!showKickFollows) + return; -// // Get a reference to the template -// const template = document.getElementById('cardTemplate'); + // Get a reference to the template + const template = document.getElementById('cardTemplate'); -// // Create a new instance of the template -// const instance = template.content.cloneNode(true); + // Create a new instance of the template + const instance = template.content.cloneNode(true); -// // Get divs -// const cardDiv = instance.querySelector("#card"); -// const headerDiv = instance.querySelector("#header"); -// const avatarDiv = instance.querySelector("#avatar"); -// const iconDiv = instance.querySelector("#icon"); -// const titleDiv = instance.querySelector("#title"); -// const contentDiv = instance.querySelector("#contentDiv"); + // Get divs + const cardDiv = instance.querySelector("#card"); + const headerDiv = instance.querySelector("#header"); + const avatarDiv = instance.querySelector("#avatar"); + const iconDiv = instance.querySelector("#icon"); + const titleDiv = instance.querySelector("#title"); + const contentDiv = instance.querySelector("#contentDiv"); -// // Set the card background colors -// cardDiv.classList.add('kick'); + // Set the card background colors + cardDiv.classList.add('kick'); -// // Set the text -// let username = data.user; -// titleDiv.innerText = `${username} followed`; + // Set the text + let username = data.user.name; + titleDiv.innerText = `${username} followed`; -// AddMessageItem(instance, data.messageId); -// } + AddMessageItem(instance, data.user.id, 'kick', data.user.id); +} async function KickSubscription(data) { if (!showKickSubs) @@ -2604,7 +2615,7 @@ async function KickRewardRedeemed(data) { // Set the text const username = data.username; const rewardName = data.reward_title; - const userInput = data.user_input; + const userInput = data.userInput; titleDiv.innerHTML = `${username} redeemed ${rewardName}`; contentDiv.innerText = `${userInput}`; @@ -3154,43 +3165,6 @@ function AddMessageItem(element, elementID, platform, userId) { }, 200); } -// I used Gemini for this shit so if it doesn't work, blame Google -function FindFirstImageUrl(jsonObject) { - if (typeof jsonObject !== 'object' || jsonObject === null) { - return null; // Handle invalid input - } - - function iterate(obj) { - if (Array.isArray(obj)) { - for (const item of obj) { - const result = iterate(item); - if (result) { - return result; - } - } - return null; - } - - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - if (key === 'imageUrl') { - return obj[key]; // Found it! Return the value. - } - - if (typeof obj[key] === 'object' && obj[key] !== null) { - const result = iterate(obj[key]); // Recursive call for nested objects - if (result) { - return result; // Propagate the found value - } - } - } - } - return null; // Key not found in this level - } - - return iterate(jsonObject); -} - function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(targetPermissions, data, platform) { return GetPermissionLevel(data, platform) >= targetPermissions; } @@ -3209,14 +3183,14 @@ function GetPermissionLevel(data, platform) { else return 10; case 'kick': - if (data.sender.identity.badges.some(item => item.type === 'broadcaster')) + if (data.user.badges.some(item => item.id === 'broadcaster')) return 40; - else if (data.sender.identity.badges.some(item => item.type === 'moderator')) + else if (data.user.badges.some(item => item.id === 'moderator')) return 30; - else if (data.sender.identity.badges.some(item => item.type === 'vip') || - data.sender.identity.badges.some(item => item.type === 'og')) + else if (data.user.badges.some(item => item.id === 'vip') || + data.user.badges.some(item => item.type === 'og')) return 20; - else if (data.sender.identity.badges.some(item => item.type === 'subscriber')) + else if (data.user.badges.some(item => item.id === 'subscriber')) return 15; else return 10; @@ -3249,26 +3223,6 @@ function GetWinnersList(gifts) { } } -function GetKickBadgeURL(data) { - switch (data.type) { - case 'subscriber': - return CalculateKickSubBadge(data.count); - default: - return `icons/badges/kick-${data.type}.svg`; - } -} - -function CalculateKickSubBadge(months) { - if (!Array.isArray(kickSubBadges)) return null; - - // Filter for eligible badges, then get the one with the highest 'months' - const badge = kickSubBadges - .filter(b => b.months <= months) - .sort((a, b) => b.months - a.months)[0]; - - return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`; -} - /////////////////////////////////// diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 47a812b..68d2f89 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -352,14 +352,6 @@ "defaultValue": true, "group": "Which YouTube messages do you want to see?" }, - { - "id": "kickUsername", - "label": "Kick Username", - "description": "", - "type": "text", - "defaultValue": "", - "group": "Which Kick messages do you want to see?" - }, { "id": "showKickMessages", "label": "Chat Messages", @@ -368,6 +360,14 @@ "defaultValue": true, "group": "Which Kick messages do you want to see?" }, + { + "id": "showKickFollows", + "label": "New Followers", + "description": "", + "type": "checkbox", + "defaultValue": false, + "group": "Which Kick messages do you want to see?" + }, { "id": "showKickSubs", "label": "New Subscribers", diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index e628056..0b22d8b 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -71,7 +71,8 @@ const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const twitchWatchStreaksAction = urlParams.get("twitchWatchStreaksAction") || ""; // Which Kick alerts do you want to see? -const kickUsername = urlParams.get("kickUsername") || ""; +const showKickFollows = GetBooleanParam("showKickFollows", false); +const kickFollowAction = urlParams.get("kickFollowAction") || ""; const showKickSubs = GetBooleanParam("showKickSubs", true); const kickSubAction = urlParams.get("kickSubAction") || ""; const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); @@ -110,6 +111,12 @@ const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") | const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false); const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; +//////////////////// +// HIDDEN OPTIONS // +//////////////////// + +let kickUsername = urlParams.get("kickUsername") || ""; + // Set avatar visibility if (!showAvatar) { avatarElement.style.display = 'none'; @@ -316,8 +323,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { // Connect and handle Pusher WebSocket async function KickConnect() { + // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) - return; + { + // Fetch from Streamer.bot + const broadcasterInfo = await client.getBroadcaster(); + + if (broadcasterInfo.platforms.kick) + kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; + else + return; + } // Channel to subscribe to (you'll need the correct channel name here) const kickIds = await GetKickIds(kickUsername); @@ -332,7 +348,7 @@ async function KickConnect() { // Reconnect websocket.onclose = function () { console.log(`Reconnecting to ${kickUsername}...`); - setTimeout(connectPusher, 5000); + setTimeout(KickConnect, 5000); }; websocket.onopen = function () { @@ -482,41 +498,11 @@ async function TwitchCheer(data) { // Set the text const username = data.message.displayName; const bits = data.bits; - let message = data.message.message; + let message = ConstructMessageFromParts(data.parts); // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?:^|[^\\w])${emoteName}(?:$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - message = message.replace(regex, emoteElement); - } - - // Render cheermotes - for (i in data.cheerEmotes) { - const bits = data.cheerEmotes[i].bits; - const imageUrl = data.cheerEmotes[i].imageUrl; - const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; - const bitsElements = `${bits}` - message = message.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); - } - UpdateAlertBox( 'twitch', avatarURL, @@ -1262,28 +1248,28 @@ function FourthwallGiftDrawEnded(data) { ); } -// async function KickFollow(data) { -// if (!showKickFollows) -// return; +async function KickFollow(data) { + if (!showKickFollows) + return; -// // Set the text -// const username = data.user; + // Set the text + const username = data.user.name; -// // Render avatars -// const avatarURL = await GetAvatar(username, 'kick'); + // Render avatars + const avatarURL = await GetAvatar(username, 'kick'); -// UpdateAlertBox( -// 'kick', -// avatarURL, -// `${username}`, -// `followed`, -// '', -// username, -// '', -// kickFollowAction, -// data -// ); -// } + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + `followed`, + '', + username, + '', + kickFollowAction, + data + ); +} async function KickSubscription(data) { if (!showKickSubs) diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json index b59bc74..61d4982 100644 --- a/multistream-alerts/settings/settings.json +++ b/multistream-alerts/settings/settings.json @@ -381,11 +381,20 @@ "group": "Which YouTube alerts do you want to see?" }, { - "id": "kickUsername", - "label": "Kick Username", + "id": "showKickFollows", + "label": "New Followers", "description": "", - "type": "text", + "type": "checkbox", + "defaultValue": false, + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "KickFollowAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", "defaultValue": "", + "showIf": "showKickFollows", "group": "Which Kick alerts do you want to see?" }, { diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js index 945a62b..0354948 100644 --- a/printer-bot/contents/script.js +++ b/printer-bot/contents/script.js @@ -51,37 +51,7 @@ async function CustomEvent(data) { subtitleEl.innerText = `${data.user}`; const messageEl = document.createElement('div'); - messageEl.innerHTML = data.message; - - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement); - } - - // Render cheermotes - for (i in data.cheerEmotes) { - const bits = data.cheerEmotes[i].bits; - const imageUrl = data.cheerEmotes[i].imageUrl; - const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; - const bitsElements = `${bits}` - messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); - } + messageEl.innerHTML = ConstructMessageFromParts(data.parts); contentEl.appendChild(messageEl); @@ -695,10 +665,6 @@ function IsValidUrl(string) { } } -function EscapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - function SetPlatformIcon(el, platform) { // Set the platform icon let baseURL = window.location.href;