//////////////// // PARAMETERS // //////////////// const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const sbServerAddress = urlParams.get("address") || "127.0.0.1"; const sbServerPort = urlParams.get("port") || "8080"; const mainContainer = document.getElementById('mainContainer'); const alertBox = document.getElementById('alertBox'); const avatarElement = document.getElementById('avatar'); const avatarSmallElement = document.getElementById('avatarButItsSmallerThanTheOneFromBeforeForPrettinessPurposes'); const usernameLabel = document.getElementById('username'); const usernameTheSecondLabel = document.getElementById('usernameTheSecond'); const descriptionLabel = document.getElementById('description'); const attributeLabel = document.getElementById('attribute'); const theContentThatShowsFirstInsteadOfSecond = document.getElementById('theContentThatShowsFirstInsteadOfSecond'); const theContentThatShowsLastInsteadOfFirst = document.getElementById('theContentThatShowsLastInsteadOfFirst'); 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 // ///////////// // Appearance const showAvatar = GetBooleanParam("showAvatar", true); const font = urlParams.get("font") || ""; const fontSize = GetIntParam("fontSize", 30); const fontColor = urlParams.get("fontColor") || "#FFFFFF"; const useCustomBackground = GetBooleanParam("useCustomBackground", true); const background = urlParams.get("background") || "#000000"; const opacity = urlParams.get("opacity") || "0.85"; const textAlignment = urlParams.get("textAlignment") || "left"; const alignment = urlParams.get("alignment") || ""; // General const hideAfter = GetIntParam("hideAfter", 8); const showAnimation = urlParams.get("showAnimation") || ""; const hideAnimation = urlParams.get("hideAnimation") || ""; const playSounds = GetBooleanParam("playSounds", true); const globalShowAction = urlParams.get("globalShowAction") || ""; const globalHideAction = urlParams.get("globalHideAction") || ""; const showMesesages = GetBooleanParam("showMesesages", true); // Which Twitch alerts do you want to see? const showTwitchFollows = GetBooleanParam("showTwitchFollows", false); const twitchFollowAction = urlParams.get("twitchFollowAction") || ""; const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); const twitchSubAction = urlParams.get("twitchSubAction") || ""; const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); const twitchChannelPointRedemptionAction = urlParams.get("twitchChannelPointRedemptionAction") || ""; const showTwitchPowerUpRedemptions = GetBooleanParam("showTwitchPowerUpRedemptions", true); const twitchPowerUpRedemptionAction = urlParams.get("twitchPowerUpRedemptionAction") || ""; const showTwitchCheers = GetBooleanParam("showTwitchCheers", true); const twitchCheerAction = urlParams.get("twitchCheerAction") || ""; const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); const twitchRaidAction = urlParams.get("twitchRaidAction") || ""; const showTwitchWatchStreaks = GetBooleanParam("showTwitchWatchStreaks", true); const twitchWatchStreaksAction = urlParams.get("twitchWatchStreaksAction") || ""; // Which Kick alerts do you want to see? const showKickFollows = GetBooleanParam("showKickFollows", false); const kickFollowAction = urlParams.get("kickFollowAction") || ""; const showKickSubs = GetBooleanParam("showKickSubs", true); const kickSubAction = urlParams.get("kickSubAction") || ""; const showKickChannelPointRedemptions = GetBooleanParam("showKickChannelPointRedemptions", true); const kickChannelPointRedemptionAction = urlParams.get("kickChannelPointRedemptionAction") || ""; const showKickHosts = GetBooleanParam("showKickHosts", true); const kickHostAction = urlParams.get("kickHostAction") || ""; const showKickGifts = GetBooleanParam("showKickGifts", true); const kickGiftAction = urlParams.get("kickGiftAction") || ""; // Which YouTube alerts do you want to see? const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true); const youtubeSuperChatAction = urlParams.get("youtubeSuperChatAction") || ""; const showYouTubeSuperStickers = GetBooleanParam("showYouTubeSuperStickers", true); 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") || ""; const showStreamElementsTips = GetBooleanParam("showStreamElementsTips", false); const streamelementsTipAction = urlParams.get("streamelementsTipAction") || ""; const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", false); const patreonMembershipActions = urlParams.get("patreonMembershipActions") || ""; const showKofiDonations = GetBooleanParam("showKofiDonations", false); const kofiDonationAction = urlParams.get("kofiDonationAction") || ""; const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", false); const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") || ""; const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false); const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; //////////////////// // HIDDEN OPTIONS // //////////////////// let twitchUsername = urlParams.get("twitchUsername") || ""; let kickUsername = urlParams.get("kickUsername") || ""; let youtubeUsername = urlParams.get("youtubeUsername") || ""; // Set avatar visibility if (!showAvatar) { avatarElement.style.display = 'none'; avatarSmallElement.style.display = 'none'; alertBox.style.padding = '0.5em 1em'; } // Set fonts for the widget document.body.style.fontFamily = `'${font}'`; document.body.style.fontSize = `${fontSize}px`; document.body.style.color = fontColor; // Set custom background if (useCustomBackground) { const opacity255 = Math.round(parseFloat(opacity) * 255); let hexOpacity = opacity255.toString(16); if (hexOpacity.length < 2) { hexOpacity = "0" + hexOpacity; } //document.body.style.background = `${background}${hexOpacity}`; //console.log(`${background}${hexOpacity}`); document.documentElement.style.setProperty('--custom-background', `${background}${hexOpacity}`); } // Set text alignment document.documentElement.style.setProperty('--text-align', textAlignment); // Set the alignment of the alert box switch (alignment) { case "align-to-top": mainContainer.style.justifyContent = 'flex-start'; break; case "align-to-center": mainContainer.style.justifyContent = 'center'; break; case "align-to-bottom": mainContainer.style.justifyContent = 'flex-end'; break; } ///////////////////////// // STREAMER.BOT CLIENT // ///////////////////////// const client = new StreamerbotClient({ host: sbServerAddress, port: sbServerPort, onConnect: (data) => { console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) console.debug(data); SetConnectionStatus(true); KickConnect(); }, onDisconnect: () => { console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) SetConnectionStatus(false); } }); client.on('Twitch.Follow', (response) => { console.debug(response.data); TwitchFollow(response.data); }) client.on('Twitch.Cheer', (response) => { console.debug(response.data); TwitchCheer(response.data); }) client.on('Twitch.Sub', (response) => { console.debug(response.data); TwitchSub(response.data); }) client.on('Twitch.ReSub', (response) => { console.debug(response.data); TwitchResub(response.data); }) client.on('Twitch.GiftSub', (response) => { console.debug(response.data); TwitchGiftSub(response.data); }) client.on('Twitch.GiftBomb', (response) => { console.debug(response.data); TwitchGiftBomb(response.data); }) client.on('Twitch.RewardRedemption', (response) => { console.debug(response.data); TwitchRewardRedemption(response.data); }) client.on('Twitch.CustomPowerUpRedemption', (response) => { console.debug(response.data); TwitchCustomPowerUpRedemption(response.data); }) client.on('Twitch.Raid', (response) => { console.debug(response.data); TwitchRaid(response.data); }) client.on('Twitch.WatchStreak', (response) => { console.debug(response.data); TwitchWatchStreak(response.data); }) client.on('YouTube.SuperChat', (response) => { console.debug(response.data); YouTubeSuperChat(response.data); }) client.on('YouTube.SuperSticker', (response) => { console.debug(response.data); YouTubeSuperSticker(response.data); }) client.on('YouTube.NewSponsor', (response) => { console.debug(response.data); YouTubeNewSponsor(response.data); }) client.on('YouTube.GiftMembershipReceived', (response) => { console.debug(response.data); YouTubeGiftMembershipReceived(response.data); }) client.on('Kick.Follow', (response) => { console.debug(response.data); KickFollow(response.data); }) client.on('Streamlabs.Donation', (response) => { console.debug(response.data); StreamlabsDonation(response.data); }) client.on('StreamElements.Tip', (response) => { console.debug(response.data); StreamElementsTip(response.data); }) client.on('Patreon.PledgeCreated', (response) => { console.debug(response.data); PatreonPledgeCreated(response.data); }) client.on('Kofi.Donation', (response) => { console.debug(response.data); KofiDonation(response.data); }) client.on('Kofi.Subscription', (response) => { console.debug(response.data); KofiSubscription(response.data); }) client.on('Kofi.Resubscription', (response) => { console.debug(response.data); KofiResubscription(response.data); }) client.on('Kofi.ShopOrder', (response) => { console.debug(response.data); KofiShopOrder(response.data); }) client.on('TipeeeStream.Donation', (response) => { console.debug(response.data); TipeeeStreamDonation(response.data); }) client.on('Fourthwall.OrderPlaced', (response) => { console.debug(response.data); FourthwallOrderPlaced(response.data); }) client.on('Fourthwall.Donation', (response) => { console.debug(response.data); FourthwallDonation(response.data); }) client.on('Fourthwall.SubscriptionPurchased', (response) => { console.debug(response.data); FourthwallSubscriptionPurchased(response.data); }) client.on('Fourthwall.GiftPurchase', (response) => { console.debug(response.data); FourthwallGiftPurchase(response.data); }) client.on('Fourthwall.GiftDrawStarted', (response) => { console.debug(response.data); FourthwallGiftDrawStarted(response.data); }) client.on('Fourthwall.GiftDrawEnded', (response) => { console.debug(response.data); FourthwallGiftDrawEnded(response.data); }) /////////////////////////// // KICK PUSHER WEBSOCKET // /////////////////////////// // Connect and handle Pusher WebSocket async function KickConnect() { // Fetch from Streamer.bot const broadcasterInfo = await client.getBroadcaster(); // This code has nothing to do with Kick, but it's a really good place to get Twitch and YouTube usernames as well, because I'm built different like that if (broadcasterInfo.platforms.twitch) twitchUsername = broadcasterInfo.platforms.twitch.broadcastUserName; if (broadcasterInfo.platforms.youtube) youtubeUsername = broadcasterInfo.platforms.youtube.broadcastUserName; // If user has not manually set Kick username, try to grab if from Streamer.bot if (!kickUsername) { if (broadcasterInfo.platforms.kick) kickUsername = broadcasterInfo.platforms.kick.broadcasterLogin; else return; } // Channel to subscribe to (you'll need the correct channel name here) const kickIds = await GetKickIds(kickUsername); const chatroomId = kickIds.chatroomId; const channelId = kickIds.channelId; // Cache subscriber badges kickSubBadges = await GetKickSubBadges(kickUsername); const websocket = new WebSocket(kickPusherWsUrl); // Reconnect websocket.onclose = function () { console.log(`Reconnecting to ${kickUsername}...`); setTimeout(KickConnect, 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}` } })); websocket.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: `channel_${channelId}` } })); 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; case 'KicksGifted': KickKicksGifted(eventArgs); break; } } catch (error) { console.error(error); } } } ////////////////////// // 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 // /////////////////////// async function TwitchFollow(data) { if (!showTwitchFollows) return; // Set the text const username = data.user_name; // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); UpdateAlertBox( 'twitch', avatarURL, `${username}`, `followed`, ``, username, ``, twitchFollowAction, data ); } async function TwitchCheer(data) { if (!showTwitchCheers) return; // Set the text const username = data.user.login; const bits = data.bits; let message = ConstructMessageFromParts(data.parts); // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); UpdateAlertBox( 'twitch', avatarURL, `${username}`, `cheered ${bits} bits`, '', username, message, twitchCheerAction, data ); } async function TwitchSub(data) { if (!showTwitchSubs) return; // Set the text const username = data.user.name; const subTier = data.sub_tier; const isPrime = data.is_prime; // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); if (!isPrime) UpdateAlertBox( 'twitch', avatarURL, `${username}`, `subscribed with Tier ${subTier.charAt(0)}`, '', username, '', twitchSubAction, data ); else UpdateAlertBox( 'twitch', avatarURL, `${username}`, `used their Prime Sub`, '', username, '', twitchSubAction, data ); } async function TwitchResub(data) { if (!showTwitchSubs) return; // Set the text const username = data.user.name; const subTier = data.subTier; const isPrime = data.isPrime; const cumulativeMonths = data.cumulativeMonths; const message = data.text; // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); if (!isPrime) UpdateAlertBox( 'twitch', avatarURL, `${username}`, `resubscribed with Tier ${subTier.charAt(0)}`, `${cumulativeMonths} months`, username, message, twitchSubAction, data ); else UpdateAlertBox( 'twitch', avatarURL, `${username}`, `used their Prime Sub`, `${cumulativeMonths} months`, username, message, twitchSubAction, data ); } async function TwitchGiftSub(data) { if (!showTwitchSubs) return; // Set the text const username = data.user.name; const subTier = data.subTier; const recipient = data.recipient.name; const cumlativeTotal = data.cumlativeTotal; const fromCommunitySubGift = data.fromCommunitySubGift; // Don't post alerts for gift bombs if (fromCommunitySubGift) return; // Render avatars const avatarURL = await GetAvatar(username, 'twitch'); let messageText = ''; if (cumlativeTotal > 0) messageText = `They've gifted ${cumlativeTotal} subs in total!`; UpdateAlertBox( 'twitch', avatarURL, `${username}`, `gifted a Tier ${subTier.charAt(0)} subscription`, `to ${recipient}`, username, messageText, twitchSubAction, data ); } async function TwitchGiftBomb(data) { if (!showTwitchSubs) return; //// The below is incorrect (Streamer.bot documentation is wrong) // const username = data.displayName; // const gifts = data.gifts; // const totalGifts = data.totalGifts; // const subTier = data.subTier; const username = data.user.name; const login = data.user.login; const gifts = data.recipients.length; const totalGifts = data.cumulative_total; const subTier = data.sub_tier.charAt(0); // Render avatars const avatarURL = await GetAvatar(login, 'twitch'); let message = ``; if (totalGifts > 0) message = `They've gifted ${totalGifts} subs in total!`; UpdateAlertBox( 'twitch', avatarURL, `${username}`, `gifted ${gifts} Tier ${subTier} subs!`, ``, username, message, twitchSubAction, data ); } async function TwitchRewardRedemption(data) { if (!showTwitchChannelPointRedemptions) return; const username = data.user_name; const rewardName = data.reward.title; const cost = data.reward.cost; const userInput = data.user_input; const channelPointIcon = ``; // Render avatars const avatarURL = await GetAvatar(data.user_login, 'twitch'); UpdateAlertBox( 'twitch', avatarURL, `${username} redeemed`, `${rewardName} ${channelPointIcon} ${cost}`, '', username, userInput, twitchChannelPointRedemptionAction, data ); } async function TwitchCustomPowerUpRedemption(data) { if (!showTwitchPowerUpRedemptions) return; let username = data.user.name; if (data.user.name.toLowerCase() != data.user.login.toLowerCase()) username = `${data.user.name} (${data.user.login})`; const powerUpName = data.customPowerUp.title; const cost = data.customPowerUp.bits; const userInput = data.userInput; const bitIcon = ``; // Render avatars const avatarURL = await GetAvatar(data.user.login, 'twitch'); UpdateAlertBox( 'twitch', avatarURL, `${username} redeemed`, `${powerUpName} ${bitIcon} ${cost}`, '', username, userInput, twitchPowerUpRedemptionAction, data ); } async function TwitchRaid(data) { if (!showTwitchRaids) return; // Render avatars const avatarURL = await GetAvatar(data.from_broadcaster_user_login, 'twitch'); // Set the text const username = data.from_broadcaster_user_login; const viewers = data.viewers; UpdateAlertBox( 'twitch', avatarURL, `${username}`, `is raiding with a party of ${viewers}`, '', username, '', twitchRaidAction, data ); } async function TwitchWatchStreak(data) { if (!showTwitchWatchStreaks) return; // Render avatars const avatarURL = await GetAvatar(data.user.login, 'twitch'); // Set the text // TODO: Streamer.bot v1.0.5-alpha3 changed the data sent with this event, so for backwards compatibility we need to check for both the old and new properties const displayName = data.displayName ?? data.user.name; const watchStreak = data.watchStreak ?? data.streak_count; UpdateAlertBox( 'twitch', avatarURL, `${displayName}`, `is currently on a ${watchStreak} stream streak!`, '', displayName, '', twitchWatchStreaksAction, data ); } function YouTubeSuperChat(data) { if (!showYouTubeSuperChats) return; // Render avatars const avatarURL = data.user.profileImageUrl; UpdateAlertBox( 'youtube', avatarURL, `${data.user.name}`, `sent a Super Chat (${data.amount})`, '', data.user.name, data.message, youtubeSuperChatAction, data ); } function YouTubeSuperSticker(data) { if (!showYouTubeSuperStickers) return; // Render avatars const avatarURL = FindFirstImageUrl(data); UpdateAlertBox( 'youtube', avatarURL, `${data.user.name}`, `sent a Super Sticker (${data.amount})`, '', data.user.name, '', youtubeSuperStickerAction, data ); } function YouTubeNewSponsor(data) { if (!showYouTubeMemberships) return; // Render avatars const avatarURL = data.user.profileImageUrl; UpdateAlertBox( 'youtube', avatarURL, `⭐ New ${data.levelName}`, `Welcome ${data.user.name}!`, '', data.user.name, '', youtubeMembershipAction, data ); } function YouTubeGiftMembershipReceived(data) { if (!showYouTubeMemberships) return; // Render avatars const avatarURL = data.user.profileImageUrl; UpdateAlertBox( 'youtube', avatarURL, `${data.gifter.name}`, `gifted a membership`, `to ${data.user.name} (${data.tier})!`, data.gifter.name, '', youtubeMembershipAction, data ); } async function StreamlabsDonation(data) { if (!showStreamlabsDonations) return; // Set the text const donater = data.from; const formattedAmount = data.formattedAmount; const currency = data.currency; const message = data.message; UpdateAlertBox( 'streamlabs', '', `${donater}`, `donated ${currency}${formattedAmount}`, ``, donater, message, streamlabsDonationAction, data ); } async function StreamElementsTip(data) { if (!showStreamElementsTips) return; // Set the text const donater = data.username; const formattedAmount = `$${data.amount}`; const currency = data.currency; const message = data.message; UpdateAlertBox( 'streamelements', '', `${donater}`, `donated ${currency}${formattedAmount}`, ``, donater, message, streamelementsTipAction, data ); } function PatreonPledgeCreated(data) { if (!showPatreonMemberships) return; const user = data.attributes.full_name; const amount = (data.attributes.will_pay_amount_cents/100).toFixed(2); const patreonIcon = ``; // Render avatars const avatarURL = 'icons/platforms/patreon.png'; UpdateAlertBox( 'patreon', avatarURL, `${user}`, `joined Patreon ($${amount})`, ``, user, ``, patreonMembershipActions, data ); } function KofiDonation(data) { if (!showKofiDonations) return; // Set the text const user = data.from; const amount = data.amount; const currency = data.currency; const message = data.message; // Render avatars const avatarURL = 'icons/platforms/kofi.png'; if (currency == "USD") UpdateAlertBox( 'kofi', avatarURL, `${user}`, `donated $${amount}`, ``, user, message, kofiDonationAction, data ); else UpdateAlertBox( 'kofi', `${user}`, `donated ${currency} ${amount}`, ``, user, message, kofiDonationAction, data ); } function KofiSubscription(data) { if (!showKofiDonations) return; // Set the text const user = data.from; const amount = data.amount; const currency = data.currency; const message = data.message; // Render avatars const avatarURL = 'icons/platforms/kofi.png'; if (currency == "USD") UpdateAlertBox( 'kofi', avatarURL, `${user}`, `subscribed ($${amount})`, ``, user, message, kofiDonationAction, data ); else UpdateAlertBox( 'kofi', `${user}`, `subscribed (${currency} ${amount})`, ``, user, message, kofiDonationAction, data ); } function KofiResubscription(data) { if (!showKofiDonations) return; // Set the text const user = data.from; const tier = data.tier; const message = data.message; // Render avatars const avatarURL = 'icons/platforms/kofi.png'; UpdateAlertBox( 'kofi', avatarURL, `${user}`, `subscribed (${tier})`, ``, user, message, kofiDonationAction, data ); } function KofiShopOrder(data) { if (!showKofiDonations) return; // Set the text const user = data.from; const amount = data.amount; const currency = data.currency; const message = data.message; const itemTotal = data.items.length; let formattedAmount = ""; if (amount == 0) formattedAmount = "" else if (currency == "USD") formattedAmount = `$${amount}`; else formattedAmount = `${currency} ${amount}`; // Render avatars const avatarURL = 'icons/platforms/kofi.png'; UpdateAlertBox( 'kofi', avatarURL, `${user}`, `ordered ${itemTotal} item(s) on Ko-fi `, `${formattedAmount}`, user, message, kofiDonationAction, data ); } function TipeeeStreamDonation(data) { if (!showTipeeeStreamDonations) return; // Set the text const user = data.username; const amount = data.amount; const currency = data.currency; const message = data.message; // Render avatars const avatarURL = 'icons/platforms/tipeeeStream.png'; if (currency == "USD") UpdateAlertBox( 'tipeeeStream', avatarURL, `${user}`, `donated $${amount}`, ``, user, message, tipeeestreamDonationAction, data ); else UpdateAlertBox( 'tipeeeStream', avatarURL, `${user}`, `donated ${currency} ${amount}`, ``, user, message, tipeeestreamDonationAction, data ); } function FourthwallOrderPlaced(data) { if (!showFourthwallAlerts) return; // Set the text let user = data.username; const orderTotal = data.total; const currency = data.currency; const item = data.variants[0].name; const itemsOrdered = data.variants.length; const message = DecodeHTMLString(data.statmessageus); const itemImageUrl = data.variants[0].image; // If there user did not provide a username, just say "Someone" if (user == undefined) user = "Someone"; let attributeText = "" // If the user ordered more than one item, write how many items they ordered if (itemsOrdered > 1) attributeText += `and ${itemsOrdered - 1} other item(s)!`; // If the user spent money, put the order total if (orderTotal == 0) attributeText += ``; else if (currency == "USD") attributeText += ` ($${orderTotal})`; else attributeText += ` (${orderTotal} ${currency})`; UpdateAlertBox( 'fourthwall', itemImageUrl, `${user}`, `ordered ${item}`, attributeText, user, message, fourthwallAlertAction, data ); } function FourthwallDonation(data) { if (!showFourthwallAlerts) return; // Set the text let user = data.username; const amount = data.amount; const currency = data.currency; const message = data.message; let formattedAmount = ''; // If the user spent money, put the order total if (currency == "USD") formattedAmount += ` $${amount}`; else formattedAmount += ` ${currency} ${amount}`; UpdateAlertBox( 'fourthwall', '', `${user}`, `donated ${formattedAmount}`, '', user, message, fourthwallAlertAction, data ); } function FourthwallSubscriptionPurchased(data) { if (!showFourthwallAlerts) return; // Set the text let user = data.nickname; const amount = data.amount; const currency = data.currency; let formattedAmount = ''; // If the user spent money, put the order total if (currency == "USD") formattedAmount += ` ($${amount})`; else formattedAmount += ` (${currency} ${amount})`; UpdateAlertBox( 'fourthwall', '', `${user}`, `subscribed ${formattedAmount}`, '', user, '', fourthwallAlertAction, data ); } function FourthwallGiftPurchase(data) { if (!showFourthwallAlerts) return; // Set the text // 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); let contents = ''; let attributesText = ''; // If there is more than one gifted item, display the number of gifts if (gifts > 1) contents += ` ${gifts} x `; // The name of the item to be given away contents += ` ${itemName}`; // If the user spent money, put the order total if (currency == "USD") attributesText = `$${total}`; else attributesText = `${currency}${total}`; UpdateAlertBox( 'fourthwall', '', // itemImageUrl, `An item has been gifted!`,// `${user}`, `${contents}`, // `gifted ${contents}`, attributesText, '', // user, '', // message, fourthwallAlertAction, data ); } function FourthwallGiftDrawStarted(data) { if (!showFourthwallAlerts) return; // Set the text const durationSeconds = data.durationSeconds; const itemName = data.offer.name; UpdateAlertBox( 'fourthwall', '', `🎁 ${itemName} Giveaway!`, `Type 'join' in the next ${durationSeconds} seconds for your chance to win!`, '', '', '', fourthwallAlertAction, data ); } function FourthwallGiftDrawEnded(data) { if (!showFourthwallAlerts) return; UpdateAlertBox( 'fourthwall', '', `🥳 GIVEAWAY ENDED 🥳`, `Congratulations ${GetWinnersList(data.gifts)}!`, '', '', '', fourthwallAlertAction, data ); } async function KickFollow(data) { if (!showKickFollows) return; // Set the text const username = data.user.name; // 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 KickKicksGifted(data) { if (!showKickGifts) return; // Set the text const username = data.sender.username; const tiktokIcon = ``; // const giftImg = ``; const kickKicksIcon = ``; // Render avatars const avatarURL = `https://files.kick.com/kicks/gifts/${data.gift.gift_id.replace('_', '-')}.webp`; UpdateAlertBox( 'kick', avatarURL, `${username}`, `sent ${kickKicksIcon} ${data.gift.amount}`, '', username, data.message, kickGiftAction, 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 ); } ////////////////////// // HELPER FUNCTIONS // ////////////////////// // I used Gemini for this shit so if it doesn't work, blame Google function FindFirstImageUrl(jsonObject) { if (typeof jsonObject !== 'object' || jsonObject === null) { return null; // Handle invalid input } function iterate(obj) { if (Array.isArray(obj)) { for (const item of obj) { const result = iterate(item); if (result) { return result; } } return null; } for (const key in obj) { if (obj.hasOwnProperty(key)) { if (key === 'imageUrl') { return obj[key]; // Found it! Return the value. } if (typeof obj[key] === 'object' && obj[key] !== null) { const result = iterate(obj[key]); // Recursive call for nested objects if (result) { return result; // Propagate the found value } } } } return null; // Key not found in this level } return iterate(jsonObject); } function GetWinnersList(gifts) { const winners = gifts.map(gift => gift.winner); const numWinners = winners.length; if (numWinners === 0) { return ""; } else if (numWinners === 1) { return winners[0]; } else if (numWinners === 2) { return `${winners[0]} and ${winners[1]}`; } else { const lastWinner = winners.pop(); const secondLastWinner = winners.pop(); return `${winners.join(", ")}, ${secondLastWinner} and ${lastWinner}`; } } function UpdateAlertBox(platform, avatarURL, headerText, descriptionText, attributeText, username, message, sbAction, sbData) { // If the page is inactive (e.g. the alert browser source is on an inactive OBS scene) // don't run the alert if (document.visibilityState != 'visible') { console.debug('Tab is inactive. Skipping alert...'); return; } // Check if the widget is in the middle of an animation // If any alerts are requested while the animation is playing, it should be added to the alert queue if (widgetLocked) { console.debug("Animation is progress, added alert to queue"); let data = { platform: platform, avatarURL: avatarURL, headerText: headerText, descriptionText: descriptionText, attributeText: attributeText, username: username, message: message, sbAction: sbAction, sbData: sbData}; alertQueue.push(data); return; } // Start the animation widgetLocked = true; // Set the card background colors alertBox.classList = ''; if (useCustomBackground) alertBox.classList.add('customBackground'); else alertBox.classList.add(platform); // Render avatars if (showAvatar) { avatarElement.src = avatarURL; avatarSmallElement.src = avatarURL; } // Set labels usernameLabel.innerHTML = headerText != null ? headerText : ''; usernameTheSecondLabel.innerHTML = username != null ? username : ''; descriptionLabel.innerHTML = descriptionText != null ? descriptionText : ''; attributeLabel.innerHTML = attributeText != null ? attributeText : ''; messageLabel.innerHTML = message != null ? `${message}` : ''; theContentThatShowsLastInsteadOfFirst.style.opacity = 0; // Start animation theContentThatShowsFirstInsteadOfSecond.style.display = 'flex'; alertBox.style.transition = 'all 0s ease-in-out'; alertBox.style.height = theContentThatShowsFirstInsteadOfSecond.offsetHeight + "px"; alertBox.style.animation = `${showAnimation} 0.5s ease-out forwards`; // Play sound effect if (playSounds) { const audio = new Audio('sfx/notification.mp3'); audio.play(); } // Add extra useful info to sbData if (!sbData) sbData = {}; sbData.boxHeight1 = theContentThatShowsFirstInsteadOfSecond.offsetHeight; sbData.boxHeight2 = (message && showMesesages) ? theContentThatShowsLastInsteadOfFirst.offsetHeight : 0; sbData.alertDuration = hideAfter * 1000; console.log(sbData); // Run the Streamer.bot action if there is one if (globalShowAction) { console.debug('Running Streamer.bot action: ' + globalShowAction); client.doAction({name: globalShowAction}, sbData); } // Run the Streamer.bot action if there is one if (sbAction) { console.debug('Running Streamer.bot action: ' + sbAction); client.doAction({name: sbAction}, sbData); } // (1) Set timeout (8 seconds by default) // (2) Set the message label // (3) Calculate the height of message label // (4) Set the height of alertBox // (a) Add in the CSS animation when this is working setTimeout(() => { alertBox.style.transition = 'all 0.5s ease-in-out'; theContentThatShowsFirstInsteadOfSecond.style.opacity = 0; // For safety, if message doesn't exist, set it to empty string anyway if (!message) message = ''; // If there is a message, show it in the second part of the animation // Else, just close the alert box and run the next alert if (message.trim().length > 0 && showMesesages) { //theContentThatShowsLastInsteadOfFirst.style.display = 'inline-block'; theContentThatShowsLastInsteadOfFirst.style.visibility = 'visible'; alertBox.style.height = theContentThatShowsLastInsteadOfFirst.offsetHeight + "px"; theContentThatShowsLastInsteadOfFirst.style.opacity = 1; setTimeout(() => { // Run the Streamer.bot action if there is one if (globalHideAction) { console.debug('Running Streamer.bot action: ' + globalHideAction); client.doAction({name: globalHideAction}, sbData); } alertBox.style.animation = `${hideAnimation} 0.5s ease-out forwards`; setTimeout(() => { alertBox.style.height = '0px'; theContentThatShowsFirstInsteadOfSecond.style.opacity = 1; theContentThatShowsLastInsteadOfFirst.style.opacity = 0; //theContentThatShowsLastInsteadOfFirst.style.display = 'none'; theContentThatShowsLastInsteadOfFirst.style.visibility = 'hidden'; widgetLocked = false; if (alertQueue.length > 0) { console.debug("Pulling next alert from the queue"); let data = alertQueue.shift(); UpdateAlertBox(data.platform, data.avatarURL, data.headerText, data.descriptionText, data.attributeText, data.username, data.message, data.sbAction, data.sbData); } }, 1000); }, hideAfter * 1000); } else { // Run the Streamer.bot action if there is one if (globalHideAction) { console.debug('Running Streamer.bot action: ' + globalHideAction); client.doAction({name: globalHideAction}, sbData); } alertBox.style.animation = `${hideAnimation} 0.5s ease-out forwards`; setTimeout(() => { alertBox.style.height = '0px'; theContentThatShowsFirstInsteadOfSecond.style.opacity = 1; theContentThatShowsLastInsteadOfFirst.style.opacity = 0; //theContentThatShowsLastInsteadOfFirst.style.display = 'none'; theContentThatShowsLastInsteadOfFirst.style.visibility = 'hidden'; widgetLocked = false; if (alertQueue.length > 0) { console.debug("Pulling next alert from the queue"); let data = alertQueue.shift(); UpdateAlertBox(data.platform, data.avatarURL, data.headerText, data.descriptionText, data.attributeText, data.username, data.message, data.sbAction, data.sbData); } }, 1000); } }, hideAfter * 1000); } 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 // //////////////////// async function testWidget() { UpdateAlertBox( 'twitch', await GetAvatar('nutty', 'twitch'), `nutty`, `subscribed with Tier 3`, '', `nutty`, `O-oooooooooo AAAAE-A-A-I-A-U- JO-oooooooooooo AAE-O-A-A-U-U-A- E-eee-ee-eee AAAAE-A-E-I-E-A-JO-ooo-oo-oo-oo EEEEO-A-AAA-AAAA` //`` ); } /////////////////////////////////// // STREAMER.BOT WEBSOCKET STATUS // /////////////////////////////////// // This function sets the visibility of the Streamer.bot status label on the overlay function SetConnectionStatus(connected) { let statusContainer = document.getElementById("statusContainer"); if (connected) { statusContainer.style.background = "#2FB774"; statusContainer.innerText = "Connected!"; statusContainer.style.opacity = 1; setTimeout(() => { statusContainer.style.transition = "all 2s ease"; statusContainer.style.opacity = 0; }, 10); } else { statusContainer.style.background = "#D12025"; statusContainer.innerText = "Connecting..."; statusContainer.style.transition = ""; statusContainer.style.opacity = 1; } }