Last active
November 22, 2025 22:21
-
-
Save jumoog/bab6f22b981588016c0e4eafe53b2381 to your computer and use it in GitHub Desktop.
Listen for new chat messages on the chat and make them clickable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Chloe Cam Modhelper++ | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.5 | |
| // @description Listen for new chat messages on the chat and make them clickable | |
| // @author You | |
| // @match https://www.chloe.cam/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| const CHAT_CONTAINER_SELECTOR = '.messages_chat'; | |
| const CLICK_DATASET_KEY = 'modhelperClickableAdded'; | |
| const HOOK_CHECK_INTERVAL_MS = 500; | |
| const HOOK_MAX_ATTEMPTS = 20; | |
| // Ask for confirmation before sending Silence | |
| const CONFIRM_BEFORE_SILENCE = true; | |
| const SILENCE_ICON_URL = 'https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/svg/1f6ab.svg'; | |
| const SILENCE_ICON_DATASET_KEY = 'modhelperSilenceIconAdded'; | |
| let cachedChatFormController = null; | |
| function getChatContainer() { | |
| return document.querySelector(CHAT_CONTAINER_SELECTOR); | |
| } | |
| function resolveMessageNode(messageLike) { | |
| if (!messageLike) { | |
| return null; | |
| } | |
| if (messageLike.domElement instanceof Element) { | |
| return messageLike.domElement; | |
| } | |
| if (messageLike.view && messageLike.view.domElement instanceof Element) { | |
| return messageLike.view.domElement; | |
| } | |
| if (messageLike instanceof Element) { | |
| return messageLike; | |
| } | |
| if (messageLike instanceof Node) { | |
| return messageLike; | |
| } | |
| return null; | |
| } | |
| function parseMessage(messageLike) { | |
| const root = resolveMessageNode(messageLike); | |
| if (!root) { | |
| return null; | |
| } | |
| const usernameDiv = root.querySelector('div[data-attribute="username"]'); | |
| const messageTextDiv = root.querySelector('div[data-attribute="text"]'); | |
| if (!usernameDiv || !messageTextDiv) { | |
| return null; | |
| } | |
| return { | |
| root, | |
| username: usernameDiv.textContent.trim(), | |
| usernameDiv, | |
| messageTextDiv, | |
| }; | |
| } | |
| function getChatFormController() { | |
| if (cachedChatFormController) { | |
| const form = cachedChatFormController.view && cachedChatFormController.view.domElement; | |
| if (form && document.contains(form)) { | |
| return cachedChatFormController; | |
| } | |
| cachedChatFormController = null; | |
| } | |
| const app = window.App; | |
| if (!app || !app.viewControllers) { | |
| return null; | |
| } | |
| const controllers = Object.values(app.viewControllers); | |
| for (let i = 0; i < controllers.length; i += 1) { | |
| const controller = controllers[i]; | |
| if (!controller || !controller.constructor) { | |
| continue; | |
| } | |
| if (controller.constructor.className === 'CIChatsFormController') { | |
| cachedChatFormController = controller; | |
| return controller; | |
| } | |
| } | |
| return null; | |
| } | |
| function buildCommandPayload(controller, commandText) { | |
| if (!controller || !controller.view || typeof controller.view.serializeForm !== 'function') { | |
| return null; | |
| } | |
| const form = controller.view.domElement; | |
| const serialized = controller.view.serializeForm(form) || ''; | |
| const key = encodeURIComponent('message[text]'); | |
| const value = encodeURIComponent(commandText).replace(/%20/g, '+'); | |
| const pattern = new RegExp(`(^|&)${key}=[^&]*`); | |
| if (pattern.test(serialized)) { | |
| return serialized.replace(pattern, `$1${key}=${value}`); | |
| } | |
| const prefix = serialized ? `${serialized}&` : ''; | |
| return `${prefix}${key}=${value}`; | |
| } | |
| function sendSilenceCommand(username) { | |
| const controller = getChatFormController(); | |
| if (!controller || !controller.view || !controller.view.domElement || !controller.data) { | |
| console.warn('Chat message helper: unable to locate chat form controller'); | |
| return false; | |
| } | |
| if (typeof controller._isReadyToSubmit === 'function' && !controller._isReadyToSubmit()) { | |
| console.warn('Chat message helper: chat form not ready to submit'); | |
| return false; | |
| } | |
| const form = controller.view.domElement; | |
| const method = (form.getAttribute('method') || 'POST').toUpperCase(); | |
| const url = form.getAttribute('action'); | |
| if (!url) { | |
| console.warn('Chat message helper: chat form submit URL not found'); | |
| return false; | |
| } | |
| const commandText = `/silence ${username}`; | |
| const payload = buildCommandPayload(controller, commandText); | |
| if (!payload) { | |
| console.warn('Chat message helper: failed to build request payload'); | |
| return false; | |
| } | |
| controller.data.request({ | |
| method, | |
| url, | |
| data: payload, | |
| element: form | |
| }, function (_response, xhr) { | |
| if (!controller.data.isRequestSuccess(xhr)) { | |
| console.warn('Chat message helper: silence request failed', xhr && xhr.status); | |
| } | |
| }); | |
| console.log(`Chat message helper: sent silence command for ${username}`); | |
| return true; | |
| } | |
| function ensureSilenceIcon(username, usernameDiv, messageTextDiv) { | |
| if (!usernameDiv || usernameDiv.dataset[SILENCE_ICON_DATASET_KEY]) { | |
| return; | |
| } | |
| const icon = document.createElement('span'); | |
| icon.style.cursor = 'pointer'; | |
| icon.style.marginRight = '0.35rem'; | |
| icon.style.userSelect = 'none'; | |
| icon.title = `Silence ${username}`; | |
| icon.setAttribute('role', 'button'); | |
| const image = document.createElement('img'); | |
| image.src = SILENCE_ICON_URL; | |
| image.alt = 'Silence'; | |
| image.width = 18; | |
| image.height = 18; | |
| image.style.display = 'inline-block'; | |
| image.style.verticalAlign = 'middle'; | |
| image.style.pointerEvents = 'none'; | |
| image.draggable = false; | |
| icon.appendChild(image); | |
| icon.addEventListener('click', event => { | |
| event.stopPropagation(); | |
| performSilence(username, messageTextDiv); | |
| }); | |
| usernameDiv.insertBefore(icon, usernameDiv.firstChild); | |
| usernameDiv.dataset[SILENCE_ICON_DATASET_KEY] = 'true'; | |
| } | |
| function performSilence(username, messageTextDiv) { | |
| if (CONFIRM_BEFORE_SILENCE) { | |
| const confirmed = window.confirm(`Silence ${username}?`); | |
| if (!confirmed) { | |
| return; | |
| } | |
| } | |
| const sent = sendSilenceCommand(username); | |
| if (!sent) { | |
| console.warn('Chat message helper: silence command could not be sent'); | |
| } | |
| } | |
| function isChaturbateMessage(messageLike) { | |
| const root = resolveMessageNode(messageLike); | |
| return !!(root && root.querySelector('[data-sender-platform="chaturbate"]')); | |
| } | |
| function processMessageCandidate(messageLike) { | |
| const parsed = parseMessage(messageLike); | |
| if (!parsed || !parsed.username) { | |
| return false; | |
| } | |
| const username = isChaturbateMessage(parsed.root) ? `${parsed.username}:chaturbate` : parsed.username; | |
| ensureSilenceIcon(username, parsed.usernameDiv, parsed.messageTextDiv); | |
| return true; | |
| } | |
| function processExistingMessages() { | |
| const container = getChatContainer(); | |
| if (!container) { | |
| return false; | |
| } | |
| let processed = false; | |
| Array.from(container.children).forEach(child => { | |
| if (processMessageCandidate(child)) { | |
| processed = true; | |
| } | |
| }); | |
| return processed; | |
| } | |
| function ensureInitialProcessing() { | |
| if (processExistingMessages()) { | |
| return; | |
| } | |
| let attempts = 0; | |
| const maxAttempts = 20; | |
| const timer = setInterval(() => { | |
| attempts += 1; | |
| if (processExistingMessages() || attempts >= maxAttempts) { | |
| clearInterval(timer); | |
| } | |
| }, HOOK_CHECK_INTERVAL_MS); | |
| } | |
| // Hook into CIAddMessageService to intercept new chat messages. | |
| let addMessageHooked = false; | |
| function hookIntoAddMessageService() { | |
| if (addMessageHooked) { | |
| return true; | |
| } | |
| const Service = window.CIAddMessageService; | |
| if (!Service || !Service.prototype || typeof Service.prototype.perform !== 'function') { | |
| return false; | |
| } | |
| const originalPerform = Service.prototype.perform; | |
| Service.prototype.perform = function(...args) { | |
| const result = originalPerform.apply(this, args); | |
| try { | |
| const snapshot = Array.isArray(this.messages) ? this.messages.slice() : []; | |
| setTimeout(() => { | |
| snapshot.forEach(message => { | |
| processMessageCandidate(message); | |
| }); | |
| }, 0); | |
| } catch (error) { | |
| console.error('Chat message helper: failed to process new messages', error); | |
| } | |
| return result; | |
| }; | |
| addMessageHooked = true; | |
| console.log('Chat message helper: hooked CIAddMessageService'); | |
| ensureInitialProcessing(); | |
| return true; | |
| } | |
| let hookAttempts = 0; | |
| const hookInterval = setInterval(() => { | |
| hookAttempts += 1; | |
| if (hookIntoAddMessageService()) { | |
| clearInterval(hookInterval); | |
| } else if (hookAttempts >= HOOK_MAX_ATTEMPTS) { | |
| clearInterval(hookInterval); | |
| console.warn('Chat message helper: failed to hook CIAddMessageService after max attempts'); | |
| } | |
| }, HOOK_CHECK_INTERVAL_MS); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment