From a3e0123cb930de06c8109d2533af4cefcfdc4359 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Fri, 27 Jun 2025 22:55:08 +1000 Subject: [PATCH 01/70] Added basic TikTok and Kick support --- multichat-overlay/icons/platforms/kick.png | Bin 0 -> 1169 bytes multichat-overlay/icons/platforms/tiktok.png | Bin 0 -> 41499 bytes multichat-overlay/index.html | 18 + multichat-overlay/script.js | 633 +++++++++++++++++-- multichat-overlay/style.css | 44 ++ 5 files changed, 635 insertions(+), 60 deletions(-) create mode 100644 multichat-overlay/icons/platforms/kick.png create mode 100644 multichat-overlay/icons/platforms/tiktok.png diff --git a/multichat-overlay/icons/platforms/kick.png b/multichat-overlay/icons/platforms/kick.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc9afaedddcf0f850be6412b56b69af9fd90ac2 GIT binary patch literal 1169 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

hD0H+2U*svw=4}=xXV9nSWwRK?`m4_P73zR zDoM|J=erCsHs)mgh_*E0zp`LXA0oasny!a~VHmF@C#$q+#cXVr;U7rJgyh5<7PHZ- z2Z**Ne12-1BTH)mkor)(l2I~LUx2AtwZt`|BqgyV)hf9t6-Y4{85o)98d&HWS%esxSs58xnHp&u7+4t?tnLtb fhN2-iKP5A*61Rq1SJy-VH86O(`njxgN@xNASjSp) literal 0 HcmV?d00001 diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 1904d30..0d4c64b 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -42,6 +42,12 @@ const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPoin const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true); +const showKickMessages = GetBooleanParam("showKickMessages", true); +const showKickFollows = GetBooleanParam("showKickFollows", false); +const showKickSubs = GetBooleanParam("showKickSubs", true); +const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); +const showKickHosts = GetBooleanParam("showKickHosts", true); + const showYouTubeMessages = GetBooleanParam("showYouTubeMessages", true); const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true); const showYouTubeSuperStickers = GetBooleanParam("showYouTubeSuperStickers", true); @@ -258,6 +264,11 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { FourthwallGiftDrawEnded(response.data); }) +client.on('Custom.CodeEvent', (response) => { + console.debug(response.data); + CustomCodeEvent(response.data); +}) + ///////////////////// @@ -393,7 +404,7 @@ async function TwitchChatMessage(data) { // Render avatars if (showAvatar) { const username = data.message.username; - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; avatar.classList.add("avatar"); @@ -1031,6 +1042,321 @@ function FourthwallGiftDrawEnded(data) { ShowAlert(message, 'fourthwall'); } +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickChatMessage": + KickChatMessage(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); + break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; + case "kickChatMessageDeleted": + KickChatMessageDeleted(eventArgs); + break; + case "kickBan": + KickBan(eventArgs); + break; + case "kickTO": + KickBan(eventArgs); + break; + + } +} + +async function KickChatMessage(data) { + if (!showKickMessages) + return; + + // Don't post messages starting with "!" + if (data.message.startsWith("!") && excludeCommands) + return; + + // Don't post messages from users from the ignore list + if (ignoreUserList.includes(data.user.toLowerCase())) + return; + + // Get a reference to the template + const template = document.getElementById('messageTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const userInfoDiv = instance.querySelector("#userInfo"); + const avatarDiv = instance.querySelector("#avatar"); + const timestampDiv = instance.querySelector("#timestamp"); + const platformDiv = instance.querySelector("#platform"); + const badgeListDiv = instance.querySelector("#badgeList"); + const pronounsDiv = instance.querySelector("#pronouns"); + const usernameDiv = instance.querySelector("#username"); + const messageDiv = instance.querySelector("#message"); + + // // Set Shared Chat + // // If the message is from Shared Chat AND the user indicated that they do NOT + // // want shared chat messages, don't show it on screen + // const isSharedChat = data.isSharedChat; + // if (isSharedChat && !showTwitchSharedChat) { + // if (!data.sharedChat.primarySource) + // return; + // } + + // Set timestamp + if (showTimestamps) { + timestampDiv.classList.add("timestamp"); + timestampDiv.innerText = GetCurrentTimeFormatted(); + } + + // Set the username info + if (showUsername) { + usernameDiv.innerText = data.user; + usernameDiv.style.color = data.color; + } + + // // Set pronouns + // const pronouns = await GetPronouns('twitch', data.message.username); + // if (pronouns && showPronouns) { + // pronounsDiv.classList.add("pronouns"); + // pronounsDiv.innerText = pronouns; + // } + + // Set the message data + let message = data.message; + const messageColor = data.color; + const role = data.role; + + // Set furry mode + if (furryMode) + message = TranslateToFurry(message); + + // Set message text + if (showMessage) { + messageDiv.innerText = message; + } + + // Set the "action" color + if (data.isAction) + messageDiv.style.color = messageColor; + + // Render platform + if (showPlatform) { + const platformElements = ``; + platformDiv.innerHTML = platformElements; + } + + // // Render badges + // if (showBadges) { + // badgeListDiv.innerHTML = ""; + // for (i in data.message.badges) { + // const badge = new Image(); + // badge.src = data.message.badges[i].imageUrl; + // badge.classList.add("badge"); + // badgeListDiv.appendChild(badge); + // } + // } + + // Render badges + if (data.isModerator && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-moderator.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + if (data.isSubscribed && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-member.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + if (data.isVip && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-verified.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + // // 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 emotes + function replaceEmotes(message) { + const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; + + return message.replace(emoteRegex, (_, id, name) => { + const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; + return `${name}`; + }); + } + messageDiv.innerHTML = replaceEmotes(message); + + // Render avatars + if (showAvatar) { + const username = data.userName; + const avatarURL = await GetAvatar(username, 'kick'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + // Hide the header if the same username sends a message twice in a row + const messageList = document.getElementById("messageList"); + if (groupConsecutiveMessages && messageList.children.length > 0) { + const lastPlatform = messageList.lastChild.dataset.platform; + const lastUserId = messageList.lastChild.dataset.userId; + if (lastPlatform == "kick" && lastUserId == data.userId) + userInfoDiv.style.display = "none"; + } + + AddMessageItem(instance, data.msgId, 'kick', data.userId); +} + +function KickSub(data) { + if (!showKickSubs) + return; + + const message = data.rawInput; + + ShowAlert(message, 'kick'); +} + +function KickGift(data) { + if (!showKickSubs) + return; + + const message = data.rawInput; + + ShowAlert(message, 'kick'); +} + +function KickGifts(data) { + if (!showKickSubs) + return; + + const username = data.user; + const gifts = data.gifts; + const message = `${username} gifted ${gifts} subs to the community!`; + + ShowAlert(message, 'kick'); +} + +function KickRewardRedeemed(data) { + if (!showKickChannelPointRedemptions) + return; + + const username = data.user; + const rewardName = data.rewardTitle; + const message = `${username} redeemed ${rewardName}`; + + ShowAlert(message, 'kick'); +} + +function KickIncomingRaid(data) { + if (!showKickHosts) + return; + + const username = data.user; + const viewers = data.viewers; + const message = `${username} is raiding with a party of ${viewers}`; + + ShowAlert(message, 'kick'); +} + +function KickChatMessageDeleted(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const messageId = data.msgId; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].id === messageId) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + messageList.removeChild(item); + }); +} + +function KickBan(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const userId = data.userId; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].dataset.userId == userId) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + messageList.removeChild(item); + }); +} + ////////////////////// @@ -1086,17 +1412,36 @@ function GetCurrentTimeFormatted() { return formattedTime; } -async function GetAvatar(username) { - if (avatarMap.has(username)) { - console.debug(`Avatar found for ${username}. Retrieving from hash map.`) - return avatarMap.get(username); +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}`); } - else { - console.debug(`No avatar found for ${username}. Retrieving from Decapi.`) - let response = await fetch('https://decapi.me/twitch/avatar/' + username); - let data = await response.text() - avatarMap.set(username, data); - return data; + + // 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 response = await fetch('https://kick.com/api/v2/channels/' + username); + console.log('https://kick.com/api/v2/channels/' + username) + let data = await response.json(); + let avatarURL = data.user.profile_pic; + if (!avatarURL) + avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + avatarMap.set(`${username}-${platform}`, avatarURL); + return avatarURL; + } } } diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json index 6b982d3..1447073 100644 --- a/horizontal-chat/settings/settings.json +++ b/horizontal-chat/settings/settings.json @@ -175,6 +175,46 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showKickMessages", + "label": "Chat Messages", + "description": "", + "type": "checkbox", + "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", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, + { + "id": "showKickChannelPointRedemptions", + "label": "Channel Point Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, + { + "id": "showKickHosts", + "label": "Raids", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, { "id": "showYouTubeMessages", "label": "Chat Messages", diff --git a/horizontal-chat/style.css b/horizontal-chat/style.css index d704677..4891d2c 100644 --- a/horizontal-chat/style.css +++ b/horizontal-chat/style.css @@ -252,6 +252,10 @@ li { background: linear-gradient(#FF6C60, #FF0707) !important; } +.kick { + background: linear-gradient(#53fc18, #005600) !important; +} + .streamlabs { background: linear-gradient(#73dabb, #397765) !important; } diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 93ed915..66e849b 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2305,12 +2305,12 @@ async function KickGifts(data) { iconDiv.appendChild(badge); // Set the text - // let username = data.user.name; + let username = data.user; // if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) // username = `${data.user.name} (${data.user.login})`; - const tier = data.tier; + // const tier = data.tier; const gifts = data.gifts; - const totalGifts = data.totalGifts; + // const totalGifts = data.totalGifts; // const recipient = data.recipient.name; //const cumlativeTotal = data.cumlativeTotal; // let username = data.user; From 2b58be65246db890572cc82c93f963149b29643f Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 5 Jul 2025 06:04:32 +1000 Subject: [PATCH 17/70] Added support for Kick alerts --- horizontal-chat/script.js | 3 + multichat-overlay/script.js | 3 + multistream-alerts/script.js | 311 +++++++++++++++++++--- multistream-alerts/settings/settings.json | 68 +++++ multistream-alerts/style.css | 4 + 5 files changed, 346 insertions(+), 43 deletions(-) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index 0d4c64b..c8c3b8b 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -1050,6 +1050,9 @@ function CustomCodeEvent(data) { case "kickChatMessage": KickChatMessage(eventArgs); break; + case "kickFollow": + KickFollow(eventArgs); + break; case "kickSub": KickSub(eventArgs); break; diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 66e849b..cbf6550 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1869,6 +1869,9 @@ function CustomCodeEvent(data) { case "kickChatMessage": KickChatMessage(eventArgs); break; + case "kickFollow": + KickFollow(eventArgs); + break; case "kickSub": KickSub(eventArgs); break; diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 91845af..76fad6b 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -60,6 +60,16 @@ const twitchCheerAction = urlParams.get("twitchCheerAction") || ""; const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const twitchRaidAction = urlParams.get("twitchRaidAction") || ""; +// Which Kick alerts do you want to see? +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); +const kickChannelPointRedemptionAction = urlParams.get("kickChannelPointRedemptionAction") || ""; +const showKickHosts = GetBooleanParam("showKickHosts", true); +const kickHostAction = urlParams.get("kickHostAction") || ""; + // Which YouTube alerts do you want to see? const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true); const youtubeSuperChatAction = urlParams.get("youtubeSuperChatAction") || ""; @@ -275,6 +285,11 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { FourthwallGiftDrawEnded(response.data); }) +client.on('Custom.CodeEvent', (response) => { + console.debug(response.data); + CustomCodeEvent(response.data); +}) + /////////////////////// @@ -289,7 +304,7 @@ async function TwitchFollow(data) { const username = data.user_name; // Render avatars - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); UpdateAlertBox( 'twitch', @@ -314,7 +329,7 @@ async function TwitchCheer(data) { let message = data.message.message; // Render avatars - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); // Render emotes for (i in data.emotes) { @@ -369,7 +384,7 @@ async function TwitchSub(data) { const isPrime = data.is_prime; // Render avatars - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); if (!isPrime) UpdateAlertBox( @@ -409,7 +424,7 @@ async function TwitchResub(data) { const message = data.text; // Render avatars - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); if (!isPrime) UpdateAlertBox( @@ -453,7 +468,7 @@ async function TwitchGiftSub(data) { return; // Render avatars - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); let messageText = ''; if (cumlativeTotal > 0) @@ -488,7 +503,7 @@ async function TwitchGiftBomb(data) { const subTier = data.sub_tier.charAt(0); // Render avatars - const avatarURL = await GetAvatar(login); + const avatarURL = await GetAvatar(login, 'twitch'); let message = ``; if (totalGifts > 0) @@ -518,7 +533,7 @@ async function TwitchRewardRedemption(data) { const channelPointIcon = ``; // Render avatars - const avatarURL = await GetAvatar(data.user_login); + const avatarURL = await GetAvatar(data.user_login, 'twitch'); UpdateAlertBox( 'twitch', @@ -538,7 +553,7 @@ async function TwitchRaid(data) { return; // Render avatars - const avatarURL = await GetAvatar(data.from_broadcaster_user_login); + const avatarURL = await GetAvatar(data.from_broadcaster_user_login, 'twitch'); // Set the text const username = data.from_broadcaster_user_login; @@ -1072,6 +1087,205 @@ function FourthwallGiftDrawEnded(data) { ); } +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickFollow": + KickFollow(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); + break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; + } +} + +async function KickFollow(data) { + if (!showKickFollows) + return; + + // Set the text + const username = data.user; + + // Render avatars + const avatarURL = await GetAvatar(username, 'kick'); + + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + `followed`, + ``, + username, + ``, + kickFollowAction, + data + ); +} + +async function KickSub(data) { + if (!showKickSubs) + return; + + // Set the text + const username = data.user; + // const subTier = data.sub_tier; + // const isPrime = data.is_prime; + + // Take the raw message and remove the first word which should be the username + const description = data.rawInput.trim().split(" ").slice(1).join(" "); + + // Render avatars + const avatarURL = await GetAvatar(username, 'kick'); + + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + description, + '', + username, + '', + kickSubAction, + data + ); +} + +async function KickGift(data) { + if (!showKickSubs) + return; + + // Set the text + const username = data.user; + // const subTier = data.subTier; + // const recipient = data.recipient.name; + // const cumlativeTotal = data.cumlativeTotal; + // const fromCommunitySubGift = data.fromCommunitySubGift; + + // Take the raw message and remove the first word which should be the username + const description = data.rawInput.trim().split(" ").slice(1, -2).join(" "); + + // Take the raw message and remove the first word which should be the username + const attribute = data.rawInput.trim().split(" ").slice(-2).join(" "); + + // // Don't post alerts for gift bombs + // if (fromCommunitySubGift) + // return; + + // Render avatars + const avatarURL = await GetAvatar(username, 'kick'); + + // let messageText = ''; + // if (cumlativeTotal > 0) + // messageText = `They've gifted ${cumlativeTotal} subs in total!`; + + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + description, + attribute, + username, + '', //messageText, + kickSubAction, + data + ); +} + +async function KickGifts(data) { + if (!showKickSubs) + return; + + // Set the text + const username = data.user; + const gifts = data.gifts; + // const subTier = data.subTier; + // const recipient = data.recipient.name; + // const cumlativeTotal = data.cumlativeTotal; + // const fromCommunitySubGift = data.fromCommunitySubGift; + + // Take the raw message and remove the first word which should be the username + const description = `gifted ${gifts} subs to the community!`; + + // Render avatars + const avatarURL = await GetAvatar(username, 'kick'); + + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + description, + '', + '', + '', //messageText, + kickSubAction, + data + ); +} + +async function KickRewardRedeemed(data) { + if (!showKickChannelPointRedemptions) + return; + + const username = data.user; + const rewardName = data.rewardTitle; + // const cost = data.reward.cost; + const userInput = data.rewardUserInput; + // const channelPointIcon = ``; + + // Render avatars + const avatarURL = await GetAvatar(data.user, 'kick'); + + UpdateAlertBox( + 'kick', + avatarURL, + `${username} redeemed`, + `${rewardName}`, + '', + username, + userInput, + kickChannelPointRedemptionAction, + data + ); +} + +async function KickIncomingRaid(data) { + if (!showKickHosts) + return; + + // Render avatars + const avatarURL = await GetAvatar(data.user, 'kick'); + + // Set the text + const username = data.user; + const viewers = data.viewers; + + UpdateAlertBox( + 'kick', + avatarURL, + `${username}`, + `is raiding with a party of ${viewers}`, + '', + username, + '', + kickHostAction, + data + ); +} + ////////////////////// @@ -1116,17 +1330,36 @@ function GetIntParam(paramName, defaultValue) { return intValue; } -async function GetAvatar(username) { - if (avatarMap.has(username)) { - console.debug(`Avatar found for ${username}. Retrieving from hash map.`) - return avatarMap.get(username); +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}`); } - else { - console.debug(`No avatar found for ${username}. Retrieving from Decapi.`) - let response = await fetch('https://decapi.me/twitch/avatar/' + username); - let data = await response.text() - avatarMap.set(username, data); - return data; + + // 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 response = await fetch('https://kick.com/api/v2/channels/' + username); + console.log('https://kick.com/api/v2/channels/' + username) + let data = await response.json(); + let avatarURL = data.user.profile_pic; + if (!avatarURL) + avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + avatarMap.set(`${username}-${platform}`, avatarURL); + return avatarURL; + } } } @@ -1348,7 +1581,7 @@ async function testWidget() { UpdateAlertBox( 'twitch', - await GetAvatar('nutty'), + await GetAvatar('nutty', 'twitch'), `nutty`, `subscribed with Tier 3`, '', @@ -1384,29 +1617,21 @@ function SetConnectionStatus(connected) { } // let data = { -// cumulative_total: 77, -// id: "6616253944106387595", -// isTest: false, -// messageId: "00e4258c-a880-4f11-a93c-4734ee92199f", -// recipients: [ -// {}, {}, {}, {}, {}, {}, {}, {}, {}, {} -// ], -// sub_tier: "1000", // Usually "1000" = Tier 1 -// systemMessage: "thetrickster1973 is gifting 10 Tier 1 Subs to nutty's community! They've gifted a total of 77 in the channel!", -// total: 10, -// user: { -// badges: [{}, {}, {}], -// color: "#1E90FF", -// id: "52175891", -// login: "thetrickster1973", -// monthsSubscribed: 14, -// name: "thetrickster1973", -// role: 2, // 2 - VIP -// subscribed: true, -// type: "twitch" -// } -// }; +// "user": "BnBriTV", +// "viewers": 34, +// "eventSource": "kick", +// "fromKick": true, +// "__source": 18002, +// "triggerId": "e7be4c30-180c-40cd-b2cf-0f1bcc04e106", +// "triggerName": "Custom Code Event", +// "triggerCategory": "Custom", +// "triggerCustomCodeEventName": "kickIncomingRaid", +// "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", +// "actionName": "Kick Data Scraper | Save Data", +// "runningActionId": "1508eed5-7e08-49a9-9699-f3285b0200d8", +// "actionQueuedAt": "2025-07-03T06:13:40.3382156+10:00" +// } -// TwitchGiftBomb(data); +// KickIncomingRaid(data); -//testWidget(); \ No newline at end of file +// testWidget(); \ No newline at end of file diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json index c2e60a6..4dd17f4 100644 --- a/multistream-alerts/settings/settings.json +++ b/multistream-alerts/settings/settings.json @@ -312,6 +312,74 @@ "showIf": "showTwitchRaids", "group": "Which Twitch alerts do you want to see?" }, + { + "id": "showKickFollows", + "label": "New Followers", + "description": "", + "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?" + }, + { + "id": "showKickSubs", + "label": "New Subscribers", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "kickSubAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", + "defaultValue": "", + "showIf": "showKickSubs", + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "showKickChannelPointRedemptions", + "label": "Channel Point Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "kickChannelPointRedemptionAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", + "defaultValue": "", + "showIf": "showKickChannelPointRedemptions", + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "showKickHosts", + "label": "Hosts", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick alerts do you want to see?" + }, + { + "id": "kickHostAction", + "label": "", + "description": "Also run this Streamer.bot Action", + "type": "sb-actions", + "defaultValue": "", + "showIf": "showKickHosts", + "group": "Which Kick alerts do you want to see?" + }, { "id": "showYouTubeSuperChats", "label": "Super Chats", diff --git a/multistream-alerts/style.css b/multistream-alerts/style.css index 1b6172e..2b2a330 100644 --- a/multistream-alerts/style.css +++ b/multistream-alerts/style.css @@ -307,6 +307,10 @@ html { background: linear-gradient(#FF6C60BF, #FF0707BF) !important; } +.kick { + background: linear-gradient(#53fc18BF, #005600BF) !important; +} + .streamlabs { background: linear-gradient(#73dabbbf, #397765bf) !important; } From 4600c9e73ea8845c5d690f57ecfa67be38495f07 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sun, 6 Jul 2025 04:30:57 +1000 Subject: [PATCH 18/70] Code cleanup --- horizontal-chat/script.js | 126 +++----- multichat-overlay/script.js | 602 ++++++++++------------------------- multistream-alerts/script.js | 100 ++---- 3 files changed, 235 insertions(+), 593 deletions(-) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index c8c3b8b..f05da84 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -269,6 +269,45 @@ client.on('Custom.CodeEvent', (response) => { CustomCodeEvent(response.data); }) +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickChatMessage": + KickChatMessage(eventArgs); + break; + case "kickFollow": + KickFollow(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); + break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; + case "kickChatMessageDeleted": + KickChatMessageDeleted(eventArgs); + break; + case "kickBan": + KickBan(eventArgs); + break; + case "kickTO": + KickBan(eventArgs); + break; + + } +} + ///////////////////// @@ -1042,45 +1081,6 @@ function FourthwallGiftDrawEnded(data) { ShowAlert(message, 'fourthwall'); } -function CustomCodeEvent(data) { - const eventName = data.eventName; - const eventArgs = data.args; - - switch (eventName) { - case "kickChatMessage": - KickChatMessage(eventArgs); - break; - case "kickFollow": - KickFollow(eventArgs); - break; - case "kickSub": - KickSub(eventArgs); - break; - case "kickGift": - KickGift(eventArgs); - break; - case "kickGifts": - KickGifts(eventArgs); - break; - case "kickRewardRedeemed": - KickRewardRedeemed(eventArgs); - break; - case "kickIncomingRaid": - KickIncomingRaid(eventArgs); - break; - case "kickChatMessageDeleted": - KickChatMessageDeleted(eventArgs); - break; - case "kickBan": - KickBan(eventArgs); - break; - case "kickTO": - KickBan(eventArgs); - break; - - } -} - async function KickChatMessage(data) { if (!showKickMessages) return; @@ -1109,15 +1109,6 @@ async function KickChatMessage(data) { const usernameDiv = instance.querySelector("#username"); const messageDiv = instance.querySelector("#message"); - // // Set Shared Chat - // // If the message is from Shared Chat AND the user indicated that they do NOT - // // want shared chat messages, don't show it on screen - // const isSharedChat = data.isSharedChat; - // if (isSharedChat && !showTwitchSharedChat) { - // if (!data.sharedChat.primarySource) - // return; - // } - // Set timestamp if (showTimestamps) { timestampDiv.classList.add("timestamp"); @@ -1130,13 +1121,6 @@ async function KickChatMessage(data) { usernameDiv.style.color = data.color; } - // // Set pronouns - // const pronouns = await GetPronouns('twitch', data.message.username); - // if (pronouns && showPronouns) { - // pronounsDiv.classList.add("pronouns"); - // pronounsDiv.innerText = pronouns; - // } - // Set the message data let message = data.message; const messageColor = data.color; @@ -1200,38 +1184,6 @@ async function KickChatMessage(data) { badgeListDiv.appendChild(badge); } - // // 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 emotes function replaceEmotes(message) { const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index cbf6550..d75b041 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -284,16 +284,104 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { FourthwallGiftDrawEnded(response.data); }) -// client.on('General.Custom', (response) => { -// console.debug(response.data); -// GeneralCustom(response.data); -// }) - client.on('Custom.CodeEvent', (response) => { console.debug(response.data); CustomCodeEvent(response.data); }) +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickChatMessage": + KickChatMessage(eventArgs); + break; + case "kickFollow": + KickFollow(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); + break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; + case "kickChatMessageDeleted": + KickChatMessageDeleted(eventArgs); + break; + case "kickBan": + KickBan(eventArgs); + break; + case "kickTO": + KickBan(eventArgs); + break; + + } +} + + + +////////////////////// +// TIKFINITY CLIENT // +////////////////////// + +let tikfinityWebsocket = null; + +function tikfinityConnect() { + if (tikfinityWebsocket) return; // Already connected + + tikfinityWebsocket = new WebSocket("ws://localhost:21213/"); + + tikfinityWebsocket.onopen = function () { + console.log(`TikFinity successfully connected...`) + } + + tikfinityWebsocket.onclose = function () { + console.error(`TikFinity disconnected...`) + tikfinityWebsocket = null; + setTimeout(connect, 1000); // Schedule a reconnect attempt + } + + tikfinityWebsocket.onerror = function () { + console.error(`TikFinity failed for some reason...`) + tikfinityWebsocket = null; + setTimeout(connect, 1000); // Schedule a reconnect attempt + } + + tikfinityWebsocket.onmessage = function (response) { + let payload = JSON.parse(response.data); + + let event = payload.event; + let data = payload.data; + + console.debug('Event: ' + event); + + switch (event) { + case 'chat': + TikTokChat(data); + break; + case 'gift': + TikTokGift(data); + break; + case 'subscribe': + TikTokSubscribe(data); + break; + } + } +} + +// Try connect when window is loaded +window.addEventListener('load', tikfinityConnect); + /////////////////////// @@ -1832,74 +1920,6 @@ function FourthwallGiftDrawEnded(data) { AddMessageItem(instance, data.id); } -// function GeneralCustom(data) { -// const eventSource = data.eventSource; - -// switch (eventSource) { -// case "kick": -// const triggerCustomCodeEventName = data.triggerCustomCodeEventName; - -// switch (triggerCustomCodeEventName) { -// case "kickChatMessage": -// KickChatMessage(data); -// break; -// case "kickSub": -// KickSub(data); -// break; -// case "kickGift": -// KickGift(data); -// break; -// case "kickGifts": -// KickGifts(data); -// break; -// case "kickRewardRedeemed": -// KickRewardRedeemed(data); -// break; -// } - -// break; -// } -// } - -function CustomCodeEvent(data) { - const eventName = data.eventName; - const eventArgs = data.args; - - switch (eventName) { - case "kickChatMessage": - KickChatMessage(eventArgs); - break; - case "kickFollow": - KickFollow(eventArgs); - break; - case "kickSub": - KickSub(eventArgs); - break; - case "kickGift": - KickGift(eventArgs); - break; - case "kickGifts": - KickGifts(eventArgs); - break; - case "kickRewardRedeemed": - KickRewardRedeemed(eventArgs); - break; - case "kickIncomingRaid": - KickIncomingRaid(eventArgs); - break; - case "kickChatMessageDeleted": - KickChatMessageDeleted(eventArgs); - break; - case "kickBan": - KickBan(eventArgs); - break; - case "kickTO": - KickBan(eventArgs); - break; - - } -} - async function KickChatMessage(data) { if (!showKickMessages) return; @@ -1942,22 +1962,6 @@ async function KickChatMessage(data) { messageContainerDiv.classList.add("highlightMessage"); } - // // Set Shared Chat - // console.log(showTwitchSharedChat); - // const isSharedChat = data.isSharedChat; - // if (isSharedChat) { - // if (showTwitchSharedChat > 1) { - // if (!data.sharedChat.primarySource) { - // const sharedChatChannel = data.sharedChat.sourceRoom.name; - // sharedChatDiv.style.display = 'block'; - // sharedChatChannelDiv.innerHTML = `💬 ${sharedChatChannel}`; - // messageContainerDiv.classList.add("highlightMessage"); - // } - // } - // else if (!data.sharedChat.primarySource && showTwitchSharedChat == 0) - // return; - // } - // Set Reply Message const isReply = data.isReply; if (isReply && showMessage) { @@ -1981,13 +1985,6 @@ async function KickChatMessage(data) { usernameDiv.style.color = data.color; } - // // Set pronouns - // const pronouns = await GetPronouns('twitch', data.message.username); - // if (pronouns && showPronouns) { - // pronounsDiv.classList.add("pronouns"); - // pronounsDiv.innerText = pronouns; - // } - // Set the message data let message = data.message; const messageColor = data.color; @@ -2066,36 +2063,6 @@ async function KickChatMessage(data) { badgeListDiv.appendChild(badge); } - // // 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 emotes function replaceEmotes(message) { const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; @@ -2217,17 +2184,6 @@ async function KickSub(data) { iconDiv.appendChild(badge); // Set the text - // let username = data.user; - // if (data.user.toLowerCase() != data.userName.toLowerCase()) - // username = `${data.user} (${data.userName})`; - // const subTier = data.sub_tier; - // const isPrime = data.is_prime; - - // if (!isPrime) - // titleDiv.innerText = `${username} subscribed with Tier ${subTier.charAt(0)}`; - // else - // titleDiv.innerText = `${username} used their Prime Sub`; - //titleDiv.innerText = `${username} subscribed on Kick`; titleDiv.innerText = data.rawInput; AddMessageItem(instance, data.messageId); @@ -2261,20 +2217,6 @@ async function KickGift(data) { iconDiv.appendChild(badge); // 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 subTier = data.subTier; - // const recipient = data.recipient.name; - // const cumlativeTotal = data.cumlativeTotal; - // let username = data.user; - // if (data.user.toLowerCase() != data.userName.toLowerCase()) - // username = `${data.user} (${data.userName})`; - - // titleDiv.innerText = `${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`; - // if (cumlativeTotal > 0) - // contentDiv.innerText = `They've gifted ${cumlativeTotal} subs in total!`; - titleDiv.innerText = `${data.rawInput}`; AddMessageItem(instance, data.messageId); @@ -2308,21 +2250,9 @@ async function KickGifts(data) { iconDiv.appendChild(badge); // Set the text - let username = data.user; - // if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) - // username = `${data.user.name} (${data.user.login})`; - // const tier = data.tier; + const username = data.user; const gifts = data.gifts; - // const totalGifts = data.totalGifts; - // const recipient = data.recipient.name; - //const cumlativeTotal = data.cumlativeTotal; - // let username = data.user; - // if (data.user.toLowerCase() != data.userName.toLowerCase()) - // username = `${data.user} (${data.userName})`; - titleDiv.innerText = `${username} gifted ${gifts} subs to the community!`; - // if (totalGifts > 0) - // contentDiv.innerText = `They've gifted ${totalGifts} subs in total!`; AddMessageItem(instance, data.messageId); } @@ -2360,14 +2290,9 @@ async function KickRewardRedeemed(data) { // Set the text let username = data.user; - // if (data.user.toLowerCase() != data.userName.toLowerCase()) - // username = `${data.user} (${data.userName})`; const rewardName = data.rewardTitle; - //const cost = data.reward.cost; const userInput = data.rewardUserInput; - const channelPointIcon = ``; - //titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; titleDiv.innerHTML = `${username} redeemed ${rewardName}`; contentDiv.innerText = `${userInput}`; @@ -2407,9 +2332,7 @@ async function KickIncomingRaid(data) { // Set the text - let username = data.user; - // if (data.from_broadcaster_user_name.toLowerCase() != data.from_broadcaster_user_login.toLowerCase()) - // username = `${data.from_broadcaster_user_name} (${data.from_broadcaster_user_login})`; + const username = data.user; const viewers = data.viewers; titleDiv.innerText = `${username} is raiding`; @@ -2466,35 +2389,15 @@ function KickBan(data) { }); } -/* -function GeneralCustom(data) { - const target = data.target; - const type = data.type; - // Check the target for the message - const path = window.location.pathname; - const firstSegment = path.split('/')[1]; - if (firstSegment != target) - return; - switch (type) - { - case "message": - CustomMessage(data); - break; - case "alert": - CustomAlert(data); - break; - } -} - -function CustomMessage(data) { +function TikTokChat(data) { // Don't post messages starting with "!" - if (data.message.startsWith("!") && excludeCommands) + if (data.comment.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.username.toLowerCase())) + if (ignoreUserList.includes(data.comment.toLowerCase())) return; // Get a reference to the template @@ -2528,15 +2431,12 @@ function CustomMessage(data) { // Set the username info if (showUsername) { - if (data.displayName.toLowerCase() == data.username.toLowerCase()) - usernameDiv.innerText = data.displayName; - else - usernameDiv.innerText = `${data.displayName} (${data.username})`; - usernameDiv.style.color = data.userColor; + usernameDiv.innerText = data.nickname; + usernameDiv.style.color = '#9e9e9e'; } // Set the message data - let message = data.message; + let message = data.comment; // Set furry mode if (furryMode) @@ -2555,22 +2455,77 @@ function CustomMessage(data) { // Render platform if (showPlatform) { - const platformElements = ``; + const platformElements = ``; platformDiv.innerHTML = platformElements; } + // Render badges + if (showBadges) { + badgeListDiv.innerHTML = ""; + + if (data.isModerator) { + const badge = new Image(); + badge.src = `icons/badges/youtube-moderator.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + for (i in data.userBadges) { + if (data.userBadges[i].type == 'image') { + const badge = new Image(); + badge.src = data.userBadges[i].url; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + } + } + // Render avatars if (showAvatar) { const avatar = new Image(); - avatar.src = data.avatar; + avatar.src = data.profilePictureUrl; avatar.classList.add("avatar"); avatarDiv.appendChild(avatar); } - AddMessageItem(instance, data.msgId, data.platform.name, data.username); + AddMessageItem(instance, data.msgId, 'tiktok', data.userId); } -function CustomAlert(data) { +function TikTokGift(data) { + if (data.giftType === 1 && !data.repeatEnd) { + // Streak in progress => show only temporary + console.debug(`${data.uniqueId} is sending gift ${data.giftName} x${data.repeatCount}`); + return; + } + + // Streak ended or non-streakable gift => process the gift with final repeat_count + console.debug(`${data.uniqueId} has sent gift ${data.giftName} x${data.repeatCount}`); + + // Get a reference to the template + const template = document.getElementById('tiktok-gift-template'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const avatarImg = instance.querySelector('.tiktok-gift-avatar'); + const usernameSpan = instance.querySelector('#tiktok-gift-username'); + const giftNameSpan = instance.querySelector('#tiktok-gift-name'); + const stickerImg = instance.querySelector('.tiktok-gift-sticker'); + const repeatCountDiv = instance.querySelector('#tiktok-gift-repeat-count'); + + avatarImg.src = data.profilePictureUrl; // Set the card header + usernameSpan.innerText = data.nickname; // Set the username + giftNameSpan.innerText = data.giftName; // Set the gift name + stickerImg.src = data.giftPictureUrl; // Set the sticker image URL + repeatCountDiv.innerText = `x${data.repeatCount}`; // Set the number of gifts sent + + AddMessageItem(instance, data.messageId); +} + +function TikTokSubscribe(data) { // Get a reference to the template const template = document.getElementById('cardTemplate'); @@ -2586,25 +2541,15 @@ function CustomAlert(data) { const contentDiv = instance.querySelector("#content"); // Set the card background colors - cardDiv.style.background = data.background; + cardDiv.classList.add('tiktok'); - // Set the card header - const icon = new Image(); - icon.src = data.icon; - icon.classList.add("badge"); - iconDiv.appendChild(icon); + const user = data.nickname; + const tiktokIcon = ``; - // // Set the text - // let username = data.displayName; - // if (data.displayName.toLowerCase() != data.username.toLowerCase()) - // username = `${data.displayName} (${data.username})`; + titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; - titleDiv.innerText = data.title; - contentDiv.innerText = data.content; - - AddMessageItem(instance, data.messageId); + AddMessageItem(instance, data.msgId); } -*/ @@ -2947,221 +2892,4 @@ function SetConnectionStatus(connected) { statusContainer.style.transition = ""; statusContainer.style.opacity = 1; } -} - - - -////////////////////// -// TIKFINITY CLIENT // -////////////////////// - -let websocket = null; - -function connect() { - if (websocket) return; // Already connected - - websocket = new WebSocket("ws://localhost:21213/"); - - websocket.onopen = function () { - console.log(`TikFinity successfully connected...`) - } - - websocket.onclose = function () { - console.error(`TikFinity disconnected...`) - websocket = null; - setTimeout(connect, 1000); // Schedule a reconnect attempt - } - - websocket.onerror = function () { - console.error(`TikFinity failed for some reason...`) - websocket = null; - setTimeout(connect, 1000); // Schedule a reconnect attempt - } - - websocket.onmessage = function (response) { - console.debug('THINGS ARE HAPPENING ON TIKTOK'); - let responseroni = JSON.parse(response.data); - - let event = responseroni.event; - let data = responseroni.data; - - console.debug('Event: ' + event); - - switch (event) { - case 'chat': - TikTokChat(data); - break; - case 'gift': - TikTokGift(data); - break; - case 'subscribe': - TikTokSubscribe(data); - break; - } - } -} - -// Try connect when window is loaded -window.addEventListener('load', connect); - - - -function TikTokChat(data) { - // Don't post messages starting with "!" - if (data.comment.startsWith("!") && excludeCommands) - return; - - // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.comment.toLowerCase())) - return; - - // Get a reference to the template - const template = document.getElementById('messageTemplate'); - - // Create a new instance of the template - const instance = template.content.cloneNode(true); - - // Get divs - const messageContainerDiv = instance.querySelector("#messageContainer"); - const firstMessageDiv = instance.querySelector("#firstMessage"); - const sharedChatDiv = instance.querySelector("#sharedChat"); - const sharedChatChannelDiv = instance.querySelector("#sharedChatChannel"); - const replyDiv = instance.querySelector("#reply"); - const replyUserDiv = instance.querySelector("#replyUser"); - const replyMsgDiv = instance.querySelector("#replyMsg"); - const userInfoDiv = instance.querySelector("#userInfo"); - const avatarDiv = instance.querySelector("#avatar"); - const timestampDiv = instance.querySelector("#timestamp"); - const platformDiv = instance.querySelector("#platform"); - const badgeListDiv = instance.querySelector("#badgeList"); - const pronounsDiv = instance.querySelector("#pronouns"); - const usernameDiv = instance.querySelector("#username"); - const messageDiv = instance.querySelector("#message"); - - // Set timestamp - if (showTimestamps) { - timestampDiv.classList.add("timestamp"); - timestampDiv.innerText = GetCurrentTimeFormatted(); - } - - // Set the username info - if (showUsername) { - usernameDiv.innerText = data.nickname; - usernameDiv.style.color = '#9e9e9e'; - } - - // Set the message data - let message = data.comment; - - // Set furry mode - if (furryMode) - message = TranslateToFurry(message); - - // Set message text - if (showMessage) { - messageDiv.innerText = message; - } - - // Remove the line break - if (inlineChat) { - instance.querySelector("#colon-separator").style.display = `inline`; - instance.querySelector("#line-space").style.display = `none`; - } - - // Render platform - if (showPlatform) { - const platformElements = ``; - platformDiv.innerHTML = platformElements; - } - - // Render badges - if (showBadges) { - badgeListDiv.innerHTML = ""; - - if (data.isModerator) { - const badge = new Image(); - badge.src = `icons/badges/youtube-moderator.svg`; - badge.style.filter = `invert(100%)`; - badge.style.opacity = 0.8; - badge.classList.add("badge"); - badgeListDiv.appendChild(badge); - } - - for (i in data.userBadges) { - if (data.userBadges[i].type == 'image') { - const badge = new Image(); - badge.src = data.userBadges[i].url; - badge.classList.add("badge"); - badgeListDiv.appendChild(badge); - } - } - } - - // Render avatars - if (showAvatar) { - const avatar = new Image(); - avatar.src = data.profilePictureUrl; - avatar.classList.add("avatar"); - avatarDiv.appendChild(avatar); - } - - AddMessageItem(instance, data.msgId, 'tiktok', data.userId); -} - -function TikTokGift(data) { - if (data.giftType === 1 && !data.repeatEnd) { - // Streak in progress => show only temporary - console.debug(`${data.uniqueId} is sending gift ${data.giftName} x${data.repeatCount}`); - return; - } - - // Streak ended or non-streakable gift => process the gift with final repeat_count - console.debug(`${data.uniqueId} has sent gift ${data.giftName} x${data.repeatCount}`); - - // Get a reference to the template - const template = document.getElementById('tiktok-gift-template'); - - // Create a new instance of the template - const instance = template.content.cloneNode(true); - - // Get divs - const avatarImg = instance.querySelector('.tiktok-gift-avatar'); - const usernameSpan = instance.querySelector('#tiktok-gift-username'); - const giftNameSpan = instance.querySelector('#tiktok-gift-name'); - const stickerImg = instance.querySelector('.tiktok-gift-sticker'); - const repeatCountDiv = instance.querySelector('#tiktok-gift-repeat-count'); - - avatarImg.src = data.profilePictureUrl; // Set the card header - usernameSpan.innerText = data.nickname; // Set the username - giftNameSpan.innerText = data.giftName; // Set the gift name - stickerImg.src = data.giftPictureUrl; // Set the sticker image URL - repeatCountDiv.innerText = `x${data.repeatCount}`; // Set the number of gifts sent - - AddMessageItem(instance, data.messageId); -} - -function TikTokSubscribe(data) { - // Get a reference to the template - const template = document.getElementById('cardTemplate'); - - // 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("#content"); - - // Set the card background colors - cardDiv.classList.add('tiktok'); - - const user = data.nickname; - const tiktokIcon = ``; - - titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; - - AddMessageItem(instance, data.msgId); } \ No newline at end of file diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 76fad6b..deedede 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -290,6 +290,32 @@ client.on('Custom.CodeEvent', (response) => { CustomCodeEvent(response.data); }) +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickFollow": + KickFollow(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); + break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; + } +} + /////////////////////// @@ -1087,32 +1113,6 @@ function FourthwallGiftDrawEnded(data) { ); } -function CustomCodeEvent(data) { - const eventName = data.eventName; - const eventArgs = data.args; - - switch (eventName) { - case "kickFollow": - KickFollow(eventArgs); - break; - case "kickSub": - KickSub(eventArgs); - break; - case "kickGift": - KickGift(eventArgs); - break; - case "kickGifts": - KickGifts(eventArgs); - break; - case "kickRewardRedeemed": - KickRewardRedeemed(eventArgs); - break; - case "kickIncomingRaid": - KickIncomingRaid(eventArgs); - break; - } -} - async function KickFollow(data) { if (!showKickFollows) return; @@ -1128,9 +1128,9 @@ async function KickFollow(data) { avatarURL, `${username}`, `followed`, - ``, + '', username, - ``, + '', kickFollowAction, data ); @@ -1170,10 +1170,6 @@ async function KickGift(data) { // Set the text const username = data.user; - // const subTier = data.subTier; - // const recipient = data.recipient.name; - // const cumlativeTotal = data.cumlativeTotal; - // const fromCommunitySubGift = data.fromCommunitySubGift; // Take the raw message and remove the first word which should be the username const description = data.rawInput.trim().split(" ").slice(1, -2).join(" "); @@ -1181,16 +1177,8 @@ async function KickGift(data) { // Take the raw message and remove the first word which should be the username const attribute = data.rawInput.trim().split(" ").slice(-2).join(" "); - // // Don't post alerts for gift bombs - // if (fromCommunitySubGift) - // return; - // Render avatars const avatarURL = await GetAvatar(username, 'kick'); - - // let messageText = ''; - // if (cumlativeTotal > 0) - // messageText = `They've gifted ${cumlativeTotal} subs in total!`; UpdateAlertBox( 'kick', @@ -1199,7 +1187,7 @@ async function KickGift(data) { description, attribute, username, - '', //messageText, + '', kickSubAction, data ); @@ -1212,10 +1200,6 @@ async function KickGifts(data) { // Set the text const username = data.user; const gifts = data.gifts; - // const subTier = data.subTier; - // const recipient = data.recipient.name; - // const cumlativeTotal = data.cumlativeTotal; - // const fromCommunitySubGift = data.fromCommunitySubGift; // Take the raw message and remove the first word which should be the username const description = `gifted ${gifts} subs to the community!`; @@ -1230,7 +1214,7 @@ async function KickGifts(data) { description, '', '', - '', //messageText, + '', kickSubAction, data ); @@ -1242,9 +1226,7 @@ async function KickRewardRedeemed(data) { const username = data.user; const rewardName = data.rewardTitle; - // const cost = data.reward.cost; const userInput = data.rewardUserInput; - // const channelPointIcon = ``; // Render avatars const avatarURL = await GetAvatar(data.user, 'kick'); @@ -1614,24 +1596,4 @@ function SetConnectionStatus(connected) { statusContainer.style.transition = ""; statusContainer.style.opacity = 1; } -} - -// let data = { -// "user": "BnBriTV", -// "viewers": 34, -// "eventSource": "kick", -// "fromKick": true, -// "__source": 18002, -// "triggerId": "e7be4c30-180c-40cd-b2cf-0f1bcc04e106", -// "triggerName": "Custom Code Event", -// "triggerCategory": "Custom", -// "triggerCustomCodeEventName": "kickIncomingRaid", -// "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", -// "actionName": "Kick Data Scraper | Save Data", -// "runningActionId": "1508eed5-7e08-49a9-9699-f3285b0200d8", -// "actionQueuedAt": "2025-07-03T06:13:40.3382156+10:00" -// } - -// KickIncomingRaid(data); - -// testWidget(); \ No newline at end of file +} \ No newline at end of file From 857e6481ae2d20740706f63b2d1a1a5281958dc4 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sun, 6 Jul 2025 06:31:04 +1000 Subject: [PATCH 19/70] Layout updates --- multichat-overlay/index.html | 56 +++++++++++++----------- multichat-overlay/script.js | 24 +++++++--- multichat-overlay/settings/settings.json | 6 +-- multichat-overlay/style.css | 24 +++++++--- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/multichat-overlay/index.html b/multichat-overlay/index.html index 17ab45e..09c8ab2 100644 --- a/multichat-overlay/index.html +++ b/multichat-overlay/index.html @@ -21,29 +21,31 @@ @@ -72,16 +74,18 @@ - - diff --git a/receipt-printer/script.js b/receipt-printer/script.js index 1ff4974..200b7d2 100644 --- a/receipt-printer/script.js +++ b/receipt-printer/script.js @@ -81,6 +81,7 @@ async function CustomEvent(data) { const avatarEl = instance.querySelector('#avatar'); const titleEl = instance.querySelector('#title'); const subtitleEl = instance.querySelector('#subtitle'); + const iconEl = instance.querySelector('#icon'); const dateEl = instance.querySelector('#date'); // Set the main contents @@ -126,6 +127,9 @@ async function CustomEvent(data) { } contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; case ('TwitchSub'): @@ -138,6 +142,9 @@ async function CustomEvent(data) { messageEl.innerHTML = 'First time subscriber!'; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; case ('TwitchReSub'): @@ -152,13 +159,17 @@ async function CustomEvent(data) { messageEl.innerHTML += `

${data.messageStripped}`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; case ('TwitchGiftSub'): { // Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits if (data.fromGiftBomb) - avatarEl.style.display = 'none'; + //avatarEl.style.display = 'none'; + return; else avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch'); titleEl.innerText = `Gifted Sub`; @@ -171,6 +182,9 @@ async function CustomEvent(data) { messageEl.innerHTML += `${data.user}!`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; case ('TwitchGiftBomb'): @@ -183,15 +197,23 @@ async function CustomEvent(data) { else subtitleEl.innerHTML += `
${data.user}`; + const messageEl = document.createElement('div'); if (data.totalGifts > 1) { - const messageEl = document.createElement('div'); - messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!`; + messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!

`; + } - contentEl.appendChild(messageEl); - } - else { - contentEl.style.display = 'none'; - } + // Get a list of all recipient users + Object.keys(data) + .filter(key => /^gift\.recipientUser\d+$/.test(key)) + .forEach((key, index) => { + const username = data[key]; + messageEl.innerHTML += `${username}
`; + }); + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; case ('TwitchRaid'): @@ -202,6 +224,9 @@ async function CustomEvent(data) { messageEl.innerHTML = `${data.user}
is raiding with a party of
${data.viewers} viewers!`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); } break; @@ -217,6 +242,9 @@ async function CustomEvent(data) { subtitleEl.innerText = `${data.user}`; contentEl.style.display = 'none'; + + // Set the platform icon + SetPlatformIcon(iconEl, 'youtube'); } break; case ('YouTubeGiftMembershipReceived'): @@ -231,6 +259,9 @@ async function CustomEvent(data) { messageEl.innerHTML = `${data.user}
received a membership from
${data.gifterUser}!`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'youtube'); } break; case ('YouTubeSuperChat'): @@ -248,6 +279,9 @@ async function CustomEvent(data) { messageEl.innerHTML += `

${data.message}`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'youtube'); } break; case ('YouTubeSuperSticker'): @@ -264,6 +298,9 @@ async function CustomEvent(data) { messageEl.innerHTML = `${data.user}
sent a Super Sticker!`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'youtube'); } break; break; @@ -283,6 +320,9 @@ async function CustomEvent(data) { messageEl.innerHTML = 'First time subscriber!'; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'kick'); } break; case ('KickGiftSubscription'): @@ -294,6 +334,9 @@ async function CustomEvent(data) { messageEl.innerHTML = `${data["recipient.userName"]}
received a sub from
${data.user}!`; contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'kick'); } break; case ('KickMassGiftSubscription'): @@ -354,6 +397,9 @@ async function CustomEvent(data) { contentEl.appendChild(messageEl); } + + // Set the platform icon + SetPlatformIcon(iconEl, 'kick'); } break; @@ -682,6 +728,14 @@ 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; + baseURL = baseURL.replace(/index\.html$/i, ''); + + el.src = `${baseURL}/icons/platforms/${platform}.png`; +} + /////////////////////////////////// diff --git a/receipt-printer/style.css b/receipt-printer/style.css index 47d60c3..ea55bb0 100644 --- a/receipt-printer/style.css +++ b/receipt-printer/style.css @@ -31,22 +31,25 @@ body { } #content { - border-top: 1px solid black; + /* border-top: 1px solid black; border-bottom: 1px solid black; border-left: none; border-right: none; - margin: 1em 0em; + margin: 1em 0em; */ padding: 0.5em 0em; } +#icon { + height: 1em; +} + #date { - margin-top: 1em; + margin: 0.5em 0em; font-size: 0.7em; text-transform: uppercase; } #footer { - } .emote { From 634a8ad5471af5d35215fdc03e0525bc775e1ac4 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Tue, 29 Jul 2025 05:46:45 +1000 Subject: [PATCH 55/70] Added support for Kick Hosts --- receipt-printer/script.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/receipt-printer/script.js b/receipt-printer/script.js index 200b7d2..1aa23e8 100644 --- a/receipt-printer/script.js +++ b/receipt-printer/script.js @@ -540,15 +540,37 @@ async function CustomEvent(data) { else avatarEl.style.display = 'none' titleEl.innerText = `New Member`; - subtitleEl.innerText = `${data["fw.nickname"]}`; + subtitleEl.innerHTML = `${data["fw.nickname"]}`; const messageEl = document.createElement('div'); - messageEl.innerText = `Thanks for joining at the ${FormatCurrency(data["fw.amount"], data["fw.currency"])} tier!`; + messageEl.innerHTML = `Thanks for joining at the ${FormatCurrency(data["fw.amount"], data["fw.currency"])} tier!`; contentEl.appendChild(messageEl); } break; + // Custom Code Events + case ('CustomCodeEvent'): + { + switch (data.triggerCustomCodeEventName) + { + case ('kickIncomingRaid'): + { + avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick')); + + const messageEl = document.createElement('div'); + messageEl.innerHTML = `${data.user}
is hosting with a party of
${data.viewers} viewers!`; + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'kick'); + } + break; + } + } + break; + // Don't print any event not excplicitly listed above default: return; From 1b9653b44f572d21b2da916dea69192f2b5fff4a Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Tue, 29 Jul 2025 07:44:03 +1000 Subject: [PATCH 56/70] Added a basic connector page --- receipt-printer/index.html | 13 ++++++- receipt-printer/script.js | 74 +++++++++++++++++++++----------------- receipt-printer/style.css | 73 ++++++++++++++++++++++++++++++++++--- 3 files changed, 122 insertions(+), 38 deletions(-) diff --git a/receipt-printer/index.html b/receipt-printer/index.html index ecae6ea..15abcca 100644 --- a/receipt-printer/index.html +++ b/receipt-printer/index.html @@ -9,7 +9,18 @@ -

+
+
+ + +
+
+ + +
+ + +

hD0H+2U*svw=4}=xXV9nSWwRK?`m4_P73zR zDoM|J=erCsHs)mgh_*E0zp`LXA0oasny!a~VHmF@C#$q+#cXVr;U7rJgyh5<7PHZ- z2Z**Ne12-1BTH)mkor)(l2I~LUx2AtwZt`|BqgyV)hf9t6-Y4{85o)98d&HWS%esxSs58xnHp&u7+4t?tnLtb fhN2-iKP5A*61Rq1SJy-VH86O(`njxgN@xNASjSp) literal 0 HcmV?d00001 diff --git a/multichat-overlay/index.html b/multichat-overlay/index.html index 568e076..30c8c6b 100644 --- a/multichat-overlay/index.html +++ b/multichat-overlay/index.html @@ -68,6 +68,24 @@ + + \ No newline at end of file diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index a1deb7d..5927111 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -448,7 +448,7 @@ async function TwitchChatMessage(data) { // Render avatars if (showAvatar) { const username = data.message.username; - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; avatar.classList.add("avatar"); @@ -615,9 +615,9 @@ async function TwitchAnnouncement(data) { 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`; @@ -626,7 +626,7 @@ async function TwitchAnnouncement(data) { // 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); } @@ -803,7 +803,7 @@ async function TwitchRewardRedemption(data) { if (showAvatar) { // Render avatars const username = data.user_login; - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; avatar.classList.add("avatar"); @@ -849,7 +849,7 @@ async function TwitchRaid(data) { if (showAvatar) { // Render avatars const username = data.from_broadcaster_user_login; - const avatarURL = await GetAvatar(username); + const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; avatar.classList.add("avatar"); @@ -1784,6 +1784,417 @@ function FourthwallGiftDrawEnded(data) { AddMessageItem(instance, data.id); } +function GeneralCustom(data) { + const eventSource = data.eventSource; + + switch (eventSource) { + case "kick": + const triggerCustomCodeEventName = data.triggerCustomCodeEventName; + + switch (triggerCustomCodeEventName) { + case "kickChatMessage": + KickChatMessage(data); + break; + } + + break; + } +} + +async function KickChatMessage(data) { + if (!showTwitchMessages) + return; + + // Don't post messages starting with "!" + if (data.message.startsWith("!") && excludeCommands) + return; + + // Don't post messages from users from the ignore list + if (ignoreUserList.includes(data.userName.toLowerCase())) + return; + + // Get a reference to the template + const template = document.getElementById('messageTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const messageContainerDiv = instance.querySelector("#messageContainer"); + const firstMessageDiv = instance.querySelector("#firstMessage"); + const sharedChatDiv = instance.querySelector("#sharedChat"); + const sharedChatChannelDiv = instance.querySelector("#sharedChatChannel"); + const replyDiv = instance.querySelector("#reply"); + const replyUserDiv = instance.querySelector("#replyUser"); + const replyMsgDiv = instance.querySelector("#replyMsg"); + const userInfoDiv = instance.querySelector("#userInfo"); + const avatarDiv = instance.querySelector("#avatar"); + const timestampDiv = instance.querySelector("#timestamp"); + const platformDiv = instance.querySelector("#platform"); + const badgeListDiv = instance.querySelector("#badgeList"); + const pronounsDiv = instance.querySelector("#pronouns"); + const usernameDiv = instance.querySelector("#username"); + const messageDiv = instance.querySelector("#message"); + + // Set First Time Chatter + const firstMessage = data.firstMessage; + if (firstMessage && showMessage) { + firstMessageDiv.style.display = 'block'; + messageContainerDiv.classList.add("highlightMessage"); + } + + // // Set Shared Chat + // console.log(showTwitchSharedChat); + // const isSharedChat = data.isSharedChat; + // if (isSharedChat) { + // if (showTwitchSharedChat > 1) { + // if (!data.sharedChat.primarySource) { + // const sharedChatChannel = data.sharedChat.sourceRoom.name; + // sharedChatDiv.style.display = 'block'; + // sharedChatChannelDiv.innerHTML = `💬 ${sharedChatChannel}`; + // messageContainerDiv.classList.add("highlightMessage"); + // } + // } + // else if (!data.sharedChat.primarySource && showTwitchSharedChat == 0) + // return; + // } + + // // Set Reply Message + // const isReply = data.isReply; + // if (isReply && showMessage) { + // const replyUser = data.message.reply.userName; + // const replyMsg = data.message.reply.msgBody; + + // replyDiv.style.display = 'block'; + // replyUserDiv.innerText = replyUser; + // replyMsgDiv.innerText = replyMsg; + // } + + // Set timestamp + if (showTimestamps) { + timestampDiv.classList.add("timestamp"); + timestampDiv.innerText = GetCurrentTimeFormatted(); + } + + // Set the username info + if (showUsername) { + if (data.userName.toLowerCase() == data.userName.toLowerCase()) + usernameDiv.innerText = data.userName; + else + usernameDiv.innerText = `${data.userName} (${data.user})`; + usernameDiv.style.color = data.color; + } + + // // Set pronouns + // const pronouns = await GetPronouns('twitch', data.message.username); + // if (pronouns && showPronouns) { + // pronounsDiv.classList.add("pronouns"); + // pronounsDiv.innerText = pronouns; + // } + + // Set the message data + let message = data.message; + const messageColor = data.color; + const role = data.role; + + // Set furry mode + if (furryMode) + message = TranslateToFurry(message); + + // Set message text + if (showMessage) { + messageDiv.innerText = message; + } + + // Set the "action" color + if (data.isAction) + messageDiv.style.color = messageColor; + + // Remove the line break + if (inlineChat) { + instance.querySelector("#colon-separator").style.display = `inline`; + instance.querySelector("#line-space").style.display = `none`; + } + + // Render platform + if (showPlatform) { + const platformElements = ``; + platformDiv.innerHTML = platformElements; + } + + // // Render badges + // if (showBadges) { + // badgeListDiv.innerHTML = ""; + // for (i in data.message.badges) { + // const badge = new Image(); + // badge.src = data.message.badges[i].imageUrl; + // badge.classList.add("badge"); + // badgeListDiv.appendChild(badge); + // } + // } + + // Render badges + // if (data.user.isOwner && showBadges) { + // const badge = new Image(); + // badge.src = `icons/badges/youtube-broadcaster.svg`; + // badge.style.filter = `invert(100%)`; + // badge.style.opacity = 0.8; + // badge.classList.add("badge"); + // badgeListDiv.appendChild(badge); + // } + + if (data.isModerator && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-moderator.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + if (data.isSubscribed && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-member.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + if (data.isVip && showBadges) { + const badge = new Image(); + badge.src = `icons/badges/youtube-verified.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + // // 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 emotes + function replaceEmotes(message) { + const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; + + return message.replace(emoteRegex, (_, id, name) => { + const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; + return `${name}`; + }); + } + messageDiv.innerHTML = replaceEmotes(data.message); + + // Render avatars + if (showAvatar) { + const username = data.user; + const avatarURL = await GetAvatar(username, 'kick'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + // Hide the header if the same username sends a message twice in a row + // EXCEPT when the scroll direction is set to reverse (scrollDirection == 2) + const messageList = document.getElementById("messageList"); + 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.userId) + userInfoDiv.style.display = "none"; + } + + // Embed image + if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'kick') && IsImageUrl(message)) { + const image = new Image(); + + image.onload = function () { + image.style.padding = "20px 0px"; + image.style.width = "100%"; + messageDiv.innerHTML = ''; + messageDiv.appendChild(image); + + AddMessageItem(instance, data.msgId, 'kick', data.userId); + }; + + const urlObj = new URL(message); + urlObj.search = ''; + urlObj.hash = ''; + + image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString(); + } + else { + AddMessageItem(instance, data.msgId, 'kick', data.userId); + } +} + +/* +function GeneralCustom(data) { + const target = data.target; + const type = data.type; + + // Check the target for the message + const path = window.location.pathname; + const firstSegment = path.split('/')[1]; + if (firstSegment != target) + return; + + switch (type) + { + case "message": + CustomMessage(data); + break; + case "alert": + CustomAlert(data); + break; + } +} + +function CustomMessage(data) { + // Don't post messages starting with "!" + if (data.message.startsWith("!") && excludeCommands) + return; + + // Don't post messages from users from the ignore list + if (ignoreUserList.includes(data.username.toLowerCase())) + return; + + // Get a reference to the template + const template = document.getElementById('messageTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const messageContainerDiv = instance.querySelector("#messageContainer"); + const firstMessageDiv = instance.querySelector("#firstMessage"); + const sharedChatDiv = instance.querySelector("#sharedChat"); + const sharedChatChannelDiv = instance.querySelector("#sharedChatChannel"); + const replyDiv = instance.querySelector("#reply"); + const replyUserDiv = instance.querySelector("#replyUser"); + const replyMsgDiv = instance.querySelector("#replyMsg"); + const userInfoDiv = instance.querySelector("#userInfo"); + const avatarDiv = instance.querySelector("#avatar"); + const timestampDiv = instance.querySelector("#timestamp"); + const platformDiv = instance.querySelector("#platform"); + const badgeListDiv = instance.querySelector("#badgeList"); + const pronounsDiv = instance.querySelector("#pronouns"); + const usernameDiv = instance.querySelector("#username"); + const messageDiv = instance.querySelector("#message"); + + // Set timestamp + if (showTimestamps) { + timestampDiv.classList.add("timestamp"); + timestampDiv.innerText = GetCurrentTimeFormatted(); + } + + // Set the username info + if (showUsername) { + if (data.displayName.toLowerCase() == data.username.toLowerCase()) + usernameDiv.innerText = data.displayName; + else + usernameDiv.innerText = `${data.displayName} (${data.username})`; + usernameDiv.style.color = data.userColor; + } + + // Set the message data + let message = data.message; + + // Set furry mode + if (furryMode) + message = TranslateToFurry(message); + + // Set message text + if (showMessage) { + messageDiv.innerText = message; + } + + // Remove the line break + if (inlineChat) { + instance.querySelector("#colon-separator").style.display = `inline`; + instance.querySelector("#line-space").style.display = `none`; + } + + // Render platform + if (showPlatform) { + const platformElements = ``; + platformDiv.innerHTML = platformElements; + } + + // Render avatars + if (showAvatar) { + const avatar = new Image(); + avatar.src = data.avatar; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + AddMessageItem(instance, data.msgId, data.platform.name, data.username); +} + +function CustomAlert(data) { + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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("#content"); + + // Set the card background colors + cardDiv.style.background = data.background; + + // Set the card header + const icon = new Image(); + icon.src = data.icon; + icon.classList.add("badge"); + iconDiv.appendChild(icon); + + // // Set the text + // let username = data.displayName; + // if (data.displayName.toLowerCase() != data.username.toLowerCase()) + // username = `${data.displayName} (${data.username})`; + + titleDiv.innerText = data.title; + contentDiv.innerText = data.content; + + AddMessageItem(instance, data.messageId); +} +*/ + ////////////////////// @@ -1841,17 +2252,36 @@ function GetCurrentTimeFormatted() { return formattedTime; } -async function GetAvatar(username) { - if (avatarMap.has(username)) { - console.debug(`Avatar found for ${username}. Retrieving from hash map.`) - return avatarMap.get(username); +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}`); } - else { - console.debug(`No avatar found for ${username}. Retrieving from Decapi.`) - let response = await fetch('https://decapi.me/twitch/avatar/' + username); - let data = await response.text() - avatarMap.set(username, data); - return data; + + // 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 response = await fetch('https://kick.com/api/v2/channels/' + username); + console.log('https://kick.com/api/v2/channels/' + username) + let data = await response.json(); + let avatarURL = data.user.profile_pic; + if (!avatarURL) + avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + avatarMap.set(`${username}-${platform}`, avatarURL); + return avatarURL; + } } } @@ -1999,6 +2429,17 @@ function GetPermissionLevel(data, platform) { return 15; else return 10; + case 'kick': + if (data.role >= 4) + return 40; + else if (data.role >= 3) + return 30; + else if (data.role >= 2) + return 20; + else if (data.role >= 2 || data.isSubscribed) + return 15; + else + return 10; case 'youtube': if (data.user.isOwner) return 40; @@ -2099,38 +2540,68 @@ function SetConnectionStatus(connected) { +////////////////////// +// TIKFINITY CLIENT // +////////////////////// +let websocket = null; +function connect() { + if (websocket) return; // Already connected + websocket = new WebSocket("ws://localhost:21213/"); -function GeneralCustom(data) { - const target = data.target; - const type = data.type; + websocket.onopen = function () { + console.log(`TikFinity successfully connected...`) + } - // Check the target for the message - const path = window.location.pathname; - const firstSegment = path.split('/')[1]; - if (firstSegment != target) - return; + websocket.onclose = function () { + console.error(`TikFinity disconnected...`) + websocket = null; + setTimeout(connect, 1000); // Schedule a reconnect attempt + } - switch (type) - { - case "message": - CustomMessage(data); - break; - case "alert": - CustomAlert(data); - break; + websocket.onerror = function () { + console.error(`TikFinity failed for some reason...`) + websocket = null; + setTimeout(connect, 1000); // Schedule a reconnect attempt + } + + websocket.onmessage = function (response) { + console.debug('THINGS ARE HAPPENING ON TIKTOK'); + let responseroni = JSON.parse(response.data); + + let event = responseroni.event; + let data = responseroni.data; + + console.debug('Event: ' + event); + + switch (event) { + case 'chat': + TikTokChat(data); + break; + case 'gift': + TikTokGift(data); + break; + case 'subscribe': + TikTokSubscribe(data); + break; + } } } -function CustomMessage(data) { +// Try connect when window is loaded +window.addEventListener('load', connect); + + + +function TikTokChat(data) { // Don't post messages starting with "!" - if (data.message.startsWith("!") && excludeCommands) + if (data.comment.startsWith("!") && excludeCommands) return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.username.toLowerCase())) + if (ignoreUserList.includes(data.comment.toLowerCase())) return; // Get a reference to the template @@ -2164,15 +2635,12 @@ function CustomMessage(data) { // Set the username info if (showUsername) { - if (data.displayName.toLowerCase() == data.username.toLowerCase()) - usernameDiv.innerText = data.displayName; - else - usernameDiv.innerText = `${data.displayName} (${data.username})`; - usernameDiv.style.color = data.userColor; + usernameDiv.innerText = data.nickname; + usernameDiv.style.color = '#9e9e9e'; } // Set the message data - let message = data.message; + let message = data.comment; // Set furry mode if (furryMode) @@ -2191,22 +2659,77 @@ function CustomMessage(data) { // Render platform if (showPlatform) { - const platformElements = ``; + const platformElements = ``; platformDiv.innerHTML = platformElements; } + // Render badges + if (showBadges) { + badgeListDiv.innerHTML = ""; + + if (data.isModerator) { + const badge = new Image(); + badge.src = `icons/badges/youtube-moderator.svg`; + badge.style.filter = `invert(100%)`; + badge.style.opacity = 0.8; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + + for (i in data.userBadges) { + if (data.userBadges[i].type == 'image') { + const badge = new Image(); + badge.src = data.userBadges[i].url; + badge.classList.add("badge"); + badgeListDiv.appendChild(badge); + } + } + } + // Render avatars if (showAvatar) { const avatar = new Image(); - avatar.src = data.avatar; + avatar.src = data.profilePictureUrl; avatar.classList.add("avatar"); avatarDiv.appendChild(avatar); } - AddMessageItem(instance, data.msgId, data.platform.name, data.username); + AddMessageItem(instance, data.msgId, 'tiktok', data.userId); } -function CustomAlert(data) { +function TikTokGift(data) { + if (data.giftType === 1 && !data.repeatEnd) { + // Streak in progress => show only temporary + console.debug(`${data.uniqueId} is sending gift ${data.giftName} x${data.repeatCount}`); + return; + } + + // Streak ended or non-streakable gift => process the gift with final repeat_count + console.debug(`${data.uniqueId} has sent gift ${data.giftName} x${data.repeatCount}`); + + // Get a reference to the template + const template = document.getElementById('tiktok-gift-template'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const avatarImg = instance.querySelector('.tiktok-gift-avatar'); + const usernameSpan = instance.querySelector('#tiktok-gift-username'); + const giftNameSpan = instance.querySelector('#tiktok-gift-name'); + const stickerImg = instance.querySelector('.tiktok-gift-sticker'); + const repeatCountDiv = instance.querySelector('#tiktok-gift-repeat-count'); + + avatarImg.src = data.profilePictureUrl; // Set the card header + usernameSpan.innerText = data.nickname; // Set the username + giftNameSpan.innerText = data.giftName; // Set the gift name + stickerImg.src = data.giftPictureUrl; // Set the sticker image URL + repeatCountDiv.innerText = `x${data.repeatCount}`; // Set the number of gifts sent + + AddMessageItem(instance, data.messageId); +} + +function TikTokSubscribe(data) { // Get a reference to the template const template = document.getElementById('cardTemplate'); @@ -2222,22 +2745,12 @@ function CustomAlert(data) { const contentDiv = instance.querySelector("#content"); // Set the card background colors - cardDiv.style.background = data.background; + cardDiv.classList.add('tiktok'); - // Set the card header - const icon = new Image(); - icon.src = data.icon; - icon.classList.add("badge"); - iconDiv.appendChild(icon); + const user = data.nickname; + const tiktokIcon = ``; - // // Set the text - // let username = data.displayName; - // if (data.displayName.toLowerCase() != data.username.toLowerCase()) - // username = `${data.displayName} (${data.username})`; - - titleDiv.innerText = data.title; - contentDiv.innerText = data.content; - - AddMessageItem(instance, data.messageId); + titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; + AddMessageItem(instance, data.msgId); } \ No newline at end of file diff --git a/multichat-overlay/style.css b/multichat-overlay/style.css index fcba6af..605b9e3 100644 --- a/multichat-overlay/style.css +++ b/multichat-overlay/style.css @@ -52,6 +52,7 @@ html { .normalScrollDirection { bottom: 0; } + .reverseScrollDirection { display: flex; top: 0; @@ -193,6 +194,45 @@ li { font-weight: 700; } +.tiktok-gift { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + padding: 20px 40px; + border-radius: var(--border-radius); + max-width: 100%; + background: linear-gradient(#262626BF, #000000BF) !important; +} + +.tiktok-gift-avatar { + height: 3em; + margin: 1px; + border-radius: 50%; + justify-content: left; + flex-shrink: 0; +} + +.tiktok-gift-message { + flex-grow: 1; + white-space: nowrap; /* keeps text in a single line */ + overflow: hidden; /* hides overflowed content */ + text-overflow: ellipsis; /* adds the "..." at the end */ +} + +.tiktok-gift-sticker { + height: 2.5em; + flex-shrink: 0; + justify-content: flex-end; +} + +#tiktok-gift-repeat-count { + justify-content: flex-end; + font-size: 1.5em; + font-style: italic; + font-weight: 500; +} + /*****************/ /** CARD COLORS **/ /*****************/ @@ -245,6 +285,10 @@ li { background: linear-gradient(#466edbbf, #1c56f5bf) !important; } +.tiktok { + background: linear-gradient(#262626BF, #000000BF) !important; +} + .blank { background: #adadad00 !important; } From b799cb183e4de39f3bc36168d7bd452a93e31891 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 03:52:51 +1000 Subject: [PATCH 02/70] Added more Kick stuff --- multichat-overlay/script.js | 197 ++++++++++++++++++++++++++++++++++-- multichat-overlay/style.css | 4 + 2 files changed, 195 insertions(+), 6 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 5927111..cb05251 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1795,6 +1795,15 @@ function GeneralCustom(data) { case "kickChatMessage": KickChatMessage(data); break; + case "kickSub": + KickSub(data); + break; + case "kickGift": + KickGift(data); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(data); + break; } break; @@ -1810,7 +1819,7 @@ async function KickChatMessage(data) { return; // Don't post messages from users from the ignore list - if (ignoreUserList.includes(data.userName.toLowerCase())) + if (ignoreUserList.includes(data.user.toLowerCase())) return; // Get a reference to the template @@ -1878,10 +1887,10 @@ async function KickChatMessage(data) { // Set the username info if (showUsername) { - if (data.userName.toLowerCase() == data.userName.toLowerCase()) - usernameDiv.innerText = data.userName; + if (data.user.toLowerCase() == data.userName.toLowerCase()) + usernameDiv.innerText = data.user; else - usernameDiv.innerText = `${data.userName} (${data.user})`; + usernameDiv.innerText = `${data.user} (${data.userName})`; usernameDiv.style.color = data.color; } @@ -2013,7 +2022,7 @@ async function KickChatMessage(data) { // Render avatars if (showAvatar) { - const username = data.user; + const username = data.username; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; @@ -2055,6 +2064,154 @@ async function KickChatMessage(data) { } } +async function KickSub(data) { + if (!showTwitchSubs) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + // // Set the card header + // for (i in data.user.badges) { + // if (data.user.badges[i].name == "subscriber") { + // const badge = new Image(); + // badge.src = data.user.badges[i].imageUrl; + // badge.classList.add("badge"); + // iconDiv.appendChild(badge); + // } + // } + + // Set the card header + const badge = new Image(); + badge.src = 'icons/platforms/kick.png'; + badge.classList.add("badge"); + iconDiv.appendChild(badge); + + // Set the text + let username = data.user; + if (data.user.toLowerCase() != data.userName.toLowerCase()) + username = `${data.user} (${data.userName})`; + // const subTier = data.sub_tier; + // const isPrime = data.is_prime; + + // if (!isPrime) + // titleDiv.innerText = `${username} subscribed with Tier ${subTier.charAt(0)}`; + // else + // titleDiv.innerText = `${username} used their Prime Sub`; + //titleDiv.innerText = `${username} subscribed on Kick`; + titleDiv.innerText = data.rawInput; + + AddMessageItem(instance, data.messageId); +} + +async function KickGift(data) { + if (!showTwitchSubs) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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("#content"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + // Set the card header + const badge = new Image(); + badge.src = 'icons/platforms/kick.png'; + badge.classList.add("badge"); + iconDiv.appendChild(badge); + + // 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 subTier = data.subTier; + // const recipient = data.recipient.name; + // const cumlativeTotal = data.cumlativeTotal; + let username = data.user; + if (data.user.toLowerCase() != data.userName.toLowerCase()) + username = `${data.user} (${data.userName})`; + + // titleDiv.innerText = `${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`; + // if (cumlativeTotal > 0) + // contentDiv.innerText = `They've gifted ${cumlativeTotal} subs in total!`; + + titleDiv.innerText = `${data.rawInput}`; + + AddMessageItem(instance, data.messageId); +} + +async function KickRewardRedeemed(data) { + if (!showTwitchChannelPointRedemptions) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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("#content"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + if (showAvatar) { + // Render avatars + const username = data.user; + const avatarURL = await GetAvatar(username, 'kick'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + // Set the text + let username = data.user; + // if (data.user.toLowerCase() != data.userName.toLowerCase()) + // username = `${data.user} (${data.userName})`; + const rewardName = data.rewardTitle; + //const cost = data.reward.cost; + const userInput = data.rewardUserInput; + const channelPointIcon = ``; + + //titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; + titleDiv.innerHTML = `${username} redeemed ${rewardName}`; + contentDiv.innerText = `${userInput}`; + + AddMessageItem(instance, data.redeemId); +} + /* function GeneralCustom(data) { const target = data.target; @@ -2753,4 +2910,32 @@ function TikTokSubscribe(data) { titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; AddMessageItem(instance, data.msgId); -} \ No newline at end of file +} + +let data = { + "user": "NestoVibe", + "userName": "nestovibe", + "userId": 66255551, + "userType": "kick", + "recipientUser": "Bifsie", + "recipientUserName": "bifsie", + "recipientUserId": 17466824, + "tier": "Tier1", + "totalSubsGifted": 1, + "monthsGifted": 1, + "rawInput": "NestoVibe gifted a sub to Bifsie.", + "rawInputEscaped": "NestoVibe gifted a sub to Bifsie.", + "eventSource": "kick", + "fromKick": true, + "__source": 18002, + "triggerId": "d87e05e5-0875-4c3d-8bc9-69fac3ea8528", + "triggerName": "Custom Code Event", + "triggerCategory": "Custom", + "triggerCustomCodeEventName": "kickGift", + "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", + "actionName": "Kick Data Scraper | Save Data", + "runningActionId": "2461d175-3188-4bfa-a3b2-4a171686c627", + "actionQueuedAt": "2025-06-28T01:53:40.6376311+10:00" +} + +KickGift(data); \ No newline at end of file diff --git a/multichat-overlay/style.css b/multichat-overlay/style.css index 605b9e3..9e479dd 100644 --- a/multichat-overlay/style.css +++ b/multichat-overlay/style.css @@ -261,6 +261,10 @@ li { background: linear-gradient(#FF6C60BF, #FF0707BF) !important; } +.kick { + background: linear-gradient(#53fc18BF, #005600BF) !important; +} + .streamlabs { background: linear-gradient(#73dabbbf, #397765bf) !important; } From e8fba92366e184de6788bb11928f6d472b1d264c Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 03:55:10 +1000 Subject: [PATCH 03/70] Oops lol --- multichat-overlay/script.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index cb05251..3f61328 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2910,32 +2910,4 @@ function TikTokSubscribe(data) { titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; AddMessageItem(instance, data.msgId); -} - -let data = { - "user": "NestoVibe", - "userName": "nestovibe", - "userId": 66255551, - "userType": "kick", - "recipientUser": "Bifsie", - "recipientUserName": "bifsie", - "recipientUserId": 17466824, - "tier": "Tier1", - "totalSubsGifted": 1, - "monthsGifted": 1, - "rawInput": "NestoVibe gifted a sub to Bifsie.", - "rawInputEscaped": "NestoVibe gifted a sub to Bifsie.", - "eventSource": "kick", - "fromKick": true, - "__source": 18002, - "triggerId": "d87e05e5-0875-4c3d-8bc9-69fac3ea8528", - "triggerName": "Custom Code Event", - "triggerCategory": "Custom", - "triggerCustomCodeEventName": "kickGift", - "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", - "actionName": "Kick Data Scraper | Save Data", - "runningActionId": "2461d175-3188-4bfa-a3b2-4a171686c627", - "actionQueuedAt": "2025-06-28T01:53:40.6376311+10:00" -} - -KickGift(data); \ No newline at end of file +} \ No newline at end of file From 70411a7e8e308989afe914d8d8b3c8d96c14ea25 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 04:10:37 +1000 Subject: [PATCH 04/70] Added support for multiple gifts --- multichat-overlay/script.js | 75 ++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 3f61328..84f6bbd 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1801,6 +1801,9 @@ function GeneralCustom(data) { case "kickGift": KickGift(data); break; + case "kickGifts": + KickGifts(data); + break; case "kickRewardRedeemed": KickRewardRedeemed(data); break; @@ -2165,6 +2168,53 @@ async function KickGift(data) { AddMessageItem(instance, data.messageId); } +async function KickGifts(data) { + if (!showTwitchSubs) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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("#content"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + // Set the card header + const badge = new Image(); + badge.src = 'icons/platforms/kick.png'; + badge.classList.add("badge"); + iconDiv.appendChild(badge); + + // 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 tier = data.tier; + const gifts = data.gifts; + const totalGifts = data.totalGifts; + // const recipient = data.recipient.name; + //const cumlativeTotal = data.cumlativeTotal; + let username = data.user; + if (data.user.toLowerCase() != data.userName.toLowerCase()) + username = `${data.user} (${data.userName})`; + + titleDiv.innerText = `${username} gifted ${gifts} subs to the community!`; + if (totalGifts > 0) + contentDiv.innerText = `They've gifted ${totalGifts} subs in total!`; + + AddMessageItem(instance, data.messageId); +} + async function KickRewardRedeemed(data) { if (!showTwitchChannelPointRedemptions) return; @@ -2910,4 +2960,27 @@ function TikTokSubscribe(data) { titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; AddMessageItem(instance, data.msgId); -} \ No newline at end of file +} + +let data = { + "user": "ciuphus", + "userName": "ciuphus", + "userId": 141383, + "userType": "kick", + "tier": "Tier1", + "gifts": 2, + "totalGifts": 2, + "eventSource": "kick", + "fromKick": true, + "__source": 18002, + "triggerId": "eaad660c-fddd-4d1d-8038-752c31edb81c", + "triggerName": "Custom Code Event", + "triggerCategory": "Custom", + "triggerCustomCodeEventName": "kickGifts", + "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", + "actionName": "Kick Data Scraper | Save Data", + "runningActionId": "dc099f97-94d9-4d99-b952-c9dfbb4e914b", + "actionQueuedAt": "2025-06-28T03:58:02.3157736+10:00" +} + +KickGifts(data); \ No newline at end of file From 31795554eb7c12bb4f6b8236a0ce3999367af04f Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 04:11:35 +1000 Subject: [PATCH 05/70] Removed multiple gifted sub test --- multichat-overlay/script.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 84f6bbd..76e7dd9 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2960,27 +2960,4 @@ function TikTokSubscribe(data) { titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`; AddMessageItem(instance, data.msgId); -} - -let data = { - "user": "ciuphus", - "userName": "ciuphus", - "userId": 141383, - "userType": "kick", - "tier": "Tier1", - "gifts": 2, - "totalGifts": 2, - "eventSource": "kick", - "fromKick": true, - "__source": 18002, - "triggerId": "eaad660c-fddd-4d1d-8038-752c31edb81c", - "triggerName": "Custom Code Event", - "triggerCategory": "Custom", - "triggerCustomCodeEventName": "kickGifts", - "actionId": "fe5fc792-7e28-4ad9-bdb5-f32f8229810f", - "actionName": "Kick Data Scraper | Save Data", - "runningActionId": "dc099f97-94d9-4d99-b952-c9dfbb4e914b", - "actionQueuedAt": "2025-06-28T03:58:02.3157736+10:00" -} - -KickGifts(data); \ No newline at end of file +} \ No newline at end of file From 2a94b87716d5d910254505c124918ad6ad62fc6a Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 04:13:13 +1000 Subject: [PATCH 06/70] Removed "totalGifts" as Kick.bot doesn't provide cumulative gift total --- multichat-overlay/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 76e7dd9..f34dab6 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2209,8 +2209,8 @@ async function KickGifts(data) { username = `${data.user} (${data.userName})`; titleDiv.innerText = `${username} gifted ${gifts} subs to the community!`; - if (totalGifts > 0) - contentDiv.innerText = `They've gifted ${totalGifts} subs in total!`; + // if (totalGifts > 0) + // contentDiv.innerText = `They've gifted ${totalGifts} subs in total!`; AddMessageItem(instance, data.messageId); } From ab5ce038661f31157cb547d2041bcfdfbbd6005a Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Sat, 28 Jun 2025 05:00:18 +1000 Subject: [PATCH 07/70] Added the ability to show replies for Kick + fixed issue with display pictures --- multichat-overlay/script.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index f34dab6..e895759 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1871,16 +1871,16 @@ async function KickChatMessage(data) { // return; // } - // // Set Reply Message - // const isReply = data.isReply; - // if (isReply && showMessage) { - // const replyUser = data.message.reply.userName; - // const replyMsg = data.message.reply.msgBody; + // Set Reply Message + const isReply = data.isReply; + if (isReply && showMessage) { + const replyUser = data["reply.userName"]; + const replyMsg = data["reply.message"]; - // replyDiv.style.display = 'block'; - // replyUserDiv.innerText = replyUser; - // replyMsgDiv.innerText = replyMsg; - // } + replyDiv.style.display = 'block'; + replyUserDiv.innerText = replyUser; + replyMsgDiv.innerHTML = replaceEmotes(replyMsg);; + } // Set timestamp if (showTimestamps) { @@ -2025,7 +2025,7 @@ async function KickChatMessage(data) { // Render avatars if (showAvatar) { - const username = data.username; + const username = data.user; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; From 5c9ce72dde6681583f1aa4b9f89008de2555cb74 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Wed, 2 Jul 2025 22:07:16 +1000 Subject: [PATCH 08/70] Added page title --- multichat-overlay/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/multichat-overlay/index.html b/multichat-overlay/index.html index 30c8c6b..17ab45e 100644 --- a/multichat-overlay/index.html +++ b/multichat-overlay/index.html @@ -1,6 +1,7 @@ + nutty | Multichat Overlay From 4c72b9360708a4fc892ea796e27e37ca4b868feb Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 03:13:04 +1000 Subject: [PATCH 09/70] Fixed bug where usernames with underscores would not show --- multichat-overlay/script.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index e895759..814a91f 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1890,10 +1890,7 @@ async function KickChatMessage(data) { // Set the username info if (showUsername) { - if (data.user.toLowerCase() == data.userName.toLowerCase()) - usernameDiv.innerText = data.user; - else - usernameDiv.innerText = `${data.user} (${data.userName})`; + usernameDiv.innerText = data.user; usernameDiv.style.color = data.color; } @@ -2025,7 +2022,7 @@ async function KickChatMessage(data) { // Render avatars if (showAvatar) { - const username = data.user; + const username = data.userName; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; @@ -2105,9 +2102,9 @@ async function KickSub(data) { iconDiv.appendChild(badge); // Set the text - let username = data.user; - if (data.user.toLowerCase() != data.userName.toLowerCase()) - username = `${data.user} (${data.userName})`; + // let username = data.user; + // if (data.user.toLowerCase() != data.userName.toLowerCase()) + // username = `${data.user} (${data.userName})`; // const subTier = data.sub_tier; // const isPrime = data.is_prime; @@ -2155,9 +2152,9 @@ async function KickGift(data) { // const subTier = data.subTier; // const recipient = data.recipient.name; // const cumlativeTotal = data.cumlativeTotal; - let username = data.user; - if (data.user.toLowerCase() != data.userName.toLowerCase()) - username = `${data.user} (${data.userName})`; + // let username = data.user; + // if (data.user.toLowerCase() != data.userName.toLowerCase()) + // username = `${data.user} (${data.userName})`; // titleDiv.innerText = `${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`; // if (cumlativeTotal > 0) @@ -2204,9 +2201,9 @@ async function KickGifts(data) { const totalGifts = data.totalGifts; // const recipient = data.recipient.name; //const cumlativeTotal = data.cumlativeTotal; - let username = data.user; - if (data.user.toLowerCase() != data.userName.toLowerCase()) - username = `${data.user} (${data.userName})`; + // let username = data.user; + // if (data.user.toLowerCase() != data.userName.toLowerCase()) + // username = `${data.user} (${data.userName})`; titleDiv.innerText = `${username} gifted ${gifts} subs to the community!`; // if (totalGifts > 0) @@ -2238,7 +2235,7 @@ async function KickRewardRedeemed(data) { if (showAvatar) { // Render avatars - const username = data.user; + const username = data.userName; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; From e2471bc9e438a7cbdb4056c913a6d51b4ee62834 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 03:30:53 +1000 Subject: [PATCH 10/70] Removed the need for SB import code --- multichat-overlay/script.js | 76 +++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 814a91f..c449a3f 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -272,9 +272,14 @@ client.on('Fourthwall.GiftDrawEnded', (response) => { FourthwallGiftDrawEnded(response.data); }) -client.on('General.Custom', (response) => { +// client.on('General.Custom', (response) => { +// console.debug(response.data); +// GeneralCustom(response.data); +// }) + +client.on('Custom.CodeEvent', (response) => { console.debug(response.data); - GeneralCustom(response.data); + CustomCodeEvent(response.data); }) @@ -1784,31 +1789,54 @@ function FourthwallGiftDrawEnded(data) { AddMessageItem(instance, data.id); } -function GeneralCustom(data) { - const eventSource = data.eventSource; +// function GeneralCustom(data) { +// const eventSource = data.eventSource; - switch (eventSource) { - case "kick": - const triggerCustomCodeEventName = data.triggerCustomCodeEventName; +// switch (eventSource) { +// case "kick": +// const triggerCustomCodeEventName = data.triggerCustomCodeEventName; - switch (triggerCustomCodeEventName) { - case "kickChatMessage": - KickChatMessage(data); - break; - case "kickSub": - KickSub(data); - break; - case "kickGift": - KickGift(data); - break; - case "kickGifts": - KickGifts(data); - break; - case "kickRewardRedeemed": - KickRewardRedeemed(data); - break; - } +// switch (triggerCustomCodeEventName) { +// case "kickChatMessage": +// KickChatMessage(data); +// break; +// case "kickSub": +// KickSub(data); +// break; +// case "kickGift": +// KickGift(data); +// break; +// case "kickGifts": +// KickGifts(data); +// break; +// case "kickRewardRedeemed": +// KickRewardRedeemed(data); +// break; +// } +// break; +// } +// } + +function CustomCodeEvent(data) { + const eventName = data.eventName; + const eventArgs = data.args; + + switch (eventName) { + case "kickChatMessage": + KickChatMessage(eventArgs); + break; + case "kickSub": + KickSub(eventArgs); + break; + case "kickGift": + KickGift(eventArgs); + break; + case "kickGifts": + KickGifts(eventArgs); + break; + case "kickRewardRedeemed": + KickRewardRedeemed(eventArgs); break; } } From a5b363688acb46ed3d97f628270449a292796d41 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 04:40:11 +1000 Subject: [PATCH 11/70] Fully implemented Kick settings --- multichat-overlay/script.js | 62 ++++++++++++++++++++++-- multichat-overlay/settings/settings.json | 32 ++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index c449a3f..966e812 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -42,6 +42,11 @@ const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPoin const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); +const showKickMessages = GetBooleanParam("showKickMessages", true); +const showKickSubs = GetBooleanParam("showKickSubs", true); +const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); +const showKickHosts = GetBooleanParam("showKickHosts", true); + const showYouTubeMessages = GetBooleanParam("showYouTubeMessages", true); const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true); const showYouTubeSuperStickers = GetBooleanParam("showYouTubeSuperStickers", true); @@ -1838,11 +1843,14 @@ function CustomCodeEvent(data) { case "kickRewardRedeemed": KickRewardRedeemed(eventArgs); break; + case "kickIncomingRaid": + KickIncomingRaid(eventArgs); + break; } } async function KickChatMessage(data) { - if (!showTwitchMessages) + if (!showKickMessages) return; // Don't post messages starting with "!" @@ -2093,7 +2101,7 @@ async function KickChatMessage(data) { } async function KickSub(data) { - if (!showTwitchSubs) + if (!showKickSubs) return; // Get a reference to the template @@ -2147,7 +2155,7 @@ async function KickSub(data) { } async function KickGift(data) { - if (!showTwitchSubs) + if (!showKickSubs) return; // Get a reference to the template @@ -2194,7 +2202,7 @@ async function KickGift(data) { } async function KickGifts(data) { - if (!showTwitchSubs) + if (!showKickSubs) return; // Get a reference to the template @@ -2241,7 +2249,7 @@ async function KickGifts(data) { } async function KickRewardRedeemed(data) { - if (!showTwitchChannelPointRedemptions) + if (!showKickChannelPointRedemptions) return; // Get a reference to the template @@ -2287,6 +2295,50 @@ async function KickRewardRedeemed(data) { AddMessageItem(instance, data.redeemId); } +async function KickIncomingRaid(data) { + if (!showKickHosts) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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("#content"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + if (showAvatar) { + // Render avatars + const username = data.user; + const avatarURL = await GetAvatar(username, 'kick'); + const avatar = new Image(); + avatar.src = avatarURL; + avatar.classList.add("avatar"); + avatarDiv.appendChild(avatar); + } + + + // Set the text + let username = data.user; + // if (data.from_broadcaster_user_name.toLowerCase() != data.from_broadcaster_user_login.toLowerCase()) + // username = `${data.from_broadcaster_user_name} (${data.from_broadcaster_user_login})`; + const viewers = data.viewers; + + titleDiv.innerText = `${username} is raiding`; + contentDiv.innerText = `with a party of ${viewers}`; + + AddMessageItem(instance, data.messageId); +} + /* function GeneralCustom(data) { const target = data.target; diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 1dfc957..6e09701 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -259,6 +259,38 @@ "defaultValue": 2, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showKickMessages", + "label": "Chat Messages", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, + { + "id": "showKickSubs", + "label": "New Subscribers", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, + { + "id": "showKickChannelPointRedemptions", + "label": "Channel Point Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, + { + "id": "showKickHosts", + "label": "Raids", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Kick messages do you want to see?" + }, { "id": "showYouTubeMessages", "label": "Chat Messages", From a835b8d5899be2499aa06226cfcb6c731d7cd3bb Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 04:49:50 +1000 Subject: [PATCH 12/70] Fixed furry mode not working with Kick --- multichat-overlay/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 966e812..337427e 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2054,7 +2054,7 @@ async function KickChatMessage(data) { return `${name}`; }); } - messageDiv.innerHTML = replaceEmotes(data.message); + messageDiv.innerHTML = replaceEmotes(message); // Render avatars if (showAvatar) { From f095b8dddafab76861b530c270a8df895b107e8f Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 05:08:21 +1000 Subject: [PATCH 13/70] Add follow alerts to Twitch and Kick --- multichat-overlay/script.js | 66 ++++++++++++++++++++++++ multichat-overlay/settings/settings.json | 16 ++++++ 2 files changed, 82 insertions(+) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 337427e..38440fc 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -37,12 +37,14 @@ const imageEmbedPermissionLevel = GetIntParam("imageEmbedPermissionLevel", 20); const showTwitchMessages = GetBooleanParam("showTwitchMessages", true); const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true); +const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); const showKickMessages = GetBooleanParam("showKickMessages", true); +const showKickFollows = GetBooleanParam("showKickFollows", false); const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickHosts = GetBooleanParam("showKickHosts", true); @@ -137,6 +139,11 @@ client.on('Twitch.Announcement', (response) => { TwitchAnnouncement(response.data); }) +client.on('Twitch.Follow', (response) => { + console.debug(response.data); + TwitchFollow(response.data); +}) + client.on('Twitch.Sub', (response) => { console.debug(response.data); TwitchSub(response.data); @@ -648,6 +655,37 @@ async function TwitchAnnouncement(data) { AddMessageItem(instance, data.messageId); } +async function TwitchFollow(data) { + if (!showTwitchFollows) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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"); + + // Set the card background colors + cardDiv.classList.add('twitch'); + + // Set the text + let username = data.user_name; + if (data.user_name.toLowerCase() != data.user_login.toLowerCase()) + username = `${data.user_name} (${data.user_login})`; + + titleDiv.innerText = `${username} followed`; + + AddMessageItem(instance, data.messageId); +} + async function TwitchSub(data) { if (!showTwitchSubs) return; @@ -2100,6 +2138,34 @@ async function KickChatMessage(data) { } } +async function KickFollow(data) { + if (!showKickFollows) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // 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"); + + // Set the card background colors + cardDiv.classList.add('kick'); + + // Set the text + let username = data.user; + titleDiv.innerText = `${username} followed`; + + AddMessageItem(instance, data.messageId); +} + async function KickSub(data) { if (!showKickSubs) return; diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 6e09701..62ec128 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -213,6 +213,14 @@ "defaultValue": true, "group": "Which Twitch messages do you want to see?" }, + { + "id": "showTwitchFollows", + "label": "New Followers", + "description": "", + "type": "checkbox", + "defaultValue": false, + "group": "Which Twitch messages do you want to see?" + }, { "id": "showTwitchSubs", "label": "New Subscribers", @@ -267,6 +275,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", From c0458ecb27b4e55108015ce4bb6ebb86af2d2b30 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 05:15:18 +1000 Subject: [PATCH 14/70] Fixed issue with profile pictures with Kick channel point redemptions --- multichat-overlay/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 38440fc..03e37d1 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -2337,7 +2337,7 @@ async function KickRewardRedeemed(data) { if (showAvatar) { // Render avatars - const username = data.userName; + const username = data.user; const avatarURL = await GetAvatar(username, 'kick'); const avatar = new Image(); avatar.src = avatarURL; From 2b460893df75956df6ac36c1c6ba3a5dd658e3d9 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 05:44:18 +1000 Subject: [PATCH 15/70] Added functionality to remove messages when Kick users are banned/timed out/message deleted --- multichat-overlay/script.js | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 03e37d1..93ed915 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1884,6 +1884,16 @@ function CustomCodeEvent(data) { case "kickIncomingRaid": KickIncomingRaid(eventArgs); break; + case "kickChatMessageDeleted": + KickChatMessageDeleted(eventArgs); + break; + case "kickBan": + KickBan(eventArgs); + break; + case "kickTO": + KickBan(eventArgs); + break; + } } @@ -2405,6 +2415,54 @@ async function KickIncomingRaid(data) { AddMessageItem(instance, data.messageId); } +function KickChatMessageDeleted(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const messageId = data.msgId; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].id === messageId) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + item.style.opacity = 0; + item.style.height = 0; + setTimeout(function () { + messageList.removeChild(item); + }, 1000); + }); +} + +function KickBan(data) { + const messageList = document.getElementById("messageList"); + + // Maintain a list of chat messages to delete + const messagesToRemove = []; + + // ID of the message to remove + const userId = data.userId; + + // Find the items to remove + for (let i = 0; i < messageList.children.length; i++) { + if (messageList.children[i].dataset.userId.toString() == userId.toString()) { + messagesToRemove.push(messageList.children[i]); + } + } + + // Remove the items + messagesToRemove.forEach(item => { + messageList.removeChild(item); + }); +} + /* function GeneralCustom(data) { const target = data.target; From c65205a5a161f48f61c6ad8a4e6d8df26c3a7b91 Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 3 Jul 2025 07:15:14 +1000 Subject: [PATCH 16/70] Added Kick support --- horizontal-chat/icons/platforms/kick.png | Bin 0 -> 1169 bytes horizontal-chat/icons/platforms/tiktok.png | Bin 0 -> 41499 bytes horizontal-chat/script.js | 367 ++++++++++++++++++++- horizontal-chat/settings/settings.json | 40 +++ horizontal-chat/style.css | 4 + multichat-overlay/script.js | 6 +- 6 files changed, 403 insertions(+), 14 deletions(-) create mode 100644 horizontal-chat/icons/platforms/kick.png create mode 100644 horizontal-chat/icons/platforms/tiktok.png diff --git a/horizontal-chat/icons/platforms/kick.png b/horizontal-chat/icons/platforms/kick.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc9afaedddcf0f850be6412b56b69af9fd90ac2 GIT binary patch literal 1169 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