diff --git a/multistream-alerts/icons/badges/twitch-channel-point.png b/multistream-alerts/icons/badges/twitch-channel-point.png new file mode 100644 index 0000000..67d0a3e Binary files /dev/null and b/multistream-alerts/icons/badges/twitch-channel-point.png differ diff --git a/multistream-alerts/icons/badges/youtube-broadcaster.svg b/multistream-alerts/icons/badges/youtube-broadcaster.svg new file mode 100644 index 0000000..655ca1c --- /dev/null +++ b/multistream-alerts/icons/badges/youtube-broadcaster.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/multistream-alerts/icons/badges/youtube-member.svg b/multistream-alerts/icons/badges/youtube-member.svg new file mode 100644 index 0000000..12720b9 --- /dev/null +++ b/multistream-alerts/icons/badges/youtube-member.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/multistream-alerts/icons/badges/youtube-moderator.svg b/multistream-alerts/icons/badges/youtube-moderator.svg new file mode 100644 index 0000000..45b2718 --- /dev/null +++ b/multistream-alerts/icons/badges/youtube-moderator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/multistream-alerts/icons/badges/youtube-verified.svg b/multistream-alerts/icons/badges/youtube-verified.svg new file mode 100644 index 0000000..5e85909 --- /dev/null +++ b/multistream-alerts/icons/badges/youtube-verified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/multistream-alerts/icons/platforms/kofi.png b/multistream-alerts/icons/platforms/kofi.png new file mode 100644 index 0000000..8812cac Binary files /dev/null and b/multistream-alerts/icons/platforms/kofi.png differ diff --git a/multistream-alerts/icons/platforms/patreon.png b/multistream-alerts/icons/platforms/patreon.png new file mode 100644 index 0000000..22c9748 Binary files /dev/null and b/multistream-alerts/icons/platforms/patreon.png differ diff --git a/multistream-alerts/icons/platforms/tipeeeStream.png b/multistream-alerts/icons/platforms/tipeeeStream.png new file mode 100644 index 0000000..cd981f3 Binary files /dev/null and b/multistream-alerts/icons/platforms/tipeeeStream.png differ diff --git a/multistream-alerts/icons/platforms/twitch.png b/multistream-alerts/icons/platforms/twitch.png new file mode 100644 index 0000000..dcbd6c8 Binary files /dev/null and b/multistream-alerts/icons/platforms/twitch.png differ diff --git a/multistream-alerts/icons/platforms/youtube.png b/multistream-alerts/icons/platforms/youtube.png new file mode 100644 index 0000000..5abdb7b Binary files /dev/null and b/multistream-alerts/icons/platforms/youtube.png differ diff --git a/multistream-alerts/index.html b/multistream-alerts/index.html new file mode 100644 index 0000000..ab66f44 --- /dev/null +++ b/multistream-alerts/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + +
Connecting...
+
+
+ +
+ hotarubi_TA +
+ gifted a Tier 3 sub +
+ to nutty +
+
+
+ + + + + \ No newline at end of file diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js new file mode 100644 index 0000000..654cdf4 --- /dev/null +++ b/multistream-alerts/script.js @@ -0,0 +1,1154 @@ +//////////////// +// 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 avatarMap = new Map(); + +const alertBox = document.getElementById('alertBox'); +const avatarElement = document.getElementById('avatar'); +const usernameLabel = document.getElementById('username'); +const descriptionLabel = document.getElementById('description'); +const attributeLabel = document.getElementById('attribute'); + +///////////// +// OPTIONS // +///////////// + +const showPlatform = GetBooleanParam("showPlatform", true); +const showAvatar = GetBooleanParam("showAvatar", true); +const showTimestamps = GetBooleanParam("showTimestamps", true); +const showBadges = GetBooleanParam("showBadges", true); +const showPronouns = GetBooleanParam("showPronouns", true); +const showUsername = GetBooleanParam("showUsername", true); +const showMessage = GetBooleanParam("showMessage", true); +const font = urlParams.get("font") || ""; +const fontSize = urlParams.get("fontSize") || "30"; +const lineSpacing = urlParams.get("lineSpacing") || "1.7"; +const background = urlParams.get("background") || "#000000"; +const opacity = urlParams.get("opacity") || "0.85"; + +const hideAfter = GetIntParam("hideAfter", 0); +const excludeCommands = GetBooleanParam("excludeCommands", true); +const ignoreChatters = urlParams.get("ignoreChatters") || ""; +const scrollDirection = GetIntParam("scrollDirection", 1); +const inlineChat = GetBooleanParam("inlineChat", false); +const imageEmbedPermissionLevel = GetIntParam("imageEmbedPermissionLevel", 20); + +const showTwitchMessages = GetBooleanParam("showTwitchMessages", true); +const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true); +const showTwitchSubs = GetBooleanParam("showTwitchSubs", true); +const showTwitchChannelPointRedemptions = GetBooleanParam("showTwitchChannelPointRedemptions", true); +const showTwitchRaids = GetBooleanParam("showTwitchRaids", true); +const showTwitchSharedChat = GetIntParam("showTwitchSharedChat", 2); + +const showYouTubeMessages = GetBooleanParam("showYouTubeMessages", true); +const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true); +const showYouTubeSuperStickers = GetBooleanParam("showYouTubeSuperStickers", true); +const showYouTubeMemberships = GetBooleanParam("showYouTubeMemberships", true); + +const showStreamlabsDonations = GetBooleanParam("showStreamlabsDonations", true) +const showStreamElementsTips = GetBooleanParam("showStreamElementsTips", true); +const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true); +const showKofiDonations = GetBooleanParam("showKofiDonations", true); +const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true); +const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true); + +const furryMode = GetBooleanParam("furryMode", false); + +// Set fonts for the widget +document.body.style.fontFamily = font; +document.body.style.fontSize = `${fontSize}px`; + +// // Set line spacing +// document.documentElement.style.setProperty('--line-spacing', `${lineSpacing}em`); + +// // Set the background color +// 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}`; + +// // Get a list of chatters to ignore +// const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || []; + +// // Set the scroll direction +// switch (scrollDirection) { +// case 1: +// document.getElementById('messageList').classList.add('normalScrollDirection'); +// break; +// case 2: +// document.getElementById('messageList').classList.add('reverseScrollDirection'); +// 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); + }, + + onDisconnect: () => { + console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) + SetConnectionStatus(false); + } +}); + +client.on('Twitch.Cheer', (response) => { + console.debug(response.data); + TwitchChatMessage(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.RewardRedemption', (response) => { + console.debug(response.data); + TwitchRewardRedemption(response.data); +}) + +client.on('Twitch.Raid', (response) => { + console.debug(response.data); + TwitchRaid(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('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); +}) + + + +/////////////////////// +// MULTICHAT OVERLAY // +/////////////////////// + +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 + if (showAvatar) { + const avatarURL = await GetAvatar(username); + avatarElement.src = avatarURL; + } + + if (!isPrime) + UpdateAlertBox('twitch', `${username}`, `subscribed with Tier ${subTier.charAt(0)}`); + else + UpdateAlertBox('twitch', `${username}`, `used their Prime Sub`); +} + +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 + if (showAvatar) { + const avatarURL = await GetAvatar(username); + avatarElement.src = avatarURL; + } + + if (!isPrime) + UpdateAlertBox( + 'twitch', + `${username}`, + `resubscribed with Tier ${subTier.charAt(0)}`, + `${cumulativeMonths} months`, + message + ); + else + UpdateAlertBox( + 'twitch', + `${username}`, + `used their Prime Sub`, + `${cumulativeMonths} months`, + message + ); + + AddMessageItem(instance, data.messageId); +} + +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; + + // Render avatars + if (showAvatar) { + const avatarURL = await GetAvatar(username); + avatarElement.src = avatarURL; + } + + let messageText = ''; + if (cumlativeTotal > 0) + messageText = `They've gifted ${cumlativeTotal} subs in total!`; + + UpdateAlertBox( + 'twitch', + `${username}`, + `gifted a Tier ${subTier.charAt(0)} subscription`, + `to ${recipient}`, + messageText + ); +} + +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 + if (showAvatar) { + const username = data.user_login; + const avatarURL = await GetAvatar(username); + avatarElement.src = avatarURL; + } + + UpdateAlertBox( + 'twitch', + `${username} redeemed`, + `${rewardName} ${channelPointIcon} ${cost}`, + '', + userInput + ); +} + +async function TwitchRaid(data) { + if (!showTwitchRaids) + return; + + // Render avatars + if (showAvatar) { + const username = data.from_broadcaster_user_login; + const avatarURL = await GetAvatar(username); + avatarElement.src = avatarURL; + } + + // Set the text + const username = data.from_broadcaster_user_login; + const viewers = data.viewers; + + UpdateAlertBox( + 'twitch', + `${username}`, + `is raiding with a party of ${viewers}`, + '', + '' + ); +} + +function YouTubeSuperChat(data) { + if (!showYouTubeSuperChats) + return; + + // Render avatars + if (showAvatar) { + avatar.src = data.user.profileImageUrl; + } + + UpdateAlertBox( + 'youtube', + `🪙 ${data.user.name}`, + `sent a Super Chat (${data.amount})`, + '', + data.message + ); +} + +function YouTubeSuperSticker(data) { + if (!showYouTubeSuperStickers) + return; + + // Render avatars + if (showAvatar) { + avatar.src = FindFirstImageUrl(data); + } + + UpdateAlertBox( + 'youtube', + `${data.user.name}`, + `sent a Super Sticker (${data.amount})`, + '', + '' + ); +} + +function YouTubeNewSponsor(data) { + if (!showYouTubeMemberships) + return; + + // Render avatars + if (showAvatar) { + avatar.src = data.user.profileImageUrl; + } + + UpdateAlertBox( + 'youtube', + `⭐ New ${data.levelName}`, + `Welcome ${data.user.name}!`, + '', + '' + ); +} + +function YouTubeGiftMembershipReceived(data) { + if (!showYouTubeMemberships) + return; + + // Render avatars + if (showAvatar) { + avatar.src = data.user.profileImageUrl; + } + + UpdateAlertBox( + 'youtube', + `${data.gifter.name}`, + `gifted a membership`, + `to ${data.user.name} (${data.tier})!`, + '' + ); +} + +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; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'streamlabs', + `${donater}`, + `donated ${currency}${formattedAmount}`, + ``, + message + ); +} + +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; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'streamelements', + `${donater}`, + `donated ${currency}${formattedAmount}`, + ``, + message + ); +} + +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 + if (showAvatar) { + avatar.src = "icons/platforms/kofi.png"; + } + + if (currency == "USD") + UpdateAlertBox( + 'kofi', + `${user}`, + `donated $${amount}`, + ``, + message + ); + else + UpdateAlertBox( + 'kofi', + `${user}`, + `donated ${currency} ${amount}`, + ``, + message + ); +} + +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 + if (showAvatar) { + avatar.src = "icons/platforms/kofi.png"; + } + + if (currency == "USD") + UpdateAlertBox( + 'kofi', + `${user}`, + `subscribed ($${amount})`, + ``, + message + ); + else + UpdateAlertBox( + 'kofi', + `${user}`, + `subscribed (${currency} ${amount})`, + ``, + message + ); +} + +function KofiResubscription(data) { + if (!showKofiDonations) + return; + + // Set the text + const user = data.from; + const tier = data.tier; + const message = data.message; + + // Render avatars + if (showAvatar) { + avatar.src = "icons/platforms/kofi.png"; + } + + UpdateAlertBox( + 'kofi', + `${user}`, + `subscribed (${tier})`, + ``, + message + ); +} + +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 + if (showAvatar) { + avatar.src = "icons/platforms/kofi.png"; + } + + UpdateAlertBox( + 'kofi', + `${user}`, + `ordered ${itemTotal} item(s) on Ko-fi `, + `${formattedAmount}`, + message + ); +} + +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 + if (showAvatar) { + avatar.src = "icons/platforms/tipeeeStream.png"; + } + + if (currency == "USD") + UpdateAlertBox( + 'tipeeeStream', + `${user}`, + `donated $${amount}`, + ``, + message + ); + else + UpdateAlertBox( + 'tipeeeStream', + `${user}`, + `donated ${currency} ${amount}`, + ``, + message + ); +} + +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"; + + // Render avatars + if (showAvatar) { + avatar.src = itemImageUrl; + } + + 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', + `${user}`, + `ordered ${item}`, + attributeText, + message + ); +} + +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}`; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'fourthwall', + `${user}`, + `donated ${formattedAmount}`, + '', + message + ); +} + +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})`; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'fourthwall', + `${user}`, + `subscribed ${formattedAmount}`, + '', + '' + ); +} + +function FourthwallGiftPurchase(data) { + console.log(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}`; + + // Render avatars + if (showAvatar) { + avatar.src = itemImageUrl; + } + + UpdateAlertBox( + 'fourthwall', + `${user}`, + `gifted ${contents}`, + attributesText, + message + ); +} + +function FourthwallGiftDrawStarted(data) { + if (!showFourthwallAlerts) + return; + + // Set the text + const durationSeconds = data.durationSeconds; + const itemName = data.offer.name; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'fourthwall', + `🎁 ${itemName} Giveaway!`, + `Type !join in the next ${durationSeconds} seconds for your chance to win!`, + '', + '' + ); +} + +function FourthwallGiftDrawEnded(data) { + if (!showFourthwallAlerts) + return; + + // Render avatars + if (showAvatar) { + avatar.src = ''; + } + + UpdateAlertBox( + 'fourthwall', + `🥳 GIVEAWAY ENDED 🥳`, + `Congratulations ${GetWinnersList(data.gifts)}!`, + '', + '' + ); +} + + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + +function GetBooleanParam(paramName, defaultValue) { + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); + + if (paramValue === null) { + return defaultValue; // Parameter not found + } + + const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity + + if (lowercaseValue === 'true') { + return true; + } else if (lowercaseValue === 'false') { + return false; + } else { + return paramValue; // Return original string if not 'true' or 'false' + } +} + +function GetIntParam(paramName, defaultValue) { + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); + + if (paramValue === null) { + return defaultValue; // or undefined, or a default value, depending on your needs + } + + console.log(paramValue); + + const intValue = parseInt(paramValue, 10); // Parse as base 10 integer + + if (isNaN(intValue)) { + return null; // or handle the error in another way, e.g., throw an error + } + + return intValue; +} + +// function GetCurrentTimeFormatted() { +// const now = new Date(); +// let hours = now.getHours(); +// const minutes = String(now.getMinutes()).padStart(2, '0'); +// const ampm = hours >= 12 ? 'PM' : 'AM'; + +// hours = hours % 12; +// hours = hours ? hours : 12; // the hour '0' should be '12' + +// const formattedTime = `${hours}:${minutes} ${ampm}`; +// return formattedTime; +// } + +async function GetAvatar(username) { + if (avatarMap.has(username)) { + console.debug(`Avatar found for ${username}. Retrieving from hash map.`) + return avatarMap.get(username); + } + 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; + } +} + +// async function GetPronouns(platform, username) { +// const response = await client.getUserPronouns(platform, username); +// const userFound = response.pronoun.userFound; +// const pronouns = `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`; + +// if (userFound) +// return `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`; +// else +// return ''; +// } + +// function IsImageUrl(url) { +// return url.match(/^http.*\.(jpeg|jpg|gif|png)$/) != null; +// } + +// function IsImageUrl(url) { +// try { +// const { pathname } = new URL(url); +// // Only check the pathname since query parameters are not included in it. +// return /\.(png|jpe?g|webp|gif)$/i.test(pathname); +// } catch (error) { +// // Return false if the URL is invalid. +// return false; +// } +// } + +// function AddMessageItem(element, elementID, platform, userId) { +// // Calculate the height of the div before inserting +// const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList'); +// const tempDivTwoElectricBoogaloo = document.createElement('div'); +// tempDivTwoElectricBoogaloo.appendChild(element); +// tempDiv.appendChild(tempDivTwoElectricBoogaloo); + +// setTimeout(function () { +// const calculatedHeight = tempDivTwoElectricBoogaloo.offsetHeight + "px"; + +// // Create a new line item to add to the message list later +// var lineItem = document.createElement('li'); +// lineItem.id = elementID; +// lineItem.dataset.platform = platform; +// lineItem.dataset.userId = userId; + +// // Set scroll direction +// if (scrollDirection == 2) +// lineItem.classList.add('reverseLineItemDirection'); + +// // Move the element from the temp div to the new line item +// lineItem.appendChild(tempDiv.firstElementChild); + +// // Add the line item to the list and animate it +// // We need to manually set the height as straight CSS can't animate on "height: auto" +// messageList.appendChild(lineItem); +// setTimeout(function () { +// lineItem.className = lineItem.className + " show"; +// lineItem.style.maxHeight = calculatedHeight; +// // After it's done animating, remove the height constraint in case the div needs to get bigger +// setTimeout(function () { +// lineItem.style.maxHeight = "none"; +// }, 1000); +// }, 10); + +// // Remove old messages that have gone off screen to save memory +// while (messageList.clientHeight > 5 * window.innerHeight) { +// messageList.removeChild(messageList.firstChild); +// } + +// if (hideAfter > 0) { +// setTimeout(function () { +// lineItem.style.opacity = 0; +// setTimeout(function () { +// messageList.removeChild(lineItem); +// }, 1000); +// }, hideAfter * 1000); +// } + +// }, 200); +// } + +function DecodeHTMLString(html) { + var txt = document.createElement("textarea"); + txt.innerHTML = html; + return txt.value; +} + +// I used Gemini for this shit so if it doesn't work, blame Google +function FindFirstImageUrl(jsonObject) { + if (typeof jsonObject !== 'object' || jsonObject === null) { + return null; // Handle invalid input + } + + function iterate(obj) { + if (Array.isArray(obj)) { + for (const item of obj) { + const result = iterate(item); + if (result) { + return result; + } + } + return null; + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === 'imageUrl') { + return obj[key]; // Found it! Return the value. + } + + if (typeof obj[key] === 'object' && obj[key] !== null) { + const result = iterate(obj[key]); // Recursive call for nested objects + if (result) { + return result; // Propagate the found value + } + } + } + } + return null; // Key not found in this level + } + + return iterate(jsonObject); +} + +// function IsThisUserAllowedToPostImagesOrNotReturnTrueIfTheyCanReturnFalseIfTheyCannot(targetPermissions, data, platform) { +// return GetPermissionLevel(data, platform) >= targetPermissions; +// } + +// function GetPermissionLevel(data, platform) { +// switch (platform) { +// case 'twitch': +// if (data.message.role >= 4) +// return 40; +// else if (data.message.role >= 3) +// return 30; +// else if (data.message.role >= 2) +// return 20; +// else if (data.message.role >= 2 || data.message.subscriber) +// return 15; +// else +// return 10; +// case 'youtube': +// if (data.user.isOwner) +// return 40; +// else if (data.user.isModerator) +// return 30; +// else if (data.user.isSponsor) +// return 15; +// else +// return 10; +// } +// } + +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 TranslateToFurry(sentence) { +// const words = sentence.toLowerCase().split(/\b/); + +// const furryWords = words.map(word => { +// if (/\w+/.test(word)) { +// let newWord = word; + +// // Common substitutions +// newWord = newWord.replace(/l/g, 'w'); +// newWord = newWord.replace(/r/g, 'w'); +// newWord = newWord.replace(/th/g, 'f'); +// newWord = newWord.replace(/you/g, 'yous'); +// newWord = newWord.replace(/my/g, 'mah'); +// newWord = newWord.replace(/me/g, 'meh'); +// newWord = newWord.replace(/am/g, 'am'); +// newWord = newWord.replace(/is/g, 'is'); +// newWord = newWord.replace(/are/g, 'are'); +// newWord = newWord.replace(/very/g, 'vewy'); +// newWord = newWord.replace(/pretty/g, 'pwetty'); +// newWord = newWord.replace(/little/g, 'wittle'); +// newWord = newWord.replace(/nice/g, 'nyce'); + +// // Random additions +// if (Math.random() < 0.15) { +// newWord += ' nya~'; +// } else if (Math.random() < 0.1) { +// newWord += ' >w<'; +// } else if (Math.random() < 0.05) { +// newWord += ' owo'; +// } + +// return newWord; +// } +// return word; +// }); + +// return furryWords.join(''); +// } + +function UpdateAlertBox(platform, usernameText, descriptionText, attributeText, message) { + // Set the card background colors + alertBox.classList = ''; + alertBox.classList.add(platform); + + usernameLabel.innerHTML = usernameText != null ? usernameText : ''; + descriptionLabel.innerHTML = descriptionText != null ? descriptionText : ''; + attributeLabel.innerHTML = attributeText != null ? attributeText : ''; +} + + +/////////////////////////////////// +// 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; + } +} \ No newline at end of file diff --git a/multistream-alerts/settings/index.html b/multistream-alerts/settings/index.html new file mode 100644 index 0000000..7ddcb25 --- /dev/null +++ b/multistream-alerts/settings/index.html @@ -0,0 +1,64 @@ + + + + + + + Settings + + + + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/multistream-alerts/settings/script.js b/multistream-alerts/settings/script.js new file mode 100644 index 0000000..45fcbf9 --- /dev/null +++ b/multistream-alerts/settings/script.js @@ -0,0 +1,29 @@ +let settingsContainer = document.getElementById('settings-container'); +settingsContainer.src = `../../.utilities/settings-page-builder?settingsJson=${window.location.href}/settings.json` +console.log(settingsContainer.src); + +function reloadWidget(data) { + let widget = document.getElementById("widget"); + widget.src = `${getParentUrl()}?${data}`; +} + +function getParentUrl() { + const currentUrl = window.location.href; + const urlParts = currentUrl.split('/'); + + // Remove the last part of the URL (the current page/file) + urlParts.pop(); + + // Remove the last part again to go one directory up + urlParts.pop(); + + // Reconstruct the URL + const parentUrl = urlParts.join('/'); + + // Ensure there's a trailing slash if necessary (if it was a directory) + if (urlParts.length > 2 && !parentUrl.endsWith('/')) { + return parentUrl + '/'; + } + + return parentUrl; +} \ No newline at end of file diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json new file mode 100644 index 0000000..412d29f --- /dev/null +++ b/multistream-alerts/settings/settings.json @@ -0,0 +1,359 @@ +{ + "settings": [ + { + "id": "showPlatform", + "label": "Show Platform", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showAvatar", + "label": "Show Avatar", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showTimestamps", + "label": "Show Timestamps", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showBadges", + "label": "Show Badges", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showPronouns", + "label": "Show Pronouns", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showUsername", + "label": "Show Username", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "showMessage", + "label": "Show Message", + "description": "Honestly, if you turn this off, you're stupid lmao", + "type": "checkbox", + "defaultValue": true, + "group": "Appearance" + }, + { + "id": "font", + "label": "Font", + "description": "", + "type": "text", + "defaultValue": "", + "group": "Appearance" + }, + { + "id": "fontSize", + "label": "Font Size", + "description": "", + "type": "number", + "min": 1, + "max": 120, + "defaultValue": 30, + "group": "Appearance" + }, + { + "id": "lineSpacing", + "label": "Line Spacing", + "description": "", + "type": "number", + "min": 0, + "defaultValue": 1.7, + "step": ".1", + "group": "Appearance" + }, + { + "id": "background", + "label": "Background", + "description": "", + "type": "color", + "defaultValue": "#000000", + "group": "Appearance" + }, + { + "id": "opacity", + "label": "Background Opacity", + "description": "", + "type": "number", + "defaultValue": "0.85", + "min": 0, + "max": 1, + "step": ".01", + "group": "Appearance" + }, + { + "id": "hideAfter", + "label": "Hide After", + "description": "Messages fade after X seconds (0 to disable)", + "type": "number", + "min": 0, + "max": 120, + "defaultValue": 0, + "group": "General" + }, + { + "id": "excludeCommands", + "label": "Exclude Commands", + "description": "Hide messages starting with \"!\"", + "type": "checkbox", + "defaultValue": true, + "group": "General" + }, + { + "id": "ignoreChatters", + "label": "Ignore Chatters", + "description": "Ignored chatters will not be shown", + "type": "text", + "defaultValue": "StreamElements,Streamlabs", + "group": "General" + }, + { + "id": "scrollDirection", + "label": "Scroll Direction", + "description": "", + "type": "select", + "options": [ + { + "value": 1, + "label": "Normal" + }, + { + "value": 2, + "label": "Reversed" + } + ], + "defaultValue": 1, + "group": "General" + }, + { + "id": "inlineChat", + "label": "In-line Chat", + "description": "Remove the line space after the username", + "type": "checkbox", + "defaultValue": false, + "group": "General" + }, + { + "id": "imageEmbedPermissionLevel", + "label": "Embed Images", + "description": "Automatically embed images for these users", + "type": "select", + "options": [ + { + "value": 40, + "label": "Broadcaster" + }, + { + "value": 30, + "label": "Mods & Broadcaster" + }, + { + "value": 20, + "label": "VIPs, Mods & Broadcaster" + }, + { + "value": 15, + "label": "Subs, VIPs, Mods & Broadcaster" + }, + { + "value": 10, + "label": "Everyone" + }, + { + "value": 69420, + "label": "Nobody" + } + ], + "defaultValue": 20, + "group": "General" + }, + { + "id": "showTwitchMessages", + "label": "Chat Messages", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showTwitchAnnouncements", + "label": "Announcements", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showTwitchSubs", + "label": "New Subscribers", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showTwitchChannelPointRedemptions", + "label": "Channel Point Redemptions", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showTwitchRaids", + "label": "Raids", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showTwitchSharedChat", + "label": "Shared Chat Messages", + "description": "", + "type": "select", + "options": [ + { + "value": 2, + "label": "Show & Highlight" + }, + { + "value": 1, + "label": "Show but do not highlight" + }, + { + "value": 0, + "label": "Do not show" + } + ], + "defaultValue": 2, + "group": "Which Twitch messages do you want to see?" + }, + { + "id": "showYouTubeMessages", + "label": "Chat Messages", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which YouTube messages do you want to see?" + }, + { + "id": "showYouTubeSuperChats", + "label": "Super Chats", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which YouTube messages do you want to see?" + }, + { + "id": "showYouTubeSuperStickers", + "label": "Super Stickers", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which YouTube messages do you want to see?" + }, + { + "id": "showYouTubeMemberships", + "label": "Memberships", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "Which YouTube messages do you want to see?" + }, + { + "id": "showStreamlabsDonations", + "label": "Streamlabs Tips", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "showStreamElementsTips", + "label": "StreamElements Tips", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "showPatreonMemberships", + "label": "Patreon Memberships", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "showKofiDonations", + "label": "Ko-fi Donations", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "showTipeeeStreamDonations", + "label": "TipeeeStream Donations", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "showFourthwallAlerts", + "label": "Fourthwall Alerts", + "description": "Click to connect", + "type": "checkbox", + "defaultValue": true, + "group": "Which donation messages do you want to see?" + }, + { + "id": "furryMode", + "label": "Furry Mode", + "description": "Meow", + "type": "checkbox", + "defaultValue": false, + "group": "Very Important Stuff" + }, + { + "id": "address", + "label": "Address", + "description": "Streamer.bot Websocket Server Address", + "type": "text", + "defaultValue": "127.0.0.1", + "group": "Advanced" + }, + { + "id": "port", + "label": "Port", + "description": "Streamer.bot Websocket Server Port", + "type": "text", + "defaultValue": "8080", + "group": "Advanced" + } + ] +} \ No newline at end of file diff --git a/multistream-alerts/style.css b/multistream-alerts/style.css new file mode 100644 index 0000000..ba631f5 --- /dev/null +++ b/multistream-alerts/style.css @@ -0,0 +1,206 @@ +* { + margin: 0; + padding: 0; + --margin: 40px; + /* --border-radius: 20px; */ + font-size: 30px; + /* --line-spacing: 1.7em; */ +} + +body { + overflow: hidden; +} + +html { + width: 100vw; + height: 100vh; + scroll-behavior: smooth; + /* background: #000000d9; */ + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + color: white; + text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); + overflow-wrap: break-word; +} + +#statusContainer { + font-weight: 500; + font-size: 30px; + text-align: center; + background-color: #D12025; + color: white; + padding: 10px; + border-radius: 10px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +#mainContainer { + display: block; + position: absolute; + width: 100%; + padding: var(--margin); + box-sizing: border-box; +} + +#alertBox { + display: flex; + padding: 0.5em; + border-radius: 0.75em; + filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1)); + position: relative; + align-items: center; + overflow: hidden; + /* text-align: center; */ +} + +/* #alertBox::before { + content: ""; + position: absolute; + top: -50px; + right: -50px; + bottom: -50px; + left: -50px; + background-image: url('https://static-cdn.jtvnw.net/jtv_user_pictures/272e643e-4d43-415f-9ee1-18916e825411-profile_image-300x300.png'); + background-color: rgba(0, 0, 0, 0.6); + background-size: cover; + background-position: center; + background-blend-mode: multiply; + filter: blur(25px); + z-index: -1; +} */ + +#avatar { + display: inline; + max-width: 100%; + height: 4em; + filter: drop-shadow(0px 0px 7px rgba(0, 0, 0, 0.5)); + object-fit: cover; + margin-right: 0.75em; +} + +#contents { + flex-grow: 1; +} + +#username { + font-size: 1.2em; + font-weight: 600; +} + +#description { + font-size: 1em; + font-weight: 400; +} + +#attribute { + font-size: 0.8em; + font-weight: 400; +} + +.profilePic { + border-radius: 50%; +} + +.square { + border-radius: 0.5em; +} + +/*****************/ +/** CARD COLORS **/ +/*****************/ + +.announcementBlue { + background: linear-gradient(#03d3d7BF, #8d49feBF) !important; +} + +.announcementGreen { + background: linear-gradient(#01da86BF, #55bee4BF) !important; +} + +.announcementOrange { + background: linear-gradient(#feb419BF, #e1df00BF) !important; +} + +.announcementPurple { + background: linear-gradient(#9548ffBF, #fc74e6BF) !important; +} + +.twitch { + background: linear-gradient(#6d5ca1bf, #9146ffBF) !important; +} + +.youtube { + background: linear-gradient(#FF6C60BF, #FF0707BF) !important; +} + +.streamlabs { + background: linear-gradient(#73dabbbf, #397765bf) !important; +} + +.streamelements { + background: linear-gradient(#263b8abf, #0a112abf) !important; +} + +.patreon { + background: linear-gradient(#c76633bf, #fd5e0abf) !important; +} + +.kofi { + background: linear-gradient(#54c7eebf, #08c2ffbf) !important; +} + +.tipeeeStream { + background: linear-gradient(#120e23bf, #5d192abf) !important; +} + +.fourthwall { + background: linear-gradient(#466edbbf, #1c56f5bf) !important; +} + +.blank { + background: #adadad00 !important; +} + +.highlightMessage { + background: #adadad46; +} + +/****************/ +/** ANIMATIONS **/ +/****************/ + +.statusConnected { + animation-name: statusConnected; + animation-duration: 2s; + animation-fill-mode: forwards; +} + +.statusDisconnected { + animation-name: statusDisconnected; + animation-duration: 0.5s; + animation-fill-mode: forwards; +} + +@keyframes statusConnected { + 0% { + opacity: 1; + background-color: #2FB774; + } + + 100% { + opacity: 0; + background-color: #2FB774; + } +} + +@keyframes statusDisconnected { + 0% { + background-color: #D12025; + } + + 100% { + background-color: #D12025; + } +} \ No newline at end of file