/* Copyright (C) 2023 Max @sigmacw_ and Matt @mattsquare_. * * You may not distribute this code without obtaining explicit permission * from Max @sigmacw_ and Matt @mattsquare_. * * All rights reserved. */ let ver = "1.0" // Defining useful global variables let msgLimit, hideAfter, hideCommands, provider, hasEmotes, widget_width; let totalMsg = 0; let ignoredUsers = []; let commands = []; let removeSelector; let displayBadges = 'no'; let displayPronouns = 'no'; let displayUpdates = true let colors_enabled = true let test_streamer, test_mod, test_sub, test_reg; let previousName = null; let previousMsg = null; let displayedMsg = [] let events = { cheer: { enabled: false, min: 1, class: "cheer" }, tip: { enabled: false, min: 1, currency: "$", class: "tip" }, sub: { enabled: false, class: "sub" }, resub: { enabled: false, class: "resub" }, gift: { enabled: false, class: "gift" }, direct: { enabled: false, class: "direct" } } // Initial setup on widget startup window.addEventListener('onWidgetLoad', async function (obj) { // Grabbing chat settings from fields const fieldData = obj.detail.fieldData; hideAfter = fieldData.hideAfter; msgLimit = fieldData.messagesLimit; hideCommands = fieldData.hideCommands; displayPronouns = fieldData.displayPronouns; events.cheer.enabled = fieldData.cheerEnabled; events.cheer.min = fieldData.cheerMin; events.tip.enabled = fieldData.tipEnabled; events.tip.min = fieldData.tipMin; events.tip.currency = fieldData.tipCurrency; events.sub.enabled = fieldData.subEnabled; events.resub.enabled = fieldData.resubEnabled; events.gift.enabled = fieldData.giftEnabled; events.direct.enabled = fieldData.directEnabled; widget_width = $(document).width(); channelName = fieldData.testMessageName test_streamer = new CustomEvent("onEventReceived", { detail: { listener: "message", event: { service: "twitch", data: { time: Date.now(), tags: { "badge-info": "", badges: "broadcaster/1,subscriber/1", color: "#5B99FF", "display-name": "StreamElements", emotes: "25:46-50", flags: "", id: "43285909-412c-4eee-b80d-89f72ba53142", mod: "1", "room-id": "85827806", subscriber: "0", "tmi-sent-ts": "1579444549265", turbo: "0", "user-id": "100135110", "user-type": "mod" }, nick: channelName, userId: "100135110", displayName: "Broadcaster", displayColor: "#5B99FF", badges: [{ type: "moderator", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3", description: "Moderator" }, { type: "partner", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3", description: "Verified" }], channel: channelName, text: "Check it out - only on Ko-Fi!", isAction: !1, emotes: [{ type: "twitch", name: "Kappa", id: "25", gif: !1, urls: { 1: "https://static-cdn.jtvnw.net/emoticons/v1/25/1.0", 2: "https://static-cdn.jtvnw.net/emoticons/v1/25/1.0", 4: "https://static-cdn.jtvnw.net/emoticons/v1/25/3.0" }, start: 46, end: 50 }], msgId: "" }, renderedText: 'Lorem Ipsum Broadcaster Test' } } }); test_mod = new CustomEvent("onEventReceived", { detail: { listener: "message", event: { service: "twitch", data: { time: Date.now(), tags: { "badge-info": "", badges: "moderator/1,partner/1", color: "#5B99FF", "display-name": "StreamElements", emotes: "25:46-50", flags: "", id: "43285909-412c-4eee-b80d-89f72ba53142", mod: "1", "room-id": "85827806", subscriber: "0", "tmi-sent-ts": "1579444549265", turbo: "0", "user-id": "100135110", "user-type": "mod" }, nick: channelName, userId: "100135110", displayName: 'Moderator', displayColor: "#5B99FF", badges: [{ type: "moderator", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3", description: "Moderator" }, { type: "partner", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3", description: "Verified" }], channel: channelName, text: "Custom alert are here too! Chat can send you their love-letters.", isAction: !1, emotes: [], msgId: "" }, renderedText: 'Test message. ' } } }); test_sub = new CustomEvent("onEventReceived", { detail: { listener: "message", event: { service: "twitch", data: { time: Date.now(), tags: { "badge-info": "", badges: "subscriber/1", color: "#5B99FF", "display-name": "StreamElements", emotes: "25:46-50", flags: "", id: "43285909-412c-4eee-b80d-89f72ba53142", mod: "1", "room-id": "85827806", subscriber: "0", "tmi-sent-ts": "1579444549265", turbo: "0", "user-id": "100135110", "user-type": "mod" }, nick: channelName, userId: "100135110", displayName: 'Subscriber', displayColor: "#5B99FF", badges: [{ type: "moderator", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3", description: "Moderator" }, { type: "partner", version: "1", url: "https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3", description: "Verified" }], channel: channelName, text: "To celebrate, a Free to Use, Valentine's themed, hearty chat widget.", isAction: !1, emotes: [], msgId: "" }, renderedText: 'Test message. ' } } }); test_reg = new CustomEvent("onEventReceived", { detail: { listener: "message", event: { service: "twitch", data: { time: Date.now(), tags: { "badge-info": "", badges: "", color: "#5B99FF", "display-name": "StreamElements", emotes: "25:46-50", flags: "", id: "43285909-412c-4eee-b80d-89f72ba53142", mod: "1", "room-id": "85827806", subscriber: "0", "tmi-sent-ts": "1579444549265", turbo: "0", "user-id": "100135110", "user-type": "mod" }, nick: channelName, userId: "100135110", displayName: channelName, displayColor: "#5B99FF", badges: [], channel: channelName, text: "Test message. Kappa", isAction: !1, emotes: [], msgId: "" }, renderedText: 'Test message. ' } } }); test_reg.detail.event.data.text = fieldData.testMessageText; // Grabbing the service used for chatbox (Twitch, YouTube, Mixer) fetch('https://api.streamelements.com/kappa/v2/channels/' + obj.detail.channel.id + '/') .then(response => response.json()).then((profile) => { provider = profile.provider; }); removeSelector = ".message-row:nth-last-child(n+" + (msgLimit + 1) + ")" // Setting up the list of users whose messages will be ignored ignoredUsers = fieldData.ignoredUsers.toLowerCase() .replace(" ", "").split(","); commands = fieldData.ignoredCommands.toLowerCase() .replace(" ", "").split(","); }); // Listen for StreamElements events window.addEventListener('onEventReceived', function (obj) { // Define what to do on different events if (obj.detail.event.listener === 'widget-button') { switch (obj.detail.event.field) { case 'testMessage': test_streamer.detail.event.data.msgId = (Math.random() + 1).toString(36).substring(7) + (Math.random() + 1).toString(36).substring(7) window.dispatchEvent(test_streamer); break; case 'testMessage1': test_mod.detail.event.data.msgId = (Math.random() + 1).toString(36).substring(7) + (Math.random() + 1).toString(36).substring(7) window.dispatchEvent(test_mod); break case 'testMessage2': test_sub.detail.event.data.msgId = (Math.random() + 1).toString(36).substring(7) + (Math.random() + 1).toString(36).substring(7) window.dispatchEvent(test_sub); break case 'testMessage3': test_reg.detail.event.data.msgId = (Math.random() + 1).toString(36).substring(7) + (Math.random() + 1).toString(36).substring(7) window.dispatchEvent(test_reg); break } } // Check for events that correspond to message deletion if (obj.detail.listener === "delete-message") { const msgId = obj.detail.event.msgId; $(`.message-row[data-msgid=${msgId}]`).remove(); return; } else if (obj.detail.listener === "delete-messages") { const sender = obj.detail.event.userId; $(`.message-row[data-sender=${sender}]`).remove(); return; } if (obj.detail.listener === "cheer-latest") { let evt = obj.detail.event; let username = evt.name; let amount = evt.amount; if (amount < events.cheer.min || !events.cheer.enabled) { return } alertCheer(username, amount); return } if (obj.detail.listener === "tip-latest") { let evt = obj.detail.event; let username = evt.name; let amount = evt.amount; if (amount < events.tip.min || !events.tip.enabled) { return } alertTip(username, amount); return } if (obj.detail.listener === "subscriber-latest") { let evt = obj.detail.event; let username = evt.name; let amount = evt.amount; let bulkGifted = evt.bulkGifted; let sender = evt.sender; let isCommunityGift = false; isCommunityGift = evt.isCommunityGift; let gifted = false; gifted = evt.gifted; if (isCommunityGift) return; if (bulkGifted && events.gift.enabled) { alertBulkGift(username, amount); } else if (gifted && events.direct.enabled) { alertGift(sender, username); } else if (amount > 1 && events.resub.enabled) { alertReSub(username, amount); } else if (events.sub.enabled) { alertSub(username); } return; } // Check if the event is a chat message if (obj.detail.listener === "message") { let data = obj.detail.event.data; if (ignoredUsers.indexOf(data.nick) !== -1) { return; } let role = check_role(data); let name = data.displayName; let message = attachEmotes(data); let badges = ``; if (displayBadges) { for (let i = 0; i < data.badges.length; i++) { badge = data.badges[i]; badges += ` `; } } const emoteOnly = isEmote(data); const isCommand = commands.some(prefix => data.text.startsWith(prefix)); if (isCommand && hideCommands === "yes") { return; } let pn = null; const pn_api = fetch('https://pronouns.alejo.io/api/users/' + data.displayName.toLowerCase()) .then((response) => response.json()) .then((user) => { if (!user.length) { return null; } else return user[0].pronoun_id; }); const printAddress = async () => { pn = await pn_api; switch (pn) { case "aeaer": pn = "ae/aer"; break; case "eem": pn = "e/em"; break; case "faefaer": pn = "fae/faer"; break; case "hehim": pn = "he/him"; break; case "heshe": pn = "he/she"; break; case "hethem": pn = "he/they"; break; case "itits": pn = "it/its"; break; case "perper": pn = "per/per"; break; case "sheher": pn = "she/her"; break; case "shethem": pn = "she/they"; break; case "theythem": pn = "they/them"; break; case "vever": pn = "ve/ver"; break; case "xexem": pn = "xe/xem"; break; case "ziehir": pn = "zie/hir"; break; default: break; } return pn; } printAddress().then(pn => addMessage(name, null, role, badges, message, emoteOnly, data.isAction, data.userId, data.msgId, pn)); } }); // A function that renders the message in DOM function addMessage(name, reply, badge, badges, message, isEmote, isAction, userId, msgId, pn) { // Advance the counter of total messages by 1 to keep track // of how many there are at any point totalMsg += 1; // Define a CSS class for a certain action const actionClass = isAction ? "action" : ""; const emoteOnly = isEmote ? "emote-only" : ""; let pn_none = "" if (!displayPronouns || pn == null) { pn_none = "style='display: none'" } const mentions = message.match(/@\S+/g) if (mentions) { for (var i = 0; i < mentions.length; i++) { message = message.replace(mentions[i], `

${mentions[i]}

`) } } let roleIcon, broadcasterClass; broadcasterClass = ''; let broadcasterBorder = ''; let broadcasterFill = ''; role = badge[0] switch (role) { case "broadcaster": roleIcon = `
` broadcasterClass = 'white-border'; broadcasterFill = 'white-fill'; broadcasterBorder = 'style="border-right: solid 4px #FFDEE6 !important;"' break case "moderator": roleIcon = `
` break case "vip": roleIcon = `
` break case "artist": roleIcon = `` break case "subscriber": roleIcon = `
` break; default: roleIcon = `` break } name_ = name.concat(""); let fullMsg = `
${roleIcon}
${name} ${pn}

${message}

` const element = $.parseHTML(fullMsg); if (hideAfter !== 999) { $(element).appendTo('.main-container').delay(hideAfter * 1000).animate({ opacity: 0 }, 'fast', function () { $(element).remove(); }); } else { $(element).appendTo('.main-container'); } // Removes messages if the total number // of messages goes over the limit if (totalMsg > msgLimit) { removeRow(); } } function appendAlert(block) { let alertTemplate = `
${block}
` let element; element = $.parseHTML(alertTemplate); if (hideAfter !== 999) { $(element).appendTo('.main-container').delay(hideAfter * 1000).animate({ opacity: 0 }, 'fast', function () { $(element).remove(); }); } else { $(element).appendTo('.main-container'); } // Removes messages if the total number // of messages goes over the limit if (totalMsg > msgLimit) { removeRow(); } } function alertSub(username) { totalMsg += 1; let block_to_append = `

${username}

Thank you for the sub!

` appendAlert(block_to_append) } function alertReSub(username, amount) { totalMsg += 1; let block_to_append = `

${username}

Resubscribed for ${amount} months!

` appendAlert(block_to_append) } function alertBulkGift(username, amount) { totalMsg += 1; let block_to_append = `

${username}

Gifted ${amount} subs to the community!

` appendAlert(block_to_append) } function alertGift(sender, target) { totalMsg += 1; let block_to_append = `

${sender}

Gifted ${target} a sub!

` appendAlert(block_to_append) } function alertCheer(username, amount) { totalMsg += 1; let block_to_append = `

${username}

Thank you for ${amount} bits!

` appendAlert(block_to_append) } function alertTip(username, amount) { totalMsg += 1; let block_to_append = `

${username}

Thank you for ${events.tip.currency}${amount}

` appendAlert(block_to_append) } // A set of helper functions to handle emotes function attachEmotes(message) { let text = html_encode(message.text); let data = message.emotes; if (data[0]) { hasEmotes = "has-emotes" } else { hasEmotes = "" } let isEmoteOnly = isEmote(message) if (typeof message.attachment !== "undefined") { if (typeof message.attachment.media !== "undefined") { if (typeof message.attachment.media.image !== "undefined") { text = `${message.text}`; } } } return text .replace( /([^\s]*)/gi, function (m, key) { let result = data.filter(emote => { return html_encode(emote.name) === key }); if (typeof result[0] !== "undefined") { let url; if (isEmoteOnly) { url = result[0]['urls'][4]; } else { url = result[0]['urls'][1]; } if (provider === "twitch") { return ``; } else { if (typeof result[0].coords === "undefined") { result[0].coords = { x: 0, y: 0 }; } let x = parseInt(result[0].coords.x); let y = parseInt(result[0].coords.y); let width = "28px"; let height = "auto"; if (provider === "mixer") { if (result[0].coords.width) { width = `${result[0].coords.width}px`; } if (result[0].coords.height) { height = `${result[0].coords.height}px`; } } return `
`; } } else return key; } ); } function html_encode(e) { return e.replace(/[<>"^]/g, function (e) { return "&#" + e.charCodeAt(0) + ";"; }); } function check_role(data) { let role let badges = data.tags.badges; if (badges.includes('broadcaster')) { role = 'broadcaster' } else if (badges.includes('moderator')) { role = 'moderator' } else if (badges.includes('vip')) { role = 'vip' } else if (badges.includes('artist-badge')) { role = 'artist' } else if (badges.includes('subscriber')) { role = 'subscriber' } let isSub = false if (badges.includes('subscriber')) { isSub = true } return [role, isSub] } function removeRow() { if (!$(removeSelector).length) { return; } $(removeSelector).stop(true) $(removeSelector).animate({ opacity: 0 }, 'fast', function () { $(removeSelector).remove(); }); } function isEmote(data) { let msg = data.text; msg = msg.replace(/\s\s+/g, ' '); let msg_split = msg.split(" "); let emotes = data.emotes; let emoteOnly = true; const emote_names = emotes.map((e) => e.name); for (let i = 0; i < msg_split.length; i++) { if (!emote_names.includes(msg_split[i])) { emoteOnly = false } } return emoteOnly; } function getRandomInt(max) { return Math.floor(Math.random() * max); }