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(`
+
+ `);
+
+ 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(`
+
+ `);
+
+ 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] = `
`;
+ }
+ });
+ }
+
+ 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] = `
`;
+ }
+ });
+ }
+
+ 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