sync from github

This commit is contained in:
minster586
2025-08-25 01:16:23 -04:00
parent 3c95ac2361
commit 9384c57f43
53 changed files with 6941 additions and 1347 deletions

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs>
<style>
.st0 {
fill: #fff;
}
.st1 {
fill: #a970ff;
}
</style>
</defs>
<polygon class="st0" points="668.5 400 482 602 208 560 224 73 678 91 668.5 400"/>
<path class="st1" d="M723.1,53.8v357l-242.4,229.5h-107.7l-134.6,127.5v-127.5H76.9V181.3L211.5,53.8h511.7ZM669.3,104.8H238.4v382.5h121.2v89.3l94.3-89.3h107.7l107.7-102V104.8ZM440.4,350.5h-53.9v-153.1h53.9v153.1ZM588.5,351.1h-53.9v-153h53.9v153Z"/>
</svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@@ -0,0 +1,34 @@
#chat .item.announcement.twitch .message {
background: rgba(145,70,255,0.75);
}
#chat .item.twitch .platform .hidden-platform {
background: rgba(145,70,255,1);
}
#chat .item.announcement.twitch.green .message {
background: linear-gradient(to bottom, rgba(0,219,132,0.75),rgba(87,190,230,0.75));
}
#chat .item.announcement.twitch.purple .message {
background: linear-gradient(to bottom, rgba(145,70,255,0.75),rgba(255,117,230,0.75));
}
#chat .item.announcement.twitch.orange .message {
background: linear-gradient(to bottom, rgba(255, 179, 26,0.75),rgba(224,224,0,0.75));
}
#chat .item.announcement.twitch.blue .message {
background: linear-gradient(to bottom, rgba(0,214,214,0.75),rgba(145,70,255,0.75));
}
#chat .event.twitch .message {
background: rgba(145,70,255,0.75);
}
#chat .item img.gigantified {
display: block;
height: 96px;
}
#statistics .platform#twitch { background: #a970ff; }

765
js/modules/twitch/module.js Normal file
View File

@@ -0,0 +1,765 @@
/* ----------------------- */
/* TWITCH MODULE VARIABLES */
/* ----------------------- */
const showTwitch = getURLParam("showTwitch", true);
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 showTwitchPronouns = getURLParam("showTwitchPronouns", false);
const showTwitchViewers = getURLParam("showTwitchViewers", true);
const twitchAvatars = new Map();
const twitchPronouns = new Map();
// TWITCH EVENTS HANDLERS
const twitchMessageHandlers = {
'Twitch.ChatMessage': (response) => {
twitchChatMessage(response.data);
},
'Twitch.Follow': (response) => {
twitchFollowMessage(response.data);
},
'Twitch.Announcement': (response) => {
twitchAnnouncementMessage(response.data);
},
'Twitch.Cheer': (response) => {
twitchBitsMessage(response.data);
},
'Twitch.AutomaticRewardRedemption': (response) => {
twitchChatMessageGiantEmote(response.data);
},
'Twitch.RewardRedemption': (response) => {
twitchRewardRedemption(response.data);
},
'Twitch.Sub': (response) => {
twitchSubMessage(response.data);
},
'Twitch.ReSub': (response) => {
twitchReSubMessage(response.data);
},
'Twitch.GiftSub': (response) => {
twitchGiftMessage(response.data);
},
'Twitch.GiftBomb': (response) => {
twitchGiftBombMessage(response.data);
},
'Twitch.Raid': (response) => {
twitchRaidMessage(response.data);
},
'Twitch.ChatMessageDeleted': (response) => {
twitchChatMessageDeleted(response.data);
},
'Twitch.UserBanned': (response) => {
twitchUserBanned(response.data);
},
'Twitch.UserTimedOut': (response) => {
twitchUserBanned(response.data);
},
'Twitch.ViewerCountUpdate': (response) => {
twitchUpdateStatistics(response.data);
},
'Twitch.ChatCleared': (response) => {
twitchChatClearMessages();
}
};
if (showTwitch) {
const twitchStatistics = `
<div class="platform" id="twitch" style="display: none;">
<img src="js/modules/twitch/images/logo-twitch.svg" alt="">
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
</div>
`;
document.querySelector('#statistics').insertAdjacentHTML('beforeend', twitchStatistics);
if (showTwitchViewers == true) { document.querySelector('#twitch').style.display = ''; }
registerPlatformHandlersToStreamerBot(twitchMessageHandlers, '[Twitch]');
}
// ---------------------------
// TWITCH EVENT FUNCTIONS
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 template = chatTemplate;
const clone = template.content.cloneNode(true);
const messageId = data.messageId;
const userId = data.message.username;
const {
'first-message': firstMessage,
'shared-chat': sharedChat,
header,
timestamp,
platform,
badges,
avatar,
pronouns: pronoun,
user,
reply,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'chat'];
const [avatarImage, fullmessage, badgeList] = await Promise.all([
getTwitchAvatar(data.message.username),
getTwitchEmotes(data),
getTwitchBadges(data)
]);
header.remove();
user.style.color = data.message.color;
user.innerHTML = `<strong>${data.message.displayName}</strong>`;
message.innerHTML = fullmessage;
if (data.message.isMe) {
message.style.color = data.message.color;
}
if (showAvatar) avatar.innerHTML = `<img src="${avatarImage}">`; else avatar.remove();
if (showBadges) badges.innerHTML = badgeList; else badges.remove();
if (data.user.role == 4) { classes.push('streamer'); }
if (data.message.firstMessage) {
classes.push('first-chatter');
firstMessage.innerHTML('beforeend', ` First-time Chatter`);
}
else { firstMessage.remove(); }
if (data.message.isReply) {
classes.push('reply');
reply.insertAdjacentHTML('beforeend', ` <strong>${data.message.reply.userName}:</strong> ${data.message.reply.msgBody}`);
}
else { reply.remove(); }
if (data.message.isSharedChat) {
if (showTwitchSharedChat == true) {
classes.push('shared-chat');
if (!data.sharedChat.primarySource) {
sharedChat.querySelector('span.origin').insertAdjacentHTML('beforeend', ` <strong>${data.sharedChat.sourceRoom.name}</strong>`);
}
}
else if (!data.sharedChat.primarySource && showTwitchSharedChat == false) {
return;
}
}
else { sharedChat.remove(); }
if (showTwitchPronouns === true) {
const userPronouns = await getTwitchUserPronouns(data.message.username);
if (userPronouns) {
pronoun.innerHTML = userPronouns;
}
}
else { pronoun.remove(); }
addMessageItem('twitch', clone, classes, userId, messageId);
}
async function twitchChatMessageGiantEmote(data) {
if (showTwitchMessages == false) return;
const userMessages = chatContainer.querySelectorAll(`.chat.twitch[data-user="${data.user_login}"]`);
if (userMessages.length === 0) return;
const firstMessage = userMessages[0];
const emoteImages = firstMessage.querySelectorAll(`img[data-emote-id="${data.gigantified_emote.id}"]`);
if (emoteImages.length === 0) return;
emoteImages.forEach(img => {
img.classList.add("gigantified");
if (img.src.endsWith("2.0")) {
img.src = img.src.replace("2.0", "3.0");
}
});
}
async function twitchFollowMessage(data) {
if (showTwitchFollows == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user_name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'follow'];
header.remove();
message.remove();
value.remove();
user.innerHTML = `<strong>${data.user_name}</strong>`;
action.innerHTML = ` followed you`;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchAnnouncementMessage(data) {
if (showTwitchAnnouncements == false) return;
const template = chatTemplate;
const clone = template.content.cloneNode(true);
const messageId = data.messageId;
const userId = data.user.name.toLowerCase();
const {
'first-message': firstMessage,
'shared-chat': sharedChat,
header,
timestamp,
platform,
badges,
avatar,
pronouns: pronoun,
user,
reply,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'announcement'];
classes.push(data.announcementColor.toLowerCase());
firstMessage.remove();
sharedChat.remove();
timestamp.remove();
//platform.remove();
avatar.remove();
pronoun.remove();
reply.remove();
const [fullmessage, badgeList] = await Promise.all([
getTwitchEmotesOnParts(data),
getTwitchAnnouncementBadges(data)
]);
header.innerHTML = `<span><i class="fa-solid fa-bullhorn"></i> Announcement</span>`;
user.style.color = data.user.color;
user.innerHTML = `<strong>${data.user.name}</strong>`;
message.innerHTML = fullmessage;
if (showBadges) badges.innerHTML = badgeList; else badges.remove();
addMessageItem('twitch', clone, classes, userId, messageId);
}
async function twitchRewardRedemption(data) {
if (showTwitchRewardRedemptions == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user_name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'reward'];
header.remove();
user.innerHTML = `<strong>${data.user_name}</strong>`;
action.innerHTML = ` redeemed `;
value.innerHTML = `<strong>${data.reward.title}</strong> (${data.reward.cost})`;
var userInput = data.user_input ? `- ${data.user_input}` : '';
message.innerHTML = `${userInput}`;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchBitsMessage(data) {
if (showTwitchBits == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = data.messageId;
const userId = data.user.name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'bits'];
header.remove();
user.innerHTML = `<strong>${data.user.name}</strong>`;
action.innerHTML = ` cheered with `;
var bits = data.message.bits > 1 ? 'bits' : 'bit';
value.innerHTML = `<strong>${data.message.bits} ${bits}</strong>`;
var fullmessage = data.message.message.replace(/\bCheer\d+\b/g, '').replace(/\s+/g, ' ').trim();
message.innerHTML = fullmessage;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchSubMessage(data) {
if (showTwitchSubs == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user.name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'sub'];
header.remove();
message.remove();
user.innerHTML = `<strong>${data.user.name}</strong>`;
action.innerHTML = ` subscribed for `;
var months = data.duration_months > 1 ? 'months' : 'month';
var tier = data.is_prime ? 'Prime' : 'Tier '+Math.floor(data.sub_tier/1000);
value.innerHTML = `<strong>${data.duration_months} ${months} (${tier})</strong>`;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchReSubMessage(data) {
if (showTwitchSubs == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user.name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'resub'];
const [fullmessage] = await Promise.all([
getTwitchEmotesOnParts(data)
]);
header.remove();
user.innerHTML = `<strong>${data.user.name}</strong>`;
action.innerHTML = ` subscribed for `;
var months = data.cumulativeMonths > 1 ? 'months' : 'month';
var tier = data.isPrime ? 'Prime' : 'Tier '+Math.floor(data.subTier/1000);
value.innerHTML = `<strong>${data.cumulativeMonths} ${months} (${tier})</strong>`;
message.innerHTML = fullmessage;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchGiftMessage(data) {
const isSub = showTwitchSubs === false;
const isGift = showTwitchGiftedSubs === false;
const isGiftTrain = showTwitchGiftedSubsUserTrain === false;
if (
(!data.fromCommunitySubGift && (isSub || isGift)) ||
(data.fromCommunitySubGift && (isSub || isGiftTrain))
) {
return;
}
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user.name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'giftsub'];
header.remove();
message.remove();
user.innerHTML = `<strong>${data.user.name}</strong>`;
var months = data.durationMonths > 1 ? 'months' : 'month';
action.innerHTML = ` gifted <strong>${data.durationMonths} ${months}</strong> subscription <strong>(Tier ${Math.floor(data.subTier/1000)})</strong> to `;
value.innerHTML = `<strong>${data.recipient.name}</strong>`;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchGiftBombMessage(data) {
if (showTwitchSubs == false || showTwitchMassGiftedSubs == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.user.name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'giftbomb'];
header.remove();
value.remove();
user.innerHTML = `<strong>${data.user.name}</strong>`;
var subs = data.total > 1 ? 'subs' : 'sub';
action.innerHTML = ` gifted <strong>${data.total} Tier ${Math.floor(data.sub_tier/1000)} ${subs}</strong> to the Community`;
message.innerHTML = `They've gifted a total of <strong>${data.cumulative_total} subs</strong>`;
addEventItem('twitch', clone, classes, userId, messageId);
}
async function twitchRaidMessage(data) {
if (showTwitchRaids == false) return;
const template = eventTemplate;
const clone = template.content.cloneNode(true);
const messageId = createRandomString(40);
const userId = data.from_broadcaster_user_name.toLowerCase();
const {
header,
platform,
user,
action,
value,
'actual-message': message
} = Object.fromEntries(
[...clone.querySelectorAll('[class]')]
.map(el => [el.className, el])
);
const classes = ['twitch', 'raid'];
header.remove();
message.remove();
user.innerHTML = `<strong>${data.from_broadcaster_user_name}</strong>`;
var viewers = data.viewers > 1 ? 'viewers' : 'viewer';
action.innerHTML = ` raided the channel with `;
value.innerHTML = `<strong>${data.viewers} ${viewers}</strong>`;
addEventItem('twitch', clone, classes, userId, messageId);
}
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.querySelectorAll(`.item.twitch`).forEach(element => {
element.remove();
});
}
async function twitchUpdateStatistics(data) {
if (showPlatformStatistics == false || showTwitchViewers == false) return;
const viewers = DOMPurify.sanitize(data.viewerCount);
document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(viewers);
}
// ---------------------------
// TWITCH UTILITY FUNCTIONS
async function getTwitchAvatar(user) {
if (twitchAvatars.has(user)) {
console.debug(`Twitch avatar found for ${user}!`);
return twitchAvatars.get(user);
}
console.debug(`Twitch avatar not found for ${user}! Getting it from DECAPI!`);
try {
const response = await fetch(`https://decapi.me/twitch/avatar/${user}`);
let avatar = await response.text();
if (!avatar) {
avatar = 'https://static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-300x300.png';
}
twitchAvatars.set(user, avatar);
return avatar;
}
catch (err) {
console.error(`Failed to fetch avatar for ${user}:`, err);
return 'https://static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-300x300.png';
}
}
async function getTwitchEmotes(data) {
const message = data.message.message;
const emotes = data.emotes.sort((a, b) => b.startIndex - a.startIndex);
const words = message.split(" ").map(word => {
const emote = emotes.find(e => e.name === word);
return emote
? `<img src="${emote.imageUrl}" data-emote-id="${emote.id}" alt="${emote.name}" class="emote">`
: word;
});
return words.join(" ");
}
async function getTwitchBadges(data) {
const badges = data.message.badges;
return badges
.map(badge => `<img src="${badge.imageUrl}" class="badge">`)
.join('');
}
async function getTwitchEmotesOnParts(data) {
let messageText = data.text;
for (const part of data.parts) {
if (part.type === 'emote') {
const emoteName = part.text;
const emoteUrl = part.imageUrl;
const emoteHTML = `<img src="${emoteUrl}" class="emote" alt="${emoteName}">`;
const escaped = emoteName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let pattern;
if (/^\w+$/.test(emoteName)) {
pattern = `\\b${escaped}\\b`;
} else {
pattern = `(?<=^|[^\\w])${escaped}(?=$|[^\\w])`;
}
const regex = new RegExp(pattern, 'g');
messageText = messageText.replace(regex, emoteHTML);
}
}
return messageText;
}
async function getTwitchAnnouncementBadges(data) {
const badges = data.user.badges;
return badges
.map(badge => `<img src="${badge.imageUrl}" class="badge">`)
.join('');
}
async function getTwitchUserPronouns(username) {
if (twitchPronouns.has(username)) {
console.debug(`Pronouns found for ${username}. Getting it from Map...`);
return twitchPronouns.get(username);
}
console.debug(`Pronouns not found for ${username} in the Map! Retrieving...`);
try {
const response = await streamerBotClient.getUserPronouns('twitch', username);
const pronoun = response?.pronoun?.userFound
? `<em>${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}</em>`
: '';
twitchPronouns.set(username, pronoun);
return pronoun;
}
catch (err) {
console.error(`Couldn't retrieve pronouns for ${username}:`, err);
return '';
}
}