functional ttv-obfuscated-names

This commit is contained in:
zomo
2026-04-18 23:44:01 -05:00
parent aa25288f10
commit 3b8c402873
11 changed files with 894 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { obfuscator } from './obfuscator'
import { NameConfigInstance, nameImages } from './options'
import {
ChatMessage,
innermostElement,
usernameImageTemplateSuffix,
usernameTemplateSuffix,
} from './util'
export function loadChatMessage(chatboxMessage: Element) {
// only chat messages
const chatboxMessageInner = chatboxMessage.querySelector(
'.chat-line__message'
)
if (!chatboxMessageInner) {
return
}
// get chatbox details
const chatboxBadgeContainer = chatboxMessage.querySelector<HTMLSpanElement>(
'.chat-line__message--badges'
)
if (!chatboxBadgeContainer) {
console.error("found message, couldn't find badges")
return
}
const chatboxUser = chatboxMessage.querySelector<HTMLSpanElement>(
'.chat-line__username'
)
if (!chatboxUser) {
console.error("found message, couldn't find user")
return
}
const chatboxUserInner = chatboxUser.querySelector<HTMLSpanElement>(
'.chat-author__display-name'
)
if (!chatboxUserInner) {
console.error("found message, couldn't find userInner")
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')
}
}
// combine details into object
const chatMessage: ChatMessage = {
username: chatboxUserInner.textContent,
isMod,
}
// run main script (process chatMessage), return new ChatMessage
const newChatMessage = obfuscator(chatMessage)
if (newChatMessage !== null) {
setUsernameDetails(newChatMessage, chatboxUserInner, chatboxUser)
}
loadReplyLine(chatboxMessage)
loadMessageMentions(chatboxMessage)
}
function loadReplyLine(chatboxMessage: Element) {
const replyline = chatboxMessage.querySelector('.ffz--fix-reply-line')
const replyUsername = replyline?.querySelector<HTMLElement>(
'p > span:nth-child(1)'
)
if (!replyUsername) {
return
}
const chatMessage: ChatMessage = {
username: replyUsername.textContent.replace(/^@/, ''),
isMod: false,
}
// run main script (process chatMessage), return new ChatMessage
const newChatMessage = obfuscator(chatMessage)
if (newChatMessage !== null) {
setUsernameDetails(newChatMessage, replyUsername, undefined, '@')
}
}
function loadMessageMentions(chatboxMessage: Element) {
function eachMention(messageMention: HTMLElement) {
const chatMessage: ChatMessage = {
username: messageMention.textContent.replace(/^@/, ''),
isMod: false,
}
// run main script (process chatMessage), return new ChatMessage
const newChatMessage = obfuscator(chatMessage)
if (newChatMessage !== null) {
setUsernameDetails(newChatMessage, messageMention, undefined, '@')
}
}
const messageMentions = chatboxMessage.querySelectorAll<HTMLElement>(
'.chat-line__message-mention'
)
for (const messageMention of messageMentions) {
eachMention(messageMention)
}
}
export function loadAdditionalUserNames(chatboxMessage: Element) {
// look for additional chat member names
const chatterNames =
chatboxMessage.querySelectorAll<HTMLElement>('.chatter-name')
for (const chatterName of chatterNames) {
chatboxMessage.classList.add('obf-loaded')
const chatterNameBox = innermostElement(chatterName)
const username = chatterNameBox.textContent
if (!username) {
continue
}
// combine details into object
const chatMessage: ChatMessage = {
username,
isMod: false,
}
// run main script (process chatMessage), return new ChatMessage
const newChatMessage = obfuscator(chatMessage)
if (newChatMessage !== null) {
setUsernameDetails(newChatMessage, chatterNameBox)
}
}
}
function setUsernameDetails(
newChatMessage: NameConfigInstance,
textbox: HTMLElement,
colorbox?: HTMLElement,
prefixStr: string = ''
) {
const username = `${newChatMessage.username}${usernameTemplateSuffix(newChatMessage)}`
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 prefix = document.createElement('span')
prefix.textContent = prefixStr
const suffix = document.createElement('span')
suffix.textContent = usernameImageTemplateSuffix(newChatMessage)
textbox.replaceChildren(prefix, img, suffix)
} else {
// text name only
textbox.textContent = `${prefixStr}${username}`
}
if (colorbox) {
colorbox.style.color = newChatMessage.color
}
}
+60
View File
@@ -0,0 +1,60 @@
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<HTMLDivElement>(
'.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)
}
}
// attach observer
const observer = new MutationObserver(observerCallback)
observer.observe(document, { attributes: true, childList: true, subtree: true })
// attach styles
const styleAddition = document.createElement('style')
styleAddition.innerHTML = `
.chat-line__message--badges {
display: none;
}
.ismod .chat-line__message--badges {
display: inline;
}
.ismod .chat-line__message--badges > span {
display: none;
}
.ismod .chat-line__message--badges > span[data-badge="moderator"],
.ismod .chat-line__message--badges > span[data-badge="broadcaster"] {
display: inline-block;
}
.seventv-paint {
background-image: none !important;
}
img.obf-image {
vertical-align: middle;
margin: -0.5rem 0;
height: 1.8rem;
}
`
document.head.appendChild(styleAddition)
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://git.zomo.dev/zomo/browser-scripts-builder/raw/branch/main/meta.schema.json",
"name": "ttv obfuscated names",
"description": "",
"namespace": "zomo.dev",
"match": ["https://www.twitch.tv/*"],
"version": "0.1",
"runat": "document-end"
}
@@ -0,0 +1,25 @@
import { NameConfigInstance, ignoreMod } from './options'
import { getRandomName, getStoredUser, setStoredUser } from './storage'
import { ChatMessage } from './util'
export function obfuscator(
chatMessage: ChatMessage
): NameConfigInstance | null {
if (ignoreMod && chatMessage.isMod) {
return null
}
chatMessage.username = chatMessage.username.toLowerCase()
// return stored data
const userData = getStoredUser(chatMessage.username)
if (userData !== null) {
return userData
}
// store new data
const newName = getRandomName()
setStoredUser(chatMessage.username, newName)
return newName
}
File diff suppressed because one or more lines are too long
+130
View File
@@ -0,0 +1,130 @@
import { NameConfigInstance, NameConfig, nameList, colorList } from './options'
type StoredUsers = {
[username: string]: NameConfigInstance
}
type NameListUsed = {
[username: string]: number
}
interface StoredData {
storedUsers: StoredUsers
nameListUsed: NameListUsed
nameListCount: number
}
let { storedUsers, nameListUsed, nameListCount } = loadStoredUserData()
export function getStoredUser(name: string): NameConfigInstance | null {
if (name in storedUsers) {
return storedUsers[name]
}
return null
}
export function setStoredUser(name: string, details: NameConfigInstance) {
storedUsers[name] = details
updateStorageData()
}
export function increaseNameListUsed(name: string) {
nameListUsed[name] = (nameListUsed[name] ?? 0) + 1
updateStorageData()
}
export function increaseNameListCount() {
nameListCount++
updateStorageData()
}
function updateStorageData() {
localStorage.setItem(
'obf-data',
JSON.stringify({
storedUsers,
nameListUsed,
nameListCount,
})
)
}
function loadStoredUserData(): StoredData {
const dataStr = localStorage.getItem('obf-data')
if (dataStr === null) {
return {
storedUsers: {},
nameListUsed: {},
nameListCount: 0,
}
}
try {
const data = JSON.parse(dataStr)
return data as StoredData
} catch (e) {
return {
storedUsers: {},
nameListUsed: {},
nameListCount: 0,
}
}
}
// TODO will be useful for better options
export function clearStoredUsers() {}
export function getRandomName(): NameConfigInstance {
const startingIndex = Math.round(Math.random() * (nameList.length - 1))
let foundName = false
let currentIndex = startingIndex
for (let i = 0; i < nameList.length; i++) {
currentIndex = (startingIndex + i) % nameList.length
let isUsed = nameListUsed[nameList[currentIndex].username] ?? 0
if (isUsed === nameListCount) {
foundName = true
break
}
}
if (!foundName) {
increaseNameListCount()
currentIndex = startingIndex
}
increaseNameListUsed(nameList[currentIndex].username)
const newName = nameList[currentIndex]
const newNameInstance: NameConfigInstance = {
username: newName.username,
image: newName.image,
color: '',
nameCount: nameListCount,
}
// choose a random color from the list
if (Array.isArray(newName.color)) {
const color =
newName.color[
Math.round(Math.random() * (newName.color.length - 1))
]
newNameInstance.color = color
return newNameInstance
}
// choose a random color from the global list
if (newName.color === null) {
const color =
colorList[Math.round(Math.random() * (colorList.length - 1))]
newNameInstance.color = color
return newNameInstance
}
// copy individual carbing
newNameInstance.color = newName.color
return newNameInstance
}
+48
View File
@@ -0,0 +1,48 @@
# obfuscator todo list
- can i detect when ffz loads?
- better options
## known issues
watch streaks
- name is just in a string
- there's no good ways to traverse the dom to reach the name's node
```html
<div class="Layout-sc-1xcs6mc-0 obf-loaded">
<div class="Layout-sc-1xcs6mc-0">
<div class="Layout-sc-1xcs6mc-0 eCiQQE">
<div style="background: var(--color-border-quote);" class="Layout-sc-1xcs6mc-0 ijxYla"></div>
<div style="overflow-wrap: break-word;" data-test-selector="user-notice-line"
class="Layout-sc-1xcs6mc-0 cglxbn">
<div class="Layout-sc-1xcs6mc-0 hDlHnO">
<div class="ScSvgWrapper-sc-wkgzod-0 dVDUDh tw-svg">
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd"
d="M5.295 8.05 10 2l3 4 2-3 3.8 5.067a11 11 0 0 1 2.2 6.6A7.333 7.333 0 0 1 13.667 22h-3.405A7.262 7.262 0 0 1 3 14.738c0-2.423.807-4.776 2.295-6.688Zm7.801 1.411 2-3L17.2 9.267a9 9 0 0 1 1.8 5.4 5.334 5.334 0 0 1-4.826 5.31 3 3 0 0 0 .174-3.748L12 13l-2.348 3.229a3 3 0 0 0 .18 3.754A5.263 5.263 0 0 1 5 14.738c0-1.978.66-3.9 1.873-5.46l3.098-3.983 3.125 4.166Z"
clip-rule="evenodd"></path>
</svg>
</div>
<div class="Layout-sc-1xcs6mc-0 doBHR">
<div class="Layout-sc-1xcs6mc-0 hDlHnO">
<span class="chatter-name chatter-name--no-outline" role="button"
tabindex="0">
<span class="CoreText-sc-1txzju1-0 cOQhxy">
<span class="CoreText-sc-1txzju1-0 kJPnWP">
<span></span>
<img class="obf-image ffz--pointer-events ffz-tooltip"
data-tooltip-type="html" data-title="person5" alt="person5"
src="data:...">
<span>2</span>
</strong>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
```
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"types": [
"../../node_modules/@types/greasemonkey",
"../../node_modules/browser-scripts-builder"
]
}
}
+31
View File
@@ -0,0 +1,31 @@
import { NameConfigInstance } from './options'
export interface ChatMessage {
username: string
isMod: boolean
}
export function innermostElement<T extends Element>(elem: T) {
if (elem.children.length === 0) {
return elem
}
return innermostElement(elem.children[0])
}
export function usernameTemplateSuffix(newChatMessage: NameConfigInstance) {
if (newChatMessage.nameCount === 0) {
return ''
}
return `${newChatMessage.nameCount}`
}
export function usernameImageTemplateSuffix(
newChatMessage: NameConfigInstance
) {
if (newChatMessage.nameCount === 0) {
return ''
}
return `${newChatMessage.nameCount}`
}