-
-
![]()
-
-
+
+
+

+
+
+
+
+
+ sent
+
+
+

+
+ x13
diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js
index a1deb7d..68f0281 100644
--- a/multichat-overlay/script.js
+++ b/multichat-overlay/script.js
@@ -10,6 +10,14 @@ const sbServerPort = urlParams.get("port") || "8080";
const avatarMap = new Map();
const pronounMap = new Map();
+/////////////////
+// GLOBAL VARS //
+/////////////////
+
+const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[&?#].*)?$/;
+const kickPusherWsUrl = 'wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=7.6.0&flash=false';
+let kickSubBadges = [];
+
/////////////
// OPTIONS //
/////////////
@@ -24,8 +32,11 @@ const showMessage = GetBooleanParam("showMessage", true);
const font = urlParams.get("font") || "";
const fontSize = urlParams.get("fontSize") || "30";
const lineSpacing = urlParams.get("lineSpacing") || "1.7";
+const useChatBubbles = GetBooleanParam("useChatBubbles", false);
+const bubbleColor = urlParams.get("bubbleColor") || "#000000";
+const bubbleOpacity = urlParams.get("bubbleOpacity") || "0.9";
const background = urlParams.get("background") || "#000000";
-const opacity = urlParams.get("opacity") || "0.85";
+const opacity = urlParams.get("opacity") || "0";
const hideAfter = GetIntParam("hideAfter", 0);
const excludeCommands = GetBooleanParam("excludeCommands", true);
@@ -34,19 +45,35 @@ const scrollDirection = GetIntParam("scrollDirection", 1);
const groupConsecutiveMessages = GetBooleanParam("groupConsecutiveMessages", false);
const inlineChat = GetBooleanParam("inlineChat", false);
const imageEmbedPermissionLevel = GetIntParam("imageEmbedPermissionLevel", 20);
+const showYouTubeLinkPreviews = GetBooleanParam("showYouTubeLinkPreviews", true);
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);
+let kickUsername = urlParams.get("kickUsername") || "";
+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);
const showYouTubeMemberships = GetBooleanParam("showYouTubeMemberships", true);
+const enableTikTokSupport = GetBooleanParam("enableTikTokSupport", false);
+const showTikTokFollows = GetBooleanParam("showTikTokFollows", false);
+const showTikTokLikes = GetBooleanParam("showTikTokLikes", false);
+const showTikTokMessages = GetBooleanParam("showTikTokMessages", false);
+const showTikTokGifts = GetBooleanParam("showTikTokGifts", false);
+const showTikTokSubs = GetBooleanParam("showTikTokSubs", false);
+
const showStreamlabsDonations = GetBooleanParam("showStreamlabsDonations", true)
const showStreamElementsTips = GetBooleanParam("showStreamElementsTips", true);
const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true);
@@ -58,6 +85,9 @@ const furryMode = GetBooleanParam("furryMode", false);
const animationSpeed = GetIntParam("animationSpeed", 0.1);
+// Kick is stupid and turns underscores into dashes which fuck everything up, therefore do a find/replace to make it work good
+kickUsername = kickUsername.replace(/_/g, "-");
+
// Set fonts for the widget
document.body.style.fontFamily = font;
document.body.style.fontSize = `${fontSize}px`;
@@ -132,6 +162,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);
@@ -272,10 +307,158 @@ client.on('Fourthwall.GiftDrawEnded', (response) => {
FourthwallGiftDrawEnded(response.data);
})
-client.on('General.Custom', (response) => {
- console.debug(response.data);
- GeneralCustom(response.data);
-})
+
+
+///////////////////////////
+// KICK PUSHER WEBSOCKET //
+///////////////////////////
+
+// Connect and handle Pusher WebSocket
+async function KickConnect() {
+ if (!kickUsername)
+ return;
+
+ // Channel to subscribe to (you'll need the correct channel name here)
+ const chatroomId = await GetKickChatroomId(kickUsername);
+
+ // Cache subscriber badges
+ kickSubBadges = await GetKickSubBadges(kickUsername);
+
+ const websocket = new WebSocket(kickPusherWsUrl);
+
+ // Reconnect
+ websocket.onclose = function () {
+ console.log(`Reconnecting to ${kickUsername}...`);
+ setTimeout(connectPusher, 5000);
+ };
+
+ websocket.onopen = function () {
+ console.log(`Kick successfully conntected to ${kickUsername}.`);
+ }
+
+ websocket.onmessage = function (response) {
+ try {
+ let data = JSON.parse(response.data);
+
+ console.debug(data);
+
+ // When connection is established, subscribe to a channel
+ if (data.event === 'pusher:connection_established') {
+ const socketData = JSON.parse(data.data);
+ console.log(`[Pusher] Socket established with ID: ${socketData.socket_id}`);
+
+ // Now subscribe to a channel
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatroom_${chatroomId}` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatrooms.${chatroomId}` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatrooms.${chatroomId}.v2` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `predictions-channel-${chatroomId}` } }));
+ console.log(`[Pusher] Sent subscription request to channel: ${chatroomId}`);
+ }
+
+ // Event handlers
+ const eventArgs = JSON.parse(data.data);
+ const event = data.event.split('\\').pop();
+ switch (event) {
+ case 'ChatMessageEvent':
+ KickChatMessage(eventArgs);
+ break;
+ //// 'Follows' unsupported by pusher
+ // case 'FollowEvent':
+ // break;
+ case 'SubscriptionEvent':
+ KickSubscription(eventArgs);
+ break;
+ case 'GiftedSubscriptionsEvent':
+ KickGiftedSubscriptions(eventArgs);
+ break;
+ case 'RewardRedeemedEvent':
+ KickRewardRedeemed(eventArgs);
+ break;
+ case 'StreamHostEvent':
+ KickStreamHost(eventArgs);
+ break;
+ case 'MessageDeletedEvent':
+ KickMessageDeleted(eventArgs);
+ break;
+ case 'UserBannedEvent':
+ KickUserBanned(eventArgs);
+ break;
+ }
+ }
+ catch (error) {
+ console.error(error);
+ }
+ }
+}
+
+// Try connect when window is loaded
+window.addEventListener('load', KickConnect);
+
+
+
+//////////////////////
+// TIKFINITY CLIENT //
+//////////////////////
+
+let tikfinityWebsocket = null;
+
+function TikfinityConnect() {
+ if (!enableTikTokSupport)
+ return;
+
+ 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(TikfinityConnect, 1000); // Schedule a reconnect attempt
+ }
+
+ tikfinityWebsocket.onerror = function () {
+ console.error(`TikFinity failed for some reason...`)
+ tikfinityWebsocket = null;
+ setTimeout(TikfinityConnect, 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 'like':
+ TikTokLikes(data);
+ break;
+
+ case 'follow':
+ TikTokFollow(data);
+ break;
+
+ case 'gift':
+ TikTokGift(data);
+ break;
+ case 'subscribe':
+ TikTokSubscribe(data);
+ break;
+ }
+ }
+}
+
+// Try connect when window is loaded
+window.addEventListener('load', TikfinityConnect);
@@ -318,6 +501,17 @@ async function TwitchChatMessage(data) {
const usernameDiv = instance.querySelector("#username");
const messageDiv = instance.querySelector("#message");
+ // Render bubbles
+ if (useChatBubbles) {
+ const opacity255 = Math.round(parseFloat(bubbleOpacity) * 255);
+ let hexOpacity = opacity255.toString(16);
+ if (hexOpacity.length < 2) {
+ hexOpacity = "0" + hexOpacity;
+ }
+ document.documentElement.style.setProperty('--bubble-color', `${bubbleColor}${hexOpacity}`);
+ messageContainerDiv.classList.add("bubble");
+ }
+
// Set First Time Chatter
const firstMessage = data.message.firstMessage;
if (firstMessage && showMessage) {
@@ -326,19 +520,36 @@ async function TwitchChatMessage(data) {
}
// 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;
+ const isFromSharedChatGuest = data.isFromSharedChatGuest;
+ if (isFromSharedChatGuest) {
+ // 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;
+
+ switch (showTwitchSharedChat)
+ {
+ // 2 = Show & Highlight
+ case 2:
+ // const sharedChatChannel = data.sharedChat.sourceRoom.name; // Twitch removed the source channel for some reason?!?!
+ const sharedChatChannel = 'Shared Chat';
sharedChatDiv.style.display = 'block';
sharedChatChannelDiv.innerHTML = `💬 ${sharedChatChannel}`;
messageContainerDiv.classList.add("highlightMessage");
- }
+ break;
+ // 1 = Show but do not highlight
+ case 1:
+ break;
+ // 0 = Do not show
+ case 0:
+ return;
}
- else if (!data.sharedChat.primarySource && showTwitchSharedChat == 0)
- return;
}
// Set Reply Message
@@ -396,6 +607,7 @@ async function TwitchChatMessage(data) {
if (inlineChat) {
instance.querySelector("#colon-separator").style.display = `inline`;
instance.querySelector("#line-space").style.display = `none`;
+ instance.querySelector(".message-contents").style.alignItems = 'center';
}
// Render platform
@@ -448,7 +660,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");
@@ -461,8 +673,10 @@ async function TwitchChatMessage(data) {
if (groupConsecutiveMessages && messageList.children.length > 0 && scrollDirection != 2) {
const lastPlatform = messageList.lastChild.dataset.platform;
const lastUserId = messageList.lastChild.dataset.userId;
- if (lastPlatform == "twitch" && lastUserId == data.user.id)
+ if (lastPlatform == "twitch" && lastUserId == data.user.id) {
userInfoDiv.style.display = "none";
+ avatarDiv.innerHTML = '';
+ }
}
// Embed image
@@ -487,6 +701,14 @@ async function TwitchChatMessage(data) {
else {
AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id);
}
+
+ // Render YouTube links
+ if (youtubeRegex.test(message)) {
+ const videoId = ExtractYouTubeVideoId(message);
+ const videoData = await GetYouTubeVideoData(videoId);
+
+ YouTubeThumbnailPreview(videoData);
+ }
}
async function TwitchAutomaticRewardRedemption(data) {
@@ -519,8 +741,8 @@ async function TwitchAutomaticRewardRedemption(data) {
const gigaEmote = data.gigantified_emote.imageUrl;
const image = new Image();
image.src = gigaEmote;
- image.style.padding = "20px 0px";
- image.style.width = "50%";
+ image.style.padding = "0px 0px";
+ image.style.width = "10em";
image.onload = function () {
messageDiv.innerHTML = '';
@@ -591,6 +813,9 @@ async function TwitchAnnouncement(data) {
content.querySelector("#colon-separator").style.display = `inline`;
content.querySelector("#line-space").style.display = `none`;
+ // Remove the avatar
+ content.querySelector("#avatar").style.display = `none`;
+
// Render platform
content.querySelector("#platform").style.display = `none`;
@@ -615,9 +840,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 +851,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);
}
@@ -638,6 +863,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;
@@ -803,7 +1059,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 +1105,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");
@@ -902,7 +1158,7 @@ function TwitchUserBanned(data) {
const messagesToRemove = [];
// ID of the message to remove
- const userId = data.user_id;
+ const userId = data.targetUser.id;
// Find the items to remove
for (let i = 0; i < messageList.children.length; i++) {
@@ -925,7 +1181,7 @@ function TwitchChatCleared(data) {
}
}
-function YouTubeMessage(data) {
+async function YouTubeMessage(data) {
if (!showYouTubeMessages)
return;
@@ -944,6 +1200,7 @@ function YouTubeMessage(data) {
const instance = template.content.cloneNode(true);
// Get divs
+ const messageContainerDiv = instance.querySelector("#messageContainer");
const userInfoDiv = instance.querySelector("#userInfo");
const avatarDiv = instance.querySelector("#avatar");
const timestampDiv = instance.querySelector("#timestamp");
@@ -952,6 +1209,17 @@ function YouTubeMessage(data) {
const usernameDiv = instance.querySelector("#username");
const messageDiv = instance.querySelector("#message");
+ // Render bubbles
+ if (useChatBubbles) {
+ const opacity255 = Math.round(parseFloat(bubbleOpacity) * 255);
+ let hexOpacity = opacity255.toString(16);
+ if (hexOpacity.length < 2) {
+ hexOpacity = "0" + hexOpacity;
+ }
+ document.documentElement.style.setProperty('--bubble-color', `${bubbleColor}${hexOpacity}`);
+ messageContainerDiv.classList.add("bubble");
+ }
+
// Set timestamp
if (showTimestamps) {
timestampDiv.classList.add("timestamp");
@@ -976,6 +1244,7 @@ function YouTubeMessage(data) {
if (inlineChat) {
instance.querySelector("#colon-separator").style.display = `inline`;
instance.querySelector("#line-space").style.display = `none`;
+ instance.querySelector(".message-contents").style.alignItems = 'center';
}
// Render platform
@@ -1070,6 +1339,14 @@ function YouTubeMessage(data) {
else {
AddMessageItem(instance, data.eventId, 'youtube', data.user.id);
}
+
+ // Render YouTube links
+ if (youtubeRegex.test(data.message)) {
+ const videoId = ExtractYouTubeVideoId(data.message);
+ const videoData = await GetYouTubeVideoData(videoId);
+
+ YouTubeThumbnailPreview(videoData);
+ }
}
function YouTubeSuperChat(data) {
@@ -1122,18 +1399,18 @@ function YouTubeSuperSticker(data) {
// Set the card background colors
cardDiv.classList.add('youtube');
- const stickerTemplate = document.getElementById('stickerTemplate');
+ avatarDiv.style.width = 'auto';
- // Create a new instance of the template
- const stickerInstance = stickerTemplate.content.cloneNode(true);
+ // Set the text
+ const user = data.user.name;
+ const amount = data.amount;
+ const stickerURL = FindFirstImageUrl(data);
+ const stickerImage = `

`;
- // Render sticker
- stickerInstance.querySelector("#stickerImg").src = FindFirstImageUrl(data);
- stickerInstance.querySelector("#stickerLabel").innerText = `${data.user.name} sent a Super Sticker (${data.amount})`;
+ avatarDiv.innerHTML = stickerImage;
+ titleDiv.innerHTML = `${user} sent a Super Sticker (${amount})`;
- contentDiv.appendChild(stickerInstance);
-
- AddMessageItem(instance, data.eventId);
+ AddMessageItem(instance);
}
function YouTubeNewSponsor(data) {
@@ -1502,9 +1779,14 @@ function FourthwallOrderPlaced(data) {
const contentDiv = instance.querySelector("#content");
// Set the card background colors
- cardDiv.classList.add('blank');
- titleDiv.classList.add('centerThatShitHomie');
- contentDiv.classList.add('centerThatShitHomie');
+ cardDiv.classList.add('fourthwall');
+
+ // // Set the card background colors
+ // cardDiv.classList.add('blank');
+ // titleDiv.classList.add('centerThatShitHomie');
+ // contentDiv.classList.add('centerThatShitHomie');
+
+ avatarDiv.style.width = 'auto';
// Set the text
let user = data.username;
@@ -1516,11 +1798,13 @@ function FourthwallOrderPlaced(data) {
const itemImageUrl = data.variants[0].image;
const fourthwallProductImage = `

`;
+ avatarDiv.innerHTML = fourthwallProductImage;
+
let contents = "";
- contents += fourthwallProductImage;
+ // contents += fourthwallProductImage;
- contents += "
";
+ // contents += "
";
// If there user did not provide a username, just say "Someone"
if (user == undefined)
@@ -1662,28 +1946,32 @@ function FourthwallGiftPurchase(data) {
const contentDiv = instance.querySelector("#content");
// Set the card background colors
- cardDiv.classList.add('blank');
- titleDiv.classList.add('centerThatShitHomie');
- contentDiv.classList.add('centerThatShitHomie');
+ cardDiv.classList.add('fourthwall');
+
+ // // Set the card background colors
+ // cardDiv.classList.add('blank');
+ // titleDiv.classList.add('centerThatShitHomie');
+ // contentDiv.classList.add('centerThatShitHomie');
// Set the text
- let user = data.username;
+ // let user = data.username;
const total = data.total;
const currency = data.currency;
const gifts = data.gifts.length;
const itemName = data.offer.name;
- const itemImageUrl = data.offer.imageUrl;
- const fourthwallProductImage = `

`;
- const message = DecodeHTMLString(data.statmessageus);
+ // const itemImageUrl = data.offer.imageUrl;
+ // const fourthwallProductImage = `

`;
+ // const message = DecodeHTMLString(data.statmessageus);
let contents = "";
- contents += fourthwallProductImage;
+ // contents += fourthwallProductImage;
- contents += "
";
+ // contents += "
";
// If the user ordered more than one item, write how many items they ordered
- contents += `${user} gifted`;
+ // contents += `${user} gifted`;
+ contents += `Someone has gifted`;
// If there is more than one gifted item, display the number of gifts
if (gifts > 1)
@@ -1700,11 +1988,11 @@ function FourthwallGiftPurchase(data) {
titleDiv.innerHTML = contents;
- // Add the custom message from the user
- if (message.trim() != "")
- contentDiv.innerHTML = `${message}`;
- else
- contentDiv.style.display = 'none'
+ // // Add the custom message from the user
+ // if (message.trim() != "")
+ // contentDiv.innerHTML = `${message}`;
+ // else
+ // contentDiv.style.display = 'none'
AddMessageItem(instance, data.id);
}
@@ -1729,8 +2017,11 @@ function FourthwallGiftDrawStarted(data) {
// Set the card background colors
cardDiv.classList.add('fourthwall');
- titleDiv.classList.add('centerThatShitHomie');
- contentDiv.classList.add('centerThatShitHomie');
+
+ // // Set the card background colors
+ // cardDiv.classList.add('fourthwall');
+ // titleDiv.classList.add('centerThatShitHomie');
+ // contentDiv.classList.add('centerThatShitHomie');
// Set the text
const durationSeconds = data.durationSeconds;
@@ -1739,10 +2030,10 @@ function FourthwallGiftDrawStarted(data) {
let contents = "";
// If the user ordered more than one item, write how many items they ordered
- contents += `
🎁 ${itemName} Giveaway!
`;
+ contents += `🎁 ${itemName} Giveaway!`;
titleDiv.innerHTML = contents;
- contentDiv.innerHTML = `Type !join in the next ${durationSeconds} seconds for your chance to win!`;
+ contentDiv.innerHTML = `Type 'join' in the next ${durationSeconds} seconds for your chance to win!`;
//contentDiv.style.display = `none`;
AddMessageItem(instance, data.id);
@@ -1768,13 +2059,16 @@ function FourthwallGiftDrawEnded(data) {
// Set the card background colors
cardDiv.classList.add('fourthwall');
- titleDiv.classList.add('centerThatShitHomie');
- contentDiv.classList.add('centerThatShitHomie');
+
+ // // Set the card background colors
+ // cardDiv.classList.add('fourthwall');
+ // titleDiv.classList.add('centerThatShitHomie');
+ // contentDiv.classList.add('centerThatShitHomie');
let contents = "";
// If the user ordered more than one item, write how many items they ordered
- contents += `
🥳 GIVEAWAY ENDED 🥳
`;
+ contents += `🥳 GIVEAWAY ENDED 🥳`;
//contents += `Congratulations ${GetWinnersList(data.gifts)}!`
titleDiv.innerHTML = contents;
@@ -1784,6 +2078,788 @@ function FourthwallGiftDrawEnded(data) {
AddMessageItem(instance, data.id);
}
+async function KickChatMessage(data) {
+ if (!showKickMessages)
+ return;
+
+ // Don't post messages starting with "!"
+ if (data.content.startsWith("!") && excludeCommands)
+ return;
+
+ // Don't post messages from users from the ignore list
+ if (ignoreUserList.includes(data.sender.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");
+
+ // Render bubbles
+ if (useChatBubbles) {
+ const opacity255 = Math.round(parseFloat(bubbleOpacity) * 255);
+ let hexOpacity = opacity255.toString(16);
+ if (hexOpacity.length < 2) {
+ hexOpacity = "0" + hexOpacity;
+ }
+ document.documentElement.style.setProperty('--bubble-color', `${bubbleColor}${hexOpacity}`);
+ messageContainerDiv.classList.add("bubble");
+ }
+
+ // // Set First Time Chatter
+ // const firstMessage = data.firstMessage;
+ // if (firstMessage && showMessage) {
+ // firstMessageDiv.style.display = 'block';
+ // messageContainerDiv.classList.add("highlightMessage");
+ // }
+
+ // Set Reply Message
+ const isReply = data.type == 'reply';
+ if (isReply && showMessage) {
+ const replyUser = data.metadata.original_sender.username;
+ const replyMsg = data.metadata.original_message.content;
+
+ replyDiv.style.display = 'block';
+ replyUserDiv.innerText = replyUser;
+ replyMsgDiv.innerHTML = replaceEmotes(replyMsg);;
+ }
+
+ // Set timestamp
+ if (showTimestamps) {
+ timestampDiv.classList.add("timestamp");
+ timestampDiv.innerText = GetCurrentTimeFormatted();
+ }
+
+ // Set the username info
+ if (showUsername) {
+ usernameDiv.innerText = data.sender.username;
+ usernameDiv.style.color = data.sender.identity.color;
+ }
+
+ // Set the message data
+ let message = data.content;
+
+ // Highlight mentions
+ const mentionRgx = new RegExp(`(^|\\s)@${kickUsername}(\\s|$)`, 'i');
+ const mention = mentionRgx.test(message);
+ if (mention && showMessage)
+ messageContainerDiv.classList.add("highlightMessage");
+
+ // 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`;
+ instance.querySelector(".message-contents").style.alignItems = 'center';
+ }
+
+ // Render platform
+ if (showPlatform) {
+ const platformElements = `

`;
+ platformDiv.innerHTML = platformElements;
+ }
+
+ // Render badges
+ if (showBadges) {
+ badgeListDiv.innerHTML = "";
+ for (i in data.sender.identity.badges) {
+ const badge = new Image();
+ badge.src = GetKickBadgeURL(data.sender.identity.badges[i]);
+ badge.classList.add("badge");
+ badgeListDiv.appendChild(badge);
+ }
+ }
+
+ // 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 `

`;
+ });
+ }
+ messageDiv.innerHTML = replaceEmotes(message);
+
+ // Render avatars
+ if (showAvatar) {
+ const username = data.sender.slug;
+ 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.sender.id)
+ 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.id, 'kick', data.sender.id);
+ };
+
+ const urlObj = new URL(message);
+ urlObj.search = '';
+ urlObj.hash = '';
+
+ image.src = "https://external-content.duckduckgo.com/iu/?u=" + urlObj.toString();
+ }
+ else {
+ AddMessageItem(instance, data.id, 'kick', data.sender.id);
+ }
+
+ // Render YouTube links
+ if (youtubeRegex.test(message)) {
+ const videoId = ExtractYouTubeVideoId(message);
+ const videoData = await GetYouTubeVideoData(videoId);
+
+ YouTubeThumbnailPreview(videoData);
+ }
+}
+
+// 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 KickSubscription(data) {
+ if (!showKickSubs)
+ 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.src = CalculateKickSubBadge(data.months);
+ badge.classList.add("badge");
+ iconDiv.appendChild(badge);
+
+ // Set the text
+ const username = data.username;
+ const months = data.months;
+
+ if (months <= 1)
+ titleDiv.innerText = `${username} just subscribed for the first time!`;
+ else
+ titleDiv.innerText = `${username} resubscribed! (${months} months)`;
+
+ AddMessageItem(instance);
+}
+
+async function KickGiftedSubscriptions(data) {
+ if (!showKickSubs)
+ 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
+ const gifter = data.gifter_username;
+ const gifts = data.gifter_total;
+ const giftedUsers = data.gifted_usernames;
+ titleDiv.innerText = `${gifter} gifted ${giftedUsers.length} subscription${giftedUsers.length === 1 ? '' : 's'} to the community!`;
+ contentDiv.innerText = `${gifts === 1 ? '' : "They've gifted " + gifts + " subscription in the channel."}`;
+
+ if (giftedUsers.length > 1)
+ AddMessageItem(instance);
+
+ // Send individual notifications for every gifted user
+ for (const username of data.gifted_usernames)
+ KickGiftToUser(gifter, username);
+}
+
+async function KickGiftToUser(gifter, username) {
+ if (!showKickSubs)
+ 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
+ titleDiv.innerText = `${gifter} gifted a sub to ${username}`;
+
+ AddMessageItem(instance);
+}
+
+async function KickRewardRedeemed(data) {
+ if (!showKickChannelPointRedemptions)
+ 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');
+
+ // 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);
+ }
+
+ // Set the text
+ const username = data.username;
+ const rewardName = data.reward_title;
+ const userInput = data.user_input;
+
+ titleDiv.innerHTML = `${username} redeemed ${rewardName}`;
+ contentDiv.innerText = `${userInput}`;
+
+ AddMessageItem(instance, data.redeemId);
+}
+
+async function KickStreamHost(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');
+
+ // Render avatars
+ if (showAvatar) {
+ const username = data.host_username;
+ const avatarURL = await GetAvatar(username, 'kick');
+ const avatar = new Image();
+ avatar.src = avatarURL;
+ avatar.classList.add("avatar");
+ avatarDiv.appendChild(avatar);
+ }
+
+ // Set the text
+ const username = data.host_username;
+ const viewers = data.number_viewers;
+
+ titleDiv.innerText = `${username} is raiding`;
+ contentDiv.innerText = `with a party of ${viewers}`;
+
+ AddMessageItem(instance);
+}
+
+function KickMessageDeleted(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.message.id;
+
+ // Add a 200ms to ensure the automod doesn't delete the message before it's been added to the overlay
+ setTimeout(() => {
+ // 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);
+ });
+ }, 500);
+}
+
+function KickUserBanned(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.user.id;
+
+ // 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);
+ });
+}
+
+
+
+async function TikTokChat(data) {
+ if (!showTikTokMessages)
+ return;
+
+ // 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");
+
+ // Render bubbles
+ if (useChatBubbles) {
+ const opacity255 = Math.round(parseFloat(bubbleOpacity) * 255);
+ let hexOpacity = opacity255.toString(16);
+ if (hexOpacity.length < 2) {
+ hexOpacity = "0" + hexOpacity;
+ }
+ document.documentElement.style.setProperty('--bubble-color', `${bubbleColor}${hexOpacity}`);
+ messageContainerDiv.classList.add("bubble");
+ }
+
+ // 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`;
+ instance.querySelector(".message-contents").style.alignItems = 'center';
+ }
+
+ // 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);
+ }
+
+ // 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 == "tiktok" && lastUserId == data.userId) {
+ userInfoDiv.style.display = "none";
+ avatarDiv.innerHTML = '';
+ }
+ }
+
+ AddMessageItem(instance, data.msgId, 'tiktok', data.userId);
+
+ // Render YouTube links
+ if (youtubeRegex.test(message)) {
+ const videoId = ExtractYouTubeVideoId(message);
+ const videoData = await GetYouTubeVideoData(videoId);
+
+ YouTubeThumbnailPreview(videoData);
+ }
+}
+
+
+function TikTokFollow(data) {
+ if (!showTikTokFollows)
+ 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('tiktok');
+
+ const user = data.nickname;
+ const tiktokIcon = `

`;
+
+ titleDiv.innerHTML = `${tiktokIcon} ${user} followed`;
+
+ AddMessageItem(instance, data.msgId, 'tiktok', data.userId);
+}
+
+
+/*
+*
+* Note to Nutty:
+* TikFinity only exposes the Like Count in batches of 15 at a time. >:(
+* So basically this code will get the latest element and adds the like count to it
+* and re-render them, preventing the "flood" it would otherwise cause.
+*
+*/
+
+function TikTokLikes(data) {
+ if (!showTikTokLikes)
+ return;
+
+
+ // Get the total number of likes
+ var likeCountTotal = parseInt(data.likeCount);
+
+ // Search for Previous Likes from the Same User
+ const previousLikeContainer = document.querySelector(`li[data-user-id="${data.userId}"]`);
+
+ // If found, fetches the previous likes, deletes the element
+ // and then creates a new count with a sum of the like count
+ if (previousLikeContainer) {
+ const likeCountElem = previousLikeContainer.querySelector('#tiktok-gift-repeat-count');
+ if (likeCountElem) {
+ var likeCountPrev = parseInt(likeCountElem.textContent.replace('x', ''));
+ likeCountTotal = Math.floor(likeCountPrev + likeCountTotal);
+ previousLikeContainer.remove();
+ }
+ }
+
+ // 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;
+ usernameSpan.innerText = data.nickname;
+ giftNameSpan.innerText = 'Likes';
+ stickerImg.src = '';
+ repeatCountDiv.innerText = `x${likeCountTotal}`;
+
+ AddMessageItem(instance, data.msgId, 'tiktok', data.userId);
+}
+
+function TikTokGift(data) {
+ if (!showTikTokGifts)
+ return;
+
+ 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.msgId, 'tiktok', data.userId);
+}
+
+
+
+function TikTokSubscribe(data) {
+ if (!showTikTokSubs)
+ 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('tiktok');
+
+ const user = data.nickname;
+ const tiktokIcon = `

`;
+
+ //titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed on TikTok`;
+ titleDiv.innerHTML = `${tiktokIcon} ${user} subscribed for ${data.subMonth} months`;
+
+ AddMessageItem(instance, data.msgId, 'tiktok', data.userId);
+}
+
+function YouTubeThumbnailPreview(data) {
+ if (!showYouTubeLinkPreviews)
+ 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('thumbnail');
+
+ avatarDiv.style.width = 'auto';
+
+ // Set the text
+ const title = data.title;
+ const author = data.author;
+ const thumbnail = `

`;
+
+ avatarDiv.innerHTML = thumbnail;
+ titleDiv.innerHTML = `${title}`;
+ contentDiv.innerHTML = `by ${author}`;
+
+ AddMessageItem(instance);
+}
+
//////////////////////
@@ -1841,17 +2917,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;
+ }
}
}
@@ -1887,6 +2982,11 @@ function IsImageUrl(url) {
}
}
+function ExtractYouTubeVideoId(url) {
+ const match = url.match(youtubeRegex);
+ return match ? match[1] : null;
+}
+
function AddMessageItem(element, elementID, platform, userId) {
// Calculate the height of the div before inserting
const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList');
@@ -1999,6 +3099,18 @@ function GetPermissionLevel(data, platform) {
return 15;
else
return 10;
+ case 'kick':
+ if (data.sender.identity.badges.some(item => item.type === 'broadcaster'))
+ return 40;
+ else if (data.sender.identity.badges.some(item => item.type === 'moderator'))
+ return 30;
+ else if (data.sender.identity.badges.some(item => item.type === 'vip') ||
+ data.sender.identity.badges.some(item => item.type === 'og'))
+ return 20;
+ else if (data.sender.identity.badges.some(item => item.type === 'subscriber'))
+ return 15;
+ else
+ return 10;
case 'youtube':
if (data.user.isOwner)
return 40;
@@ -2071,6 +3183,54 @@ function EscapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
+async function GetKickChatroomId(username) {
+ const url = `https://kick.com/api/v2/channels/${username}`;
+
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP error ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data.chatroom && data.chatroom.id) {
+ return data.chatroom.id;
+ } else {
+ throw new Error("Chatroom ID not found in response.");
+ }
+ } catch (error) {
+ console.error("Failed to fetch chatroom ID:", error.message);
+ return null;
+ }
+}
+
+async function GetKickSubBadges(username) {
+ const response = await fetch(`https://kick.com/api/v2/channels/${username}`);
+ const data = await response.json();
+
+ return data.subscriber_badges || [];
+}
+
+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`;
+}
+
///////////////////////////////////
@@ -2097,147 +3257,24 @@ function SetConnectionStatus(connected) {
}
}
+async function GetYouTubeVideoData(videoId) {
+ const url = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+ const data = await response.json();
-
-
-
-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;
+ return {
+ title: data.title,
+ author: data.author_name,
+ thumbnail: data.thumbnail_url,
+ };
+ } catch (error) {
+ console.error('Error fetching YouTube video data:', error);
+ return null;
}
-}
-
-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);
-
}
\ No newline at end of file
diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json
index 1dfc957..d1383cc 100644
--- a/multichat-overlay/settings/settings.json
+++ b/multichat-overlay/settings/settings.json
@@ -5,7 +5,7 @@
"label": "Show Platform",
"description": "",
"type": "checkbox",
- "defaultValue": true,
+ "defaultValue": false,
"group": "Appearance"
},
{
@@ -71,7 +71,7 @@
"type": "number",
"min": 1,
"max": 120,
- "defaultValue": 30,
+ "defaultValue": 20,
"group": "Appearance"
},
{
@@ -84,6 +84,35 @@
"step": ".1",
"group": "Appearance"
},
+ {
+ "id": "useChatBubbles",
+ "label": "Chat Bubbles",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "Appearance"
+ },
+ {
+ "id": "bubbleColor",
+ "label": "Bubble Color",
+ "description": "",
+ "type": "color",
+ "defaultValue": "#1d1d1d",
+ "showIf": "useChatBubbles",
+ "group": "Appearance"
+ },
+ {
+ "id": "bubbleOpacity",
+ "label": "Bubble Opacity",
+ "description": "",
+ "type": "number",
+ "defaultValue": "0.90",
+ "min": 0,
+ "max": 1,
+ "step": ".01",
+ "showIf": "useChatBubbles",
+ "group": "Appearance"
+ },
{
"id": "background",
"label": "Background",
@@ -97,7 +126,7 @@
"label": "Background Opacity",
"description": "",
"type": "number",
- "defaultValue": "0.85",
+ "defaultValue": "0",
"min": 0,
"max": 1,
"step": ".01",
@@ -197,6 +226,14 @@
"defaultValue": 20,
"group": "General"
},
+ {
+ "id": "showYouTubeLinkPreviews",
+ "label": "Show YouTube Link Previews",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "General"
+ },
{
"id": "showTwitchMessages",
"label": "Chat Messages",
@@ -213,6 +250,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",
@@ -291,6 +336,99 @@
"defaultValue": true,
"group": "Which YouTube messages do you want to see?"
},
+ {
+ "id": "kickUsername",
+ "label": "Kick Username",
+ "description": "",
+ "type": "text",
+ "defaultValue": "",
+ "group": "Which Kick messages do you want to see?"
+ },
+ {
+ "id": "showKickMessages",
+ "label": "Chat Messages",
+ "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": "Hosts",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "group": "Which Kick messages do you want to see?"
+ },
+ {
+ "id": "enableTikTokSupport",
+ "label": "Enable TikTok Support",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "Which TikTok messages do you want to see?"
+ },
+ {
+ "id": "showTikTokMessages",
+ "label": "Chat Messages",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok messages do you want to see?"
+ },
+ {
+ "id": "showTikTokFollows",
+ "label": "New Followers",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok messages do you want to see?"
+ },
+ {
+ "id": "showTikTokLikes",
+ "label": "Likes",
+ "description": "Self-updating Likes",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok messages do you want to see?"
+ },
+ {
+ "id": "showTikTokGifts",
+ "label": "Gifts",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok messages do you want to see?"
+ },
+ {
+ "id": "showTikTokSubs",
+ "label": "New Subscribers",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok messages do you want to see?"
+ },
{
"id": "showStreamlabsDonations",
"label": "Streamlabs Tips",
diff --git a/multichat-overlay/style.css b/multichat-overlay/style.css
index fcba6af..143748c 100644
--- a/multichat-overlay/style.css
+++ b/multichat-overlay/style.css
@@ -1,8 +1,8 @@
* {
margin: 0;
padding: 0;
- --margin: 40px;
- --border-radius: 20px;
+ --margin: 20px;
+ --border-radius: 0.5em;
/* --line-spacing: 1.7em; */
}
@@ -52,6 +52,7 @@ html {
.normalScrollDirection {
bottom: 0;
}
+
.reverseScrollDirection {
display: flex;
top: 0;
@@ -92,17 +93,47 @@ li {
font-size: 0.7em;
}
-.highlightMessage {
- background: #adadad46;
- padding: 20px 40px;
+.bubble {
+ background: var(--bubble-color) !important;
+ padding: 0.5em 1em;
border-radius: var(--border-radius);
}
+.highlightMessage {
+ /* background: #adadad46 !important; */
+ background: #414141e5 !important;
+ box-shadow: inset 0 0 0 0.1em rgb(94, 94, 94);
+ padding: 0.5em 1em;
+ border-radius: var(--border-radius);
+}
+
+.message-contents {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5em;
+}
+
+.add-this-class-to-the-thing-to-make-all-the-text-centered-vertically-because-that-looks-much-better {
+ align-items: center;
+}
+
+#avatar {
+ width: 2em;
+}
+
+#avatar:empty {
+ display: none;
+}
+
.avatar {
- height: 2em;
- margin: 1px;
- transform: translate(0px, 0.25em);
- border-radius: 50%
+ width: 2em; /* Set width equal to height */
+ height: 2em;
+ margin: 1px;
+ transform: translate(0px, 0.25em);
+ border-radius: 50%;
+ object-fit: cover; /* Crop to fill the circle */
+ overflow: hidden; /* Ensure no overflow outside the circle */
+ display: inline-block; /* Just in case */
}
.timestamp,
@@ -136,16 +167,22 @@ li {
}
.productImage {
- height: 15em;
+ height: 3em;
border-radius: 0.25em;
transform: translate(0px, 0.25em);
}
-.centerThatShitHomie {
+.youtubeThumbnail {
+ width: 5em;
+ border-radius: 0.25em;
+ transform: translate(0px, 0.25em);
+}
+
+/* .centerThatShitHomie {
display: inline-block;
width: 100%;
text-align: center;
-}
+} */
#badgeList {
margin: 0px 5px;
@@ -163,8 +200,9 @@ li {
#reply {
display: none;
overflow: hidden;
- white-space: nowrap;
+ white-space: normal; /* Allow line wrapping */
text-overflow: ellipsis;
+ word-break: break-word; /* Break long words if necessary */
font-size: 0.7em;
opacity: 0.5;
}
@@ -176,21 +214,53 @@ li {
#card {
background: #adadad46;
- padding: 20px 40px;
+ padding: 0.5em 1em;
border-radius: var(--border-radius);
}
-#youtubeSuperSticker {
- display: block;
+.youtube-super-sticker {
+ height: 2em;
+ margin: 1px;
+ transform: translate(0px, 0.25em);
}
-#sticker-img {
- height: 5em;
+.tiktok-gift {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 1em;
+ padding: 0.5em 1em;
+ border-radius: var(--border-radius);
+ max-width: 100%;
+ background: linear-gradient(#262626BF, #000000BF) !important;
}
-#stickerLabel {
- text-align: center;
- font-weight: 700;
+.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;
}
/*****************/
@@ -221,6 +291,10 @@ li {
background: linear-gradient(#FF6C60BF, #FF0707BF) !important;
}
+.kick {
+ background: linear-gradient(#53fc18BF, #005600BF) !important;
+}
+
.streamlabs {
background: linear-gradient(#73dabbbf, #397765bf) !important;
}
@@ -245,6 +319,19 @@ li {
background: linear-gradient(#466edbbf, #1c56f5bf) !important;
}
+.tiktok {
+ background: linear-gradient(#262626BF, #000000BF) !important;
+}
+
+.fourthwall {
+ background: linear-gradient(#466edbbf, #1c56f5bf) !important;
+}
+
+.thumbnail {
+ background: linear-gradient(#494949bf, #202020bf) !important;
+}
+
+
.blank {
background: #adadad00 !important;
}
diff --git a/multistream-alerts/icons/platforms/kick.png b/multistream-alerts/icons/platforms/kick.png
new file mode 100644
index 0000000..9cc9afa
Binary files /dev/null and b/multistream-alerts/icons/platforms/kick.png differ
diff --git a/multistream-alerts/icons/platforms/tiktok.png b/multistream-alerts/icons/platforms/tiktok.png
new file mode 100644
index 0000000..c9e0da5
Binary files /dev/null and b/multistream-alerts/icons/platforms/tiktok.png differ
diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js
index 91845af..30eacaf 100644
--- a/multistream-alerts/script.js
+++ b/multistream-alerts/script.js
@@ -24,6 +24,15 @@ const messageLabel = document.getElementById('message');
let widgetLocked = false; // Needed to lock animation from overlapping
let alertQueue = [];
+/////////////////
+// GLOBAL VARS //
+/////////////////
+
+const kickPusherWsUrl = 'wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=7.6.0&flash=false';
+let kickSubBadges = [];
+
+
+
/////////////
// OPTIONS //
/////////////
@@ -60,6 +69,15 @@ const twitchCheerAction = urlParams.get("twitchCheerAction") || "";
const showTwitchRaids = GetBooleanParam("showTwitchRaids", true);
const twitchRaidAction = urlParams.get("twitchRaidAction") || "";
+// Which Kick alerts do you want to see?
+let kickUsername = urlParams.get("kickUsername") || "";
+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") || "";
@@ -68,6 +86,13 @@ const youtubeSuperStickerAction = urlParams.get("youtubeSuperStickerAction") ||
const showYouTubeMemberships = GetBooleanParam("showYouTubeMemberships", true);
const youtubeMembershipAction = urlParams.get("youtubeMembershipAction") || "";
+// Which TikTok alerts do you want to see?
+const enableTikTokSupport = GetBooleanParam("enableTikTokSupport", false);
+const showTikTokGifts = GetBooleanParam("showTikTokGifts", true);
+const tiktokGiftAction = urlParams.get("tiktokGiftAction") || "";
+const showTikTokSubs = GetBooleanParam("showTikTokSubs", true);
+const tiktokSubAction = urlParams.get("tiktokSubAction") || "";
+
// Which donation alerts do you want to see?
const showStreamlabsDonations = GetBooleanParam("showStreamlabsDonations", false);
const streamlabsDonationAction = urlParams.get("streamlabsDonationAction") || "";
@@ -82,6 +107,9 @@ const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") |
const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false);
const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || "";
+// Kick is stupid and turns underscores into dashes which fuck everything up, therefore do a find/replace to make it work good
+kickUsername = kickUsername.replace(/_/g, "-");
+
// Set avatar visibility
if (!showAvatar) {
avatarElement.style.display = 'none';
@@ -102,7 +130,7 @@ if (useCustomBackground) {
hexOpacity = "0" + hexOpacity;
}
//document.body.style.background = `${background}${hexOpacity}`;
- console.log(`${background}${hexOpacity}`);
+ //console.log(`${background}${hexOpacity}`);
document.documentElement.style.setProperty('--custom-background', `${background}${hexOpacity}`);
}
@@ -277,6 +305,138 @@ client.on('Fourthwall.GiftDrawEnded', (response) => {
+///////////////////////////
+// KICK PUSHER WEBSOCKET //
+///////////////////////////
+
+// Connect and handle Pusher WebSocket
+async function KickConnect() {
+ if (!kickUsername)
+ return;
+
+ // Channel to subscribe to (you'll need the correct channel name here)
+ const chatroomId = await GetKickChatroomId(kickUsername);
+
+ // Cache subscriber badges
+ kickSubBadges = await GetKickSubBadges(kickUsername);
+
+ const websocket = new WebSocket(kickPusherWsUrl);
+
+ // Reconnect
+ websocket.onclose = function () {
+ console.log(`Reconnecting to ${kickUsername}...`);
+ setTimeout(connectPusher, 5000);
+ };
+
+ websocket.onopen = function () {
+ console.log(`Kick successfully conntected to ${kickUsername}.`);
+ }
+
+ websocket.onmessage = function (response) {
+ try {
+ let data = JSON.parse(response.data);
+
+ console.debug(data);
+
+ // When connection is established, subscribe to a channel
+ if (data.event === 'pusher:connection_established') {
+ const socketData = JSON.parse(data.data);
+ console.log(`[Pusher] Socket established with ID: ${socketData.socket_id}`);
+
+ // Now subscribe to a channel
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatroom_${chatroomId}` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatrooms.${chatroomId}` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `chatrooms.${chatroomId}.v2` } }));
+ websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `predictions-channel-${chatroomId}` } }));
+ console.log(`[Pusher] Sent subscription request to channel: ${chatroomId}`);
+ }
+
+ // Event handlers
+ const eventArgs = JSON.parse(data.data);
+ const event = data.event.split('\\').pop();
+ switch (event) {
+ //// 'Follows' unsupported by pusher
+ // case 'FollowEvent':
+ // break;
+ case 'SubscriptionEvent':
+ KickSubscription(eventArgs);
+ break;
+ case 'GiftedSubscriptionsEvent':
+ KickGiftedSubscriptions(eventArgs);
+ break;
+ case 'RewardRedeemedEvent':
+ KickRewardRedeemed(eventArgs);
+ break;
+ case 'StreamHostEvent':
+ KickStreamHost(eventArgs);
+ break;
+ }
+ }
+ catch (error) {
+ console.error(error);
+ }
+ }
+}
+
+// Try connect when window is loaded
+window.addEventListener('load', KickConnect);
+
+
+
+//////////////////////
+// TIKFINITY CLIENT //
+//////////////////////
+
+let tikfinityWebsocket = null;
+
+function TikfinityConnect() {
+ if (!enableTikTokSupport)
+ return;
+
+ 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(TikfinityConnect, 1000); // Schedule a reconnect attempt
+ }
+
+ tikfinityWebsocket.onerror = function () {
+ console.error(`TikFinity failed for some reason...`)
+ tikfinityWebsocket = null;
+ setTimeout(TikfinityConnect, 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 'gift':
+ TikTokGift(data);
+ break;
+ case 'subscribe':
+ TikTokSubscribe(data);
+ break;
+ }
+ }
+}
+
+// Try connect when window is loaded
+window.addEventListener('load', TikfinityConnect);
+
+
+
///////////////////////
// MULTICHAT OVERLAY //
///////////////////////
@@ -289,7 +449,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 +474,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 +529,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 +569,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 +613,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 +648,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 +678,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 +698,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;
@@ -988,18 +1148,17 @@ function FourthwallSubscriptionPurchased(data) {
}
function FourthwallGiftPurchase(data) {
- console.log(data);
if (!showFourthwallAlerts)
return;
// Set the text
- let user = data.username;
+ // let user = data.username;
const total = data.total;
const currency = data.currency;
const gifts = data.gifts.length;
const itemName = data.offer.name;
- const itemImageUrl = data.offer.imageUrl;
- const message = DecodeHTMLString(data.statmessageus);
+ // const itemImageUrl = data.offer.imageUrl;
+ // const message = DecodeHTMLString(data.statmessageus);
let contents = '';
let attributesText = '';
@@ -1019,12 +1178,12 @@ function FourthwallGiftPurchase(data) {
UpdateAlertBox(
'fourthwall',
- itemImageUrl,
- `${user}`,
- `gifted ${contents}`,
+ '', // itemImageUrl,
+ `An item has been gifted!`,// `${user}`,
+ `${contents}`, // `gifted ${contents}`,
attributesText,
- user,
- message,
+ '', // user,
+ '', // message,
fourthwallAlertAction,
data
);
@@ -1042,7 +1201,7 @@ function FourthwallGiftDrawStarted(data) {
'fourthwall',
'',
`
🎁 ${itemName} Giveaway!`,
- `Type !join in the next ${durationSeconds} seconds for your chance to win!`,
+ `Type 'join' in the next ${durationSeconds} seconds for your chance to win!`,
'',
'',
'',
@@ -1054,14 +1213,10 @@ function FourthwallGiftDrawStarted(data) {
function FourthwallGiftDrawEnded(data) {
if (!showFourthwallAlerts)
return;
-
- // Render avatars
- if (showAvatar) {
- avatar.src = '';
- }
UpdateAlertBox(
'fourthwall',
+ '',
`
🥳 GIVEAWAY ENDED 🥳`,
`Congratulations ${GetWinnersList(data.gifts)}!`,
'',
@@ -1072,6 +1227,202 @@ function FourthwallGiftDrawEnded(data) {
);
}
+// 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 KickSubscription(data) {
+ if (!showKickSubs)
+ return;
+
+ // Set the text
+ const username = data.username;
+ const months = data.months;
+
+ let description = '';
+ if (months <= 1)
+ description = `just subscribed for the first time!`;
+ else
+ description = `resubscribed!`;
+
+ const attribute = `${months} months`;
+
+ // Render avatars
+ const avatarURL = await GetAvatar(username, 'kick');
+
+ UpdateAlertBox(
+ 'kick',
+ avatarURL,
+ `${username}`,
+ description,
+ attribute,
+ username,
+ '',
+ kickSubAction,
+ data
+ );
+}
+
+async function KickGiftedSubscriptions(data) {
+ if (!showKickSubs)
+ return;
+
+ // Set the text
+ const gifter = data.gifter_username;
+ const giftedUsers = data.gifted_usernames;
+
+ let description = '';
+ let attribute = '';
+
+ if (giftedUsers.length <= 1)
+ {
+ description = `gifted a sub to`;
+ attribute = `${giftedUsers[0]}`;
+ }
+ else
+ description = `gifted ${giftedUsers.length} subscription${giftedUsers.length === 1 ? '' : 's'} to the community!`;
+
+ // Render avatars
+ const avatarURL = await GetAvatar(gifter, 'kick');
+
+ UpdateAlertBox(
+ 'kick',
+ avatarURL,
+ `${gifter}`,
+ description,
+ attribute,
+ gifter,
+ '',
+ kickSubAction,
+ data
+ );
+}
+
+async function KickRewardRedeemed(data) {
+ if (!showKickChannelPointRedemptions)
+ return;
+
+ const username = data.username;
+ const rewardName = data.reward_title;
+ const userInput = data.user_input;
+
+ // Render avatars
+ const avatarURL = await GetAvatar(username, 'kick');
+
+ UpdateAlertBox(
+ 'kick',
+ avatarURL,
+ `${username} redeemed`,
+ `${rewardName}`,
+ '',
+ username,
+ userInput,
+ kickChannelPointRedemptionAction,
+ data
+ );
+}
+
+async function KickStreamHost(data) {
+ if (!showKickHosts)
+ return;
+
+ // Render avatars
+ const avatarURL = await GetAvatar(data.host_username, 'kick');
+
+ // Set the text
+ const username = data.host_username;
+ const viewers = data.number_viewers;
+
+ UpdateAlertBox(
+ 'kick',
+ avatarURL,
+ `${username}`,
+ `is raiding with a party of ${viewers}`,
+ '',
+ username,
+ '',
+ kickHostAction,
+ data
+ );
+}
+
+async function TikTokGift(data) {
+ if (!showTikTokGifts)
+ return;
+
+ 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}`);
+
+ // Set the text
+ const username = data.nickname;
+ const tiktokIcon = `

`;
+ const giftImg = `

`;
+
+ // Render avatars
+ const avatarURL = 'icons/platforms/tiktok.png';
+
+ UpdateAlertBox(
+ 'tiktok',
+ avatarURL,
+ `${username}`,
+ `sent ${giftImg}x${data.repeatCount}`,
+ '',
+ username,
+ '',
+ tiktokGiftAction,
+ data
+ );
+}
+
+async function TikTokSubscribe(data) {
+ if (!showTikTokSubs)
+ return;
+
+ // Set the text
+ const username = data.nickname;
+ const tiktokIcon = `

`;
+
+ // Render avatars
+ const avatarURL = 'icons/platforms/tiktok.png';
+
+ UpdateAlertBox(
+ 'tiktok',
+ avatarURL,
+ `${username}`,
+ `subscribed on TikTok`,
+ '',
+ username,
+ '',
+ tiktokSubAction,
+ data
+ );
+}
+
//////////////////////
@@ -1116,17 +1467,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;
+ }
}
}
@@ -1340,6 +1710,54 @@ function UpdateAlertBox(platform, avatarURL, headerText, descriptionText, attrib
}
+async function GetKickChatroomId(username) {
+ const url = `https://kick.com/api/v2/channels/${username}`;
+
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP error ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data.chatroom && data.chatroom.id) {
+ return data.chatroom.id;
+ } else {
+ throw new Error("Chatroom ID not found in response.");
+ }
+ } catch (error) {
+ console.error("Failed to fetch chatroom ID:", error.message);
+ return null;
+ }
+}
+
+async function GetKickSubBadges(username) {
+ const response = await fetch(`https://kick.com/api/v2/channels/${username}`);
+ const data = await response.json();
+
+ return data.subscriber_badges || [];
+}
+
+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`;
+}
+
////////////////////
// TEST FUNCTIONS //
////////////////////
@@ -1348,7 +1766,7 @@ async function testWidget()
{
UpdateAlertBox(
'twitch',
- await GetAvatar('nutty'),
+ await GetAvatar('nutty', 'twitch'),
`nutty`,
`subscribed with Tier 3`,
'',
@@ -1381,32 +1799,4 @@ function SetConnectionStatus(connected) {
statusContainer.style.transition = "";
statusContainer.style.opacity = 1;
}
-}
-
-// 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"
-// }
-// };
-
-// TwitchGiftBomb(data);
-
-//testWidget();
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json
index c2e60a6..87cb412 100644
--- a/multistream-alerts/settings/settings.json
+++ b/multistream-alerts/settings/settings.json
@@ -363,6 +363,109 @@
"showIf": "showYouTubeMemberships",
"group": "Which YouTube alerts do you want to see?"
},
+ {
+ "id": "kickUsername",
+ "label": "Kick Username",
+ "description": "",
+ "type": "text",
+ "defaultValue": "",
+ "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": "enableTikTokSupport",
+ "label": "Enable TikTok Support",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": false,
+ "group": "Which TikTok alerts do you want to see?"
+ },
+ {
+ "id": "showTikTokGifts",
+ "label": "Gifts",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok alerts do you want to see?"
+ },
+ {
+ "id": "tiktokGiftAction",
+ "label": "",
+ "description": "Also run this Streamer.bot Action",
+ "type": "sb-actions",
+ "defaultValue": "",
+ "showIf": "showTikTokGifts",
+ "group": "Which TikTok alerts do you want to see?"
+ },
+ {
+ "id": "showTikTokSubs",
+ "label": "New Subscribers",
+ "description": "",
+ "type": "checkbox",
+ "defaultValue": true,
+ "showIf": "enableTikTokSupport",
+ "group": "Which TikTok alerts do you want to see?"
+ },
+ {
+ "id": "tiktokSubAction",
+ "label": "",
+ "description": "Also run this Streamer.bot Action",
+ "type": "sb-actions",
+ "defaultValue": "",
+ "showIf": "showTikTokSubs",
+ "group": "Which TikTok alerts do you want to see?"
+ },
{
"id": "showStreamlabsDonations",
"label": "Streamlabs Tips",
diff --git a/multistream-alerts/style.css b/multistream-alerts/style.css
index 1b6172e..58a1cd1 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;
}
@@ -331,6 +335,10 @@ html {
background: linear-gradient(#466edbbf, #1c56f5bf) !important;
}
+.tiktok {
+ background: linear-gradient(#262626BF, #000000BF) !important;
+}
+
.blank {
background: #adadad00 !important;
}
diff --git a/printer-bot/icons/platforms/kick.png b/printer-bot/icons/platforms/kick.png
new file mode 100644
index 0000000..9cc9afa
Binary files /dev/null and b/printer-bot/icons/platforms/kick.png differ
diff --git a/printer-bot/icons/platforms/kofi.png b/printer-bot/icons/platforms/kofi.png
new file mode 100644
index 0000000..8812cac
Binary files /dev/null and b/printer-bot/icons/platforms/kofi.png differ
diff --git a/printer-bot/icons/platforms/patreon.png b/printer-bot/icons/platforms/patreon.png
new file mode 100644
index 0000000..22c9748
Binary files /dev/null and b/printer-bot/icons/platforms/patreon.png differ
diff --git a/printer-bot/icons/platforms/tiktok.png b/printer-bot/icons/platforms/tiktok.png
new file mode 100644
index 0000000..c9e0da5
Binary files /dev/null and b/printer-bot/icons/platforms/tiktok.png differ
diff --git a/printer-bot/icons/platforms/tipeeeStream.png b/printer-bot/icons/platforms/tipeeeStream.png
new file mode 100644
index 0000000..cd981f3
Binary files /dev/null and b/printer-bot/icons/platforms/tipeeeStream.png differ
diff --git a/printer-bot/icons/platforms/twitch.png b/printer-bot/icons/platforms/twitch.png
new file mode 100644
index 0000000..dcbd6c8
Binary files /dev/null and b/printer-bot/icons/platforms/twitch.png differ
diff --git a/printer-bot/icons/platforms/youtube.png b/printer-bot/icons/platforms/youtube.png
new file mode 100644
index 0000000..5abdb7b
Binary files /dev/null and b/printer-bot/icons/platforms/youtube.png differ
diff --git a/printer-bot/index.html b/printer-bot/index.html
new file mode 100644
index 0000000..34f0478
--- /dev/null
+++ b/printer-bot/index.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/printer-bot/script.js b/printer-bot/script.js
new file mode 100644
index 0000000..81e303b
--- /dev/null
+++ b/printer-bot/script.js
@@ -0,0 +1,804 @@
+///////////////////
+// PAGE ELEMENTS //
+///////////////////
+
+const headerEl = document.getElementById('header');
+const contentEl = document.getElementById('content');
+const footerEl = document.getElementById('footer');
+
+const avatarEl = document.getElementById('avatar');
+const titleEl = document.getElementById('title');
+const subtitleEl = document.getElementById('subtitle');
+const dateEl = document.getElementById('date');
+
+
+
+//////////////////////
+// GLOBAL VARIABLES //
+//////////////////////
+
+const avatarMap = new Map();
+
+
+
+/////////////////////////
+// STREAMER.BOT CLIENT //
+/////////////////////////
+
+// Check local storage
+if (localStorage.getItem('sbServerAddress') === null)
+ localStorage.setItem('sbServerAddress', '127.0.0.1');
+if (localStorage.getItem('sbServerPort') === null)
+ localStorage.setItem('sbServerPort', '8080');
+
+document.getElementById('ip').value = localStorage.getItem('sbServerAddress');
+document.getElementById('port').value = localStorage.getItem('sbServerPort');
+
+let sbServerAddress = document.getElementById('ip').value;
+let sbServerPort = document.getElementById('port').value;
+
+let client = new StreamerbotClient({
+ host: sbServerAddress,
+ port: sbServerPort,
+
+ onConnect: (data) => {
+ console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
+ console.debug(data);
+
+ SetConnectionState(true);
+ },
+
+ onDisconnect: () => {
+ console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
+ SetConnectionState(false);
+ }
+});
+
+client.on('General.Custom', (response) => {
+ console.debug(response.data);
+ CustomEvent(response.data);
+})
+
+
+////////////////////
+// STREAM PRINTER //
+////////////////////
+
+async function CustomEvent(data) {
+ if (data.actionName != 'Printer Bot | Events')
+ return;
+
+ // Get a reference to the template
+ const template = document.getElementById('receipt-template');
+
+ // Create a new instance of the template
+ const instance = template.content.cloneNode(true);
+
+ // Get divs
+ const headerEl = instance.querySelector('#header');
+ const contentEl = instance.querySelector('#content');
+ const footerEl = instance.querySelector('#footer');
+ 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
+ switch (data.__source) {
+ // Twitch events
+ case ('TwitchCheer'):
+ {
+ avatarEl.src = await GetAvatar(data.userName, 'twitch');
+ titleEl.innerText = `${data.bits} BITS`;
+ subtitleEl.innerText = `${data.user}`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = data.message;
+
+ // Render emotes
+ for (i in data.emotes) {
+ const emoteElement = `

`;
+ const emoteName = EscapeRegExp(data.emotes[i].name);
+
+ let regexPattern = emoteName;
+
+ // Check if the emote name consists only of word characters (alphanumeric and underscore)
+ if (/^\w+$/.test(emoteName)) {
+ regexPattern = `\\b${emoteName}\\b`;
+ }
+ else {
+ // For non-word emotes, ensure they are surrounded by non-word characters or boundaries
+ regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
+ }
+
+ const regex = new RegExp(regexPattern, 'g');
+ messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
+ }
+
+ // Render cheermotes
+ for (i in data.cheerEmotes) {
+ const bits = data.cheerEmotes[i].bits;
+ const imageUrl = data.cheerEmotes[i].imageUrl;
+ const name = data.cheerEmotes[i].name;
+ const cheerEmoteElement = `

`;
+ const bitsElements = `
${bits}`
+ messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
+ }
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'twitch');
+ }
+ break;
+ case ('TwitchSub'):
+ {
+ avatarEl.src = await GetAvatar(data.userName, 'twitch');
+ titleEl.innerText = `${data.tier} subscriber`;
+ subtitleEl.innerText = `${data.user}`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = '
First time subscriber!';
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'twitch');
+ }
+ break;
+ case ('TwitchReSub'):
+ {
+ avatarEl.src = await GetAvatar(data.userName, 'twitch');
+ titleEl.innerText = `${data.tier} subscriber`;
+ subtitleEl.innerText = `${data.user}`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`;
+ if (data.messageStripped)
+ 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';
+ return;
+ else
+ avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch');
+ titleEl.innerText = `Gifted Sub`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML += `
${data.recipientUser}received a ${data.tier} sub from
`;
+ if (data.anonymous)
+ messageEl.innerHTML += `a mysterious admirer...`;
+ else
+ messageEl.innerHTML += `
${data.user}!`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'twitch');
+ }
+ break;
+ case ('TwitchGiftBomb'):
+ {
+ avatarEl.src = await GetAvatar(data.userName, 'twitch');
+ titleEl.innerHTML = `${data.gifts} × Gifted Subs`;
+ subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`;
+ if (data.anonymous)
+ subtitleEl.innerHTML += `
From a mystery person...`;
+ else
+ subtitleEl.innerHTML += `
${data.user}`;
+
+ const messageEl = document.createElement('div');
+ if (data.totalGifts > 1) {
+ messageEl.innerHTML = `They've gifted
${data.totalGifts} subs in total!`;
+ }
+
+ // 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'):
+ {
+ avatarEl.src = await GetAvatar(data.userName, 'twitch');
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.user}is raiding with a party of
${data.viewers} viewers!`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'twitch');
+ }
+ break;
+
+ // YouTube Events
+ case ('YouTubeNewSponsor'):
+ {
+ if (data.userProfileUrl)
+ avatarEl.src = data.userProfileUrl;
+ else
+ avatarEl.style.display = 'none';
+
+ titleEl.innerText = `${data.levelName}`;
+ subtitleEl.innerText = `${data.user}`;
+
+ contentEl.style.display = 'none';
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'youtube');
+ }
+ break;
+ case ('YouTubeGiftMembershipReceived'):
+ {
+ if (data.gifterProfileUrl)
+ avatarEl.src = data.gifterProfileUrl;
+ else
+ avatarEl.style.display = 'none';
+ titleEl.innerText = `Gifted Membership`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.user}received a membership from
${data.gifterUser}!`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'youtube');
+ }
+ break;
+ case ('YouTubeSuperChat'):
+ {
+ if (data.userProfileUrl)
+ avatarEl.src = data.userProfileUrl;
+ else
+ avatarEl.style.display = 'none';
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = `${data.amount}`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.user}sent a Super Chat!`;
+ if (data.message)
+ messageEl.innerHTML += `
${data.message}`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'youtube');
+ }
+ break;
+ case ('YouTubeSuperSticker'):
+ {
+ if (data.stickerImageUrl)
+ avatarEl.src = data.stickerImageUrl;
+ else
+ avatarEl.style.display = 'none';
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = `${data.amount}`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.user}sent a Super Sticker!`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'youtube');
+ }
+ break;
+ break;
+
+ // Kick Events
+ case ('KickSubscription'):
+ case ('KickResubscription'):
+ {
+ avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
+ titleEl.innerText = `Subscriber`;
+ subtitleEl.innerText = `${data.user}`;
+
+ const messageEl = document.createElement('div');
+ if (data.duration > 1)
+ messageEl.innerHTML = `
${data.duration} months`;
+ else
+ messageEl.innerHTML = '
First time subscriber!';
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'kick');
+ }
+ break;
+ case ('KickGiftSubscription'):
+ {
+ avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
+ titleEl.innerText = `Gifted Sub`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data["recipient.userName"]}received a sub from
${data.user}!`;
+
+ contentEl.appendChild(messageEl);
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'kick');
+ }
+ break;
+ case ('KickMassGiftSubscription'):
+ {
+ // There is only one sub, so use the same template for a single gifted sub
+ if ('recipient.userName' in data) {
+ avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
+ titleEl.innerText = `Gifted Sub`;
+
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data["recipient.userName"]}received a sub from
${data.user}!`;
+
+ contentEl.appendChild(messageEl);
+ }
+ else {
+ avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
+
+ // Calculate how many subs were gived
+ let maxIndex = -1;
+ for (const key in data) {
+ const match = key.match(/^recipient\.(\d+)\./);
+ if (match) {
+ const index = parseInt(match[1], 10);
+ if (index > maxIndex) {
+ maxIndex = index;
+ }
+ }
+ }
+ const totalGifts = maxIndex + 1;
+
+ titleEl.innerHTML = `${totalGifts} × Gifted Subs`;
+ subtitleEl.innerText = `${data.user}`;
+
+ const messageEl = document.createElement('div');
+
+ // Loop through each recipient and include it in the receipt
+ const recipients = {};
+
+ // Reconstruct recipient objects
+ for (const key in data) {
+ const match = key.match(/^recipient\.(\d+)\.(.+)$/);
+ if (match) {
+ const index = match[1];
+ const field = match[2];
+
+ if (!recipients[index]) {
+ recipients[index] = {};
+ }
+
+ recipients[index][field] = data[key];
+ }
+ }
+
+ // Loop through and print userName
+ for (const index in recipients) {
+ messageEl.innerHTML += `${recipients[index].userName}
`;
+ }
+
+ contentEl.appendChild(messageEl);
+ }
+
+ // Set the platform icon
+ SetPlatformIcon(iconEl, 'kick');
+ }
+ break;
+
+ // StreamElements Events
+ case ('StreamElementsTip'):
+ {
+ const avatarURL = await GetAvatar(data.tipUsername, 'twitch');
+ if (IsValidUrl(avatarURL))
+ avatarEl.src = avatarURL;
+ else
+ avatarEl.style.display = 'none'
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency);
+ subtitleEl.innerText = `${data.tipUsername}`;
+
+ if (data.tipMessage) {
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.tipMessage}`;
+
+ contentEl.appendChild(messageEl);
+ }
+ else {
+ contentEl.style.display = 'none';
+ }
+ }
+ break;
+
+ // Streamlabs Events
+ case ('StreamlabsDonation'):
+ {
+ const avatarURL = await GetAvatar(data.donationFrom, 'twitch');
+ if (IsValidUrl(avatarURL))
+ avatarEl.src = avatarURL;
+ else
+ avatarEl.style.display = 'none'
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = data.donationFormattedAmount;
+ subtitleEl.innerText = `${data.donationFrom}`;
+
+ if (data.donationMessage) {
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data.donationMessage}`;
+
+ contentEl.appendChild(messageEl);
+ }
+ else {
+ contentEl.style.display = 'none';
+ }
+ }
+ break;
+
+ // Fourthwall Events
+ case ('FourthwallDonation'):
+ {
+ const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
+ if (IsValidUrl(avatarURL))
+ avatarEl.src = avatarURL;
+ else
+ avatarEl.style.display = 'none'
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]);
+ if (data["fw.username"])
+ subtitleEl.innerText = `${data["fw.username"]}`;
+ else if (data["fw.email"])
+ subtitleEl.innerText = `${data["fw.email"]}`;
+
+ if (data["fw.message"]) {
+ const messageEl = document.createElement('div');
+ messageEl.innerHTML = `
${data["fw.message"]}`;
+
+ contentEl.appendChild(messageEl);
+ }
+ else {
+ contentEl.style.display = 'none';
+ }
+ }
+ break;
+ // case ('FourthwallGiftPurchase'):
+ // break;
+ case ('FourthwallOrderPlaced'):
+ {
+ // Only print non-free orders
+ if (data["fw.total"] <= 0)
+ return;
+
+ const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
+ if (IsValidUrl(avatarURL))
+ avatarEl.src = avatarURL;
+ else
+ avatarEl.style.display = 'none'
+ titleEl.style.fontSize = '2em';
+ titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]);
+ if (data["fw.username"])
+ subtitleEl.innerText = `${data["fw.username"]}`;
+ else if (data["fw.email"])
+ subtitleEl.innerText = `${data["fw.email"]}`;
+
+ // Compile a list of all items bought
+ const variants = [];
+
+ // Iterate through all keys in the data object
+ for (const key in data) {
+ const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/);
+ if (match) {
+ const index = Number(match[1]);
+ const field = match[2];
+
+ // Make sure the array slot exists
+ if (!variants[index]) {
+ variants[index] = {};
+ }
+
+ // Assign the field to the appropriate variant object
+ variants[index][field] = data[key];
+ }
+ }
+
+ // Print each item on the receipt
+ const messageEl = document.createElement('div');
+ variants.forEach((variant, i) => {
+ messageEl.innerHTML += `${variant.quantity} × ${variant.name}
`;
+ });
+ messageEl.style.textAlign = 'left';
+
+ // Check if they left a custom message
+ let customMessageEl = document.createElement('div');
+ const customMessage = data["fw.statmessageus"];
+ if (customMessage)
+ {
+ const txt = document.createElement("textarea");
+ txt.innerHTML = customMessage;
+ customMessageEl.innerHTML += `
${txt.value}`;
+ }
+
+ // Add a cute thank you message because you're uwu like that
+ const thankYouEl = document.createElement('div');
+ thankYouEl.innerHTML += `
Thank you for your purchase!`;
+
+ contentEl.appendChild(messageEl);
+ contentEl.appendChild(customMessageEl);
+ contentEl.appendChild(thankYouEl);
+ }
+ break;
+ case ('FourthwallSubscriptionPurchased'):
+ {
+ const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch');
+ if (IsValidUrl(avatarURL))
+ avatarEl.src = avatarURL;
+ else
+ avatarEl.style.display = 'none'
+ titleEl.innerText = `New Member`;
+ subtitleEl.innerHTML = `${data["fw.nickname"]}`;
+
+ const messageEl = document.createElement('div');
+ 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;
+ }
+
+ // Set the timestamp
+ const { DateTime } = luxon;
+ const now = DateTime.local();
+ const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss");
+
+ // Add ordinal suffix manually
+ function addOrdinal(n) {
+ if (n >= 11 && n <= 13) return 'th';
+ switch (n % 10) {
+ case 1: return 'st';
+ case 2: return 'nd';
+ case 3: return 'rd';
+ default: return 'th';
+ }
+ }
+
+ const day = now.day;
+ const ordinal = addOrdinal(day);
+ const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`);
+
+ dateEl.textContent = fullFormatted;
+
+ // Send it to the print routine!
+ const receiptHTML = await GetRenderedHTML(instance);
+ console.log(receiptHTML);
+ client.doAction({ name: 'Printer Bot | Print Routine' }, {
+ receiptHTML: receiptHTML,
+ isTest: data.isTest
+ });
+}
+
+
+//////////////////////
+// HELPER FUNCTIONS //
+//////////////////////
+
+async function GetAvatar(username, platform) {
+
+ // First, check if the username is hashed already
+ if (avatarMap.has(`${username}-${platform}`)) {
+ console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`)
+ return avatarMap.get(`${username}-${platform}`);
+ }
+
+ // If code reaches this point, the username hasn't been hashed, so retrieve avatar
+ switch (platform) {
+ case 'twitch':
+ {
+ console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`)
+ let response = await fetch('https://decapi.me/twitch/avatar/' + username);
+ let data = await response.text();
+ avatarMap.set(`${username}-${platform}`, data);
+ return data;
+ }
+ case 'kick':
+ {
+ console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`)
+ try {
+ 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;
+ }
+ catch (error) {
+ console.debug(error);
+ return 'https://kick.com/img/default-profile-pictures/default2.jpeg';
+ }
+ }
+ }
+}
+
+async function GetRenderedHTML(fragment) {
+ if (!(fragment instanceof DocumentFragment)) {
+ throw new Error('Argument must be a DocumentFragment');
+ }
+
+ // Filter out comment nodes from fragment content
+ const nodes = Array.from(fragment.childNodes).filter(
+ node => node.nodeType !== Node.COMMENT_NODE
+ );
+
+ const bodyContent = nodes
+ .map(node => node.outerHTML || node.textContent)
+ .join('');
+
+ // Get inline
+
+
+ ${bodyContent}
+
+