/* ----------------------- */
/* 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 = `
`;
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 = `${data.message.displayName}`;
message.innerHTML = fullmessage;
if (data.message.isMe) {
message.style.color = data.message.color;
}
if (showAvatar) avatar.innerHTML = `
`; 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', ` ${data.message.reply.userName}: ${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', ` ${data.sharedChat.sourceRoom.name}`);
}
}
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 = `${data.user_name}`;
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 = ` Announcement`;
user.style.color = data.user.color;
user.innerHTML = `${data.user.name}`;
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 = `${data.user_name}`;
action.innerHTML = ` redeemed `;
value.innerHTML = `${data.reward.title} (${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 = `${data.user.name}`;
action.innerHTML = ` cheered with `;
var bits = data.message.bits > 1 ? 'bits' : 'bit';
value.innerHTML = `${data.message.bits} ${bits}`;
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 = `${data.user.name}`;
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 = `${data.duration_months} ${months} (${tier})`;
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 = `${data.user.name}`;
action.innerHTML = ` subscribed for `;
var months = data.cumulativeMonths > 1 ? 'months' : 'month';
var tier = data.isPrime ? 'Prime' : 'Tier '+Math.floor(data.subTier/1000);
value.innerHTML = `${data.cumulativeMonths} ${months} (${tier})`;
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 = `${data.user.name}`;
var months = data.durationMonths > 1 ? 'months' : 'month';
action.innerHTML = ` gifted ${data.durationMonths} ${months} subscription (Tier ${Math.floor(data.subTier/1000)}) to `;
value.innerHTML = `${data.recipient.name}`;
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 = `${data.user.name}`;
var subs = data.total > 1 ? 'subs' : 'sub';
action.innerHTML = ` gifted ${data.total} Tier ${Math.floor(data.sub_tier/1000)} ${subs} to the Community`;
message.innerHTML = `They've gifted a total of ${data.cumulative_total} subs`;
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 = `${data.from_broadcaster_user_name}`;
var viewers = data.viewers > 1 ? 'viewers' : 'viewer';
action.innerHTML = ` raided the channel with `;
value.innerHTML = `${data.viewers} ${viewers}`;
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
? `
`
: word;
});
return words.join(" ");
}
async function getTwitchBadges(data) {
const badges = data.message.badges;
return badges
.map(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 = `
`;
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 => `
`)
.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
? `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`
: '';
twitchPronouns.set(username, pronoun);
return pronoun;
}
catch (err) {
console.error(`Couldn't retrieve pronouns for ${username}:`, err);
return '';
}
}