diff --git a/dist/ttv-obfuscated-names.user.js b/dist/ttv-obfuscated-names.user.js index 19c5e1a..91a07cb 100644 --- a/dist/ttv-obfuscated-names.user.js +++ b/dist/ttv-obfuscated-names.user.js @@ -4,6 +4,7 @@ // @match https://www.twitch.tv/* // @version 0.1 // @runat document-end +// @grant unsafeWindow // @downloadURL https://git.zomo.dev/zomo/browser-scripts/raw/branch/main/dist/ttv-obfuscated-names.user.js // @supportURL https://git.zomo.dev/zomo/browser-scripts/issues // @homepageURL https://git.zomo.dev/zomo/browser-scripts @@ -31,6 +32,8 @@ var nameList = [ // image: 'personimage1', // }, ] +var usernameExtraSuffix = nameConfig => + nameConfig.nameCount > 0 ? `${nameConfig.nameCount + 1}` : '' var { storedUsers, nameListUsed, nameListCount } = loadStoredUserData() function getStoredUser(name) { @@ -81,6 +84,16 @@ function loadStoredUserData() { } } } +function clearStoredUsers() { + localStorage.removeItem('obf-data') + storedUsers = {} + nameListUsed = {} + nameListCount = 0 +} +unsafeWindow.obfClear = () => { + clearStoredUsers() + location.reload() +} function getRandomName() { const startingIndex = Math.round(Math.random() * (nameList.length - 1)) let foundName = false @@ -127,6 +140,7 @@ function obfuscator(chatMessage) { if (ignoreMod && chatMessage.isMod) { return null } + console.log(`[OBFUSCATOR] Updating Username: ${chatMessage.username}`) chatMessage.username = chatMessage.username.toLowerCase() const userData = getStoredUser(chatMessage.username) if (userData !== null) { @@ -141,19 +155,17 @@ function innermostElement(elem) { if (elem.children.length === 0) { return elem } - return innermostElement(elem.children[0]) + let child = elem.children[0] + return innermostElement(child) } -function usernameTemplateSuffix(newChatMessage) { - if (newChatMessage.nameCount === 0) { - return '' +function elementTreeFind(elem, fn) { + if (fn(elem)) { + return elem } - return `${newChatMessage.nameCount}` -} -function usernameImageTemplateSuffix(newChatMessage) { - if (newChatMessage.nameCount === 0) { - return '' + if (!elem.parentElement) { + return null } - return `${newChatMessage.nameCount}` + return elementTreeFind(elem.parentElement, fn) } function loadChatMessage(chatboxMessage) { @@ -166,15 +178,14 @@ function loadChatMessage(chatboxMessage) { const chatboxBadgeContainer = chatboxMessage.querySelector( '.chat-line__message--badges' ) - if (!chatboxBadgeContainer) { - console.error("found message, couldn't find badges") - return - } const chatboxUser = chatboxMessage.querySelector('.chat-line__username') if (!chatboxUser) { console.error("found message, couldn't find user") return } + if (chatboxUser.querySelector('.obf-name')) { + return + } const chatboxUserInner = chatboxUser.querySelector( '.chat-author__display-name' ) @@ -182,19 +193,17 @@ function loadChatMessage(chatboxMessage) { console.error("found message, couldn't find userInner") return } - chatboxMessage.classList.add('obf-loaded') - if (chatboxUser.classList.contains('seventv-paint')) { - chatboxUser.classList.remove('seventv-paint') - } let isMod = false - for (const badge of chatboxBadgeContainer.children) { - if ( - badge.hasAttribute('data-badge') && - (badge.getAttribute('data-badge') === 'moderator' || - badge.getAttribute('data-badge') === 'broadcaster') - ) { - isMod = true - chatboxMessage.classList.add('ismod') + if (chatboxBadgeContainer) { + for (const badge of chatboxBadgeContainer.children) { + if ( + badge.hasAttribute('data-badge') && + (badge.getAttribute('data-badge') === 'moderator' || + badge.getAttribute('data-badge') === 'broadcaster') + ) { + isMod = true + chatboxMessage.classList.add('ismod') + } } } const chatMessage = { @@ -203,7 +212,7 @@ function loadChatMessage(chatboxMessage) { } const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, chatboxUserInner, chatboxUser) + setUsernameDetails(newChatMessage, chatboxUserInner, true) } loadReplyLine(chatboxMessage) loadMessageMentions(chatboxMessage) @@ -214,24 +223,30 @@ function loadReplyLine(chatboxMessage) { if (!replyUsername) { return } + if (replyUsername.querySelector('.obf-name')) { + return + } const chatMessage = { username: replyUsername.textContent.replace(/^@/, ''), isMod: false, } const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, replyUsername, void 0, '@') + setUsernameDetails(newChatMessage, replyUsername, false, '@') } } function loadMessageMentions(chatboxMessage) { function eachMention(messageMention) { + if (messageMention.querySelector('.obf-name')) { + return + } const chatMessage = { username: messageMention.textContent.replace(/^@/, ''), isMod: false, } const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, messageMention, void 0, '@') + setUsernameDetails(newChatMessage, messageMention, false, '@') } } const messageMentions = chatboxMessage.querySelectorAll( @@ -244,8 +259,10 @@ function loadMessageMentions(chatboxMessage) { function loadAdditionalUserNames(chatboxMessage) { const chatterNames = chatboxMessage.querySelectorAll('.chatter-name') for (const chatterName of chatterNames) { - chatboxMessage.classList.add('obf-loaded') const chatterNameBox = innermostElement(chatterName) + if (chatterNameBox.querySelector('.obf-name')) { + continue + } const username = chatterNameBox.textContent if (!username) { continue @@ -260,53 +277,72 @@ function loadAdditionalUserNames(chatboxMessage) { } } } -function setUsernameDetails(newChatMessage, textbox, colorbox, prefixStr = '') { - const username = `${newChatMessage.username}${usernameTemplateSuffix(newChatMessage)}` +function setUsernameDetails( + newChatMessage, + usernamebox, + doColor = false, + prefix = '' +) { + const suffix = usernameExtraSuffix(newChatMessage) + const username = `${newChatMessage.username}${suffix}` + const container = document.createElement('span') + container.classList.add('obf-name') const imageName = newChatMessage.image if (imageName) { const image = nameImages[imageName] - const img = document.createElement('img') - img.classList.add('obf-image', 'ffz--pointer-events', 'ffz-tooltip') - img.setAttribute('data-tooltip-type', 'html') - img.setAttribute('data-title', username) - img.setAttribute('alt', username) - img.setAttribute('src', image) - const prefix = document.createElement('span') - prefix.textContent = prefixStr - const suffix = document.createElement('span') - suffix.textContent = usernameImageTemplateSuffix(newChatMessage) - textbox.replaceChildren(prefix, img, suffix) + const imgElem = document.createElement('img') + imgElem.classList.add('obf-image', 'ffz--pointer-events', 'ffz-tooltip') + imgElem.setAttribute('data-tooltip-type', 'html') + imgElem.setAttribute('data-title', username) + imgElem.setAttribute('alt', username) + imgElem.setAttribute('src', image) + const prefixElem = document.createElement('span') + prefixElem.textContent = prefix + const suffixElem = document.createElement('span') + suffixElem.textContent = suffix + container.replaceChildren(prefixElem, imgElem, suffixElem) } else { - textbox.textContent = `${prefixStr}${username}` + container.textContent = `${prefix}${username}` } - if (colorbox) { - colorbox.style.color = newChatMessage.color + if (doColor) { + container.style.color = newChatMessage.color + } + usernamebox.replaceChildren(container) +} +function eachMutation(record) { + const target = record.target + if (target.classList.contains('chat-scrollable-area__message-container')) { + for (const target2 of record.addedNodes) { + eachMutationTarget(record, target2) + } + } else { + eachMutationTarget(record, record.target) } } - -var OBSERVER_RATE_LIMIT = 1 -var observerLastRun = 0 -function observerCallback() { - if (Date.now() - observerLastRun < OBSERVER_RATE_LIMIT) { - return - } - observerLastRun = Date.now() - const chatbox = document.querySelector( - '.chat-scrollable-area__message-container' +function eachMutationTarget(record, target) { + const chatboxMessage = elementTreeFind( + target, + elem => + elem.parentElement?.classList.contains( + 'chat-scrollable-area__message-container' + ) || false ) - const chatboxMessages = Array.from(chatbox?.children ?? []) - for (const chatboxMessage of chatboxMessages) { - if (chatboxMessage.classList.contains('obf-loaded')) { - continue - } + if (chatboxMessage) { + console.warn('[OBFUSCATOR] mutated message', chatboxMessage, record) loadChatMessage(chatboxMessage) loadAdditionalUserNames(chatboxMessage) } } -var observer = new MutationObserver(observerCallback) + +var observer = new MutationObserver(mutationRecords => { + for (const record of mutationRecords) { + eachMutation(record) + } +}) observer.observe(document, { attributes: true, childList: true, subtree: true }) var styleAddition = document.createElement('style') styleAddition.innerHTML = ` +/* hide badges, except broadcaster and moderator */ .chat-line__message--badges { display: none; } @@ -321,10 +357,17 @@ styleAddition.innerHTML = ` display: inline-block; } +/* disable 7tv paint colors */ .seventv-paint { background-image: none !important; } +/* disable username translations */ +.chat-author__intl-login { + display: none; +} + +/* style image when used as name */ img.obf-image { vertical-align: middle; margin: -0.5rem 0; diff --git a/scripts/ttv-obfuscated-names/dom.ts b/scripts/ttv-obfuscated-names/dom.ts index 5e41cb4..41ce017 100644 --- a/scripts/ttv-obfuscated-names/dom.ts +++ b/scripts/ttv-obfuscated-names/dom.ts @@ -1,13 +1,8 @@ import { obfuscator } from './obfuscator' -import { NameConfigInstance, nameImages } from './options' -import { - ChatMessage, - innermostElement, - usernameImageTemplateSuffix, - usernameTemplateSuffix, -} from './util' +import { NameConfigInstance, nameImages, usernameExtraSuffix } from './options' +import { ChatMessage, elementTreeFind, innermostElement } from './util' -export function loadChatMessage(chatboxMessage: Element) { +function loadChatMessage(chatboxMessage: Element) { // only chat messages const chatboxMessageInner = chatboxMessage.querySelector( '.chat-line__message' @@ -20,10 +15,6 @@ export function loadChatMessage(chatboxMessage: Element) { const chatboxBadgeContainer = chatboxMessage.querySelector( '.chat-line__message--badges' ) - if (!chatboxBadgeContainer) { - console.error("found message, couldn't find badges") - return - } const chatboxUser = chatboxMessage.querySelector( '.chat-line__username' @@ -33,6 +24,11 @@ export function loadChatMessage(chatboxMessage: Element) { return } + // already applied username + if (chatboxUser.querySelector('.obf-name')) { + return + } + const chatboxUserInner = chatboxUser.querySelector( '.chat-author__display-name' ) @@ -41,24 +37,18 @@ export function loadChatMessage(chatboxMessage: Element) { return } - // got the data, so we're loaded - chatboxMessage.classList.add('obf-loaded') - - // hide 7tv extra colors - if (chatboxUser.classList.contains('seventv-paint')) { - chatboxUser.classList.remove('seventv-paint') - } - // check if mod let isMod = false - for (const badge of chatboxBadgeContainer.children) { - if ( - badge.hasAttribute('data-badge') && - (badge.getAttribute('data-badge') === 'moderator' || - badge.getAttribute('data-badge') === 'broadcaster') - ) { - isMod = true - chatboxMessage.classList.add('ismod') + if (chatboxBadgeContainer) { + for (const badge of chatboxBadgeContainer.children) { + if ( + badge.hasAttribute('data-badge') && + (badge.getAttribute('data-badge') === 'moderator' || + badge.getAttribute('data-badge') === 'broadcaster') + ) { + isMod = true + chatboxMessage.classList.add('ismod') + } } } @@ -72,7 +62,7 @@ export function loadChatMessage(chatboxMessage: Element) { const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, chatboxUserInner, chatboxUser) + setUsernameDetails(newChatMessage, chatboxUserInner, true) } loadReplyLine(chatboxMessage) @@ -89,6 +79,11 @@ function loadReplyLine(chatboxMessage: Element) { return } + // already applied username + if (replyUsername.querySelector('.obf-name')) { + return + } + const chatMessage: ChatMessage = { username: replyUsername.textContent.replace(/^@/, ''), isMod: false, @@ -98,12 +93,17 @@ function loadReplyLine(chatboxMessage: Element) { const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, replyUsername, undefined, '@') + setUsernameDetails(newChatMessage, replyUsername, false, '@') } } function loadMessageMentions(chatboxMessage: Element) { function eachMention(messageMention: HTMLElement) { + // already applied username + if (messageMention.querySelector('.obf-name')) { + return + } + const chatMessage: ChatMessage = { username: messageMention.textContent.replace(/^@/, ''), isMod: false, @@ -113,7 +113,7 @@ function loadMessageMentions(chatboxMessage: Element) { const newChatMessage = obfuscator(chatMessage) if (newChatMessage !== null) { - setUsernameDetails(newChatMessage, messageMention, undefined, '@') + setUsernameDetails(newChatMessage, messageMention, false, '@') } } @@ -126,15 +126,21 @@ function loadMessageMentions(chatboxMessage: Element) { } } -export function loadAdditionalUserNames(chatboxMessage: Element) { +function loadAdditionalUserNames(chatboxMessage: Element) { // look for additional chat member names const chatterNames = chatboxMessage.querySelectorAll('.chatter-name') for (const chatterName of chatterNames) { - chatboxMessage.classList.add('obf-loaded') + const chatterNameBox = innermostElement( + chatterName + ) + + // already applied username + if (chatterNameBox.querySelector('.obf-name')) { + continue + } - const chatterNameBox = innermostElement(chatterName) const username = chatterNameBox.textContent if (!username) { continue @@ -157,37 +163,69 @@ export function loadAdditionalUserNames(chatboxMessage: Element) { function setUsernameDetails( newChatMessage: NameConfigInstance, - textbox: HTMLElement, - colorbox?: HTMLElement, - prefixStr: string = '' + usernamebox: HTMLElement, + doColor = false, + prefix: string = '' ) { - const username = `${newChatMessage.username}${usernameTemplateSuffix(newChatMessage)}` + const suffix = usernameExtraSuffix(newChatMessage) + const username = `${newChatMessage.username}${suffix}` + + const container = document.createElement('span') + container.classList.add('obf-name') const imageName = newChatMessage.image if (imageName) { // image name const image = nameImages[imageName] - const img = document.createElement('img') - img.classList.add('obf-image', 'ffz--pointer-events', 'ffz-tooltip') - img.setAttribute('data-tooltip-type', 'html') - img.setAttribute('data-title', username) - img.setAttribute('alt', username) - img.setAttribute('src', image) + const imgElem = document.createElement('img') + imgElem.classList.add('obf-image', 'ffz--pointer-events', 'ffz-tooltip') + imgElem.setAttribute('data-tooltip-type', 'html') + imgElem.setAttribute('data-title', username) + imgElem.setAttribute('alt', username) + imgElem.setAttribute('src', image) - const prefix = document.createElement('span') - prefix.textContent = prefixStr + const prefixElem = document.createElement('span') + prefixElem.textContent = prefix - const suffix = document.createElement('span') - suffix.textContent = usernameImageTemplateSuffix(newChatMessage) + const suffixElem = document.createElement('span') + suffixElem.textContent = suffix - textbox.replaceChildren(prefix, img, suffix) + container.replaceChildren(prefixElem, imgElem, suffixElem) } else { // text name only - textbox.textContent = `${prefixStr}${username}` + container.textContent = `${prefix}${username}` } - if (colorbox) { - colorbox.style.color = newChatMessage.color + if (doColor) { + container.style.color = newChatMessage.color + } + + usernamebox.replaceChildren(container) +} + +export function eachMutation(record: MutationRecord) { + const target = record.target as HTMLElement + if (target.classList.contains('chat-scrollable-area__message-container')) { + for (const target of record.addedNodes) { + eachMutationTarget(record, target as HTMLElement) + } + } else { + eachMutationTarget(record, record.target as HTMLElement) + } +} + +function eachMutationTarget(record: MutationRecord, target: HTMLElement) { + const chatboxMessage = elementTreeFind( + target, + elem => + elem.parentElement?.classList.contains( + 'chat-scrollable-area__message-container' + ) || false + ) + if (chatboxMessage) { + console.warn('[OBFUSCATOR] mutated message', chatboxMessage, record) + loadChatMessage(chatboxMessage) + loadAdditionalUserNames(chatboxMessage) } } diff --git a/scripts/ttv-obfuscated-names/main.ts b/scripts/ttv-obfuscated-names/main.ts index 5441501..8b0effb 100644 --- a/scripts/ttv-obfuscated-names/main.ts +++ b/scripts/ttv-obfuscated-names/main.ts @@ -1,37 +1,17 @@ -import { loadAdditionalUserNames, loadChatMessage } from './dom' - -const OBSERVER_RATE_LIMIT = 1 -let observerLastRun: number = 0 - -function observerCallback() { - if (Date.now() - observerLastRun < OBSERVER_RATE_LIMIT) { - return - } - observerLastRun = Date.now() - - const chatbox = document.querySelector( - '.chat-scrollable-area__message-container' - ) - const chatboxMessages = Array.from(chatbox?.children ?? []) - - for (const chatboxMessage of chatboxMessages) { - // set flag on message - if (chatboxMessage.classList.contains('obf-loaded')) { - continue - } - - loadChatMessage(chatboxMessage) - loadAdditionalUserNames(chatboxMessage) - } -} +import { eachMutation } from './dom' // attach observer -const observer = new MutationObserver(observerCallback) +const observer = new MutationObserver((mutationRecords: MutationRecord[]) => { + for (const record of mutationRecords) { + eachMutation(record) + } +}) observer.observe(document, { attributes: true, childList: true, subtree: true }) // attach styles const styleAddition = document.createElement('style') styleAddition.innerHTML = ` +/* hide badges, except broadcaster and moderator */ .chat-line__message--badges { display: none; } @@ -46,10 +26,17 @@ styleAddition.innerHTML = ` display: inline-block; } +/* disable 7tv paint colors */ .seventv-paint { background-image: none !important; } +/* disable username translations */ +.chat-author__intl-login { + display: none; +} + +/* style image when used as name */ img.obf-image { vertical-align: middle; margin: -0.5rem 0; diff --git a/scripts/ttv-obfuscated-names/meta.json b/scripts/ttv-obfuscated-names/meta.json index 31384cd..6ab66ad 100644 --- a/scripts/ttv-obfuscated-names/meta.json +++ b/scripts/ttv-obfuscated-names/meta.json @@ -5,5 +5,6 @@ "namespace": "zomo.dev", "match": ["https://www.twitch.tv/*"], "version": "0.1", - "runat": "document-end" + "runat": "document-end", + "grant": ["unsafeWindow"] } diff --git a/scripts/ttv-obfuscated-names/obfuscator.ts b/scripts/ttv-obfuscated-names/obfuscator.ts index 94f1867..2b3bdf9 100644 --- a/scripts/ttv-obfuscated-names/obfuscator.ts +++ b/scripts/ttv-obfuscated-names/obfuscator.ts @@ -9,6 +9,8 @@ export function obfuscator( return null } + console.log(`[OBFUSCATOR] Updating Username: ${chatMessage.username}`) + chatMessage.username = chatMessage.username.toLowerCase() // return stored data diff --git a/scripts/ttv-obfuscated-names/options.ts b/scripts/ttv-obfuscated-names/options.ts index 1147f55..9999e24 100644 --- a/scripts/ttv-obfuscated-names/options.ts +++ b/scripts/ttv-obfuscated-names/options.ts @@ -14,6 +14,8 @@ export interface NameConfigInstance { nameCount: number } +export type UsernameExtraSuffix = (nameConfig: NameConfigInstance) => string + export const ignoreMod: boolean = false export const nameImages = { @@ -40,3 +42,6 @@ export const nameList: NameConfig[] = [ // image: 'personimage1', // }, ] + +export const usernameExtraSuffix: UsernameExtraSuffix = nameConfig => + nameConfig.nameCount > 0 ? `${nameConfig.nameCount + 1}` : '' diff --git a/scripts/ttv-obfuscated-names/storage.ts b/scripts/ttv-obfuscated-names/storage.ts index 89a58f9..76bf88c 100644 --- a/scripts/ttv-obfuscated-names/storage.ts +++ b/scripts/ttv-obfuscated-names/storage.ts @@ -1,4 +1,4 @@ -import { NameConfigInstance, NameConfig, nameList, colorList } from './options' +import { NameConfigInstance, nameList, colorList } from './options' type StoredUsers = { [username: string]: NameConfigInstance @@ -72,7 +72,17 @@ function loadStoredUserData(): StoredData { } // TODO will be useful for better options -export function clearStoredUsers() {} +export function clearStoredUsers() { + localStorage.removeItem('obf-data') + storedUsers = {} + nameListUsed = {} + nameListCount = 0 +} + +;(unsafeWindow as any).obfClear = () => { + clearStoredUsers() + location.reload() +} export function getRandomName(): NameConfigInstance { const startingIndex = Math.round(Math.random() * (nameList.length - 1)) diff --git a/scripts/ttv-obfuscated-names/todo.md b/scripts/ttv-obfuscated-names/todo.md index f111c25..f80c09f 100644 --- a/scripts/ttv-obfuscated-names/todo.md +++ b/scripts/ttv-obfuscated-names/todo.md @@ -1,6 +1,5 @@ # obfuscator todo list -- can i detect when ffz loads? - better options ## known issues @@ -44,5 +43,25 @@ watch streaks - +``` + +raid message + +```html +
+
+
+
+
+
+
+
thezomo is raiding with a party of + 12.
+
+
+
+
+
+
``` diff --git a/scripts/ttv-obfuscated-names/util.ts b/scripts/ttv-obfuscated-names/util.ts index 3df257c..8860bcc 100644 --- a/scripts/ttv-obfuscated-names/util.ts +++ b/scripts/ttv-obfuscated-names/util.ts @@ -1,31 +1,33 @@ -import { NameConfigInstance } from './options' - export interface ChatMessage { username: string isMod: boolean } -export function innermostElement(elem: T) { +export function innermostElement( + elem: T +): U { if (elem.children.length === 0) { - return elem + // U is the type of the innermost element + // since we're at the innermost element, we know T = U + // so we can cast elem (T) to U + return elem as any as U } - return innermostElement(elem.children[0]) + let child = elem.children[0] + return innermostElement(child) } -export function usernameTemplateSuffix(newChatMessage: NameConfigInstance) { - if (newChatMessage.nameCount === 0) { - return '' +export function elementTreeFind( + elem: T, + fn: (elem: Element) => boolean +): U | null { + if (fn(elem)) { + // U is the type of the element that satisfies fn() + // since we're at the element that satisfies fn(), we know T = U + // so we can cast elem (T) to U + return elem as any as U } - - return `${newChatMessage.nameCount}` -} - -export function usernameImageTemplateSuffix( - newChatMessage: NameConfigInstance -) { - if (newChatMessage.nameCount === 0) { - return '' + if (!elem.parentElement) { + return null } - - return `${newChatMessage.nameCount}` + return elementTreeFind(elem.parentElement, fn) }