diff --git a/css/app.css b/css/app.css new file mode 100644 index 0000000..9ea9a15 --- /dev/null +++ b/css/app.css @@ -0,0 +1,342 @@ +/* Basic reset and styling */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.sn-notify-outline { --sn-notify-background-color: #27272a; } +.sn-notify-outline .sn-notify-title { --sn-notify-title-color: #FFFFFF; } +.sn-notify-outline .sn-notify-text { --sn-notify-text-color: rgba(255,255,255,0.5); } + +/* Full-page container styling */ +html { + width: 100vw; + height: 100vh; + font-family: "Inter", sans-serif; + scroll-behavior: smooth; + overflow-wrap: break-word; +} + + +body { + background-color: #121212; + overflow: hidden; +} + +.wrapper { + position: absolute; + width: 100%; + height: 100%; +} + +#chat { + position: absolute; + width: 100%; + bottom: 0px; + padding: 20px; +} + +#chat .message { + color: #FFF; + font-size: 18px; + line-height: 150%; + text-shadow: 2px 2px 2px rgba(0,0,0,0.75); + transition: all ease-in-out 300ms; + margin: 5px 0; +} + +#chat .message img { height: 22px; vertical-align: middle; } + +#chat .message .text img.gigantify { + height: 84px; + display: block; + margin: 10px 0 5px 0; +} + +#chat .message .user { font-weight: bold; } + +#chat .message .time { + display: inline-block; + background: rgba(18, 18, 18, 0.5); + font-size: 12px; + padding: 0px 10px; + border-radius: 5px; +} + +#chat .message .avatar { + display: inline-block; + margin: 2px 0px; +} + +#chat .message .avatar img { + height: 28px; + border-radius: 100px; +} + +#chat .message .platform { + display: inline-flex; + justify-content: center; + align-items: center; + background: #000; + width: 24px; + height: 24px; + font-size: 14px; + border-radius: 5px; + text-shadow: 2px 2px 2px rgba(0,0,0,0.25); +} + +#chat .message .badges { +} + +#chat .message .badges i { + display: inline-flex; + justify-content: center; + align-items: center; + background: #000; + width: 24px; + height: 24px; + font-size: 12px; + border-radius: 5px; + margin: 0 5px 0 0; + text-shadow: 2px 2px 2px rgba(0,0,0,0.25); +} + +#chat .message .reply { + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: block; + + font-size: 14px; + color: #999; +} + +#chat .message .reply i { + transform: rotate(90deg); + margin: 0 5px; +} + + +#chat .message .shared { + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: block; + text-shadow: none; + font-size: 14px; + margin: 2px 0; + color: #999; +} + +#chat .message .shared span { + display: inline-block; + border-radius: 5px; + padding: 2px 10px 2px 5px; + background: #a970ff; + color: #fff; +} + +#chat .message .shared i { + margin: 0 3px; +} + + +#chat .message.event { + text-shadow: 2px 2px 2px rgba(0,0,0,0.25); + font-size: 16px; +} + +#chat .message.event .platform { + background: none; + margin: 2px 7px 0 6px; +} + +#chat .message.event > div { + padding: 5px 15px 5px 5px; + margin: 2px 0 0 0; + border-radius: 5px; + background: rgba(0,0,0,0.15); + display: inline-flex; +} + + +#chat .message.twitch .platform { background: #a970ff; } +#chat .message.twitch .badges img { margin: 0 2px; } +#chat .message.twitch.event > div { + background: rgb(169,112,255); + background: linear-gradient(180deg, rgba(169,112,255,1) 0%, rgba(95,67,138,1) 100%); +} +#chat .message.twitch.event .platform { background: transparent; } + +#chat .message.twitch.announcement > div { + padding: 10px 15px 13px 10px; + display: inline-block; + border-bottom: 2px solid #ffcc00; + background: rgb(18,18,18); + background: linear-gradient(180deg, rgba(18,18,18,1) 1%, rgba(0,0,0,1) 100%); +} + +#chat .message.twitch.announcement .reply { + padding: 2px; + color: #ffcc00; +} + +#chat .message.twitch.announcement .reply i { + transform: rotate(0deg); +} + +#chat .message.twitch.announcement .platform { + margin: 0; +} + +#chat .message.twitch.rewards-redemption > div { + padding: 10px 15px 13px 10px; + display: inline-block; + border-bottom: 2px solid #ffcc00; + background: rgb(18,18,18); + background: linear-gradient(180deg, rgba(18,18,18,1) 1%, rgba(0,0,0,1) 100%); +} +#chat .message.twitch.rewards-redemption .reply { + padding: 2px; + color: #FFF; +} + +#chat .message.twitch.rewards-redemption .reply i { + transform: rotate(0deg); + color: #ffcc00; +} + +#chat .message.twitch.rewards-redemption .platform { + margin: 0; +} + + + + + + + + +#chat .message.youtube .platform { background: #ff0000; } +#chat .message.youtube.event > div { + background: rgb(255,0,0); + background: linear-gradient(180deg, rgba(255,0,0,1) 1%, rgba(136,0,0,1) 100%); +} +#chat .message.youtube.event .platform { background: transparent; } + + +#chat .message.youtube.owner .user { + background: #ffd600; + color: #121212 !important; + border-radius: 5px; + padding: 2px 5px; + margin-right: 5px; + text-shadow: 2px 2px 2px rgba(0,0,0,0); +} + +#chat .message.youtube.owner .badges i.fa-solid.fa-video { background: #ff0000 } + +#chat .message.youtube.mod .user { color: #5e84f1 !important; } +#chat .message.youtube.mod .badges i.fa-solid.fa-wrench { background: #5e84f1 } + +#chat .message.youtube.sub .user { color: #2ba640 !important; } +#chat .message.youtube.sub .badges i.fa-solid.fa-star { background: #2ba640; } + +#chat .message.youtube .badges i.fa-solid.fa-check { background: #999; } + + + + + + +#chat .message.streamlabs .platform { background: #80f5d2; } +#chat .message.streamlabs.event > div { + background: rgb(128,245,210); + background: linear-gradient(180deg, rgba(128,245,210,1) 1%, rgba(8,108,78,1) 100%); +} +#chat .message.streamlabs.event .platform { background: transparent; } + + + +#chat .message.streamelements .platform { background: #2700ff; } +#chat .message.streamelements.event > div { + background: rgb(39,0,255); + background: linear-gradient(180deg, rgba(39,0,255,1) 1%, rgba(13,0,88,1) 100%); +} +#chat .message.streamelements.event .platform { background: transparent; } + + + + + + + +#chat .message.tiktok .platform { background: #000; } + + +#chat .message.tiktok.event > div { + background: rgb(255,0,80); + background: linear-gradient(180deg, rgba(255,0,80,1) 1%, rgba(120,0,38,1) 100%); +} +#chat .message.tiktok.event .platform { background: transparent; } + +#chat .message.tiktok.mod .badges i.fa-solid.fa-wrench { + background: #121212; +} + + + + +#statistics { + position: fixed; + z-index: 11; + width: 100%; + top: 0; left: 0; + padding: 20px; + font-size: 14px; + display: flex; + gap: 15px; + justify-content: flex-end; + background: rgba(18, 18, 18, 0.9); + backdrop-filter: blur(5px); + font-weight: bold; + -webkit-mask-image: linear-gradient(to top, transparent, black 40%); + mask-image: linear-gradient(to top, transparent, black 40%); +} + + +#statistics .platform { + background: #000; + color: #FFF; + padding: 10px 15px; + border-radius: 100px; + + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; +} + +#statistics .platform i.fa-brands { + margin-right: 5px; +} + +#statistics .platform > span { + display: inline-block; + margin: 0 5px; +} + +#statistics .platform#twitch { + background: #a970ff; +} + +#statistics .platform#youtube { + background: #FF0000; +} + +#statistics .platform#tiktok { + background: #ff0050; +} \ No newline at end of file diff --git a/css/settings.css b/css/settings.css new file mode 100644 index 0000000..526b17d --- /dev/null +++ b/css/settings.css @@ -0,0 +1,248 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Inter", sans-serif; + scroll-behavior: smooth; + overflow-wrap: break-word; + background-color: #121212; + color: #FFF; + text-align: center; + padding: 30px; + font-size: 16px; +} + +a { color: #ffcc00; } + + +#chat-divided { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 30px; +} + +#chat-settings { width: 640px; } +#chat-preview { width: calc(100% - 640px); } + +#chat-preview iframe { + position: sticky; + top: 30px; + left: 0; + width: 100%; + height: calc(100vh - 60px); +} + +.url-bar { + margin: 20px 0; + font-size: 16px; +} + +.url-bar input[type=text] { + font-family: "Inter", sans-serif; + border: none; + background: #171717; + color: #FFF; + padding: 15px 30px; + border-radius: 10px; + width: 100%; + text-align: center; + outline: none; + font-size: 14px; +} + + +.url-bar button { + font-family: "Inter", sans-serif; + font-weight: bold; + border: none; + padding: 15px 30px; + margin-top: 10px; + border-radius: 10px; + cursor: pointer; + background: #252525; + color: #FFF; + transition: .4s; + width: 100%; + font-size: 14px; +} + +.url-bar button:hover { + background: #292929; +} + +.tab-content { + padding: 20px 30px; + border-radius: 20px; + background: #171717; + margin: 20px auto; + text-align: left; +} + +.tab-content h2 { + margin-bottom: 15px; + font-size: 20px; + padding: 15px 0px; + border-bottom: 1px solid #222; +} + +.tab-content h2 i { + margin-right: 5px; +} + +.tab-content .setting { + display: flex; + justify-content: space-between; + padding: 5px 0px; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; +} + +.tab-content .setting i { + font-size: 14px; + color: #666; + margin: 0 10px; +} + +.tab-content .setting i.fa-arrow-turn-up { + transform: rotate(90deg); +} + + +.tab-content .setting small { color: #777; } + +.tab-content .setting input[type=text], +.tab-content .setting input[type=number] { + font-family: "Inter", sans-serif; + border: none; + background:#222; + color: #FFF; + padding: 10px 20px; + border-radius: 10px; + outline: none; +} + +.tab-content .setting.select select { + font-family: "Inter", sans-serif; + border: none; + background:#222; + color: #FFF; + padding: 10px 20px 10px 10px; + border-radius: 10px; + outline: none; +} + +.tab-content .setting .switch { + position: relative; + display: inline-block; + width: 50px; + height: 27px; +} + +/* Hide default HTML checkbox */ +.tab-content .setting .switch input[type=checkbox] { + opacity: 1; + width: 0; + height: 0; +} + +/* The slider */ +.tab-content .setting .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #CCC; + transition: .4s; + border-radius: 30px; +} + +.tab-content .setting .slider:before { + position: absolute; + content: ""; + height: 1.3em; + width: 1.2em; + border-radius: 16px; + left: 5px; + top: 3px; + bottom: 0; + background-color: white; + transition: .4s; +} + +.tab-content .setting input[type=checkbox]:checked + .slider { + background-color: #03c4de; +} + +.tab-content .setting input[type=checkbox]:checked + .slider:before { + transform: translateX(1.4em); +} + +.tab-content#twitch .setting input[type=checkbox]:checked + .slider { + background-color: #a970ff; +} +.tab-content#youtube .setting input[type=checkbox]:checked + .slider { + background-color: #FF0000; +} +.tab-content#tiktok .setting input[type=checkbox]:checked + .slider { + background-color: #ff0050; +} + +.tab-content#extras .setting input[type=checkbox]:checked + .slider { + background-color: #5fdd54; +} + +.tab-content .setting input[type=checkbox]:disabled + .slider { + background-color: #000 !important; +} + + +footer { + position: sticky; + bottom: 0; + padding: 20px; + background: rgba(18,18,18,0.5); + backdrop-filter: blur(10px); + margin-top: 20px; + font-size: 14px; +} + +footer a { + display: inline-block; + margin: 10px 5px; + font-size: 20px; + color: #ffcc00; +} + +@media only screen and (max-width: 768px) { + + #chat-divided { + flex-direction: column; + } + + #chat-settings { + width: 100%; + } + + #chat-settings .field { + width: 100%; + display: block; + } + + #chat-settings .field input { + width: 100%; + margin: 10px 0; + } + + #chat-preview { + display: none; + } + + footer { font-size: 12px; } +} \ No newline at end of file diff --git a/js/app-mockup.js b/js/app-mockup.js new file mode 100644 index 0000000..53abc4b --- /dev/null +++ b/js/app-mockup.js @@ -0,0 +1,329 @@ +/* ----------------------- */ +/* MOCKUP SYSTEM */ +/* ----------------------- */ + +let mockupInterval = null; +let isMockupActive = false; +const mockupDelay = 2500; // 2 seconds between events +let mockupConnectionState = false; // Track mock connection state + +// Sample data for mockup events +const mockData = { + users: [ + { id: 'user1', name: 'ViewerPro', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/75305d54-c7cc-40d1-bb9c-91fbe85943c7-profile_image-70x70.png' }, + { id: 'user2', name: 'StreamFan42', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/41780b5a-def8-11e9-94d9-784f43822e80-profile_image-70x70.png' }, + { id: 'user3', name: 'ProGamerVIP', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/dbdc9198-def8-11e9-8681-784f43822e80-profile_image-70x70.png' }, + { id: 'user4', name: 'GameQueen', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/de130ab0-def7-11e9-b668-784f43822e80-profile_image-70x70.png' }, + ], + messages: [ + 'Hey everyone! How\'s the stream going?', + 'This game looks awesome!', + 'LOL that was hilarious', + 'GG WP!', + 'When are you playing Minecraft next?', + 'Love the new overlay!', + 'First time watching, this is great!', + 'Can you explain that strategy again?', + 'Greetings from Germany!', + 'What\'s your favorite game?' + ], + emotes: [ + '', + '', + '' + ], + rewards: [ + 'Highlight My Message', + 'Play Sound Effect', + 'Choose Next Game', + 'Song Request', + 'Dad Joke', + 'Hydration Check' + ], + announcements: [ + 'Welcome to the stream everyone!', + 'Don\'t forget to follow for stream notifications!', + 'We\'re going to raid someone awesome after this game!', + 'Thanks for all the subs today!', + 'New emotes coming next week!' + ] +}; + +// Function to generate a random mockup event +function generateMockEvent() { + const eventTypes = [ + 'chat', 'chat', 'chat', 'chat', 'chat', 'chat', 'chat', 'chat', // More weight to regular chat messages + 'follow', 'sub', 'bits', 'raid', 'superchat', 'gift', + 'announcement', 'reward', 'resub', 'giftsub' + ]; + + // Select random event type and user + const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)]; + const user = mockData.users[Math.floor(Math.random() * mockData.users.length)]; + const messageId = createRandomString(40); + + switch(eventType) { + case 'chat': + // Generate a regular chat message + const message = mockData.messages[Math.floor(Math.random() * mockData.messages.length)]; + // Randomly add an emote + const includeEmote = Math.random() > 0.7; + const fullMessage = includeEmote ? + message + ' ' + mockData.emotes[Math.floor(Math.random() * mockData.emotes.length)] : + message; + + const platform = Math.random() > 0.5 ? 'twitch' : (Math.random() > 0.5 ? 'youtube' : 'tiktok'); + + const messageData = { + classes: Math.random() > 0.8 ? 'sub' : '', + avatar: user.avatar, + badges: Math.random() > 0.8 ? '' : '', + userName: user.name, + color: `hsl(${Math.random() * 360}, 100%, 75%)`, + message: fullMessage, + reply: '', + shared: '' + }; + + addMessageToChat(user.id, messageId, platform, messageData); + break; + + case 'follow': + const followData = { + classes: 'follow', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.follow(), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', followData); + break; + + case 'sub': + const subData = { + classes: 'sub', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.sub({ + months: Math.floor(Math.random() * 24) + 1, + isPrime: Math.random() > 0.5, + tier: Math.floor(1000 * randomIntFromInterval(1,3)) + }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', subData); + break; + + case 'resub': + const resubData = { + classes: 'sub', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.resub({ + months: Math.floor(Math.random() * 24) + 1, + isPrime: Math.random() > 0.5, + tier: Math.floor(1000 * randomIntFromInterval(1,3)) + }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', resubData); + break; + + case 'bits': + const bitsAmount = Math.floor(Math.random() * 5000) + 100; + const bitsData = { + classes: 'bits', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.bits({ bits: bitsAmount }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', bitsData); + break; + + case 'raid': + const viewerCount = Math.floor(Math.random() * 500) + 10; + const raidData = { + classes: 'raid', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.raid({ viewers: viewerCount }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', raidData); + break; + + case 'superchat': + const amount = (Math.random() * 100 + 5).toFixed(2); + const currencies = ['USD', 'EUR', 'CAD', 'GBP', 'AUD']; + const currency = currencies[Math.floor(Math.random() * currencies.length)]; + + const superChatData = { + classes: 'superchat', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.youtube.superchat({ + money: formatCurrency(amount, currency), + message: mockData.messages[Math.floor(Math.random() * mockData.messages.length)] + }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'youtube', superChatData); + break; + + case 'gift': + const giftData = { + classes: 'gift', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.tiktok.gift({ + gift: 'Rose', + count: Math.floor(Math.random() * 50) + 1, + coins: Math.floor(Math.random() * 1000) + 100 + }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'tiktok', giftData); + break; + + case 'announcement': + const announcementText = mockData.announcements[Math.floor(Math.random() * mockData.announcements.length)]; + const announcementData = { + classes: 'announcement', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: ` ${announcementText}`, + reply: currentLang.twitch.announcement(), + }; + + addEventToChat(user.id, messageId, 'twitch', announcementData); + break; + + case 'reward': + const rewardTitle = mockData.rewards[Math.floor(Math.random() * mockData.rewards.length)]; + const userInput = Math.random() > 0.5 ? mockData.messages[Math.floor(Math.random() * mockData.messages.length)] : ''; + + const rewardData = { + classes: 'rewards-redemption', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: ` ${userInput}`, + reply: currentLang.twitch.channelpoints({ title : rewardTitle }), + }; + + addEventToChat(user.id, messageId, 'twitch', rewardData); + break; + + case 'giftsub': + const recipientUser = mockData.users[Math.floor(Math.random() * mockData.users.length)]; + + const giftsubData = { + classes: 'sub', + avatar: user.avatar, + badges: '', + userName: user.name, + color: '#FFF', + message: currentLang.twitch.gifted({ + gifted: recipientUser.name, + months: Math.floor(Math.random() * 12) + 1, + tier: Math.floor(1000 * randomIntFromInterval(1,3)), + total: Math.floor(Math.random() * 50) + 1 + }), + reply: '', + }; + + addEventToChat(user.id, messageId, 'twitch', giftsubData); + break; + } +} + +// Function to start the mockup system +function startMockupSystem() { + if (!isMockupActive) { + console.debug('Starting mockup system...'); + isMockupActive = true; + mockupConnectionState = false; + + // Add a notification about mockup mode + notifyInfo({ + title: currentLang.streamerbotdisconnected || "Streamer.Bot Disconnected", + text: "Running in mockup mode. Showing sample events." + }); + + // Start with a few initial events + for (let i = 0; i < 3; i++) { + setTimeout(() => generateMockEvent(), i * 500); + } + + // Set interval for regular events + mockupInterval = setInterval(generateMockEvent, mockupDelay); + + // Update statistics for demo + updateMockStatistics(); + } +} + +// Function to stop the mockup system +function stopMockupSystem() { + if (isMockupActive) { + console.debug('Stopping mockup system...'); + isMockupActive = false; + mockupConnectionState = true; + clearInterval(mockupInterval); + mockupInterval = null; + + // Clear chat to start fresh with real events + chatContainer.innerHTML = ''; + } +} + +// Function to update mock statistics +function updateMockStatistics() { + if (showPlatformStatistics) { + if (showTwitchViewers) { + document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(Math.floor(Math.random() * 500) + 50); + } + + if (showYouTubeStatistics) { + document.querySelector('#statistics #youtube .viewers span').textContent = formatNumber(Math.floor(Math.random() * 300) + 20); + document.querySelector('#statistics #youtube .likes span').textContent = formatNumber(Math.floor(Math.random() * 1000) + 100); + } + + if (showTikTokStatistics) { + document.querySelector('#statistics #tiktok .viewers span').textContent = formatNumber(Math.floor(Math.random() * 800) + 200); + document.querySelector('#statistics #tiktok .likes span').textContent = formatNumber(Math.floor(Math.random() * 5000) + 500); + } + } +} + + + + +function randomIntFromInterval(min, max) { // min and max included + return Math.floor(Math.random() * (max - min + 1) + min); +} diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..be35ded --- /dev/null +++ b/js/app.js @@ -0,0 +1,287 @@ +/* ----------------------- */ +/* OPTIONS */ +/* ----------------------- */ + +const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1"); +const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080"); +const chatThreshhold = 50; +const chatContainer = document.querySelector('#chat'); +const currentLang = lang[getURLParam("language", 'ptbr')]; +const showPlatform = getURLParam("showPlatform", false); +const showAvatar = getURLParam("showAvatar", false); +const showTimestamps = getURLParam("showTimestamps", false); +const ampmTimeStamps = getURLParam("ampmTimeStamps", false); +const showBadges = getURLParam("showBadges", true); +const showPlatformStatistics = getURLParam("showPlatformStatistics", false); +const hideAfter = getURLParam("hideAfter", 0); +const ignoreChatters = getURLParam("ignoreChatters", ""); +const excludeCommands = getURLParam("excludeCommands", true); + +const avatars = new Map(); +const userColors = new Map(); + +const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || []; + +/* ----------------------- */ +/* START */ +/* ----------------------- */ + +if (showPlatformStatistics == false) { document.querySelector('#statistics').style.display = 'none'; } + +/* ----------------------- */ +/* STREAMER.BOT CONNECTION */ +/* ----------------------- */ + +const streamerBotClient = new StreamerbotClient({ + host: streamerBotServerAddress, + port: streamerBotServerPort, + onConnect: (data) => { + console.debug( currentLang.streamerbotconnected ); + console.debug(data); + notifySuccess({ + title: currentLang.streamerbotconnected, + text: `` + }); + stopMockupSystem(); + }, + onDisconnect: () => { + console.error(currentLang.streamerbotdisconnected); + startMockupSystem(); + } +}); + +streamerBotClient.on('WebsocketClient.Message', (response) => { + console.debug('Event Received:', response); +}); + + + + +/* ----------------------- */ +/* UTILITIES */ +/* ----------------------- */ + + + +async function addMessageToChat(userID, messageID, platform, data) { + + const html = DOMPurify.sanitize(` +
+
+ + ${!data.shared ? '' : data.shared} + + ${showTimestamps == true ? ''+whatTimeIsIt()+'' : ''} + + ${showPlatform == true ? '' : '' } + + ${showAvatar == true ? '' : ''} + + ${showBadges == true ? ''+data.badges+'' : ''} + + ${data.userName}: + + ${!data.reply ? '' : data.reply} + + ${data.message} +
+
+ `); + + chatContainer.insertAdjacentHTML('beforeend', html); + + const messageElement = document.getElementById(messageID); + + if (hideAfter > 0) { + setTimeout(function () { + messageElement.style.opacity = 0; + setTimeout(function () { + messageElement.remove(); + }, 1000); + }, Math.floor(hideAfter * 1000)); + } + + removeExtraChatMessages(); +} + + + +async function addEventToChat(userID, messageID, platform, data) { + + const html = DOMPurify.sanitize(` +
+
+ ${!data.reply ? '' : data.reply} + + ${showPlatform == true ? '' : '  ' } + + + + ${data.userName} + ${data.message} + +
+
+ `); + + chatContainer.insertAdjacentHTML('beforeend', html); + + const messageElement = document.getElementById(messageID); + + if (hideAfter > 0) { + setTimeout(function () { + messageElement.style.opacity = 0; + setTimeout(function () { + messageElement.remove(); + }, 1000); + }, Math.floor(hideAfter * 1000)); + } + + removeExtraChatMessages(); +} + + +const whatTimeIsIt = () => { + const now = new Date(); + const hours24 = now.getHours(); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const ampm = hours24 >= 12 ? 'PM' : 'AM'; + const hours12 = (hours24 % 12) || 12; + + if (ampmTimeStamps == true) { return `${hours12}:${minutes} ${ampm}`; } + else { return `${hours24}:${minutes}`; } +}; + + +function removeExtraChatMessages() { + + const chatMessages = chatContainer.querySelectorAll('div.message').length; + + if (chatMessages > chatThreshhold) { + for (let i = 0; i < Math.floor(chatThreshhold/2); i++) { + chatContainer.removeChild(chatContainer.firstElementChild); + } + } +} + + +// Function to format large numbers (e.g., 1000 => '1K') +function formatNumber(num) { + if (num >= 1000000) { + let numStr = (num / 1000000).toFixed(1); + if (numStr.endsWith('.0')) { + numStr = numStr.slice(0, -2); + } + return numStr + 'M'; + } + else if (num >= 1000) { + let numStr = (num / 1000).toFixed(1); + if (numStr.endsWith('.0')) { + numStr = numStr.slice(0, -2); + } + return numStr + 'K'; + } + return num.toString(); +} + + +function formatCurrency(amount, currencyCode) { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: currencyCode, + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }).format(amount); +} + + + +function createRandomColor(platform, username) { + if (userColors.get(platform).has(username)) { + return userColors.get(platform).get(username); + } + else { + const randomColor = "hsl(" + Math.random() * 360 + ", 100%, 75%)"; + userColors.get(platform).set(username, randomColor); + return randomColor; + } +} + + +function createRandomString(length) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +function getURLParam(param, defaultValue) { + const chatQueryString = window.location.search; + const urlParams = new URLSearchParams(chatQueryString); + const paramVar = urlParams.get(param); + + switch (paramVar) { + case 'true': + return true; + + case 'false': + return false; + + case null: + case undefined: + return defaultValue; + + default: + return paramVar; + } +} + + +const pushNotify = (data) => { + + const SimpleNotify = { + effect: 'fade', + speed: 500, + customClass: 'toasty', + customIcon: '', + showIcon: true, + showCloseButton: true, + autoclose: true, + autotimeout: 5000, + notificationsGap: null, + notificationsPadding: null, + type: 'outline', + position: 'x-center bottom', + customWrapper: '', + }; + + const mergedData = { + ...SimpleNotify, + ...data + } + + new Notify (mergedData); +} + +const notifyError = (err) => { + err.status = 'error'; + pushNotify(err); +} + +const notifyInfo = (info) => { + info.status = 'info'; + pushNotify(info); +} + +const notifyWarning = (warn) => { + warn.status = 'warning'; + pushNotify(warn); +} + + +const notifySuccess = (success) => { + success.status = 'success'; + pushNotify(success); +} diff --git a/js/lang/en.js b/js/lang/en.js new file mode 100644 index 0000000..ae450ef --- /dev/null +++ b/js/lang/en.js @@ -0,0 +1,72 @@ +const en = { + streamerbotconnected: 'Streamer.bot Online!', + streamerbotdisconnected: 'Streamer.bot Disconnected!', + + twitch : { + follow : () => ` followed the channel.`, + announcement : () => `
📢 Announcement
`, + channelpoints : ({ title }) => `
Channel Points - ${title}
`, + bits : ({ bits, message }) => ` cheered ${bits} bits${message ? ' - '+message : ''}`, + + sub : ({ months, isPrime, tier }) => ` subscribed for + ${isPrime == true ? '' : ''} + ${months || 1 } ${months == 1 ? 'month' : 'months'} + (${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})`, + + resub : ({ months, isPrime, tier }) => ` subscribed for + ${isPrime == true ? '' : ''} + ${months || 1 } ${months == 1 ? 'month' : 'months'} + (${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})`, + + gifted : ({ gifted, months, tier, total }) => ` gifted + ${months || 1 } ${months == 1 ? 'month' : 'months'} + of Tier ${tier.toString().charAt(0)} sub + to ${gifted}`, + + giftedbomb : ({ count, total, tier }) => ` gifted ${count} Tier ${tier.toString().charAt(0)} subs to the Community, ${total || 1} ${total == 1 ? 'gift' : 'gifts'} in total.`, + + raid : ({ viewers }) => ` raided the channel with ${viewers} viewers` + + }, + + + youtube : { + superchat : ({ money, message }) => ` superchatted ${money} + ${message ? ' - '+message : ''} + `, + + supersticker : ({ money, message }) => ` sent a supersticker of ${money} + ${message ? ' - '+message : ''} + `, + + member : ({ months, tier }) => ` became a member for + + ${months || 1 } ${months == 1 ? 'month' : 'months'} + (Tier ${tier})`, + + giftedmembers : ({ total, tier }) => ` gifted ${total} Tier ${tier} memberships`, + + giftedtrainmembers : ({ gifted, tier }) => ` gifted a membership + (${tier}) + to ${gifted}`, + + }, + + + streamlabs : { + tip : ({ money, message }) => ` donated 🪙 ${money}`, + }, + + + streamelements : { + tip : ({ money, message }) => ` donated 🪙 ${money}`, + }, + + + tiktok : { + follow : () => ` followed the channel.`, + sub : ({ months }) => ` subscribed for ${months || 1 } ${months == 1 ? 'month' : 'months'}.`, + gift : ({ gift, count, coins }) => ` gifted ${gift} x${count} (🪙 ${coins} ${coins == 1 ? 'coin' : 'coins'}).`, + + } +} \ No newline at end of file diff --git a/js/lang/lang.js b/js/lang/lang.js new file mode 100644 index 0000000..723afb5 --- /dev/null +++ b/js/lang/lang.js @@ -0,0 +1,4 @@ +const lang = { + ptbr : ptbr, + en : en +} \ No newline at end of file diff --git a/js/lang/ptbr.js b/js/lang/ptbr.js new file mode 100644 index 0000000..a88a39e --- /dev/null +++ b/js/lang/ptbr.js @@ -0,0 +1,72 @@ +const ptbr = { + streamerbotconnected: 'Streamer.bot Conectado!', + streamerbotdisconnected: 'Streamer.bot Desconectado!', + + twitch : { + follow : () => ` seguiu o canal.`, + announcement : () => `
📢 Anúncio
`, + channelpoints : ({ title }) => `
Pontos do Canal - ${title}
`, + bits : ({ bits, message }) => ` doou ${bits} bits${message ? ' - '+message : ''}`, + + sub : ({ months, isPrime, tier }) => ` se inscreveu por + ${isPrime == true ? '' : ''} + ${months || 1 } ${months == 1 ? 'mês' : 'meses'} + (${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})`, + + resub : ({ months, isPrime, tier }) => ` se inscreveu por + ${isPrime == true ? '' : ''} + ${months || 1 } ${months == 1 ? 'mês' : 'meses'} + (${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})`, + + gifted : ({ gifted, months, tier }) => ` doou + ${months || 1 } ${months == 1 ? 'mês' : 'meses'} + de Tier ${tier.toString().charAt(0)} + para ${gifted}`, + + giftedbomb : ({ count, total, tier }) => ` doou ${count} inscrições Tier ${tier.toString().charAt(0)} para a Comunidade, totalizando ${total || 1} ${total == 1 ? 'doação' : 'doações'}.`, + + raid : ({ viewers }) => ` raidou o canal com ${viewers} pessoas` + + }, + + + youtube : { + superchat : ({ money, message }) => ` fez um superchat de ${money} + ${message ? ' - '+message : ''} + `, + + supersticker : ({ money, message }) => ` enviou um superchat de ${money} + ${message ? ' - '+message : ''} + `, + + member : ({ months, tier }) => ` se inscreveu por + + ${months || 1 } ${months == 1 ? 'mês' : 'meses'} + (Tier ${tier})`, + + giftedmembers : ({ total, tier }) => ` doou ${total} inscrições (Tier ${tier})`, + + giftedtrainmembers : ({ gifted, tier }) => ` doou uma assinatura + (${tier}) + para ${gifted}`, + + }, + + + streamlabs : { + tip : ({ money, message }) => ` doou 🪙 ${money}`, + }, + + + streamelements : { + tip : ({ money, message }) => ` doou 🪙 ${money}`, + }, + + + tiktok : { + follow : () => ` seguiu o canal.`, + sub : ({ months }) => ` se inscreveu por ${months || 1 } ${months == 1 ? 'mês' : 'meses'}.`, + gift : ({ gift, count, coins }) => ` doou ${gift} x${count} (🪙 ${coins} ${coins == 1 ? 'moeda' : 'moedas'}).`, + + } +} \ No newline at end of file diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..1bc8825 --- /dev/null +++ b/js/settings.js @@ -0,0 +1,139 @@ +function saveSettingsToLocalStorage() { + const checkboxes = document.querySelectorAll("input[type=checkbox]"); + const textfields = document.querySelectorAll("input[type=text]"); + const numberfields = document.querySelectorAll("input[type=number]"); + const selects = document.querySelectorAll("select"); + + const settings = {}; + + checkboxes.forEach((checkbox) => { + settings[checkbox.name] = checkbox.checked; + }); + textfields.forEach((textfield) => { + settings[textfield.name] = textfield.value; + }); + numberfields.forEach((numberfield) => { + settings[numberfield.name] = numberfield.value; + }); + selects.forEach((select) => { + settings[select.name] = select.value; + }); + + localStorage.setItem("chatWidgetSettings", JSON.stringify(settings)); +} + + +function loadSettingsFromLocalStorage() { + const saved = localStorage.getItem("chatWidgetSettings"); + if (!saved) return; + + const settings = JSON.parse(saved); + + Object.keys(settings).forEach((key) => { + const input = document.querySelector(`[name="${key}"]`); + if (input) { + if (input.type === "checkbox") { + input.checked = settings[key]; + } else { + input.value = settings[key]; + } + } + }); +} + + +function pushChangeEvents() { + const checkboxes = document.querySelectorAll("input[type=checkbox]"); + const textfields = document.querySelectorAll("input[type=text]"); + const numberfields = document.querySelectorAll("input[type=number]"); + const selects = document.querySelectorAll("select"); + + checkboxes.forEach((checkbox) => { + checkbox.addEventListener('change', () => { + generateUrl(); + saveSettingsToLocalStorage(); + }); + }); + textfields.forEach((textfield) => { + textfield.addEventListener('input', () => { + generateUrl(); + saveSettingsToLocalStorage(); + }); + }); + numberfields.forEach((numberfield) => { + numberfield.addEventListener('input', () => { + generateUrl(); + saveSettingsToLocalStorage(); + }); + }); + selects.forEach((select) => { + select.addEventListener('change', () => { + generateUrl(); + saveSettingsToLocalStorage(); + }); + }); +} + + +function generateUrl() { + document.getElementById("outputUrl").value = ''; + + var runThisLocally = document.querySelector("input[type=checkbox][name=runThisLocally]").checked; + console.log(runThisLocally); + var baseUrl = ''; + + if (runThisLocally == false) { + baseUrl = 'https://vortisrd.github.io/chatrd/chat.html' + } + + const checkboxes = document.querySelectorAll("input[type=checkbox]"); + const textfields = document.querySelectorAll("input[type=text]"); + const numberfields = document.querySelectorAll("input[type=number]"); + const selects = document.querySelectorAll("select"); + + const params = new URLSearchParams(); + + + selects.forEach((select) => { + params.set(select.name, select.value); + }); + checkboxes.forEach((checkbox) => { + params.set(checkbox.name, checkbox.checked); + }); + textfields.forEach((textfield) => { + params.set(textfield.name, textfield.value); + }); + numberfields.forEach((numberfield) => { + params.set(numberfield.name, numberfield.value); + }); + + document.getElementById("outputUrl").value = baseUrl + '?' + params.toString(); + document.querySelector('#chat-preview iframe').src = 'chat.html?'+params.toString(); +} + +function copyUrl() { + + const output = document.getElementById("outputUrl"); + + output.select(); + document.execCommand("copy"); + + const button = document.querySelector('.url-bar button'); + const buttonDefaulText = 'Copy URL'; + + button.textContent = 'ChatRD URL Copied!'; + button.style.backgroundColor = "#00dd63"; + + setTimeout(() => { + button.textContent = buttonDefaulText; + button.removeAttribute('style'); + }, 3000); + + +} + +window.addEventListener('load', () => { + loadSettingsFromLocalStorage(); + generateUrl(); + pushChangeEvents(); +}); diff --git a/js/streamelements/module.js b/js/streamelements/module.js new file mode 100644 index 0000000..da9b2a5 --- /dev/null +++ b/js/streamelements/module.js @@ -0,0 +1,44 @@ +const showStreamElementsTips = getURLParam("showStreamElementsTips", true); + +const streamElementsHandlers = { + 'StreamElements.Tip': (response) => { + console.debug(response.data); + if (showStreamElementsTips == false) + return; + streamElementsEventMessage(response.data); + }, +}; +for (const [event, handler] of Object.entries(streamElementsHandlers)) { + streamerBotClient.on(event, handler); +} + + + +async function streamElementsEventMessage(data) { + const { + username: userName, + amount: moneyFromUser, + currency: currencyFromUser, + message: messageFromUser, + } = data; + const userID = createRandomString(40); + const messageID = createRandomString(40); + const [avatar, message] = await Promise.all([ + '', + currentLang.streamlabs.tip({ + money : formatCurrency(moneyFromUser,currencyFromUser), + message : messageFromUser + }) + ]); + const classes = 'streamelements'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + addEventToChat(userID, messageID, 'dollar-sign', messageData); +} diff --git a/js/streamlabs/module.js b/js/streamlabs/module.js new file mode 100644 index 0000000..60e3510 --- /dev/null +++ b/js/streamlabs/module.js @@ -0,0 +1,42 @@ +const showStreamlabsDonations = getURLParam("showStreamlabsDonations", true); + +const streamLabsHandlers = { + 'Streamlabs.Donation': (response) => { + console.debug(response.data); + if (showStreamlabsDonations == false) + return; + streamLabsEventMessage(response.data); + }, +}; +for (const [event, handler] of Object.entries(streamLabsHandlers)) { + streamerBotClient.on(event, handler); +} + +async function streamLabsEventMessage(data) { + const { + from: userName, + formattedAmount: moneyFromUser, + currency: currencyFromUser, + message: messageFromUser, + } = data; + const userID = createRandomString(40); + const messageID = createRandomString(40); + const [avatar, message] = await Promise.all([ + '', + currentLang.streamlabs.tip({ + money : formatCurrency(moneyFromUser,currencyFromUser), + message : messageFromUser + }) + ]); + const classes = 'streamlabs'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + addEventToChat(userID, messageID, 'dollar-sign', messageData); +} \ No newline at end of file diff --git a/js/tiktok/module.js b/js/tiktok/module.js new file mode 100644 index 0000000..39104f3 --- /dev/null +++ b/js/tiktok/module.js @@ -0,0 +1,222 @@ +/* ----------------------------------------------------------------------------------------- */ +/* TikFinity >< Streamer.Bot */ +/* ----------------------------------------------------------------------------------------- */ +/* In Streamer.bot, go into Servers/Clients, then Websocket clients, */ +/* and add the server info for TikFinity Desktop App. */ +/* ----------------------------------------------------------------------------------------- */ +/* If it's also running on the same computer, the address will be: ws://127.0.0.1:21213/ */ +/* ----------------------------------------------------------------------------------------- */ + +const showTikTokMessages = getURLParam("showTikTokMessages", true); +const showTikTokFollows = getURLParam("showTikTokFollows", true); +const showTikTokGifts = getURLParam("showTikTokGifts", true); +const showTikTokSubs = getURLParam("showTikTokSubs", true); +const showTikTokStatistics = getURLParam("showTikTokStatistics", true); + +userColors.set('tiktok', new Map()); + +if (showTikTokStatistics == false) { document.querySelector('#statistics #tiktok').style.display = 'none'; } + + + +streamerBotClient.on('General.Custom', (response) => { + if (response.data.platform === 'TikTok') { + + let json = response.data; + let jsonData = json.data.data; + + switch (json.data.event) { + case 'roomUser' : + if (showPlatformStatistics == false || showTikTokStatistics == false) return; + tiktokUpdateStatistics(jsonData, 'viewers'); + break; + case 'like' : + if (showPlatformStatistics == false || showTikTokStatistics == false) return; + tiktokUpdateStatistics(jsonData, 'likes'); + break; + case 'chat' : + console.debug(json); + if (showTikTokMessages == false) return; + if (ignoreUserList.includes(jsonData.nickname.toLowerCase())) return; + tiktokChatMessage(jsonData); + break; + case 'follow' : + if (showTikTokFollows == false) return; + tiktokFollowMessage(jsonData); + break; + case 'subscribe' : + if (showTikTokSubs == false) return; + tiktokSubMessage(jsonData); + break; + case 'gift' : + if (showTikTokGifts == false) return; + if (jsonData.giftType === 1 && !jsonData.repeatEnd) {} + else { + tiktokGiftMessage(jsonData); + } + break; + default: + //console.debug(json); + } + } + +}); + + + + +async function tiktokChatMessage(data) { + if (data.comment.startsWith("!") && excludeCommands == true) + return; + + + const { + userId: userID, + msgId: messageID, + profilePictureUrl: avatar, + comment: message, + emotes, + nickname: userName, + isSubscriber, + isModerator, + } = data; + + const badgesHTML = [ + isSubscriber && '', + isModerator && '', + ].filter(Boolean).join(''); + + const classes = [ + isSubscriber && 'sub', + isModerator && 'mod', + ].filter(Boolean); + + var fullmessage = message; + + emotes.forEach(emote => { + var emotetoadd = ` `; + var position = emote.placeInComment; + fullmessage = [fullmessage.slice(0, position), emotetoadd, fullmessage.slice(position)].join(''); + }); + + const messageData = { + classes: classes.join(' '), + avatar, + badges: badgesHTML, + userName, + color: await createRandomColor('tiktok', userID), + message: fullmessage, + reply: '', + }; + + addMessageToChat(userID, messageID, 'tiktok', messageData); +} + + + +async function tiktokFollowMessage(data) { + const { + userId: userID, + msgId: messageID, + profilePictureUrl: avatar, + nickname: userName, + } = data; + + const message = currentLang.tiktok.follow(); + const classes = 'follow' + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'tiktok', messageData); +} + + + +async function tiktokSubMessage(data) { + const { + userId: userID, + msgId: messageID, + profilePictureUrl: avatar, + nickname: userName, + } = data; + + const message = currentLang.tiktok.sub({ + months : data.subMonth + }); + + const classes = 'sub' + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'tiktok', messageData); +} + + + +async function tiktokGiftMessage(data) { + const { + userId: userID, + msgId: messageID, + profilePictureUrl: avatar, + nickname: userName, + } = data; + + var coins = Math.floor(data.repeatCount*data.diamondCount); + + const message = currentLang.tiktok.gift({ + gift : data.giftName, + count : data.repeatCount, + coins : coins + }); + + const classes = 'gift' + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'tiktok', messageData); +} + + + + + + +async function tiktokUpdateStatistics(data, type) { + + + if (type == 'viewers') { + const viewers = DOMPurify.sanitize(data.viewerCount); + document.querySelector('#statistics #tiktok .viewers span').textContent = formatNumber(viewers); + } + + if (type == 'likes') { + const likes = DOMPurify.sanitize(data.totalLikeCount); + document.querySelector('#statistics #tiktok .likes span').textContent = formatNumber(likes); + } + + +} \ No newline at end of file diff --git a/js/twitch/module.js b/js/twitch/module.js new file mode 100644 index 0000000..659e3d7 --- /dev/null +++ b/js/twitch/module.js @@ -0,0 +1,586 @@ +const showTwitchMessages = getURLParam("showTwitchMessages", true); +const showTwitchFollows = getURLParam("showTwitchFollows", true); +const showTwitchBits = getURLParam("showTwitchBits", true); +const showTwitchAnnouncements = getURLParam("showTwitchAnnouncements", true); +const showTwitchSubs = getURLParam("showTwitchSubs", true); +const showTwitchGiftedSubs = getURLParam("showTwitchGiftedSubs", true); +const showTwitchGiftedSubsUserTrain = getURLParam("showTwitchGiftedSubsUserTrain", true); +const showTwitchMassGiftedSubs = getURLParam("showTwitchMassGiftedSubs", true); +const showTwitchRewardRedemptions = getURLParam("showTwitchRewardRedemptions", true); +const showTwitchRaids = getURLParam("showTwitchRaids", true); +const showTwitchSharedChat = getURLParam("showTwitchSharedChat", true); +const showTwitchViewers = getURLParam("showTwitchViewers", true); + + +if (showTwitchViewers == false) { document.querySelector('#statistics #twitch').style.display = 'none'; } + + +const twitchMessageHandlers = { + 'Twitch.ChatMessage': (response) => { + console.debug('Twitch Chat', response.data); + if (showTwitchMessages == false) + return; + if (ignoreUserList.includes(response.data.message.username.toLowerCase())) + return; + twitchChatMessage(response.data); + }, + 'Twitch.Follow': (response) => { + console.debug(response.data); + if (showTwitchFollows == false) + return; + twitchFollowMessage(response.data); + }, + 'Twitch.Announcement': (response) => { + console.debug(response.data); + if (showTwitchAnnouncements == false) return; + twitchAnnouncementMessage(response.data); + }, + 'Twitch.Cheer': (response) => { + console.debug(response.data); + if (showTwitchBits == false) return; + twitchBitsMessage(response.data); + }, + 'Twitch.AutomaticRewardRedemption': (response) => { + console.debug(response.data); + if (showTwitchMessages == false) return; + twitchChatMessageGiantEmote(response.data); + }, + 'Twitch.RewardRedemption': (response) => { + console.debug(response.data); + if (showTwitchRewardRedemptions == false) return; + twitchRewardRedemption(response.data); + }, + 'Twitch.Sub': (response) => { + console.debug(response.data); + if (showTwitchSubs == false) return; + twitchSubMessage(response.data); + }, + 'Twitch.ReSub': (response) => { + console.debug(response.data); + if (showTwitchSubs == false) return; + twitchReSubMessage(response.data); + }, + 'Twitch.GiftSub': (response) => { + console.debug(response.data); + if (response.data.fromCommunitySubGift === false) { + if (showTwitchSubs == false || showTwitchGiftedSubs == false) return; + twitchGiftMessage(response.data); + } + else { + if (showTwitchSubs == false || showTwitchGiftedSubsUserTrain == false) return; + twitchGiftMessage(response.data); + } + + }, + 'Twitch.GiftBomb': (response) => { + console.debug(response.data); + if (showTwitchSubs == false || showTwitchMassGiftedSubs == false) return; + twitchGiftSubsMessage(response.data); + }, + 'Twitch.Raid': (response) => { + console.debug(response.data); + if (showTwitchRaids == false) return; + twitchRaidMessage(response.data); + }, + 'Twitch.ChatMessageDeleted': (response) => { + console.debug(response.data); + twitchChatMessageDeleted(response.data); + }, + 'Twitch.UserBanned': (response) => { + console.debug(response.data); + twitchUserBanned(response.data); + }, + 'Twitch.UserTimedOut': (response) => { + console.debug(response.data); + twitchUserBanned(response.data); + }, + 'Twitch.ViewerCountUpdate': (response) => { + console.debug(response.data); + if (showPlatformStatistics == false || showTwitchViewers == false) return; + twitchUpdateStatistics(response.data); + }, + 'Twitch.ChatCleared': (response) => { + console.debug(response.data); + twitchChatClearMessages(); + } +}; + +for (const [event, handler] of Object.entries(twitchMessageHandlers)) { + streamerBotClient.on(event, handler); +} + + +async function twitchChatMessage(data) { + if (data.message.message.startsWith("!") && excludeCommands == true) + return; + + const { + message: { + username: userID, + color, + displayName: userName, + message: text, + firstMessage, + isReply, + isSharedChat, + reply: replyData, + }, + messageId, + } = data; + + const [avatar, message, badges] = await Promise.all([ + getTwitchAvatar(userID), + getTwitchEmotes(data), + getTwitchBadges(data), + ]); + + const classes = firstMessage ? ['first-message'] : []; + const replyHTML = isReply ? + `
${replyData.userName}: ${replyData.msgBody}
` : + ''; + + var sharedChat = ''; + + if (isSharedChat) { + if (showTwitchSharedChat == true) { + if (!data.sharedChat.primarySource) + { + var sharedChat = `
${data.sharedChat.sourceRoom.name}
`; + } + } + else if (!data.sharedChat.primarySource && showTwitchSharedChat == false) { + return; + } + } + + const messageData = { + classes: classes.join(' '), + avatar, + badges, + userName, + color, + message, + shared: sharedChat, + reply: replyHTML, + }; + + addMessageToChat(userID, messageId, 'twitch', messageData); + } + + + + async function twitchChatMessageGiantEmote(data) { + const { user_login: userID, gigantified_emote: { id: emoteGigantify } } = data; + + const userMessages = chatContainer.querySelectorAll(`.twitch.message[data-user="${userID}"]`); + if (userMessages.length === 0) return; + + const firstMessage = userMessages[0]; + const emoteImages = firstMessage.querySelectorAll(`img[data-emote-id="${emoteGigantify}"]`); + if (emoteImages.length === 0) return; + + emoteImages.forEach(img => { + img.classList.add("gigantify"); + if (img.src.endsWith("2.0")) { + img.src = img.src.replace("2.0", "3.0"); + } + }); + } + + + + async function twitchFollowMessage(data) { + const { + user_id : userID, + user_name : userName + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.follow(), + ]); + + const classes = 'follow'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchBitsMessage(data) { + + const { + messageId : messageID, + user : { + id : userID, + name : userName + } + } = data; + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.bits({ + bits: data.message.bits, + message : data.message.message.replace(/\bCheer\d+\b/g, '').replace(/\s+/g, ' ').trim() + }), + ]); + + const classes = 'bits'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchAnnouncementMessage(data) { + + const { + messageId : messageID, + user : { + id : userID, + name : userName + } + } = data; + + data.message = { + message: await getTwitchAnnouncementEmotes(data) + }; + + const replyHTML = currentLang.twitch.announcement(); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + ` ${data.message.message}` + ]); + + const classes = 'announcement'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: replyHTML, + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchRewardRedemption(data) { + + const { + user_id : userID, + user_name : userName, + } = data; + + const messageID = createRandomString(40); + + const replyHTML = currentLang.twitch.channelpoints({ title : data.reward.title }); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + ` ${data.user_input}` + ]); + + const classes = 'rewards-redemption'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: replyHTML, + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchSubMessage(data) { + + const { + user : { + id : userID, + name : userName + } + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.sub({ + months : data.duration_months, + isPrime : data.isPrime, + tier : data.sub_tier + }) + ]); + + const classes = 'sub'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchReSubMessage(data) { + + const { + user : { + id : userID, + name : userName + } + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.resub({ + months : data.cumulativeMonths, + isPrime : data.isPrime, + tier : data.subTier + }) + ]); + + const classes = 'sub'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchGiftMessage(data) { + + const { + user : { + id : userID, + name : userName + } + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.gifted({ + gifted : data.recipient.name, + months : data.durationMonths, + tier : data.subTier, + total : data.cumlativeTotal + }) + ]); + + const classes = 'sub'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchGiftSubsMessage(data) { + + const { + user : { + id : userID, + name : userName + } + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.giftedbomb({ count : data.total, tier : data.sub_tier, total : data.cumulative_total }) + ]); + + const classes = 'sub'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchRaidMessage(data) { + + const { + from_broadcaster_user_login: userID, + from_broadcaster_user_name: userName + } = data; + + const messageID = createRandomString(40); + + const [avatar, message] = await Promise.all([ + getTwitchAvatar(userID), + currentLang.twitch.raid({ viewers : data.viewers }) + ]); + + const classes = 'raid'; + + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + }; + + addEventToChat(userID, messageID, 'twitch', messageData); + } + + + + async function twitchChatMessageDeleted(data) { + document.getElementById(data.messageId)?.remove(); + } + + async function twitchUserBanned(data) { + chatContainer.querySelectorAll(`[data-user="${data.user_login}"]`).forEach(element => { + element.remove(); + }); + } + + async function twitchChatClearMessages() { + chatContainer.innerHTML = ''; + } + + async function twitchUpdateStatistics(data) { + const viewers = DOMPurify.sanitize(data.viewers); + document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(viewers); + } + + + + async function getTwitchEmotes(data) { + const message = data.message.message; + const emotes = data.emotes; + + const words = message.split(" "); + emotes.sort((a, b) => b.startIndex - a.startIndex); + + for (let i = 0; i < words.length; i++) { + emotes.forEach(emote => { + if (words[i] === emote.name) { + words[i] = `${emote.name}`; + } + }); + } + + return words.join(" "); + } + + + async function getTwitchAnnouncementEmotes(data) { + const message = data.text; + const emotes = data.parts; + + const words = message.split(" "); + emotes.sort((a, b) => b.startIndex - a.startIndex); + + for (let i = 0; i < words.length; i++) { + emotes.forEach(emote => { + if (words[i] === emote.text) { + words[i] = `${emote.text}`; + } + }); + } + + return words.join(" "); + } + + + async function getTwitchBadges(data) { + const badges = data.message.badges; + var htmlBadges = ''; + badges.forEach((badge) => { + htmlBadges += ``; + }); + + return htmlBadges; + } + + + async function getTwitchAvatar(user) { + if (avatars.has(user)) { + console.debug(`Avatar found for ${user}!`); + return avatars.get(user); + } + else { + console.debug(`Avatar not found for ${user}! Getting it from DECAPI!`); + var decapi = await fetch('https://decapi.me/twitch/avatar/' + user); + var newavatar = await decapi.text() + avatars.set(user, newavatar); + return newavatar; + } + } \ No newline at end of file diff --git a/js/youtube/module.js b/js/youtube/module.js new file mode 100644 index 0000000..4b588c3 --- /dev/null +++ b/js/youtube/module.js @@ -0,0 +1,344 @@ +const showYouTubeMessages = getURLParam("showYouTubeMessages", true); +const showYouTubeSuperChats = getURLParam("showYouTubeSuperChats", true); +const showYouTubeSuperStickers = getURLParam("showYouTubeSuperStickers", false); +const showYouTubeMemberships = getURLParam("showYouTubeMemberships", true); +const showYouTubeGiftMemberships = getURLParam("showYouTubeGiftMemberships", true); +const showYouTubeMembershipsTrain = getURLParam("showYouTubeMembershipsTrain", true); +const showYouTubeStatistics = getURLParam("showYouTubeStatistics", true); + +userColors.set('youtube', new Map()); + +if (showYouTubeStatistics == false) { document.querySelector('#statistics #youtube').style.display = 'none'; } + +const youtubeMessageHandlers = { + 'YouTube.Message': (response) => { + console.debug('YouTube Chat', response.data); + if (showYouTubeMessages == false) + return; + if (ignoreUserList.includes(response.data.user.name.toLowerCase())) + return; + youTubeChatMessage(response.data); + }, + /*'YouTube.SuperChat': (response) => { + console.debug('YouTube SuperChat', response.data); + if (showYouTubeSuperChats == false) return; + youTubeSuperChatMessage(response.data); + }, + 'YouTube.SuperSticker': (response) => { + console.debug('YouTube SuperSticker', response.data); + if (showYouTubeSuperStickers == false) return; + youTubeSuperStickerMessage(response.data); + }, + 'YouTube.NewSponsor': (response) => { + console.debug('YouTube New Member', response.data); + if (showYouTubeMemberships == false) return; + youTubeNewSponsorMessage(response.data); + }, + 'YouTube.MemberMileStone': (response) => { + console.debug('YouTube Member Milestone', response.data); + if (showYouTubeMemberships == false) return; + youTubeNewSponsorMessage(response.data); + }, + 'YouTube.MembershipGift': (response) => { + console.debug('YouTube Gifted Membership', response.data); + if (showYouTubeGiftMemberships == false) return; + youTubeGiftedMembersMessage(response.data); + }, + 'YouTube.GiftMembershipReceived': (response) => { + console.debug('YouTube Gifted Membership Bomb', response.data); + if (showYouTubeMembershipsTrain == false) return; + youTubeGiftedMembersMessage(response.data); + },*/ + 'YouTube.StatisticsUpdated': (response) => { + console.debug(response.data); + if (showYouTubeStatistics == false) return; + YouTubeGiftReceivedMessage(response.data); + } +}; + + +for (const [event, handler] of Object.entries(youtubeMessageHandlers)) { + streamerBotClient.on(event, handler); +} + + + + + +streamerBotClient.on('General.Custom', (response) => { + if (response.data.platform === 'YouTube') { + + let json = response.data; + let ytdata = response.data.data; + + switch (json.data.eventname) { + + case 'Super Chat' : + console.debug('YouTube Super Chat', ytdata); + if (showYouTubeSuperChats == false) return; + youTubeSuperChatMessage(ytdata); + break; + + case 'Super Sticker' : + console.debug('YouTube Super Sticker', ytdata); + if (showYouTubeSuperStickers == false) return; + youTubeSuperStickerMessage(ytdata); + break; + + case 'New Sponsor' : + console.debug('YouTube New Member', ytdata); + if (showYouTubeMemberships == false) return; + youTubeNewSponsorMessage(ytdata); + break; + + case 'Member Milestone' : + console.debug('YouTube Member Milestone', ytdata); + if (showYouTubeMemberships == false) return; + youTubeNewSponsorMessage(ytdata); + break; + + case 'Membership Gift' : + console.debug('YouTube Membership Gift', ytdata); + if (showYouTubeGiftMemberships == false) return; + youTubeGiftedMembersMessage(ytdata); + break; + + case 'Gift Membership Received' : + console.debug('YouTube Gift Bomb Membership', ytdata); + if (showYouTubeMembershipsTrain == false) return; + YouTubeGiftReceivedMessage(ytdata); + break; + + default: + console.debug('General YouTube Data', ytdata); + } + } + +}); + + + + +async function youTubeChatMessage(data) { + if (data.message.startsWith("!") && excludeCommands == true) + return; + + const { + user: { + id: userID, + profileImageUrl: avatar, + name: userName, + isVerified, + isSponsor, + isModerator, + isOwner, + }, + eventId: messageID, + message, + } = data; + + const badgesHTML = [ + isVerified && '', + isSponsor && '', + isModerator && '', + isOwner && '', + ].filter(Boolean).join(''); + + const classes = [ + isSponsor && 'sub', + isModerator && 'mod', + isOwner && 'owner', + ].filter(Boolean); + + const messageData = { + classes: classes.join(' '), + avatar, + badges: badgesHTML, + userName, + color: await createRandomColor('youtube', userID), + message, + reply: '', + }; + addMessageToChat(userID, messageID, 'youtube', messageData); +} + + +async function youTubeSuperChatMessage(data) { + const { + user: { + id: userID, + name: userName, + }, + eventId: messageID, + currencyCode: currency, + amount, + message : textmessage + } = data; + + /*var moneycurrency = currency || 'USD'; + var money = formatCurrency(amount, moneycurrency);*/ + + var money = amount; + + const [avatar, message] = await Promise.all([ + ``, + currentLang.youtube.superchat({ + money : money, + message : textmessage + }) + ]); + + const classes = 'superchat'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + } + addEventToChat(userID, messageID, 'youtube', messageData); +} + + +async function youTubeSuperStickerMessage(data) { + const { + user: { + id: userID, + name: userName, + }, + eventId: messageID, + currency, + amount, + message : textmessage + } = data; + + /*var moneycurrency = currency || 'USD'; + var money = formatCurrency(amount, moneycurrency);*/ + + var money = amount; + + const [avatar, message] = await Promise.all([ + ``, + currentLang.youtube.superchat({ + money : money, + message : textmessage + }) + ]); + const classes = 'supersticker'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + } + addEventToChat(userID, messageID, 'youtube', messageData); +} + +async function youTubeNewSponsorMessage(data) { + const { + user: { + id: userID, + name: userName, + }, + eventId: messageID, + levelName, + months, + tier, + } = data; + const [avatar, message] = await Promise.all([ + ``, + currentLang.youtube.member({ + months : months, + tier : levelName, + }) + ]); + const classes = 'member'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + } + addEventToChat(userID, messageID, 'youtube', messageData); +} + + +async function youTubeGiftedMembersMessage(data) { + const { + user: { + id: userID, + name: userName, + }, + eventId: messageID, + tier, + count + } = data; + const [avatar, message] = await Promise.all([ + ``, + currentLang.youtube.giftedmembers({ + total : count, + tier : tier + }) + ]); + const classes = 'giftedmembers'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + } + addEventToChat(userID, messageID, 'youtube', messageData); +} + +async function YouTubeGiftReceivedMessage(data) { + const { + user: { + id: userID, + name: userName, + }, + gifter: { + id : gifterUserId, + name: gifterUserName + }, + eventId: messageID, + tier + } = data; + const [avatar, message] = await Promise.all([ + ``, + currentLang.youtube.giftedmembers({ + gifted : gifterUserName, + tier : tier + + }) + ]); + const classes = 'giftedtrainmembers'; + const messageData = { + classes: classes, + avatar, + badges: '', + userName, + color: '#FFF', + message, + reply: '', + } + addEventToChat(userID, messageID, 'youtube', messageData); +} + + +async function youTubeUpdateStatistics(data) { + const viewers = DOMPurify.sanitize(data.concurrentViewers); + const likes = DOMPurify.sanitize(data.likeCount); + document.querySelector('#statistics #youtube .viewers span').textContent = formatNumber(viewers); + document.querySelector('#statistics #youtube .likes span').textContent = formatNumber(likes); +} \ No newline at end of file