Add files via upload

This commit is contained in:
Rodrigo Emanuel
2025-04-11 17:19:41 -03:00
committed by GitHub
parent 3b62b32a23
commit 864cf86a44
13 changed files with 2731 additions and 0 deletions

342
css/app.css Normal file
View File

@@ -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;
}

248
css/settings.css Normal file
View File

@@ -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; }
}

329
js/app-mockup.js Normal file
View File

@@ -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: [
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/1.0" class="emote">',
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/425671/default/dark/1.0" class="emote">',
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/301428702/default/dark/1.0" class="emote">'
],
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 ? '<i class="fa-solid fa-star"></i>' : '',
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);
}

287
js/app.js Normal file
View File

@@ -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(`
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message" style="">
<div class="animate__animated animate__fadeInUp animate__faster">
${!data.shared ? '' : data.shared}
${showTimestamps == true ? '<span class="time">'+whatTimeIsIt()+'</span>' : ''}
${showPlatform == true ? '<i class="platform fa-brands fa-'+platform+'"></i>' : '' }
${showAvatar == true ? '<span class="avatar"><img src="'+data.avatar+'"></span>' : ''}
${showBadges == true ? '<span class="badges">'+data.badges+'</span>' : ''}
<span style="color: ${data.color}" class="user">${data.userName}:</span>
${!data.reply ? '' : data.reply}
<span class="text">${data.message}</span>
</div>
</div>
`);
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(`
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message event" style="">
<div class="animate__animated animate__faster animate__fadeInUp">
${!data.reply ? '' : data.reply}
${showPlatform == true ? '<i class="platform '+(platform == 'money' ? 'fa-solid' : 'fa-brands')+' fa-'+platform+'"></i>' : '&nbsp;&nbsp;' }
<span class="info">
<!--<span class="avatar"><img src="${data.avatar}"></span>-->
<span style="color: ${data.color}" class="user">${data.userName}</span>
<span class="text">${data.message}</span>
</span>
</div>
</div>
`);
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);
}

72
js/lang/en.js Normal file
View File

@@ -0,0 +1,72 @@
const en = {
streamerbotconnected: 'Streamer.bot Online!',
streamerbotdisconnected: 'Streamer.bot Disconnected!',
twitch : {
follow : () => ` followed the channel.`,
announcement : () => ` <div class="reply">📢 <strong>Announcement</strong></div>`,
channelpoints : ({ title }) => ` <div class="reply"><i class="fa-solid fa-wand-magic-sparkles"></i> <strong>Channel Points - ${title}</strong></div>`,
bits : ({ bits, message }) => ` cheered <i class="fa-regular fa-gem fall-and-bounce"></i> <strong>${bits} bits</strong>${message ? ' - '+message : ''}`,
sub : ({ months, isPrime, tier }) => ` subscribed for
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
resub : ({ months, isPrime, tier }) => ` subscribed for
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
gifted : ({ gifted, months, tier, total }) => ` gifted
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
of Tier ${tier.toString().charAt(0)} sub</strong>
to <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
giftedbomb : ({ count, total, tier }) => ` gifted <i class="fa-solid fa-gift"></i> <strong>${count} Tier ${tier.toString().charAt(0)} subs</strong> to the Community, <strong>${total || 1} ${total == 1 ? 'gift' : 'gifts'} in total.</strong>`,
raid : ({ viewers }) => ` raided the channel with <i class="fa-solid fa-users"></i> <strong>${viewers} viewers</strong>`
},
youtube : {
superchat : ({ money, message }) => ` superchatted <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${message ? ' - '+message : ''}
`,
supersticker : ({ money, message }) => ` sent a supersticker of <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${message ? ' - '+message : ''}
`,
member : ({ months, tier }) => ` became a member for
<i class="fa-solid fa-star"></i>
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
(Tier ${tier})</strong>`,
giftedmembers : ({ total, tier }) => ` gifted <i class="fa-solid fa-gift"></i> <strong>${total} Tier ${tier} memberships</strong>`,
giftedtrainmembers : ({ gifted, tier }) => ` gifted a membership
<strong>(${tier})</strong>
to <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
},
streamlabs : {
tip : ({ money, message }) => ` donated 🪙 <strong>${money}</strong><!-- ${message} -->`,
},
streamelements : {
tip : ({ money, message }) => ` donated 🪙 <strong>${money}</strong><!-- ${message} -->`,
},
tiktok : {
follow : () => ` followed the channel.`,
sub : ({ months }) => ` subscribed for <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${months == 1 ? 'month' : 'months'}.</strong>`,
gift : ({ gift, count, coins }) => ` gifted <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${coins == 1 ? 'coin' : 'coins'}).</strong>`,
}
}

4
js/lang/lang.js Normal file
View File

@@ -0,0 +1,4 @@
const lang = {
ptbr : ptbr,
en : en
}

72
js/lang/ptbr.js Normal file
View File

@@ -0,0 +1,72 @@
const ptbr = {
streamerbotconnected: 'Streamer.bot Conectado!',
streamerbotdisconnected: 'Streamer.bot Desconectado!',
twitch : {
follow : () => ` seguiu o canal.`,
announcement : () => ` <div class="reply">📢 <strong>Anúncio</strong></div>`,
channelpoints : ({ title }) => ` <div class="reply"><i class="fa-solid fa-wand-magic-sparkles"></i> <strong>Pontos do Canal - ${title}</strong></div>`,
bits : ({ bits, message }) => ` doou <i class="fa-regular fa-gem fall-and-bounce"></i> <strong>${bits} bits</strong>${message ? ' - '+message : ''}`,
sub : ({ months, isPrime, tier }) => ` se inscreveu por
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
resub : ({ months, isPrime, tier }) => ` se inscreveu por
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
gifted : ({ gifted, months, tier }) => ` doou
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
de Tier ${tier.toString().charAt(0)}</strong>
para <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
giftedbomb : ({ count, total, tier }) => ` doou <i class="fa-solid fa-gift"></i> <strong>${count} inscrições Tier ${tier.toString().charAt(0)}</strong> para a Comunidade, totalizando <strong>${total || 1} ${total == 1 ? 'doação' : 'doações'}.</strong>`,
raid : ({ viewers }) => ` raidou o canal com <i class="fa-solid fa-users"></i> <strong>${viewers} pessoas</strong>`
},
youtube : {
superchat : ({ money, message }) => ` fez um superchat de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${message ? ' - '+message : ''}
`,
supersticker : ({ money, message }) => ` enviou um superchat de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${message ? ' - '+message : ''}
`,
member : ({ months, tier }) => ` se inscreveu por
<i class="fa-solid fa-star"></i>
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
(Tier ${tier})</strong>`,
giftedmembers : ({ total, tier }) => ` doou <i class="fa-solid fa-gift"></i> <strong>${total} inscrições (Tier ${tier})</strong>`,
giftedtrainmembers : ({ gifted, tier }) => ` doou uma assinatura
<strong>(${tier})</strong>
para <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
},
streamlabs : {
tip : ({ money, message }) => ` doou 🪙 <strong>${money}</strong><!-- ${message} -->`,
},
streamelements : {
tip : ({ money, message }) => ` doou 🪙 <strong>${money}</strong><!-- ${message} -->`,
},
tiktok : {
follow : () => ` seguiu o canal.`,
sub : ({ months }) => ` se inscreveu por <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}.</strong>`,
gift : ({ gift, count, coins }) => ` doou <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${coins == 1 ? 'moeda' : 'moedas'}).</strong>`,
}
}

139
js/settings.js Normal file
View File

@@ -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();
});

View File

@@ -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);
}

42
js/streamlabs/module.js Normal file
View File

@@ -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);
}

222
js/tiktok/module.js Normal file
View File

@@ -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 && '<i class="fa-solid fa-star"></i>',
isModerator && '<i class="fa-solid fa-user-gear"></i>',
].filter(Boolean).join('');
const classes = [
isSubscriber && 'sub',
isModerator && 'mod',
].filter(Boolean);
var fullmessage = message;
emotes.forEach(emote => {
var emotetoadd = ` <img src="${emote.emoteImageUrl}" class="emote" data-emote-id="${emote.emoteId}"> `;
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);
}
}

586
js/twitch/module.js Normal file
View File

@@ -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 ?
`<div class="reply"><i class="fa-solid fa-arrow-turn-up"></i> <strong>${replyData.userName}:</strong> ${replyData.msgBody}</div>` :
'';
var sharedChat = '';
if (isSharedChat) {
if (showTwitchSharedChat == true) {
if (!data.sharedChat.primarySource)
{
var sharedChat = `<div class="shared"><span><i class="fa-solid fa-comments"></i> <strong>${data.sharedChat.sourceRoom.name}</strong></span> <i class="fa-solid fa-arrow-turn-down"></i></div>`;
}
}
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] = `<img src="${emote.imageUrl}" data-emote-id="${emote.id}" alt="${emote.name}" class="emote">`;
}
});
}
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] = `<img src="${emote.imageUrl}" alt="${emote.text}" class="emote">`;
}
});
}
return words.join(" ");
}
async function getTwitchBadges(data) {
const badges = data.message.badges;
var htmlBadges = '';
badges.forEach((badge) => {
htmlBadges += `<img src="${badge.imageUrl}" class="badge">`;
});
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;
}
}

344
js/youtube/module.js Normal file
View File

@@ -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 && '<i class="fa-solid fa-check"></i>',
isSponsor && '<i class="fa-solid fa-star"></i>',
isModerator && '<i class="fa-solid fa-wrench"></i>',
isOwner && '<i class="fa-solid fa-video"></i>',
].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);
}