Skip to content

Instantly share code, notes, and snippets.

@jumoog
Last active November 22, 2025 22:21
Show Gist options
  • Select an option

  • Save jumoog/bab6f22b981588016c0e4eafe53b2381 to your computer and use it in GitHub Desktop.

Select an option

Save jumoog/bab6f22b981588016c0e4eafe53b2381 to your computer and use it in GitHub Desktop.
Listen for new chat messages on the chat and make them clickable
// ==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