/* ----------------------- */ /* TIKTOK MODULE VARIABLES */ /* ----------------------- */ const showTiktok = getURLParam("showTiktok", true); const showTikTokMessages = getURLParam("showTikTokMessages", true); const showTikTokFollows = getURLParam("showTikTokFollows", true); const showTikTokLikes = getURLParam("showTikTokLikes", true); const showTikTokGifts = getURLParam("showTikTokGifts", true); const showTikTokSubs = getURLParam("showTikTokSubs", true); const showTikTokStatistics = getURLParam("showTikTokStatistics", true); userColors.set('tiktok', new Map()); document.addEventListener('DOMContentLoaded', () => { if (showTiktok) { const tiktokStatistics = ` `; document.querySelector('#statistics').insertAdjacentHTML('beforeend', tiktokStatistics); if (showTikTokStatistics == true) { document.querySelector('#statistics #tiktok').style.display = ''; } console.debug('[TikTok][Debug] DOMContentLoaded fired'); tiktokConnection(); } }); // ----------------------- // TIKTOK CONNECT HANDLER async function tiktokConnection() { const tikfinityWebSocketURL = 'ws://localhost:21213/'; // Replace with real URL const reconnectDelay = 10000; // 10 seconds const maxTries = 20; let retryCount = 0; function connect() { const tikfinityWebSocket = new WebSocket(tikfinityWebSocketURL); tikfinityWebSocket.onopen = () => { console.debug(`[TikFinity] Connected to TikFinity successfully!`); retryCount = 0; // Reset retry count on success notifySuccess({ title: 'Connected to TikFinity', text: `` }); }; tikfinityWebSocket.onmessage = (response) => { const data = JSON.parse(response.data); const tiktokData = data.data; console.debug(`[TikTok] ${data.event}`, data.data); switch (data.event) { case 'roomUser' : tiktokUpdateStatistics(tiktokData, 'viewers'); break; case 'like': tiktokLikesMessage(tiktokData); tiktokUpdateStatistics(tiktokData, 'likes'); break; case 'chat': tiktokChatMessage(tiktokData); break; case 'follow': tiktokFollowMessage(tiktokData); break; case 'gift': tiktokGiftMessage(tiktokData); break; case 'subscribe': tiktokSubMessage(tiktokData); break; } }; tikfinityWebSocket.onclose = (event) => { setTimeout(() => { connect(); }, reconnectDelay); /*console.error(`[TikFinity] Disconnected (code: ${event.code})`); if (retryCount < maxTries) { retryCount++; console.warn(`[TikFinity] Attempt ${retryCount}/${maxTries} - Reconnecting in ${reconnectDelay / 1000}s...`); notifyError({ title: 'TikFinity Disconnected', text: `Attempt ${retryCount}/${maxTries} - Reconnecting in ${reconnectDelay / 1000}...` }); setTimeout(() => { connect(); }, reconnectDelay); } else { notifyError({ title: 'TikFinity Reconnect Failed', text: `Maximum retries (${maxTries}) reached. Reload ChatRD to try again.
(Check DevTools Debug for more info).` }); console.error('[TikFinity] Max reconnect attempts reached. Giving up.'); }*/ }; tikfinityWebSocket.onerror = (error) => { console.error(`[TikFinity] Connection error:`, error); // Force close to trigger onclose and centralize retry logic if (tikfinityWebSocket.readyState !== WebSocket.CLOSED) { tikfinityWebSocket.close(); } }; return tikfinityWebSocket; } return connect(); // Returns the initial WebSocket instance } // --------------------------- // TIKTOK UTILITY FUNCTIONS async function tiktokChatMessage(data) { if (showTikTokMessages == false) return; if (ignoreUserList.includes(data.nickname.toLowerCase())) return; if (data.comment.startsWith("!") && excludeCommands == true) return; const template = chatTemplate; const clone = template.content.cloneNode(true); const messageId = data.msgId; const userId = data.userId; 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 = ['tiktok', 'chat']; if (data.isModerator) classes.push('mod'); if (data.isSubscriber) classes.push('sub'); const [avatarImage, messageHTML, badgesHTML] = await Promise.all([ getTikTokAvatar(data), getTikTokEmotes(data), getTikTokBadges(data), ]); header.remove(); firstMessage.remove(); sharedChat.remove(); reply.remove(); pronoun.remove(); if (showAvatar) avatar.innerHTML = ``; else avatar.remove(); if (showBadges) { if (!badgesHTML) { badges.remove(); } else { badges.innerHTML = badgesHTML; } } else { badges.remove(); } var color = await createRandomColor('tiktok', data.uniqueId); user.style.color = color; user.innerHTML = `${data.nickname}`; message.innerHTML = messageHTML; addMessageItem('tiktok', clone, classes, userId, messageId); } async function tiktokFollowMessage(data) { if (showTikTokFollows == false) return; const template = eventTemplate; const clone = template.content.cloneNode(true); const messageId = data.msgId; const userId = data.userId; const { header, platform, user, action, value, 'actual-message': message } = Object.fromEntries( [...clone.querySelectorAll('[class]')] .map(el => [el.className, el]) ); const classes = ['tiktok', 'follow']; header.remove(); message.remove(); value.remove(); user.innerHTML = `${data.nickname}`; action.innerHTML = ` followed you`; addEventItem('tiktok', clone, classes, userId, messageId); } async function tiktokLikesMessage(data) { if (showTikTokLikes == false) return; const template = eventTemplate; const clone = template.content.cloneNode(true); const messageId = data.msgId; const userId = data.userId; const { header, platform, user, action, value, 'actual-message': message } = Object.fromEntries( [...clone.querySelectorAll('[class]')] .map(el => [el.className, el]) ); const classes = ['tiktok', 'likes']; var likeCountTotal = parseInt(data.likeCount); // Search for Previous Likes from the Same User const previousLikeContainer = chatContainer.querySelector(`div.event.tiktok.likes[data-user="${data.userId}"]`); // If found, fetches the previous likes, deletes the element // and then creates a new count with a sum of the like count if (previousLikeContainer) { const likeCountElem = previousLikeContainer.querySelector('.value strong'); if (likeCountElem) { var likeCountPrev = parseInt(likeCountElem.textContent); likeCountTotal = Math.floor(likeCountPrev + likeCountTotal); removeItem(previousLikeContainer); } } header.remove(); user.innerHTML = `${data.nickname}`; action.innerHTML = ` sent you `; var likes = likeCountTotal > 1 ? 'likes' : 'like'; value.innerHTML = `${likeCountTotal} ${likes} ❤️`; message.remove(); addEventItem('tiktok', clone, classes, userId, messageId); } async function tiktokSubMessage(data) { if (showTikTokSubs == false) return; const template = eventTemplate; const clone = template.content.cloneNode(true); const messageId = data.msgId; const userId = data.userId; const { header, platform, user, action, value, 'actual-message': message } = Object.fromEntries( [...clone.querySelectorAll('[class]')] .map(el => [el.className, el]) ); const classes = ['tiktok', 'sub']; header.remove(); user.innerHTML = `${data.nickname}`; action.innerHTML = ` subscribed for `; var months = data.subMonth > 1 ? 'months' : 'month'; value.innerHTML = `${data.subMonth} ${months}`; message.remove(); addEventItem('tiktok', clone, classes, userId, messageId); } async function tiktokGiftMessage(data) { if (showTikTokGifts == false) return; if (data.giftType === 1 && !data.repeatEnd) return; const template = eventTemplate; const clone = template.content.cloneNode(true); const messageId = data.msgId; const userId = data.userId; const { header, platform, user, action, value, 'actual-message': message } = Object.fromEntries( [...clone.querySelectorAll('[class]')] .map(el => [el.className, el]) ); const classes = ['tiktok', 'gift']; header.remove(); user.innerHTML = `${data.nickname}`; action.innerHTML = ` has sent you `; value.innerHTML = `x${data.repeatCount} ${data.giftName} `; message.remove(); addEventItem('tiktok', clone, classes, userId, messageId); } async function getTikTokEmotes(data) { const { comment: message, emotes, } = data; var fullmessage = message; if (emotes.length > 0) { emotes.forEach(emote => { var emotetoadd = ` `; var position = emote.placeInComment; fullmessage = [fullmessage.slice(0, position), emotetoadd, fullmessage.slice(position)].join(''); }); } return fullmessage; } async function getTikTokAvatar(data) { const { profilePictureUrl } = data; return profilePictureUrl; } async function getTikTokBadges(data) { const { isSubscriber, isModerator, } = data; let badgesHTML = [ isSubscriber && '', isModerator && '', ].filter(Boolean).join(''); return badgesHTML; } 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); } }