• Kick username will now be fetched (option hidden)

• Added Kick Follow support
• Improved emote support (needs testing)
This commit is contained in:
nutty
2026-02-24 16:00:31 +11:00
parent 91f0a6b2d7
commit 022b296016
8 changed files with 476 additions and 546 deletions
+146 -80
View File
@@ -12,39 +12,39 @@ const pronounMap = new Map();
////////////////////// //////////////////////
function GetBooleanParam(paramName, defaultValue) { function GetBooleanParam(paramName, defaultValue) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const paramValue = urlParams.get(paramName); const paramValue = urlParams.get(paramName);
if (paramValue === null) { if (paramValue === null) {
return defaultValue; // Parameter not found return defaultValue; // Parameter not found
} }
const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity
if (lowercaseValue === 'true') { if (lowercaseValue === 'true') {
return true; return true;
} else if (lowercaseValue === 'false') { } else if (lowercaseValue === 'false') {
return false; return false;
} else { } else {
return paramValue; // Return original string if not 'true' or 'false' return paramValue; // Return original string if not 'true' or 'false'
} }
} }
function GetIntParam(paramName, defaultValue) { function GetIntParam(paramName, defaultValue) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const paramValue = urlParams.get(paramName); const paramValue = urlParams.get(paramName);
if (paramValue === null) { if (paramValue === null) {
return defaultValue; // or undefined, or a default value, depending on your needs return defaultValue; // or undefined, or a default value, depending on your needs
} }
const intValue = parseInt(paramValue, 10); // Parse as base 10 integer const intValue = parseInt(paramValue, 10); // Parse as base 10 integer
if (isNaN(intValue)) { if (isNaN(intValue)) {
return null; // or handle the error in another way, e.g., throw an error return null; // or handle the error in another way, e.g., throw an error
} }
return intValue; return intValue;
} }
async function GetKickIds(username) { async function GetKickIds(username) {
@@ -143,82 +143,89 @@ async function GetAvatar(username, platform) {
} }
async function GetPronouns(platform, username) { async function GetPronouns(platform, username) {
if (pronounMap.has(username)) { if (pronounMap.has(username)) {
console.debug(`Pronouns found for ${username}. Retrieving from hash map.`) console.debug(`Pronouns found for ${username}. Retrieving from hash map.`)
return pronounMap.get(username); return pronounMap.get(username);
} }
else { else {
console.debug(`No pronouns found for ${username}. Retrieving from alejo.io.`) console.debug(`No pronouns found for ${username}. Retrieving from alejo.io.`)
const response = await client.getUserPronouns(platform, username); const response = await client.getUserPronouns(platform, username);
const userFound = response.pronoun.userFound; const userFound = response.pronoun.userFound;
const pronouns = userFound ? `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}` : ''; const pronouns = userFound ? `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}` : '';
pronounMap.set(username, pronouns); pronounMap.set(username, pronouns);
return pronouns; return pronouns;
} }
} }
function GetCurrentTimeFormatted() { function GetCurrentTimeFormatted() {
const now = new Date(); const now = new Date();
let hours = now.getHours(); let hours = now.getHours();
const minutes = String(now.getMinutes()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM'; const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12; hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12' hours = hours ? hours : 12; // the hour '0' should be '12'
const formattedTime = `${hours}:${minutes} ${ampm}`; const formattedTime = `${hours}:${minutes} ${ampm}`;
return formattedTime; return formattedTime;
} }
function DecodeHTMLString(html) { function DecodeHTMLString(html) {
var txt = document.createElement("textarea"); var txt = document.createElement("textarea");
txt.innerHTML = html; txt.innerHTML = html;
return txt.value; return txt.value;
} }
function TranslateToFurry(sentence) { function TranslateToFurry(sentence) {
const words = sentence.toLowerCase().split(/\b/); // Split on <img ...> tags, keeping them in the result
const parts = sentence.split(/(<img\b[^>]*>)/gi);
const furryWords = words.map(word => { const furryParts = parts.map(part => {
if (/\w+/.test(word)) { // If this part is an <img>, leave it unchanged
let newWord = word; if (/^<img\b[^>]*>$/.test(part)) {
return part;
}
// Common substitutions // Otherwise, apply furry translation
newWord = newWord.replace(/l/g, 'w'); const words = part.toLowerCase().split(/\b/);
newWord = newWord.replace(/r/g, 'w');
newWord = newWord.replace(/th/g, 'f');
newWord = newWord.replace(/you/g, 'yous');
newWord = newWord.replace(/my/g, 'mah');
newWord = newWord.replace(/me/g, 'meh');
newWord = newWord.replace(/am/g, 'am');
newWord = newWord.replace(/is/g, 'is');
newWord = newWord.replace(/are/g, 'are');
newWord = newWord.replace(/very/g, 'vewy');
newWord = newWord.replace(/pretty/g, 'pwetty');
newWord = newWord.replace(/little/g, 'wittle');
newWord = newWord.replace(/nice/g, 'nyce');
// Random additions return words.map(word => {
if (Math.random() < 0.15) { if (/\w+/.test(word)) {
newWord += ' nya~'; let newWord = word;
} else if (Math.random() < 0.1) {
newWord += ' >w<';
} else if (Math.random() < 0.05) {
newWord += ' owo';
}
return newWord; // Common substitutions
} newWord = newWord.replace(/l/g, 'w');
return word; newWord = newWord.replace(/r/g, 'w');
}); newWord = newWord.replace(/th/g, 'f');
newWord = newWord.replace(/you/g, 'yous');
newWord = newWord.replace(/my/g, 'mah');
newWord = newWord.replace(/me/g, 'meh');
newWord = newWord.replace(/am/g, 'am');
newWord = newWord.replace(/is/g, 'is');
newWord = newWord.replace(/are/g, 'are');
newWord = newWord.replace(/very/g, 'vewy');
newWord = newWord.replace(/pretty/g, 'pwetty');
newWord = newWord.replace(/little/g, 'wittle');
newWord = newWord.replace(/nice/g, 'nyce');
return furryWords.join(''); // Random additions
} if (Math.random() < 0.15) {
newWord += ' nya~';
} else if (Math.random() < 0.1) {
newWord += ' >w<';
} else if (Math.random() < 0.05) {
newWord += ' owo';
}
function EscapeRegExp(string) { return newWord;
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string }
return word;
}).join('');
});
return furryParts.join('');
} }
// Given a string, return a random hex code. The same input always results in the same output // Given a string, return a random hex code. The same input always results in the same output
@@ -261,3 +268,62 @@ function RandomHex(str) {
return hslToHex(hue, saturation, lightness); return hslToHex(hue, saturation, lightness);
} }
// Used to construct a message from "parts" variable commonly found in Streamer.bot chat messages
function ConstructMessageFromParts(parts) {
return parts.map(part => {
switch (part.type)
{
case "text":
return part.text;
case "cheer":
// Render the cheer emote image
const emoteImg = `<img src="${part.imageUrl}" alt="${part.text}" title="${part.text}" class="emote">`;
// Render the bits count
const bitLabel = `<span class="bits">${part.bits}</span>`;
return emoteImg + bitLabel;
default:
return `<img src="${part.imageUrl}" alt="${part.text}" title="${part.text}" class="emote">`;
}
}).join('');
}
// Used to construct a message from "emotes" variable commonly found in Streamer.bot chat messages
function RenderMessageWithEmotesHTML(originalMessage, emotes) {
if (!emotes || emotes.length === 0) return originalMessage;
// Sort emotes by startIndex
emotes.sort((a, b) => a.startIndex - b.startIndex);
let html = '';
let cursor = 0;
emotes.forEach(emote => {
// Add text before the emote
if (emote.startIndex > cursor) {
html += escapeHTML(originalMessage.slice(cursor, emote.startIndex));
}
// Add emote image
html += `<img src="${emote.imageUrl}" alt="${escapeHTML(emote.name)}" title="${escapeHTML(emote.name)}" class="emote">`;
cursor = emote.endIndex + 1;
});
// Add remaining text after last emote
if (cursor < originalMessage.length) {
html += escapeHTML(originalMessage.slice(cursor));
}
// Simple HTML escape function
function escapeHTML(str) {
return str.replace(/[&<>"']/g, match => {
const escape = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
return escape[match];
});
}
return html;
}
+101 -152
View File
@@ -50,9 +50,8 @@ const showTwitchRaids = GetBooleanParam("showTwitchRaids", true);
const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true);
const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true); const showTwitchSharedChat = GetBooleanParam("showTwitchSharedChat", true);
const kickUsername = urlParams.get("kickUsername") || "";
const showKickMessages = GetBooleanParam("showKickMessages", true); const showKickMessages = GetBooleanParam("showKickMessages", true);
// const showKickFollows = GetBooleanParam("showKickFollows", false); const showKickFollows = GetBooleanParam("showKickFollows", false);
const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickSubs = GetBooleanParam("showKickSubs", true);
const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true);
const showKickHosts = GetBooleanParam("showKickHosts", true); const showKickHosts = GetBooleanParam("showKickHosts", true);
@@ -86,6 +85,7 @@ const animationSpeed = GetIntParam("animationSpeed", 0.5);
const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false);
const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeColor = urlParams.get("youtubeColor") || "#f70000";
const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || "";
const kickUsername = urlParams.get("kickUsername") || "";
@@ -233,6 +233,37 @@ client.on('YouTube.GiftMembershipReceived', (response) => {
YouTubeGiftMembershipReceived(); YouTubeGiftMembershipReceived();
}) })
client.on('Kick.ChatMessage', (response) => {
console.debug(response.data);
KickChatMessage(response.data);
})
client.on('Kick.Follow', (response) => {
console.debug(response.data);
KickFollow(response.data);
})
// // TODO: Seems to be missing data, using Pusher until this is fixed
// client.on('Kick.Subscription', (response) => {
// console.debug(response.data);
// KickSubscription(response.data);
// })
// client.on('Kick.Resubscription', (response) => {
// console.debug(response.data);
// KickSubscription(response.data);
// })
// client.on('Kick.GiftSubscription', (response) => {
// console.debug(response.data);
// KickGiftedSubscriptions(response.data);
// })
// client.on('Kick.RewardRedemption', (response) => {
// console.debug(response.data);
// KickRewardRedeemed(response.data);
// })
client.on('Streamlabs.Donation', (response) => { client.on('Streamlabs.Donation', (response) => {
console.debug(response.data); console.debug(response.data);
StreamlabsDonation(response.data); StreamlabsDonation(response.data);
@@ -311,8 +342,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => {
// Connect and handle Pusher WebSocket // Connect and handle Pusher WebSocket
async function KickConnect() { async function KickConnect() {
// If user has not manually set Kick username, try to grab if from Streamer.bot
if (!kickUsername) if (!kickUsername)
return; {
// Fetch from Streamer.bot
const broadcasterInfo = await client.getBroadcaster();
if (broadcasterInfo.platforms.kick)
kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin;
else
return;
}
// Channel to subscribe to (you'll need the correct channel name here) // Channel to subscribe to (you'll need the correct channel name here)
const kickIds = await GetKickIds(kickUsername); const kickIds = await GetKickIds(kickUsername);
@@ -327,7 +367,7 @@ async function KickConnect() {
// Reconnect // Reconnect
websocket.onclose = function () { websocket.onclose = function () {
console.log(`Reconnecting to ${kickUsername}...`); console.log(`Reconnecting to ${kickUsername}...`);
setTimeout(connectPusher, 5000); setTimeout(KickConnect, 5000);
}; };
websocket.onopen = function () { websocket.onopen = function () {
@@ -358,9 +398,9 @@ async function KickConnect() {
const eventArgs = JSON.parse(data.data); const eventArgs = JSON.parse(data.data);
const event = data.event.split('\\').pop(); const event = data.event.split('\\').pop();
switch (event) { switch (event) {
case 'ChatMessageEvent': // case 'ChatMessageEvent':
KickChatMessage(eventArgs); // KickChatMessage(eventArgs);
break; // break;
//// 'Follows' unsupported by pusher //// 'Follows' unsupported by pusher
// case 'FollowEvent': // case 'FollowEvent':
// break; // break;
@@ -397,7 +437,6 @@ async function KickConnect() {
window.addEventListener('load', KickConnect); window.addEventListener('load', KickConnect);
////////////////////// //////////////////////
// TIKFINITY CLIENT // // TIKFINITY CLIENT //
////////////////////// //////////////////////
@@ -522,7 +561,7 @@ async function TwitchChatMessage(data) {
} }
// Set the message data // Set the message data
let message = data.message.message; let message = ConstructMessageFromParts(data.parts);
const messageColor = data.message.color; const messageColor = data.message.color;
const role = data.message.role; const role = data.message.role;
@@ -532,7 +571,7 @@ async function TwitchChatMessage(data) {
// Set message text // Set message text
if (showMessage) { if (showMessage) {
messageDiv.innerText = message; messageDiv.innerHTML = message;
} }
// Set the "action" color // Set the "action" color
@@ -545,7 +584,6 @@ async function TwitchChatMessage(data) {
platformDiv.innerHTML = platformElements; platformDiv.innerHTML = platformElements;
} }
// Render badges // Render badges
if (showBadges) { if (showBadges) {
badgeListDiv.innerHTML = ""; badgeListDiv.innerHTML = "";
@@ -557,38 +595,6 @@ async function TwitchChatMessage(data) {
} }
} }
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
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 = `<img src="${data.cheerEmotes[i].imageUrl}" class="emote"/>`;
// 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 = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const username = data.message.username; const username = data.message.username;
@@ -660,29 +666,7 @@ async function TwitchAnnouncement(data) {
break; break;
} }
let message = data.text; let message = ConstructMessageFromParts(data.parts);
// Render emotes
for (i in data.parts) {
if (data.parts[i].type == `emote`) {
const emoteElement = `<img src="${data.parts[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.parts[i].text);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
message = message.replace(regex, emoteElement);
}
}
ShowAlert(message, background); ShowAlert(message, background);
} }
@@ -918,13 +902,17 @@ function YouTubeMessage(data) {
usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red
} }
if (showMessage) { // Set the message data
messageDiv.innerText = data.message; let message = RenderMessageWithEmotesHTML(data.message, data.emotes);
}
// Set furry mode // Set furry mode
if (furryMode) if (furryMode)
messageDiv.innerText = TranslateToFurry(data.message); message = TranslateToFurry(message);
// Set message text
if (showMessage) {
messageDiv.innerHTML = message;
}
// Render platform // Render platform
if (showPlatform) { if (showPlatform) {
@@ -972,12 +960,6 @@ function YouTubeMessage(data) {
badgeListDiv.appendChild(badge); badgeListDiv.appendChild(badge);
} }
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement);
}
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const avatar = new Image(); const avatar = new Image();
@@ -1305,11 +1287,11 @@ async function KickChatMessage(data) {
return; return;
// Don't post messages starting with "!" // Don't post messages starting with "!"
if (data.content.startsWith("!") && excludeCommands) if (data.text.startsWith("!") && excludeCommands)
return; return;
// Don't post messages from users from the ignore list // Don't post messages from users from the ignore list
if (ignoreUserList.includes(data.sender.username.toLowerCase())) if (ignoreUserList.includes(data.user.name.toLowerCase()))
return; return;
// Get a reference to the template // Get a reference to the template
@@ -1336,12 +1318,12 @@ async function KickChatMessage(data) {
// Set the username info // Set the username info
if (showUsername) { if (showUsername) {
usernameDiv.innerText = data.sender.username; usernameDiv.innerText = data.user.name;
usernameDiv.style.color = data.sender.identity.color; usernameDiv.style.color = data.user.color;
} }
// Set the message data // Set the message data
let message = data.content; let message = ConstructMessageFromParts(data.parts);
// Set furry mode // Set furry mode
if (furryMode) if (furryMode)
@@ -1349,7 +1331,7 @@ async function KickChatMessage(data) {
// Set message text // Set message text
if (showMessage) { if (showMessage) {
messageDiv.innerText = message; messageDiv.innerHTML = message;
} }
// Render platform // Render platform
@@ -1361,28 +1343,38 @@ async function KickChatMessage(data) {
// Render badges // Render badges
if (showBadges) { if (showBadges) {
badgeListDiv.innerHTML = ""; badgeListDiv.innerHTML = "";
for (i in data.sender.identity.badges) { for (i in data.user.badges) {
const badge = new Image(); const badge = new Image();
badge.src = GetKickBadgeURL(data.sender.identity.badges[i]); if (data.user.badges[i].imageUrl)
badge.src = data.user.badges[i].imageUrl;
else
badge.src = GetKickBadgeURL(data.user.badges[i]);
badge.classList.add("badge"); badge.classList.add("badge");
badgeListDiv.appendChild(badge); badgeListDiv.appendChild(badge);
} }
} }
function GetKickBadgeURL(data) {
switch (data.id) {
case 'subscriber':
return CalculateKickSubBadge(data.count);
default:
return `icons/badges/kick-${data.id}.svg`;
}
function CalculateKickSubBadge(months) {
if (!Array.isArray(kickSubBadges)) return null;
// Render emotes // Filter for eligible badges, then get the one with the highest 'months'
function replaceEmotes(message) { const badge = kickSubBadges
const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; .filter(b => b.months <= months)
.sort((a, b) => b.months - a.months)[0];
return message.replace(emoteRegex, (_, id, name) => { return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`;
const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; }
return `<img src="${imgUrl}" alt="${name}" title="${name}" class="emote" />`;
});
} }
messageDiv.innerHTML = replaceEmotes(message);
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const username = data.sender.slug; const username = data.user.login;
const avatarURL = await GetAvatar(username, 'kick'); const avatarURL = await GetAvatar(username, 'kick');
const avatar = new Image(); const avatar = new Image();
avatar.src = avatarURL; avatar.src = avatarURL;
@@ -1399,7 +1391,21 @@ async function KickChatMessage(data) {
userInfoDiv.style.display = "none"; userInfoDiv.style.display = "none";
} }
AddMessageItem(instance, data.id, 'kick', data.sender.id); AddMessageItem(instance, data.messageId, 'kick', data.user.id);
}
function KickFollow(data) {
if (!showKickFollows)
return;
// Set the text
let username = data.user.name;
if (data.user.name.toLowerCase() != data.user.login.toLowerCase())
username = `${data.user.name} (${data.user.login})`;
const message = `${username} followed`;
ShowAlert(message, 'kick');
} }
function KickSubscription(data) { function KickSubscription(data) {
@@ -1408,7 +1414,7 @@ function KickSubscription(data) {
// Set the text // Set the text
const username = data.username; const username = data.username;
const months = data.months; const months = data.months
let message = ''; let message = '';
if (months <= 1) if (months <= 1)
@@ -1710,43 +1716,6 @@ function AddMessageItem(element, elementID, platform, userId) {
}, 200); }, 200);
} }
// I used Gemini for this shit so if it doesn't work, blame Google
function FindFirstImageUrl(jsonObject) {
if (typeof jsonObject !== 'object' || jsonObject === null) {
return null; // Handle invalid input
}
function iterate(obj) {
if (Array.isArray(obj)) {
for (const item of obj) {
const result = iterate(item);
if (result) {
return result;
}
}
return null;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === 'imageUrl') {
return obj[key]; // Found it! Return the value.
}
if (typeof obj[key] === 'object' && obj[key] !== null) {
const result = iterate(obj[key]); // Recursive call for nested objects
if (result) {
return result; // Propagate the found value
}
}
}
}
return null; // Key not found in this level
}
return iterate(jsonObject);
}
function ShowAlert(message, background = null, duration = animationDuration) { function ShowAlert(message, background = null, duration = animationDuration) {
// Check if the widget is in the middle of an animation // Check if the widget is in the middle of an animation
@@ -1810,26 +1779,6 @@ function GetWinnersList(gifts) {
} }
} }
function GetKickBadgeURL(data) {
switch (data.type) {
case 'subscriber':
return CalculateKickSubBadge(data.count);
default:
return `icons/badges/kick-${data.type}.svg`;
}
}
function CalculateKickSubBadge(months) {
if (!Array.isArray(kickSubBadges)) return null;
// Filter for eligible badges, then get the one with the highest 'months'
const badge = kickSubBadges
.filter(b => b.months <= months)
.sort((a, b) => b.months - a.months)[0];
return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`;
}
/////////////////////////////////// ///////////////////////////////////
+8 -8
View File
@@ -231,14 +231,6 @@
"defaultValue": true, "defaultValue": true,
"group": "Which YouTube messages do you want to see?" "group": "Which YouTube messages do you want to see?"
}, },
{
"id": "kickUsername",
"label": "Kick Username",
"description": "",
"type": "text",
"defaultValue": "",
"group": "Which Kick messages do you want to see?"
},
{ {
"id": "showKickMessages", "id": "showKickMessages",
"label": "Chat Messages", "label": "Chat Messages",
@@ -247,6 +239,14 @@
"defaultValue": true, "defaultValue": true,
"group": "Which Kick messages do you want to see?" "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", "id": "showKickSubs",
"label": "New Subscribers", "label": "New Subscribers",
+157 -203
View File
@@ -55,9 +55,8 @@ const showTwitchRaids = GetBooleanParam("showTwitchRaids", true);
const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true);
const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2);
const kickUsername = urlParams.get("kickUsername") || "";
const showKickMessages = GetBooleanParam("showKickMessages", true); const showKickMessages = GetBooleanParam("showKickMessages", true);
// const showKickFollows = GetBooleanParam("showKickFollows", false); const showKickFollows = GetBooleanParam("showKickFollows", false);
const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickSubs = GetBooleanParam("showKickSubs", true);
const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true);
const showKickHosts = GetBooleanParam("showKickHosts", true); const showKickHosts = GetBooleanParam("showKickHosts", true);
@@ -92,6 +91,7 @@ const animationSpeed = GetIntParam("animationSpeed", 0.1);
const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false);
const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeColor = urlParams.get("youtubeColor") || "#f70000";
const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || "";
let kickUsername = urlParams.get("kickUsername") || "";
@@ -268,6 +268,37 @@ client.on('YouTube.GiftMembershipReceived', (response) => {
YouTubeGiftMembershipReceived(response.data); YouTubeGiftMembershipReceived(response.data);
}) })
client.on('Kick.ChatMessage', (response) => {
console.debug(response.data);
KickChatMessage(response.data);
})
client.on('Kick.Follow', (response) => {
console.debug(response.data);
KickFollow(response.data);
})
// // TODO: Seems to be missing data, using Pusher until this is fixed
// client.on('Kick.Subscription', (response) => {
// console.debug(response.data);
// KickSubscription(response.data);
// })
// client.on('Kick.Resubscription', (response) => {
// console.debug(response.data);
// KickSubscription(response.data);
// })
// client.on('Kick.GiftSubscription', (response) => {
// console.debug(response.data);
// KickGiftedSubscriptions(response.data);
// })
// client.on('Kick.RewardRedemption', (response) => {
// console.debug(response.data);
// KickRewardRedeemed(response.data);
// })
client.on('Streamlabs.Donation', (response) => { client.on('Streamlabs.Donation', (response) => {
console.debug(response.data); console.debug(response.data);
StreamlabsDonation(response.data); StreamlabsDonation(response.data);
@@ -351,8 +382,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => {
// Connect and handle Pusher WebSocket // Connect and handle Pusher WebSocket
async function KickConnect() { async function KickConnect() {
// If user has not manually set Kick username, try to grab if from Streamer.bot
if (!kickUsername) if (!kickUsername)
return; {
// Fetch from Streamer.bot
const broadcasterInfo = await client.getBroadcaster();
if (broadcasterInfo.platforms.kick)
kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin;
else
return;
}
// Channel to subscribe to (you'll need the correct channel name here) // Channel to subscribe to (you'll need the correct channel name here)
const kickIds = await GetKickIds(kickUsername); const kickIds = await GetKickIds(kickUsername);
@@ -367,7 +407,7 @@ async function KickConnect() {
// Reconnect // Reconnect
websocket.onclose = function () { websocket.onclose = function () {
console.log(`Reconnecting to ${kickUsername}...`); console.log(`Reconnecting to ${kickUsername}...`);
setTimeout(connectPusher, 5000); setTimeout(KickConnect, 5000);
}; };
websocket.onopen = function () { websocket.onopen = function () {
@@ -398,9 +438,9 @@ async function KickConnect() {
const eventArgs = JSON.parse(data.data); const eventArgs = JSON.parse(data.data);
const event = data.event.split('\\').pop(); const event = data.event.split('\\').pop();
switch (event) { switch (event) {
case 'ChatMessageEvent': // case 'ChatMessageEvent':
KickChatMessage(eventArgs); // KickChatMessage(eventArgs);
break; // break;
//// 'Follows' unsupported by pusher //// 'Follows' unsupported by pusher
// case 'FollowEvent': // case 'FollowEvent':
// break; // break;
@@ -617,7 +657,7 @@ async function TwitchChatMessage(data) {
} }
// Set the message data // Set the message data
let message = data.message.message; let message = ConstructMessageFromParts(data.parts);
const messageColor = data.message.color; const messageColor = data.message.color;
const role = data.message.role; const role = data.message.role;
@@ -627,7 +667,7 @@ async function TwitchChatMessage(data) {
// Set message text // Set message text
if (showMessage) { if (showMessage) {
messageDiv.innerText = message; messageDiv.innerHTML = message;
} }
// Set the "action" color // Set the "action" color
@@ -658,36 +698,6 @@ async function TwitchChatMessage(data) {
} }
} }
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
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 = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const username = data.message.username; const username = data.message.username;
@@ -893,7 +903,7 @@ async function TwitchAnnouncement(data) {
else else
content.querySelector("#username").innerText = `${data.user.name} (${data.user.login})`; content.querySelector("#username").innerText = `${data.user.name} (${data.user.login})`;
content.querySelector("#username").style.color = data.user.color; content.querySelector("#username").style.color = data.user.color;
content.querySelector("#message").innerText = data.text; content.querySelector("#message").innerHTML = ConstructMessageFromParts(data.parts);
// Remove the line break // Remove the line break
content.querySelector("#colon-separator").style.display = `inline`; content.querySelector("#colon-separator").style.display = `inline`;
@@ -921,28 +931,6 @@ async function TwitchAnnouncement(data) {
content.querySelector("#pronouns").innerText = pronouns; content.querySelector("#pronouns").innerText = pronouns;
} }
// Render emotes
for (i in data.parts) {
if (data.parts[i].type == `emote`) {
const emoteElement = `<img src="${data.parts[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.parts[i].text);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
content.querySelector("#message").innerHTML = content.querySelector("#message").innerHTML.replace(regex, emoteElement);
}
}
// Insert the modified template instance into the DOM // Insert the modified template instance into the DOM
instance.querySelector("#content").appendChild(content); instance.querySelector("#content").appendChild(content);
@@ -1244,30 +1232,10 @@ async function TwitchWatchStreak(data) {
const displayName = data.displayName; const displayName = data.displayName;
const watchStreak = data.watchStreak; const watchStreak = data.watchStreak;
const message = data.message; const message = RenderMessageWithEmotesHTML(data.message, data.emotes);
titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `; titleDiv.innerText = `${displayName} is currently on a ${watchStreak} stream streak! `;
contentDiv.innerText = `${message}`; contentDiv.innerHTML = message;
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.emotes[i].name);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
contentDiv.innerHTML = contentDiv.innerHTML.replace(regex, emoteElement);
}
AddMessageItem(instance, data.messageId); AddMessageItem(instance, data.messageId);
} }
@@ -1382,13 +1350,17 @@ async function YouTubeMessage(data) {
usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red usernameDiv.style.color = youtubeColor; // YouTube users do not have colors, so just set it to red
} }
if (showMessage) { // Set the message data
messageDiv.innerText = data.message; let message = RenderMessageWithEmotesHTML(data.message, data.emotes);
}
// Set furry mode // Set furry mode
if (furryMode) if (furryMode)
messageDiv.innerText = TranslateToFurry(data.message); message = TranslateToFurry(message);
// Set message text
if (showMessage) {
messageDiv.innerHTML = message;
}
// Remove the line break // Remove the line break
if (inlineChat) { if (inlineChat) {
@@ -1443,13 +1415,6 @@ async function YouTubeMessage(data) {
badgeListDiv.appendChild(badge); badgeListDiv.appendChild(badge);
} }
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
// messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement);
messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement);
}
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const avatar = new Image(); const avatar = new Image();
@@ -1483,7 +1448,6 @@ async function YouTubeMessage(data) {
} }
// Embed image // Embed image
const message = data.message;
if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'youtube') && IsImageUrl(message)) { if (IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(imageEmbedPermissionLevel, data, 'youtube') && IsImageUrl(message)) {
const image = new Image(); const image = new Image();
@@ -1573,6 +1537,43 @@ function YouTubeSuperSticker(data) {
const stickerURL = FindFirstImageUrl(data); const stickerURL = FindFirstImageUrl(data);
const stickerImage = `<img src="${stickerURL}" class="youtube-super-sticker"/>`; const stickerImage = `<img src="${stickerURL}" class="youtube-super-sticker"/>`;
// I used Gemini for this shit so if it doesn't work, blame Google
function FindFirstImageUrl(jsonObject) {
if (typeof jsonObject !== 'object' || jsonObject === null) {
return null; // Handle invalid input
}
function iterate(obj) {
if (Array.isArray(obj)) {
for (const item of obj) {
const result = iterate(item);
if (result) {
return result;
}
}
return null;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === 'imageUrl') {
return obj[key]; // Found it! Return the value.
}
if (typeof obj[key] === 'object' && obj[key] !== null) {
const result = iterate(obj[key]); // Recursive call for nested objects
if (result) {
return result; // Propagate the found value
}
}
}
}
return null; // Key not found in this level
}
return iterate(jsonObject);
}
avatarDiv.innerHTML = stickerImage; avatarDiv.innerHTML = stickerImage;
titleDiv.innerHTML = `${user} sent a Super Sticker (${amount})`; titleDiv.innerHTML = `${user} sent a Super Sticker (${amount})`;
@@ -2249,11 +2250,11 @@ async function KickChatMessage(data) {
return; return;
// Don't post messages starting with "!" // Don't post messages starting with "!"
if (data.content.startsWith("!") && excludeCommands) if (data.text.startsWith("!") && excludeCommands)
return; return;
// Don't post messages from users from the ignore list // Don't post messages from users from the ignore list
if (ignoreUserList.includes(data.sender.username.toLowerCase())) if (ignoreUserList.includes(data.user.name.toLowerCase()))
return; return;
// Get a reference to the template // Get a reference to the template
@@ -2298,10 +2299,10 @@ async function KickChatMessage(data) {
// } // }
// Set Reply Message // Set Reply Message
const isReply = data.type == 'reply'; const isReply = data.isReply;
if (isReply && showMessage) { if (isReply && showMessage) {
const replyUser = data.metadata.original_sender.username; const replyUser = data.reply.sender.name;
const replyMsg = data.metadata.original_message.content; const replyMsg = data.reply.sender.content;
replyDiv.style.display = 'block'; replyDiv.style.display = 'block';
replyUserDiv.innerText = replyUser; replyUserDiv.innerText = replyUser;
@@ -2316,12 +2317,12 @@ async function KickChatMessage(data) {
// Set the username info // Set the username info
if (showUsername) { if (showUsername) {
usernameDiv.innerText = data.sender.username; usernameDiv.innerText = data.user.name;
usernameDiv.style.color = data.sender.identity.color; usernameDiv.style.color = data.user.color;
} }
// Set the message data // Set the message data
let message = data.content; let message = ConstructMessageFromParts(data.parts);
// Highlight mentions // Highlight mentions
const mentionRgx = new RegExp(`(^|\\s)@${kickUsername}(\\s|$)`, 'i'); const mentionRgx = new RegExp(`(^|\\s)@${kickUsername}(\\s|$)`, 'i');
@@ -2335,7 +2336,7 @@ async function KickChatMessage(data) {
// Set message text // Set message text
if (showMessage) { if (showMessage) {
messageDiv.innerText = message; messageDiv.innerHTML = message;
} }
// Remove the line break // Remove the line break
@@ -2354,28 +2355,38 @@ async function KickChatMessage(data) {
// Render badges // Render badges
if (showBadges) { if (showBadges) {
badgeListDiv.innerHTML = ""; badgeListDiv.innerHTML = "";
for (i in data.sender.identity.badges) { for (i in data.user.badges) {
const badge = new Image(); const badge = new Image();
badge.src = GetKickBadgeURL(data.sender.identity.badges[i]); if (data.user.badges[i].imageUrl)
badge.src = data.user.badges[i].imageUrl;
else
badge.src = GetKickBadgeURL(data.user.badges[i]);
badge.classList.add("badge"); badge.classList.add("badge");
badgeListDiv.appendChild(badge); badgeListDiv.appendChild(badge);
} }
} }
function GetKickBadgeURL(data) {
switch (data.id) {
case 'subscriber':
return CalculateKickSubBadge(data.count);
default:
return `icons/badges/kick-${data.id}.svg`;
}
function CalculateKickSubBadge(months) {
if (!Array.isArray(kickSubBadges)) return null;
// Render emotes // Filter for eligible badges, then get the one with the highest 'months'
function replaceEmotes(message) { const badge = kickSubBadges
const emoteRegex = /\[emote:(\d+):([^\]]+)\]/g; .filter(b => b.months <= months)
.sort((a, b) => b.months - a.months)[0];
return message.replace(emoteRegex, (_, id, name) => { return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`;
const imgUrl = `https://files.kick.com/emotes/${id}/fullsize`; }
return `<img src="${imgUrl}" alt="${name}" class="emote" />`;
});
} }
messageDiv.innerHTML = replaceEmotes(message);
// Render avatars // Render avatars
if (showAvatar) { if (showAvatar) {
const username = data.sender.slug; const username = data.user.login;
const avatarURL = await GetAvatar(username, 'kick'); const avatarURL = await GetAvatar(username, 'kick');
const avatar = new Image(); const avatar = new Image();
avatar.src = avatarURL; avatar.src = avatarURL;
@@ -2389,7 +2400,7 @@ async function KickChatMessage(data) {
if (groupConsecutiveMessages && messageList.children.length > 0 && scrollDirection != 2) { if (groupConsecutiveMessages && messageList.children.length > 0 && scrollDirection != 2) {
const lastPlatform = messageList.lastChild.dataset.platform; const lastPlatform = messageList.lastChild.dataset.platform;
const lastUserId = messageList.lastChild.dataset.userId; const lastUserId = messageList.lastChild.dataset.userId;
if (lastPlatform == "kick" && lastUserId == data.sender.id) { if (lastPlatform == "kick" && lastUserId == data.user.id) {
userInfoDiv.style.display = "none"; userInfoDiv.style.display = "none";
avatarDiv.style.visibility = "hidden"; avatarDiv.style.visibility = "hidden";
avatarDiv.style.height = "0px"; avatarDiv.style.height = "0px";
@@ -2406,7 +2417,7 @@ async function KickChatMessage(data) {
messageDiv.innerHTML = ''; messageDiv.innerHTML = '';
messageDiv.appendChild(image); messageDiv.appendChild(image);
AddMessageItem(instance, data.id, 'kick', data.sender.id); AddMessageItem(instance, data.messageId, 'kick', data.user.id);
}; };
const urlObj = new URL(message); const urlObj = new URL(message);
@@ -2416,7 +2427,7 @@ async function KickChatMessage(data) {
image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString(); image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString();
} }
else { else {
AddMessageItem(instance, data.id, 'kick', data.sender.id); AddMessageItem(instance, data.messageId, 'kick', data.user.id);
} }
// Render YouTube links // Render YouTube links
@@ -2428,33 +2439,33 @@ async function KickChatMessage(data) {
} }
} }
// async function KickFollow(data) { async function KickFollow(data) {
// if (!showKickFollows) if (!showKickFollows)
// return; return;
// // Get a reference to the template // Get a reference to the template
// const template = document.getElementById('cardTemplate'); const template = document.getElementById('cardTemplate');
// // Create a new instance of the template // Create a new instance of the template
// const instance = template.content.cloneNode(true); const instance = template.content.cloneNode(true);
// // Get divs // Get divs
// const cardDiv = instance.querySelector("#card"); const cardDiv = instance.querySelector("#card");
// const headerDiv = instance.querySelector("#header"); const headerDiv = instance.querySelector("#header");
// const avatarDiv = instance.querySelector("#avatar"); const avatarDiv = instance.querySelector("#avatar");
// const iconDiv = instance.querySelector("#icon"); const iconDiv = instance.querySelector("#icon");
// const titleDiv = instance.querySelector("#title"); const titleDiv = instance.querySelector("#title");
// const contentDiv = instance.querySelector("#contentDiv"); const contentDiv = instance.querySelector("#contentDiv");
// // Set the card background colors // Set the card background colors
// cardDiv.classList.add('kick'); cardDiv.classList.add('kick');
// // Set the text // Set the text
// let username = data.user; let username = data.user.name;
// titleDiv.innerText = `${username} followed`; titleDiv.innerText = `${username} followed`;
// AddMessageItem(instance, data.messageId); AddMessageItem(instance, data.user.id, 'kick', data.user.id);
// } }
async function KickSubscription(data) { async function KickSubscription(data) {
if (!showKickSubs) if (!showKickSubs)
@@ -2604,7 +2615,7 @@ async function KickRewardRedeemed(data) {
// Set the text // Set the text
const username = data.username; const username = data.username;
const rewardName = data.reward_title; const rewardName = data.reward_title;
const userInput = data.user_input; const userInput = data.userInput;
titleDiv.innerHTML = `${username} redeemed ${rewardName}`; titleDiv.innerHTML = `${username} redeemed ${rewardName}`;
contentDiv.innerText = `${userInput}`; contentDiv.innerText = `${userInput}`;
@@ -3154,43 +3165,6 @@ function AddMessageItem(element, elementID, platform, userId) {
}, 200); }, 200);
} }
// I used Gemini for this shit so if it doesn't work, blame Google
function FindFirstImageUrl(jsonObject) {
if (typeof jsonObject !== 'object' || jsonObject === null) {
return null; // Handle invalid input
}
function iterate(obj) {
if (Array.isArray(obj)) {
for (const item of obj) {
const result = iterate(item);
if (result) {
return result;
}
}
return null;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === 'imageUrl') {
return obj[key]; // Found it! Return the value.
}
if (typeof obj[key] === 'object' && obj[key] !== null) {
const result = iterate(obj[key]); // Recursive call for nested objects
if (result) {
return result; // Propagate the found value
}
}
}
}
return null; // Key not found in this level
}
return iterate(jsonObject);
}
function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(targetPermissions, data, platform) { function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(targetPermissions, data, platform) {
return GetPermissionLevel(data, platform) >= targetPermissions; return GetPermissionLevel(data, platform) >= targetPermissions;
} }
@@ -3209,14 +3183,14 @@ function GetPermissionLevel(data, platform) {
else else
return 10; return 10;
case 'kick': case 'kick':
if (data.sender.identity.badges.some(item => item.type === 'broadcaster')) if (data.user.badges.some(item => item.id === 'broadcaster'))
return 40; return 40;
else if (data.sender.identity.badges.some(item => item.type === 'moderator')) else if (data.user.badges.some(item => item.id === 'moderator'))
return 30; return 30;
else if (data.sender.identity.badges.some(item => item.type === 'vip') || else if (data.user.badges.some(item => item.id === 'vip') ||
data.sender.identity.badges.some(item => item.type === 'og')) data.user.badges.some(item => item.type === 'og'))
return 20; return 20;
else if (data.sender.identity.badges.some(item => item.type === 'subscriber')) else if (data.user.badges.some(item => item.id === 'subscriber'))
return 15; return 15;
else else
return 10; return 10;
@@ -3249,26 +3223,6 @@ function GetWinnersList(gifts) {
} }
} }
function GetKickBadgeURL(data) {
switch (data.type) {
case 'subscriber':
return CalculateKickSubBadge(data.count);
default:
return `icons/badges/kick-${data.type}.svg`;
}
}
function CalculateKickSubBadge(months) {
if (!Array.isArray(kickSubBadges)) return null;
// Filter for eligible badges, then get the one with the highest 'months'
const badge = kickSubBadges
.filter(b => b.months <= months)
.sort((a, b) => b.months - a.months)[0];
return badge?.badge_image?.src || `icons/badges/kick-subscriber.svg`;
}
/////////////////////////////////// ///////////////////////////////////
+8 -8
View File
@@ -352,14 +352,6 @@
"defaultValue": true, "defaultValue": true,
"group": "Which YouTube messages do you want to see?" "group": "Which YouTube messages do you want to see?"
}, },
{
"id": "kickUsername",
"label": "Kick Username",
"description": "",
"type": "text",
"defaultValue": "",
"group": "Which Kick messages do you want to see?"
},
{ {
"id": "showKickMessages", "id": "showKickMessages",
"label": "Chat Messages", "label": "Chat Messages",
@@ -368,6 +360,14 @@
"defaultValue": true, "defaultValue": true,
"group": "Which Kick messages do you want to see?" "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", "id": "showKickSubs",
"label": "New Subscribers", "label": "New Subscribers",
+39 -53
View File
@@ -71,7 +71,8 @@ const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true);
const twitchWatchStreaksAction = urlParams.get("twitchWatchStreaksAction") || ""; const twitchWatchStreaksAction = urlParams.get("twitchWatchStreaksAction") || "";
// Which Kick alerts do you want to see? // Which Kick alerts do you want to see?
const kickUsername = urlParams.get("kickUsername") || ""; const showKickFollows = GetBooleanParam("showKickFollows", false);
const kickFollowAction = urlParams.get("kickFollowAction") || "";
const showKickSubs = GetBooleanParam("showKickSubs", true); const showKickSubs = GetBooleanParam("showKickSubs", true);
const kickSubAction = urlParams.get("kickSubAction") || ""; const kickSubAction = urlParams.get("kickSubAction") || "";
const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true);
@@ -110,6 +111,12 @@ const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") |
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false);
const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || "";
////////////////////
// HIDDEN OPTIONS //
////////////////////
let kickUsername = urlParams.get("kickUsername") || "";
// Set avatar visibility // Set avatar visibility
if (!showAvatar) { if (!showAvatar) {
avatarElement.style.display = 'none'; avatarElement.style.display = 'none';
@@ -316,8 +323,17 @@ client.on('Fourthwall.GiftDrawEnded', (response) => {
// Connect and handle Pusher WebSocket // Connect and handle Pusher WebSocket
async function KickConnect() { async function KickConnect() {
// If user has not manually set Kick username, try to grab if from Streamer.bot
if (!kickUsername) if (!kickUsername)
return; {
// Fetch from Streamer.bot
const broadcasterInfo = await client.getBroadcaster();
if (broadcasterInfo.platforms.kick)
kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin;
else
return;
}
// Channel to subscribe to (you'll need the correct channel name here) // Channel to subscribe to (you'll need the correct channel name here)
const kickIds = await GetKickIds(kickUsername); const kickIds = await GetKickIds(kickUsername);
@@ -332,7 +348,7 @@ async function KickConnect() {
// Reconnect // Reconnect
websocket.onclose = function () { websocket.onclose = function () {
console.log(`Reconnecting to ${kickUsername}...`); console.log(`Reconnecting to ${kickUsername}...`);
setTimeout(connectPusher, 5000); setTimeout(KickConnect, 5000);
}; };
websocket.onopen = function () { websocket.onopen = function () {
@@ -482,41 +498,11 @@ async function TwitchCheer(data) {
// Set the text // Set the text
const username = data.message.displayName; const username = data.message.displayName;
const bits = data.bits; const bits = data.bits;
let message = data.message.message; let message = ConstructMessageFromParts(data.parts);
// Render avatars // Render avatars
const avatarURL = await GetAvatar(username, 'twitch'); const avatarURL = await GetAvatar(username, 'twitch');
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.emotes[i].name);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?:^|[^\\w])${emoteName}(?:$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
message = message.replace(regex, emoteElement);
}
// Render cheermotes
for (i in data.cheerEmotes) {
const bits = data.cheerEmotes[i].bits;
const imageUrl = data.cheerEmotes[i].imageUrl;
const name = data.cheerEmotes[i].name;
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
message = message.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
UpdateAlertBox( UpdateAlertBox(
'twitch', 'twitch',
avatarURL, avatarURL,
@@ -1262,28 +1248,28 @@ function FourthwallGiftDrawEnded(data) {
); );
} }
// async function KickFollow(data) { async function KickFollow(data) {
// if (!showKickFollows) if (!showKickFollows)
// return; return;
// // Set the text // Set the text
// const username = data.user; const username = data.user.name;
// // Render avatars // Render avatars
// const avatarURL = await GetAvatar(username, 'kick'); const avatarURL = await GetAvatar(username, 'kick');
// UpdateAlertBox( UpdateAlertBox(
// 'kick', 'kick',
// avatarURL, avatarURL,
// `${username}`, `${username}`,
// `followed`, `followed`,
// '', '',
// username, username,
// '', '',
// kickFollowAction, kickFollowAction,
// data data
// ); );
// } }
async function KickSubscription(data) { async function KickSubscription(data) {
if (!showKickSubs) if (!showKickSubs)
+12 -3
View File
@@ -381,11 +381,20 @@
"group": "Which YouTube alerts do you want to see?" "group": "Which YouTube alerts do you want to see?"
}, },
{ {
"id": "kickUsername", "id": "showKickFollows",
"label": "Kick Username", "label": "New Followers",
"description": "", "description": "",
"type": "text", "type": "checkbox",
"defaultValue": false,
"group": "Which Kick alerts do you want to see?"
},
{
"id": "KickFollowAction",
"label": "",
"description": "Also run this Streamer.bot Action",
"type": "sb-actions",
"defaultValue": "", "defaultValue": "",
"showIf": "showKickFollows",
"group": "Which Kick alerts do you want to see?" "group": "Which Kick alerts do you want to see?"
}, },
{ {
+1 -35
View File
@@ -51,37 +51,7 @@ async function CustomEvent(data) {
subtitleEl.innerText = `${data.user}`; subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = data.message; messageEl.innerHTML = ConstructMessageFromParts(data.parts);
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.emotes[i].name);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
}
// Render cheermotes
for (i in data.cheerEmotes) {
const bits = data.cheerEmotes[i].bits;
const imageUrl = data.cheerEmotes[i].imageUrl;
const name = data.cheerEmotes[i].name;
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
@@ -695,10 +665,6 @@ function IsValidUrl(string) {
} }
} }
function EscapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function SetPlatformIcon(el, platform) { function SetPlatformIcon(el, platform) {
// Set the platform icon // Set the platform icon
let baseURL = window.location.href; let baseURL = window.location.href;