Add files via upload

This commit is contained in:
Rodrigo Emanuel
2025-04-23 20:25:33 -03:00
committed by GitHub
parent 58c0b9e2d4
commit c72f082abb
19 changed files with 4638 additions and 0 deletions

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

@@ -0,0 +1,685 @@
/* ----------------------- */
/* 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 = {
avatars: [
'https://static-cdn.jtvnw.net/user-default-pictures-uv/dbdc9198-def8-11e9-8681-784f43822e80-profile_image-300x300.png',
'https://static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-300x300.png',
'https://static-cdn.jtvnw.net/user-default-pictures-uv/215b7342-def9-11e9-9a66-784f43822e80-profile_image-300x300.png',
'https://static-cdn.jtvnw.net/user-default-pictures-uv/ce57700a-def9-11e9-842d-784f43822e80-profile_image-300x300.png'
],
users: [
{ name: 'Ninja' },
{ name: 'SypherPK' },
{ name: 'CouRageJD', },
{ name: 'Gaules' },
{ name: 'Nadeshot' },
{ name: 'WILDCAT', },
{ name: 'NickEh30' },
{ name: 'LEGIQN' },
{ name: 'moistcr1tikal' },
{ name: 'FISHNOTHING' },
{ name: 'smii7y' },
{ name: 'kinggothalion', },
{ name: 'harrisheller' },
{ name: 'kaicenat' },
{ name: 'caseoh_' }
],
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?",
"Let's gooooo!",
"That clutch tho 🔥",
"I can't stop laughing 😂",
"You just destroyed them!",
"What a play!",
"Did anyone else see that??",
"Streamer luck confirmed 😆",
"Please do that move again!",
"Bro you cracked?",
"This chat is wild tonight",
"Can you shout out my friend?",
"What setup are you using?",
"Mic sounds super clean",
"W stream fr",
"Hydrate check! 💧",
"Pet the dog on stream pls",
"That reaction time 😳",
"You're my comfort streamer 🧡",
"This game brings back memories",
"Why is this so intense omg",
"Chat, whats your favorite snack?",
"That edit was slick",
"Mobile gang where you at?",
"Sheeesh 🥶",
"This is better than Netflix",
"How long you been streaming?",
"Can we get some hype in the chat?",
"I wish I was this good",
"Can mods ban that guy?",
"Backseat gaming intensifies 😅",
"This reminds me of old-school Twitch",
"Nice aim bot... jk (I hope)",
"Wanna 1v1?",
"The vibes are immaculate today",
"Who else is vibing with the music?",
"That moment needs to be clipped",
"Streamer out here cooking 🔥",
"Yo, this community is chill",
"Day made. Thanks for the laughs!",
"Okay but that was actually insane"
],
emotes: [
{
"id": "emotesv2_9eade28238d64e83b0219a9025d4692d",
"type": "Twitch",
"name": "AnotherRecord",
"startIndex": 24,
"endIndex": 36,
"imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/emotesv2_9eade28238d64e83b0219a9025d4692d/default/dark/2.0"
},
{
"id": "301428702",
"type": "Twitch",
"name": "BOP",
"startIndex": 20,
"endIndex": 22,
"imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/301428702/default/dark/2.0"
},
{
"id": "354",
"type": "Twitch",
"name": "4Head",
"startIndex": 14,
"endIndex": 18,
"imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/354/default/dark/2.0"
},
{
"id": "425618",
"type": "Twitch",
"name": "LUL",
"startIndex": 10,
"endIndex": 12,
"imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/2.0"
},
{
"id": "305954156",
"type": "Twitch",
"name": "PogChamp",
"startIndex": 0,
"endIndex": 7,
"imageUrl": "https://static-cdn.jtvnw.net/emoticons/v2/305954156/default/dark/2.0"
}
],
emotesInsideMessages: [
'PogChamp',
'LUL',
'BOP',
'4Head',
'AnotherRecord'
],
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!'
],
badges: [
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/28ac9d77-bba9-4281-aba9-716081aee210/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/c4a29737-e8a5-4420-917a-314a447f083e/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/ae1c6c62-c057-4fad-a1d4-663bf988701f/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/2de71f4f-b152-4308-a426-127a4cf8003a/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/4149750c-9582-4515-9e22-da7d5437643b/3" },
{ "imageUrl": "https://static-cdn.jtvnw.net/badges/v1/5864739a-5e58-4623-9450-a2c0555ef90b/3" }
],
superstickers: [
{ "imageUrl": "https://lh3.googleusercontent.com/G2OgWJkuvSullUPp2i09zG_WR0IpQu-6Ti4pFXn_FJ1OkR6zU5GdiP9cBavimQopETyojInsRCe8uefjJBqn=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/-21C0x6zYcDxpJVYKl8CCKroyjW2Hdvh2FWBipCTFhonaPy2cSJZWTGvmjsoBJu-LedOHQrw1Qu7TYXxIlxv=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/kpwCxm65pv0p2YMunCEHaYcD1A0TnwTg4uSJMDsBriu6cZSGOAjXw_CPvV5PajvWEq1LANypR_WHRpA7HU8=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/y4t53UFh4eWO9FmuXgELtXn0cWsZEAJOWCExbumx2vcNclm2VYJkd4Omo7lKLxOg78zaXBmukrN0ONPRDwM=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/aQTC6r0gjuns5TwJTMoA_mqOH3mizXxzlAJqh_CpLx8lWyKiUgS_EjTATRTX0Qzm8MlZyXAg7r6kFzlH8HgS=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/oUIeg07YsvEuUcF7wOg6U3o1dOCANoBWuF1DYr2jGPFOyQ-bEFRiFm-6gU3urJPaX_AqtZgsNpGb0KNimA=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/gauEuKs0cTW_YrtkKit45UrShY7KuK2-Kh9RV3H3Eirtx2KY6PHLeaDHbFz-l9OGMYISF0F57Wk2lzTiHw=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/PQlPxOdeVk_oY3D_Ow0JRXvV3AbVIEoApzvenbfgsAHoLF4_EgxCV7Dsd-kqMsCAqhHhNG0vUY2Ssoa03iW_=s148-rwa" },
{ "imageUrl": "https://lh3.googleusercontent.com/Cmmj_3s8DpgpuHdOhUmIZQU0Gmex9IISD2SNk4UQY-HA1jHfSPYCk6-gZ-PEpLKGHgyEfZNRCiAV_lHC_Q=s148-rwa" },
],
tiktokGifts: [
{ name: "Rose", coins: 1 },
{ name: "Finger Heart", coins: 5 },
{ name: "TikTok", coins: 10 },
{ name: "Confetti", coins: 100 },
{ name: "Galaxy", coins: 1000 }
]
};
// Function to generate a random mockup event
function generateMockEvent() {
const eventTypes = [
'twitch-chat', 'twitch-chat', 'twitch-chat', 'twitch-chat', 'twitch-chat',
'twitch-follow', 'twitch-bits', 'twitch-sub', 'twitch-resub',
'twitch-giftsub', 'twitch-giftbomb', 'twitch-raid',
'twitch-announcement', 'twitch-reward', 'twitch-gigantifyemote',
'youtube-chat', 'youtube-chat', 'youtube-chat', 'youtube-chat', 'youtube-chat',
'youtube-superchat', 'youtube-supersticker', 'youtube-newsponsor', 'youtube-membermilestone', 'youtube-membergift',
'tiktok-chat', 'tiktok-chat', 'tiktok-chat', 'tiktok-chat', 'tiktok-chat',
'tiktok-follow', 'tiktok-sub', 'tiktok-gift',
'streamlabs-tip', 'streamelements-tip',
];
// Select random event type and user
const fakeAvatar = mockData.avatars[Math.floor(Math.random() * mockData.avatars.length)];
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
const user = mockData.users[Math.floor(Math.random() * mockData.users.length)];
const announcement = mockData.announcements[Math.floor(Math.random() * mockData.announcements.length)];
const reward = mockData.rewards[Math.floor(Math.random() * mockData.rewards.length)];
const emote = mockData.emotes[Math.floor(Math.random() * mockData.emotes.length)];
const emotesInsideMessages = mockData.emotesInsideMessages[Math.floor(Math.random() * mockData.emotesInsideMessages.length)];
const messagetext = mockData.messages[Math.floor(Math.random() * mockData.messages.length)];
const messageId = randomString(40);
const shuffledBadges = [...mockData.badges].sort(() => Math.random() - 0.5);
const badgeCount = Math.floor(Math.random() * 3) + 1; // 1 to 3
const badgeschosen = shuffledBadges.slice(0, badgeCount);
const randomIndex = Math.floor(Math.random() * mockData.superstickers.length);
const randomStickerUrl = mockData.superstickers[randomIndex].imageUrl;
const tiktokGift = mockData.tiktokGifts[Math.floor(Math.random() * mockData.tiktokGifts.length)];
const firstMessage = Math.random() < 0.1;
switch(eventType) {
case 'twitch-chat' :
var data = {
emotes: mockData.emotes,
message: {
username: user.name.toLowerCase(),
color: randomColor(),
displayName: user.name,
message: messagetext + ' ' + emotesInsideMessages,
firstMessage: firstMessage,
badges: badgeschosen,
},
messageId: messageId,
};
const ifHasReply = Math.random() < 0.05;
if (ifHasReply) {
data.message.isReply = true;
var replier = mockData.users[Math.floor(Math.random() * mockData.users.length)];
data.message.reply = {
userName: replier.name,
msgBody: mockData.messages[Math.floor(Math.random() * mockData.messages.length)]
};
}
const ifHasShared = Math.random() < 0.05;
if (ifHasShared) {
data.message.isSharedChat = true;
var sharedParentUser = mockData.users[Math.floor(Math.random() * mockData.users.length)];
data.sharedChat = {
sourceRoom: {
name: sharedParentUser.name
}
};
}
twitchChatMessage(data);
break;
case 'twitch-follow' :
var data = {
user_id: user.name.toLowerCase(),
user_name: user.name,
};
twitchFollowMessage(data);
break;
case 'twitch-bits' :
var data = {
emotes: mockData.emotes,
user: {
id: user.name.toLowerCase(),
name: user.name
},
message: {
bits: Math.floor(Math.random() * 10000) + 1,
message: messagetext,
},
messageId: messageId,
};
twitchBitsMessage(data);
break;
case 'twitch-sub' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name
},
duration_months: 1,
sub_tier: parseInt(Math.floor(Math.random() * 3) + 1),
isPrime: Math.random() < 0.1,
text: messagetext
}
twitchSubMessage(data);
break;
case 'twitch-resub' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name
},
cumulativeMonths: Math.floor(Math.random() * 50) + 1,
subTier: parseInt(Math.floor(Math.random() * 3) + 1),
isPrime: Math.random() < 0.1,
text: messagetext
}
twitchReSubMessage(data);
break;
case 'twitch-giftsub' :
var gifterUser = mockData.users[Math.floor(Math.random() * mockData.users.length)];
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name
},
recipient: { name: gifterUser.name },
durationMonths: Math.floor(Math.random() * 50) + 1,
subTier: parseInt(Math.floor(Math.random() * 3) + 1),
cumlativeTotal: Math.floor(Math.random() * 200) + 1
}
twitchGiftMessage(data);
break;
case 'twitch-giftbomb' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name
},
total: Math.floor(Math.random() * 50) + 1,
sub_tier: parseInt(Math.floor(Math.random() * 3) + 1),
cumulative_total: Math.floor(Math.random() * 200) + 1
}
twitchGiftSubsMessage(data);
break;
case 'twitch-raid' :
var data = {
from_broadcaster_user_login: user.name.toLowerCase(),
from_broadcaster_user_name: user.name,
viewers: Math.floor(Math.random() * 200) + 1
}
twitchRaidMessage(data);
break;
case 'twitch-announcement' :
var data = {
messageId: messageId,
user: {
id: user.name.toLowerCase(),
name: user.name
},
text: announcement,
parts: mockData.emotes
}
twitchAnnouncementMessage(data);
break;
case 'twitch-reward' :
var data = {
user_id: user.name.toLowerCase(),
user_name: user.name,
user_input: announcement,
reward: {
title: reward
}
}
twitchRewardRedemption(data);
break;
case 'youtube-chat' :
var amIMod = Math.random() < 0.1;
var amISub = Math.random() < 0.1;
var amIOwner = Math.random() < 0.1;
if (amIOwner == true) {
amIMod = false;
amISub = false;
}
var data = {
user : {
id: user.name.toLowerCase(),
profileImageUrl: fakeAvatar,
name: user.name,
isVerified: Math.random() < 0.2,
isSponsor: amISub,
isModerator: amIMod,
isOwner: amIOwner,
},
emotes: mockData.emotes,
message: messagetext,
eventId: messageId,
};
youTubeChatMessage(data);
break;
case 'youtube-superchat' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name,
},
eventId: messageId,
amount: '$' + Math.floor(Math.random() * 2000) + 1,
message : messagetext
};
youTubeSuperChatMessage(data);
break;
case 'youtube-supersticker' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name
},
eventId: messageId,
amount: '$' + Math.floor(Math.random() * 2000) + 1,
_fVK15WwYFFCcGLW0zi1jLCJXj3f: {
imageUrl: randomStickerUrl
}
};
youTubeSuperStickerMessage(data);
break;
case 'youtube-newsponsor' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name,
},
eventId: messageId,
levelName: parseInt(Math.floor(Math.random() * 3) + 1),
months: 1
};
youTubeNewSponsorMessage(data);
break;
case 'youtube-membermilestone' :
var data = {
user: {
id: user.name.toLowerCase(),
name: user.name,
},
eventId: messageId,
levelName: parseInt(Math.floor(Math.random() * 3) + 1),
months: Math.floor(Math.random() * 50) + 1,
message: messagetext
};
youTubeNewSponsorMessage(data);
break;
case 'youtube-membergift' :
var data = {
user : {
id: user.name.toLowerCase(),
name: user.name,
},
tier: parseInt(Math.floor(Math.random() * 3) + 1),
count: Math.floor(Math.random() * 50) + 1,
eventId: messageId,
};
youTubeGiftedMembersMessage(data);
break;
case 'tiktok-chat' :
var data = {
userId: user.name.toLowerCase(),
nickname: user.name,
profilePictureUrl: fakeAvatar,
isSubscriber: Math.random() < 0.2,
isModerator: Math.random() < 0.1,
isOwner: Math.random() < 0.05,
emotes: {},
comment: messagetext,
msgId: messageId,
};
tiktokChatMessage(data);
break;
case 'tiktok-follow' :
var data = {
userId: user.name.toLowerCase(),
nickname: user.name,
profilePictureUrl: fakeAvatar,
msgId: messageId,
};
tiktokFollowMessage(data);
break;
case 'tiktok-sub' :
var data = {
userId: user.name.toLowerCase(),
nickname: user.name,
profilePictureUrl: fakeAvatar,
msgId: messageId,
subMonth: Math.floor(Math.random() * 50) + 1
};
tiktokSubMessage(data);
break;
case 'tiktok-gift' :
var data = {
userId: user.name.toLowerCase(),
nickname: user.name,
profilePictureUrl: fakeAvatar,
msgId: messageId,
giftName: tiktokGift.name,
repeatCount: Math.floor(Math.random() * 50) + 1,
diamondCount: tiktokGift.coins,
};
tiktokGiftMessage(data);
break;
case 'streamlabs-tip' :
var data = {
from: user.name,
formattedAmount: Math.floor(Math.random() * 2000) + 1,
currency: 'USD',
message: messagetext
};
streamLabsEventMessage(data);
break;
case 'streamelements-tip' :
var data = {
username: user.name,e,
amount: Math.floor(Math.random() * 2000) + 1,
currency: 'USD',
message: messagetext
};
streamElementsEventMessage(data);
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: "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;
document.querySelector('#statistics #twitch .viewers span').textContent = '0';
document.querySelector('#statistics #youtube .viewers span').textContent = '0';
document.querySelector('#statistics #youtube .likes span').textContent = '0';
document.querySelector('#statistics #tiktok .viewers span').textContent = '0';
document.querySelector('#statistics #tiktok .likes span').textContent = '0';
}
}
// 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) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomString(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 randomColor() {
const randomColor = "hsl(" + Math.random() * 360 + ", 100%, 75%)";
return randomColor;
}

292
js/app.js Normal file
View File

@@ -0,0 +1,292 @@
/* ----------------------- */
/* OPTIONS */
/* ----------------------- */
const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1");
const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080");
let streamerBotConnected = false;
const chatThreshhold = 50;
const chatContainer = document.querySelector('#chat');
const currentLang = lang[getURLParam("language", 'ptbr')];
const eventsMockup = getURLParam("eventsMockup", true);
const chatHorizontal = getURLParam("chatHorizontal", false);
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'; }
if (chatHorizontal == true) { chatContainer.classList.add('horizontal'); }
/* ----------------------- */
/* STREAMER.BOT CONNECTION */
/* ----------------------- */
const streamerBotClient = new StreamerbotClient({
host: streamerBotServerAddress,
port: streamerBotServerPort,
onConnect: (data) => {
console.debug( currentLang.streamerbotconnected );
console.debug(data);
streamerBotConnected = true;
notifySuccess({
title: currentLang.streamerbotconnected,
text: ``
});
if (eventsMockup == true) { stopMockupSystem(); }
},
onDisconnect: () => {
console.error(currentLang.streamerbotdisconnected);
streamerBotConnected = false;
if (eventsMockup == true) { startMockupSystem(); }
}
});
/* ----------------------- */
/* 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 ${chatHorizontal == true ? 'animate__fadeInRight' : '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 ${chatHorizontal == true ? 'animate__fadeInRight' : 'animate__fadeInUp'} animate__faster">
${!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);
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

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

@@ -0,0 +1,76 @@
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 ? '<br>'+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, message }) => ` 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>
${message ? '<br>'+message : '' }`,
gifted : ({ gifted, months, tier }) => ` gifted
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
of Tier ${tier.toString().charAt(0)} ${months == 1 ? 'sub' : 'subs'}</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} subs (Tier ${tier.toString().charAt(0)})</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 ? '<br>'+message : ''}
`,
supersticker : ({ money, sticker }) => `
${sticker ? '<br>': ''}
sent a supersticker of <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${sticker ? '</span></span><span class="sticker"><img src="'+sticker+'"></span>': ''}
`,
member : ({ months, tier, message }) => ` became a member for
<i class="fa-solid fa-star"></i>
<strong>${months || 1 } ${months && months > 1 ? 'months' : 'month'}
(Tier ${tier})</strong>
${message ? '<br>'+message : ''}`,
giftedmembers : ({ total, tier }) => ` gifted <i class="fa-solid fa-gift"></i> <strong>${total} ${total == 1 ? 'membership' : 'memberships'} (Tier ${tier}) to the channel</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 ? '<br>'+message : ''}`,
},
streamelements : {
tip : ({ money, message }) => ` donated 🪙 <strong>${money}</strong>${message ? '<br>'+message : ''}`,
},
tiktok : {
follow : () => ` followed the channel`,
sub : ({ months }) => ` subscribed for <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${(months && months > 1) ? 'months' : 'month'}</strong>`,
gift : ({ gift, count, coins }) => ` gifted <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${(coins && coins > 1) ? 'coins' : 'coin'})</strong>`,
}
}

69
js/lang/es.js Normal file
View File

@@ -0,0 +1,69 @@
const es = {
streamerbotconnected: '¡Streamer.bot en línea!',
streamerbotdisconnected: '¡Streamer.bot desconectado!',
twitch : {
follow : () => ` siguió el canal`,
announcement : () => ` <div class="reply">📢 <strong>Anuncio</strong></div>`,
channelpoints : ({ title }) => ` <div class="reply"><i class="fa-solid fa-wand-magic-sparkles"></i> <strong>Puntos del canal - ${title}</strong></div>`,
bits : ({ bits, message }) => ` envió <i class="fa-regular fa-gem fall-and-bounce"></i> <strong>${bits} bits</strong>${message ? '<br>'+message : ''}`,
sub : ({ months, isPrime, tier }) => ` se suscribió por
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'mes' : 'meses'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
resub : ({ months, isPrime, tier, message }) => ` se volvió a suscribir por
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
<strong>${months || 1 } ${months == 1 ? 'mes' : 'meses'}
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>
${message ? '<br>'+message : '' }`,
gifted : ({ gifted, months, tier }) => ` regaló
<strong>${months || 1 } ${months == 1 ? 'mes' : 'meses'}
de Tier ${tier.toString().charAt(0)} ${months == 1 ? 'suscripción' : 'suscripciones'}</strong>
a <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
giftedbomb : ({ count, total, tier }) => ` regaló <i class="fa-solid fa-gift"></i> <strong>${count} suscripciones (Tier ${tier.toString().charAt(0)})</strong> a la comunidad, <strong>${total || 1} ${total == 1 ? 'regalo' : 'regalos'} en total</strong>`,
raid : ({ viewers }) => ` hizo una raid al canal con <i class="fa-solid fa-users"></i> <strong>${viewers} espectadores</strong>`
},
youtube : {
superchat : ({ money, message }) => ` envió un superchat <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${message ? '<br>'+message : ''}
`,
supersticker : ({ money, sticker }) => `
${sticker ? '<br>': ''}
envió un supersticker de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${sticker ? '</span></span><span class="sticker"><img src="'+sticker+'"></span>': ''}
`,
member : ({ months, tier, message }) => ` se hizo miembro por
<i class="fa-solid fa-star"></i>
<strong>${months || 1 } ${months && months > 1 ? 'meses' : 'mes'}
(Tier ${tier})</strong>
${message ? '<br>'+message : ''}`,
giftedmembers : ({ total, tier }) => ` regaló <i class="fa-solid fa-gift"></i> <strong>${total} ${total == 1 ? 'membresía' : 'membresías'} (Tier ${tier}) al canal</strong>`,
giftedtrainmembers : ({ gifted, tier }) => ` regaló una membresía
<strong>(Tier ${tier})</strong>
a <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
},
streamlabs : {
tip : ({ money, message }) => ` donó 🪙 <strong>${money}</strong>${message ? '<br>'+message : ''}`,
},
streamelements : {
tip : ({ money, message }) => ` donó 🪙 <strong>${money}</strong>${message ? '<br>'+message : ''}`,
},
tiktok : {
follow : () => ` siguió el canal`,
sub : ({ months }) => ` se suscribió por <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${(months && months > 1) ? 'meses' : 'mes'}</strong>`,
gift : ({ gift, count, coins }) => ` regaló <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${(coins && coins > 1) ? 'monedas' : 'moneda'})</strong>`,
}
}

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

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

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

@@ -0,0 +1,76 @@
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 ? '<br>'+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, message }) => ` 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>
${message ? '<br>'+message : '' }`,
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 ? '<br>'+message : ''}
`,
supersticker : ({ money, sticker }) => `
${sticker ? '<br>': ''}
enviou um super sticker de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
${sticker ? '</span></span><span class="sticker"><img src="'+sticker+'"></span>': ''}
`,
member : ({ months, tier, message }) => ` se inscreveu por
<i class="fa-solid fa-star"></i>
<strong>${months || 1 } ${months && months > 1 ? 'meses' : 'mês'}
(Tier ${tier})</strong>
${message ? '<br>'+message : ''}`,
giftedmembers : ({ total, tier }) => ` doou <i class="fa-solid fa-gift"></i> <strong>${total} ${total == 1 ? 'inscrição' : 'inscrições'} (Tier ${tier}) para o canal</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 ? '<br>'+message : ''}`,
},
streamelements : {
tip : ({ money, message }) => ` doou 🪙 <strong>${money}</strong>${message ? '<br>'+message : ''}`,
},
tiktok : {
follow : () => ` seguiu o canal`,
sub : ({ months }) => ` se inscreveu por <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${(months && months > 1) ? 'meses' : 'mês'}</strong>`,
gift : ({ gift, count, coins }) => ` doou <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${(coins && coins > 1) ? 'moedas' : 'moeda'})</strong>`,
}
}

316
js/settings.js Normal file
View File

@@ -0,0 +1,316 @@
let streamerBotClient;
let streamerBotConnected = false;
async function saveSettingsToLocalStorage() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const hiddenField = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
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));
if (streamerBotConnected == true) {
streamerBotClient.doAction(
{ name : "YouTube Custom Emotes" },
{
"chatrdytcustomemotes": JSON.stringify(hiddenField.value.trim()),
}
).then( (setglobals) => {
console.debug('Saving YouTube Emotes from Streamer.Bot', setglobals);
});
}
}
async 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];
}
}
});
var streamerBotServerAddress = document.querySelector('input[type=text][name=streamerBotServerAddress]').value;
var streamerBotServerPort = document.querySelector('input[type=text][name=streamerBotServerPort]').value;
streamerBotClient = new StreamerbotClient({
host: streamerBotServerAddress,
port: streamerBotServerPort,
onConnect: (data) => {
streamerBotConnected = true;
var sbstatus = document.getElementById('memberemotesbstatus');
sbstatus.style.color = '#00dd63';
sbstatus.textContent = 'Streamer.Bot is Online!';
streamerBotClient.getGlobals().then( (getglobals) => {
const settings = JSON.parse(getglobals.variables.chatrdytcustomemotes.value);
console.debug('Getting YouTube Emotes from Streamer.Bot', settings);
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
textarea.value = settings;
populateEmoteList();
});
},
onDisconnect: () => {
console.error('Streamer.bot Disconnected!');
streamerBotConnected = false;
var sbstatus = document.getElementById('memberemotesbstatus');
sbstatus.style.color = '#ff0000';
sbstatus.textContent = 'Streamer.Bot Needs to be Online!';
}
});
}
async function pushChangeEvents() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
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();
});
});
textfields.forEach((textfield) => {
textfield.addEventListener('input', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
}
async function generateUrl() {
document.getElementById("outputUrl").value = '';
var runThisLocally = document.querySelector("input[type=checkbox][name=runThisLocally]").checked;
var baseUrl = '';
if (runThisLocally == false) {
baseUrl = 'https://vortisrd.github.io/chatrd/chat.html'
}
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
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();
}
async 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);
}
async function setupAddEmoteModal() {
const modal = document.getElementById("addEmoteModal");
const nameInput = document.getElementById("newEmoteName");
const urlInput = document.getElementById("newEmoteURL");
const confirmBtn = document.getElementById("confirmAddEmote");
const cancelBtn = document.getElementById("cancelAddEmote");
const addButton = document.querySelector("#youtube .emote-item:last-child .add");
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
if (!modal || !addButton || !textarea) return;
// Show modal
addButton.onclick = () => {
if (streamerBotConnected == true) {
nameInput.value = "";
urlInput.value = "";
modal.classList.remove("hidden");
nameInput.focus();
}
else {
alert("Streamer.bot is Offline!");
return;
}
};
// Cancel
cancelBtn.onclick = () => {
modal.classList.add("hidden");
};
// Confirm
confirmBtn.onclick = () => {
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name || !url) {
alert("Both fields are required.");
return;
}
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (err) {
console.error("Invalid JSON", err);
alert("Emote data is invalid.");
return;
}
if (emotes[name]) {
alert(`Emote "${name}" already exists.`);
return;
}
// Add and update
emotes[name] = url;
textarea.value = JSON.stringify(emotes, null, 4);
modal.classList.add("hidden");
populateEmoteList();
};
}
async function populateEmoteList() {
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
const emoteList = document.querySelector("#youtube .emote-list");
if (!textarea || !emoteList) return;
const addButtonSpan = emoteList.querySelector(".emote-item:last-child");
// Remove all emote items except the add button
emoteList.querySelectorAll(".emote-item").forEach(item => {
if (item !== addButtonSpan) {
item.remove();
}
});
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (e) {
console.error("Invalid JSON in YouTube Emotes textarea", e);
return;
}
// Recreate each emote item
for (const [emoteName, emoteUrl] of Object.entries(emotes)) {
const span = document.createElement("span");
span.classList.add("emote-item");
span.innerHTML = `
<img data-emote="${emoteName}" src="${emoteUrl}" alt="">
<em>${emoteName}</em>
<button class="delete"><i class="fa-solid fa-trash-can"></i></button>
`;
// Add delete handler directly to the button
const deleteBtn = span.querySelector(".delete");
deleteBtn.addEventListener("click", () => {
if (confirm(`Are you sure you want to delete '${emoteName}'?`)) {
delete emotes[emoteName];
textarea.value = JSON.stringify(emotes, null, 4);
populateEmoteList(); // Re-render everything
}
});
emoteList.insertBefore(span, addButtonSpan);
}
setupAddEmoteModal();
generateUrl();
saveSettingsToLocalStorage();
}
window.addEventListener('load', () => {
loadSettingsFromLocalStorage();
generateUrl();
pushChangeEvents();
populateEmoteList();
});

View File

@@ -0,0 +1,45 @@
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);
}

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

@@ -0,0 +1,43 @@
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);
}

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

@@ -0,0 +1,231 @@
/* ----------------------------------------------------------------------------------------- */
/* 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' :
tiktokUpdateStatistics(jsonData, 'viewers');
break;
case 'like' :
tiktokUpdateStatistics(jsonData, 'likes');
break;
case 'chat' :
console.log('TikTok Chat', jsonData);
tiktokChatMessage(jsonData);
break;
case 'follow' :
console.log('TikTok Follow', jsonData);
tiktokFollowMessage(jsonData);
break;
case 'subscribe' :
console.log('TikTok Sub', jsonData);
tiktokSubMessage(jsonData);
break;
case 'gift' :
console.log('TikTok Gift', jsonData);
tiktokGiftMessage(jsonData);
break;
default:
//console.debug(json);
}
}
});
async function tiktokChatMessage(data) {
if (showTikTokMessages == false) return;
if (ignoreUserList.includes(data.nickname.toLowerCase())) return;
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;
if (emotes.length > 0) {
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) {
if (showTikTokFollows == false) return;
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) {
if (showTikTokSubs == false) return;
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) {
if (showTikTokGifts == false) return;
if (data.giftType === 1 && !data.repeatEnd) {}
else {
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 (showPlatformStatistics == false || showTikTokStatistics == false) return;
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);
}
}

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

@@ -0,0 +1,579 @@
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);
twitchChatMessage(response.data);
},
'Twitch.Follow': (response) => {
console.debug('Twitch Follow', response.data);
twitchFollowMessage(response.data);
},
'Twitch.Announcement': (response) => {
console.debug('Twitch Announcements', response.data);
twitchAnnouncementMessage(response.data);
},
'Twitch.Cheer': (response) => {
console.debug('Twitch Cheer/Bits', response.data);
twitchBitsMessage(response.data);
},
'Twitch.AutomaticRewardRedemption': (response) => {
console.debug('Twitch Auto Reward Redemption', response.data);
twitchChatMessageGiantEmote(response.data);
},
'Twitch.RewardRedemption': (response) => {
console.debug('Twitch Reward Redemption', response.data);
twitchRewardRedemption(response.data);
},
'Twitch.Sub': (response) => {
console.debug('Twitch Sub', response.data);
twitchSubMessage(response.data);
},
'Twitch.ReSub': (response) => {
console.debug('Twitch Resub', response.data);
twitchReSubMessage(response.data);
},
'Twitch.GiftSub': (response) => {
console.debug('Twitch Gift Sub', response.data);
twitchGiftMessage(response.data);
},
'Twitch.GiftBomb': (response) => {
console.debug('Twitch Gift Bomb', response.data);
twitchGiftSubsMessage(response.data);
},
'Twitch.Raid': (response) => {
console.debug('Twitch Raid', response.data);
twitchRaidMessage(response.data);
},
'Twitch.ChatMessageDeleted': (response) => {
console.debug('Twitch Chat Deleted', response.data);
twitchChatMessageDeleted(response.data);
},
'Twitch.UserBanned': (response) => {
console.debug('Twitch Ban', response.data);
twitchUserBanned(response.data);
},
'Twitch.UserTimedOut': (response) => {
console.debug('Twitch Timeout', response.data);
twitchUserBanned(response.data);
},
'Twitch.ViewerCountUpdate': (response) => {
console.debug('Twitch View Count Update', response.data);
twitchUpdateStatistics(response.data);
},
'Twitch.ChatCleared': (response) => {
console.debug('Twitch Chat Clear', response.data);
twitchChatClearMessages();
}
};
for (const [event, handler] of Object.entries(twitchMessageHandlers)) {
streamerBotClient.on(event, handler);
}
async function twitchChatMessage(data) {
if (showTwitchMessages == false) return;
if (ignoreUserList.includes(data.message.username.toLowerCase())) return;
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) {
if (showTwitchMessages == false) return;
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) {
if (showTwitchFollows == false) return;
const {
user_id : userID,
user_name : userName
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
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) {
if (showTwitchBits == false) return;
const {
messageId : messageID,
user : {
id : userID,
name : userName
}
} = data;
data.message.message = data.message.message.replace(/\bCheer\d+\b/g, '').replace(/\s+/g, ' ').trim();
const [avatar, message] = await Promise.all([
'',
currentLang.twitch.bits({
bits: data.message.bits,
message : await getTwitchEmotes(data)
}),
]);
const classes = 'bits';
const messageData = {
classes: classes,
avatar,
badges: '',
userName,
color: '#FFF',
message,
reply: '',
};
addEventToChat(userID, messageID, 'twitch', messageData);
}
async function twitchAnnouncementMessage(data) {
if (showTwitchAnnouncements == false) return;
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([
'',
` ${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) {
if (showTwitchRewardRedemptions == false) return;
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([
'',
` ${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) {
if (showTwitchSubs == false) return;
const {
user : {
id : userID,
name : userName
}
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
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) {
if (showTwitchSubs == false) return;
const {
user : {
id : userID,
name : userName
},
text
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
currentLang.twitch.resub({
months : data.cumulativeMonths,
isPrime : data.isPrime,
tier : data.subTier,
message : text
})
]);
const classes = 'sub';
const messageData = {
classes: classes,
avatar,
badges: '',
userName,
color: '#FFF',
message,
reply: '',
};
addEventToChat(userID, messageID, 'twitch', messageData);
}
async function twitchGiftMessage(data) {
if (data.fromCommunitySubGift === false) {
if (showTwitchSubs == false || showTwitchGiftedSubs == false) return;
}
else {
if (showTwitchSubs == false || showTwitchGiftedSubsUserTrain == false) return;
}
const {
user : {
id : userID,
name : userName
}
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
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) {
if (showTwitchSubs == false || showTwitchMassGiftedSubs == false) return;
const {
user : {
id : userID,
name : userName
}
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
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) {
if (showTwitchRaids == false) return;
const {
from_broadcaster_user_login: userID,
from_broadcaster_user_name: userName
} = data;
const messageID = createRandomString(40);
const [avatar, message] = await Promise.all([
'',
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) {
if (showPlatformStatistics == false || showTwitchViewers == false) return;
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 (showAvatar == true) {
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;
}
}
}

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

@@ -0,0 +1,401 @@
const showYouTubeMessages = getURLParam("showYouTubeMessages", true);
const showYouTubeSuperChats = getURLParam("showYouTubeSuperChats", true);
const showYouTubeSuperStickers = getURLParam("showYouTubeSuperStickers", true);
const showYouTubeSuperStickerGif = getURLParam("showYouTubeSuperStickerGif", true);
const showYouTubeMemberships = getURLParam("showYouTubeMemberships", true);
const showYouTubeGiftMemberships = getURLParam("showYouTubeGiftMemberships", true);
const showYouTubeMembershipsTrain = getURLParam("showYouTubeMembershipsTrain", true);
const showYouTubeStatistics = getURLParam("showYouTubeStatistics", true);
let youTubeCustomEmotes = [];
let youTubeBTTVEmotes = [];
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);
youTubeChatMessage(response.data);
},
'YouTube.UserBanned': (response) => {
console.debug('YouTube Timeout/Hide/Ban', response.data);
youTubeUserBanned(response.data);
},
'YouTube.SuperChat': (response) => {
console.debug('YouTube SuperChat', response.data);
youTubeSuperChatMessage(response.data);
},
'YouTube.SuperSticker': (response) => {
console.debug('YouTube Super Sticker', response.data);
youTubeSuperStickerMessage(response.data);
},
'YouTube.NewSponsor': (response) => {
console.debug('YouTube New Member', response.data);
youTubeNewSponsorMessage(response.data);
},
'YouTube.MemberMileStone': (response) => {
console.debug('YouTube Member Milestone', response.data);
youTubeNewSponsorMessage(response.data);
},
'YouTube.MembershipGift': (response) => {
console.debug('YouTube Gifted Membership', response.data);
youTubeGiftedMembersMessage(response.data);
},
'YouTube.GiftMembershipReceived': (response) => {
console.debug('YouTube Gifted Membership Bomb', response.data);
YouTubeGiftReceivedMessage(response.data);
},
'YouTube.StatisticsUpdated': (response) => {
console.debug(response.data);
youTubeUpdateStatistics(response.data);
}
};
for (const [event, handler] of Object.entries(youtubeMessageHandlers)) {
streamerBotClient.on(event, handler);
}
async function youTubeChatMessage(data) {
if (showYouTubeMessages == false) return;
if (ignoreUserList.includes(data.user.name.toLowerCase())) return;
if (data.message.startsWith("!") && excludeCommands == true) return;
if (streamerBotConnected == true) {
if (youTubeCustomEmotes.length == 0) {
streamerBotClient.getGlobals().then( (getglobals) => {
youTubeCustomEmotes = JSON.parse(JSON.parse(getglobals.variables.chatrdytcustomemotes.value));
console.debug('Getting YouTube Emotes from Streamer.Bot', youTubeCustomEmotes);
});
}
}
const {
user: {
id: userID,
profileImageUrl: avatar,
name: userName,
isVerified,
isSponsor,
isModerator,
isOwner,
},
eventId: messageID,
} = data;
var messageHTML = await getYouTubeEmotes(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 : messageHTML,
reply: '',
};
addMessageToChat(userID, messageID, 'youtube', messageData);
}
async function youTubeUserBanned(data) {
chatContainer.querySelectorAll(`[data-user="${data.bannedUser.id}"]`).forEach(element => {
element.remove();
});
}
async function youTubeSuperChatMessage(data) {
if (showYouTubeSuperChats == false) return;
const {
user: {
id: userID,
name: userName,
},
eventId: messageID,
amount,
message : textmessage
} = data;
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) {
if (showYouTubeSuperStickers == false) return;
const {
user: {
id: userID,
name: userName,
},
eventId: messageID,
amount
} = data;
var money = amount;
var youtubeStickerUrl = '';
if (showYouTubeSuperStickerGif == true) {
youtubeStickerUrl = await getYouTubeStickerImage(data);
}
const [avatar, message] = await Promise.all([
``,
currentLang.youtube.supersticker({
money : money,
sticker : youtubeStickerUrl
})
]);
const classes = 'supersticker';
const messageData = {
classes: classes,
avatar,
badges: '',
userName,
color: '#FFF',
message,
reply: '',
}
addEventToChat(userID, messageID, 'youtube', messageData);
}
async function youTubeNewSponsorMessage(data) {
if (showYouTubeMemberships == false) return;
const {
user: {
id: userID,
name: userName,
},
eventId: messageID,
levelName,
months,
message: messagetext,
} = data;
const [avatar, message] = await Promise.all([
``,
currentLang.youtube.member({
months : months,
tier : levelName,
message: messagetext
})
]);
const classes = 'member';
const messageData = {
classes: classes,
avatar,
badges: '',
userName,
color: '#FFF',
message,
reply: '',
}
addEventToChat(userID, messageID, 'youtube', messageData);
}
async function youTubeGiftedMembersMessage(data) {
if (showYouTubeGiftMemberships == false) return;
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) {
if (showYouTubeMembershipsTrain == false) return;
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(gifterUserId, messageID, 'youtube', messageData);
}
async function youTubeUpdateStatistics(data) {
if (showYouTubeStatistics == false) return;
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);
}
async function getYouTubeEmotes(data) {
let message = data.message;
const channelId = data.broadcast?.channelId;
if (!channelId) return message;
// Load emotes if not already loaded
if (youTubeBTTVEmotes.length === 0) {
try {
const res = await fetch(`https://api.betterttv.net/3/cached/users/youtube/${channelId}`);
const emoteData = await res.json();
youTubeBTTVEmotes = [
...(emoteData.sharedEmotes || []),
...(emoteData.channelEmotes || [])
];
} catch (err) {
console.warn("Failed to load BTTV emotes:", err);
}
}
// Replace BTTV emotes
for (const emote of youTubeBTTVEmotes) {
const escapedCode = escapeRegex(emote.code);
const emoteRegex = new RegExp(`(?<!\\S)${escapedCode}(?!\\S)`, 'g');
const imageUrl = `https://cdn.betterttv.net/emote/${emote.id}/1x`;
const emoteElement = `<img src="${imageUrl}" class="emote">`;
message = message.replace(emoteRegex, emoteElement);
}
// Replace built-in YouTube emotes
if (data.emotes) {
for (const emote of data.emotes) {
const emoteRegex = new RegExp(escapeRegex(emote.name), 'g');
const emoteElement = `<img src="${emote.imageUrl}" class="emote">`;
message = message.replace(emoteRegex, emoteElement);
}
}
// Replace Custom Member Emotes Defined at Settings.
// Shows if user is a Member
if (data.user.isSponsor == true) {
message = message.replace(/:([a-zA-Z0-9_]+):/g, (match, emoteName) => {
if (youTubeCustomEmotes[emoteName]) {
return `<img src="${youTubeCustomEmotes[emoteName]}" class="emote">`;
}
return match;
});
}
return message;
}
// ChatGPT created this. :)
async function getYouTubeStickerImage(data) {
const stack = [data];
while (stack.length) {
const current = stack.pop();
if (current && typeof current === 'object') {
if ('imageUrl' in current && typeof current.imageUrl === 'string') {
return current.imageUrl;
}
for (const key in current) {
if (Object.hasOwn(current, key)) {
stack.push(current[key]);
}
}
}
}
return null;
}