//////////////// // 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"; /////////////////// // 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 // ///////////////////////// 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('General.Custom', (response) => { console.debug(response.data); CustomEvent(response.data); }) //////////////////// // STREAM PRINTER // //////////////////// async function CustomEvent(data) { if (data.actionName != 'Receipt Printer | 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 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); } 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); } 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); } 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'; 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); } 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}`; if (data.totalGifts > 1) { const messageEl = document.createElement('div'); messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!`; contentEl.appendChild(messageEl); } else { contentEl.style.display = 'none'; } } 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); } break; // YouTube Events case ('YouTubeNewSponsor'): { if (data.profileImageUrl) avatarEl.src = data.profileImageUrl; else avatarEl.style.display = 'none'; titleEl.innerText = `${data.levelName}`; subtitleEl.innerText = `${data.user}`; contentEl.style.display = 'none'; } break; case ('YouTubeGiftMembershipReceived'): { if (data.profileImageUrl) avatarEl.src = data.profileImageUrl; 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); } break; case ('YouTubeSuperChat'): { if (data.profileImageUrl) avatarEl.src = data.profileImageUrl; 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); } break; case ('YouTubeSuperSticker'): { // TODO: Need to add image URL of Super Sticker if (data.profileImageUrl) avatarEl.src = data.profileImageUrl; 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); } 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.innerText = `${data.duration} months`; else messageEl.innerHTML = 'First time subscriber!'; contentEl.appendChild(messageEl); } break; case ('KickGiftSubscription'): { avatarEl.src = ConvertWEBPToPNG(data["recipient.profilePicture"]); titleEl.innerText = `Gifted Sub`; const messageEl = document.createElement('div'); messageEl.innerHTML = `${data["recipient.userName"]}
received a sub from
${data.user}!`; contentEl.appendChild(messageEl); } 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(data["recipient.profilePicture"]); 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); } } 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.message) { const messageEl = document.createElement('div'); messageEl.innerHTML = `${data.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'; // 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(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.innerText = `${data["fw.nickname"]}`; const messageEl = document.createElement('div'); messageEl.innerText = `Thanks for joining at the ${FormatCurrency(data["fw.amount"], data["fw.currency"])} tier!`; contentEl.appendChild(messageEl); } 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); client.doAction({ name: 'Receipt Printer | 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} `.trim(); return fullHTML; } function ConvertWEBPToPNG(URL) { return `https://images.weserv.nl/?url=${URL}&output=png`; } function FormatCurrency(amount, currency) { const isISOCode = /^[A-Z]{3}$/.test(currency); if (isISOCode) { try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency, currencyDisplay: 'symbol', }).format(amount); } catch { return `${amount.toFixed(2)} ${currency}`; } } // Handle some common symbols that go before the number const symbolsBefore = ['$', '€', '£', '¥', '₹']; if (symbolsBefore.includes(currency)) { return `${currency}${amount.toFixed(2)}`; } // Otherwise default to appending after return `${amount.toFixed(2)} ${currency}`; } function IsValidUrl(string) { try { new URL(string); return true; } catch { return false; } } function EscapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } /////////////////////////////////// // 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; } }