// ==UserScript== // @name ChatGPT DeMod // @namespace pl.4as.chatgpt // @version 4.4 // @description Hides moderation results during conversations with ChatGPT // @author 4as // @match *://chatgpt.com/* // @match *://chat.openai.com/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @downloadURL https://raw.githubusercontent.com/4as/ChatGPT-DeMod/main/ChatGPT%20DeMod.user.js // @updateURL https://raw.githubusercontent.com/4as/ChatGPT-DeMod/main/ChatGPT%20DeMod.user.js // @run-at document-start // @grant none // ==/UserScript== 'use strict'; var demod_init = async function() { 'use strict'; function main () { const DEMOD_ID = 'demod-cont'; if( document.getElementById(DEMOD_ID) !== null ) return; const DEMOD_KEY = 'DeModState'; var is_on = false; var is_over = false; // Adding the "hover" area for the DeMod button. const demod_div = document.createElement('div'); demod_div.setAttribute('id', DEMOD_ID); demod_div.style.position = 'fixed'; demod_div.style.top = '0px'; demod_div.style.left = '50%'; demod_div.style.transform = 'translate(-50%, 0%)'; demod_div.style.width = '254px'; demod_div.style.height = '24px'; demod_div.style.display = 'inline-block'; demod_div.style.verticalAlign = 'top'; demod_div.style.zIndex = 999; // Adding the actual DeMod button const demod_button = document.createElement('button'); demod_button.style.color = 'white'; demod_button.style.height = '6px'; demod_button.style.width = '124px'; demod_button.style.border = 'none'; demod_button.style.cursor = 'pointer'; demod_button.style.outline = 'none'; demod_button.style.display = 'inline-block'; demod_button.style.verticalAlign = 'top'; demod_div.appendChild(demod_button); const demod_space = document.createElement('div'); demod_space.style.width = '4px'; demod_space.style.display = 'inline-block'; demod_space.style.verticalAlign = 'top'; demod_div.appendChild(demod_space); // Adding the last message status indicator const demod_status = document.createElement('div'); demod_status.style.color = 'white'; demod_status.style.height = '6px'; demod_status.style.border = '0px'; demod_status.style.padding = '0px'; demod_status.style.width = '124px'; demod_status.style.fontSize = '0px'; demod_status.style.border = 'none'; demod_status.style.outline = 'none'; demod_status.style.display = 'inline-block'; demod_status.style.verticalAlign = 'top'; demod_status.style.textAlign = 'center'; demod_status.style.backgroundColor ='#9A9A9A'; demod_status.textContent = "Latest: None"; demod_div.appendChild(demod_status); demod_div.onmouseover = function() { is_over = true; demod_status.style.fontSize = '10px'; demod_status.style.height = '32px'; demod_status.style.padding = '7px 3px'; updateDeModState(); }; demod_div.onmouseout = function() { is_over = false; demod_status.style.fontSize = '0px'; demod_status.style.height = '6px'; demod_status.style.padding = '0px'; updateDeModState(); }; demod_button.addEventListener('click', () => { is_on = !is_on; setDeModState(is_on); updateDeModState(); }); const ButtonState = { DISABLED : 0, OFF : 1, ON : 2, }; function updateDeModState() { if( is_on ) { updateButton(demod_button, ButtonState.ON, "DeMod:"); } else { updateButton(demod_button, ButtonState.OFF, "DeMod:"); } } const ModerationResult = { UNKNOWN : 0, SAFE : 1, FLAGGED : 2, BLOCKED : 3, }; function updateDeModMessageState(mod_result) { switch(mod_result) { case ModerationResult.UNKNOWN: demod_status.style.border = '0px'; demod_status.textContent = "Latest: None"; demod_status.style.backgroundColor ='#9A9A9A'; break; case ModerationResult.SAFE: demod_status.style.border = '0px'; demod_status.textContent = "Latest: Safe"; demod_status.style.backgroundColor ='#4CAF50'; break; case ModerationResult.FLAGGED: demod_status.style.border = '1px dotted white'; demod_status.textContent = "Latest: Flagged"; demod_status.style.backgroundColor ='#ACA950'; break; case ModerationResult.BLOCKED: demod_status.style.border = '1px solid white'; demod_status.textContent = "Latest: BLOCKED"; demod_status.style.backgroundColor ='#AF4C50'; break; } } function updateButton(button, state, label) { if( is_over ) { button.style.height = 'auto'; button.style.border = '0px'; button.style.padding = '4px 12px'; button.style.opacity = 1; button.style.borderRadius = '4px'; switch(state) { case ButtonState.DISABLED: button.style.opacity = 0.5; button.style.backgroundColor ='#AAAAAA'; button.textContent = label + " N/A"; break; case ButtonState.OFF: button.style.backgroundColor ='#AF4C50'; button.textContent = label + " Off"; break; case ButtonState.ON: button.style.border = '1px dotted white'; button.style.padding = '3px 11px'; button.style.backgroundColor ='#4CAF50'; button.textContent = label + " On"; break; } } else { button.textContent = ""; button.style.height = '6px'; button.style.padding = '0px'; button.style.opacity = 1; button.style.borderRadius = '0px'; switch(state) { case ButtonState.DISABLED: button.style.opacity = 0.5; button.style.backgroundColor ='#AAAAAA'; break; case ButtonState.OFF: button.style.backgroundColor ='#AF4C50'; break; case ButtonState.ON: button.style.border = '1px dotted white'; button.style.backgroundColor ='#4CAF50'; break; } } } updateDeModState(); function hasFlagging(text) { return text.match(/(\"flagged\"|\"blocked\"): ?true/ig); } function clearFlagging(text) { text = text.replaceAll(/\"flagged\": ?true/ig, "\"flagged\": false"); text = text.replaceAll(/\"blocked\": ?true/ig, "\"blocked\": false"); return text; } const ConversationType = { UNKNOWN : 0, INIT : 1, PROMPT : 2, }; var target_window = typeof(unsafeWindow)==='undefined' ? window : unsafeWindow; // DeMod state control function getDeModState() { var state = target_window.localStorage.getItem(DEMOD_KEY); if (state == null) return true; return (state == "false") ? false : true; } function setDeModState(demod_on) { target_window.localStorage.setItem(DEMOD_KEY, demod_on); } // EXPERIMENTAL // Redraw tests are used to detect updates in the browser's window. When conversation is lagging those updates will not be present. // If lag is detected DeMod will throttle text streaming so the browser has time to catch up. var redraw_test = 0; var has_redraw = false; function testRedraw(timestamp) { has_redraw = true; redraw_test = target_window.requestAnimationFrame(testRedraw); } function stopRedrawTests() { target_window.cancelAnimationFrame(redraw_test); redraw_test = 0; } function startRedrawTests() { if( redraw_test !== 0 ) stopRedrawTests(); redraw_test = target_window.requestAnimationFrame(testRedraw); } // Interceptors shared data var init_cache = null; var original_fetch = target_window.fetch; var is_blocked = false; var payload = null; var last_response = null; var mod_result = ModerationResult.UNKNOWN; var decoder = new TextDecoder(); var encoder = new TextEncoder(); function decodeData(data) { if (typeof data == 'string') { return data; } else if (data.byteLength != undefined) { return decoder.decode(new Uint8Array(data)); } return null; } function cloneRequest(request, fetch_url, method, body) { return new Request(fetch_url, { method: method, headers: request.headers, body: JSON.stringify(body), referrer: request.referrer, referrerPolicy: request.referrerPolicy, mode: request.mode, credentials: request.credentials, cache: request.cache, redirect: request.redirect, integrity: request.integrity, }); } function cloneEvent(event, new_data) { return new MessageEvent('message', { data: new_data, origin: event.origin, lastEventId: event.lastEventId, source: event.source, ports: event.ports }); } async function redownloadLatest() { var latest = null; var init_redownload = original_fetch(...init_cache); var redownload_result = await init_redownload; if( redownload_result.ok ) { var redownload_text = await redownload_result.text(); var redownload_object = null; try { redownload_object = JSON.parse(redownload_text); } catch(e) { } if( redownload_object !== null && redownload_object.hasOwnProperty('mapping') ) { var latest_time = 0; for( var map_key in redownload_object.mapping ) { var map_obj = redownload_object.mapping[map_key]; if( map_obj.hasOwnProperty('message') && map_obj.message != null && map_obj.message.hasOwnProperty('create_time') && map_obj.message.create_time > latest_time ) { latest = map_obj.message; latest_time = latest.create_time; } } } } return latest; } class ChatResponse { chunk; chunk_start; payload; conversation_id; is_done = false; is_blocked = false; has_content = false; handle_latest = false; mod_result = ModerationResult.SAFE; last_text; queue = []; constructor(existing_payload, decoded_chunk, download_latest) { this.payload = existing_payload; this.chunk = decoded_chunk; this.chunk_start = this.chunk.indexOf("data: "); this.handle_latest = download_latest; } async process(is_blocked) { this.is_blocked = is_blocked; if( this.chunk_start == -1 ) { this.queue.push(this.chunk); return; } while( this.chunk_start != -1 && !this.is_done ) { var chunk_end = this.chunk.indexOf("\n", this.chunk_start); if( chunk_end == -1 ) chunk_end = this.chunk.length-1; var chunk_text = this.chunk.substring(this.chunk_start+5, chunk_end).trim(); if( chunk_text == "[DONE]" ) { this.is_done = true; if( this.handle_latest && this.is_blocked) { var latest = await redownloadLatest(); if( latest !== null ) { this.payload.message = latest; this.queue.push("data: "+JSON.stringify(this.payload)+"\n\n"); } else { this.payload.message.content.parts[0] = "DeMod: Request completed, but DeMod failed to access the history. Try refreshing the conversation instead."; this.queue.push("data: "+JSON.stringify(this.payload)+"\n\n"); } } this.queue.push("data: "+chunk_text+"\n\n"); } else { var chunk_data = null; try { chunk_data = JSON.parse(chunk_text); if( chunk_data.hasOwnProperty('conversation_id')) this.conversation_id = chunk_data.conversation_id; if( this.payload === null ) { this.payload = {"conversation_id":this.conversation_id, "message":null}; } if( (this.payload.message == null && chunk_data.hasOwnProperty('message')) || (this.payload.message.create_date === null && chunk_data.message.create_date !== null) ) { this.payload = JSON.parse(chunk_text); // parse again to make a copy this.payload.message.content = {content_type: "text", "parts": [""]}; } } catch(e) { } this.has_content = chunk_data !== null && chunk_data.hasOwnProperty('message') && chunk_data.message.hasOwnProperty('content'); if( chunk_data !== null ) { if( chunk_data.hasOwnProperty('moderation_response') ) { var has_flag = chunk_data.moderation_response.flagged === true; var has_block = chunk_data.moderation_response.blocked === true; if( has_flag || has_block ) { if( has_flag ) { if( this.mod_result !== ModerationResult.BLOCKED ) this.mod_result = ModerationResult.FLAGGED; } console.log("Received chunk contains flagging/blocking properties, clearing"); chunk_data.moderation_response.flagged = false; chunk_data.moderation_response.blocked = false; } if( has_block ) { console.log("Message has been BLOCKED. Waiting for ChatGPT to finalize the request..."); this.mod_result = ModerationResult.BLOCKED; this.is_blocked = true; if( this.payload !== null && this.payload.message !== null ) { this.payload.message.author.role = "assistant"; this.payload.message.weight = 1; this.payload.message.content.parts[0] = "DeMod: Moderation has intercepted the response and is actively blocking it. Waiting for ChatGPT to finalize the request so DeMod can fetch it from the conversation's history..."; chunk_data = this.payload; } } } if( this.has_content && chunk_data.message.content.hasOwnProperty('content_type') && chunk_data.message.content.content_type === 'text' && chunk_data.message.content.hasOwnProperty('parts') ) { this.last_text = JSON.parse(JSON.stringify(chunk_data)); // cloning } else { this.queue.push("data: "+JSON.stringify(chunk_data)+"\n\n"); } } else { this.queue.push("data: "+chunk_text+"\n\n"); } } this.chunk_start = this.chunk.indexOf("data: ", chunk_end+1); } return true; } replace(latest) { if( latest !== null ) { this.payload.message = latest; return "data: "+JSON.stringify(this.payload)+"\n\n"; } else { this.payload.message.content.parts[0] = "DeMod: Request completed, but DeMod failed to access the history. Try refreshing the conversation instead."; return "data: "+JSON.stringify(this.payload)+"\n\n" } } } class ChatEvent { event; response_data; response_object; response_body; response = null; payload = null; constructor(current_payload, event) { this.event = event; this.payload = current_payload; this.response_data = decodeData(event.data); if( this.response_data != null ) { this.response_object = JSON.parse(this.response_data); if( this.has_body ) { this.response_body = atob(this.response_object.data.body); this.response = new ChatResponse(this.payload, this.response_body, false); } } } get is_valid() { return this.response != null; } get has_body() { return this.response_object != null && this.response_object.type == "message" && this.response_object.dataType == "json" && this.response_object.data.body != null; } get has_content() { return this.is_valid && this.has_body && this.response.has_content; } get is_blocked() { return this.response.is_blocked; } get is_done() { return this.response.is_done; } get mod_result() { return this.response.mod_result; } async process(is_blocked) { await this.response.process(is_blocked); var data = ""; for(const entry of this.response.queue) { data += entry; } this.setBody(data); } replace(latest) { if( this.response == null ) { this.response = new ChatResponse(this.payload, "", false); } var data = this.response.replace(latest); this.setBody(data); } setBody(body) { this.response_body = body; this.response_object.data.body = btoa(this.response_body); this.response_data = JSON.stringify(this.response_object); this.event = cloneEvent(this.event, this.response_data); } } // Intercepter for old fetch() based communication target_window.fetch = async function(...arg) { if( !is_on ) { return original_fetch(...arg); } var original_arg = arg; var fetch_url = arg[0]; var is_request = false; if( typeof(fetch_url) !== 'string' ) { fetch_url = fetch_url.url; is_request = true; } if( fetch_url.indexOf('/share/create') != -1 ) { return new Response("", { status: 404, statusText: "Not found" } ); } var is_conversation = fetch_url.indexOf('/complete')!=-1 || (fetch_url.indexOf('/conversation') != -1 && fetch_url.indexOf('/conversations') == -1); var convo_type = ConversationType.UNKNOWN; if( is_conversation ) { if( fetch_url.indexOf("/gen_title") != -1 ) { var init_url = fetch_url.replace("/gen_title", ""); if( is_request ) { arg = cloneRequest(arg[0], init_url, "GET", null); arg.headers.delete("Content-Type"); } else { arg = JSON.parse(JSON.stringify(arg)); arg[0] = init_url; arg[1].method = "GET"; delete arg[1].headers["Content-Type"]; delete arg[1].body; } } var conv_request = null; if( is_request ) { if( arg[0] !== undefined && arg[0].hasOwnProperty('text') && (typeof arg[0].text === 'function') ) { conv_request = await arg[0].text(); } } else { if( arg[1] !== undefined && arg[1].hasOwnProperty('body') ) { conv_request = arg[1].body; } } if( conv_request ) { convo_type = ConversationType.PROMPT; var conv_body = JSON.parse( conv_request ); conv_body.supports_modapi = false; if( is_request ) { arg[0] = cloneRequest(arg[0], fetch_url, arg[0].method, conv_body); } else { arg[1].body = JSON.stringify(conv_body); } } else { convo_type = ConversationType.INIT; init_cache = arg; } } var original_promise = original_fetch(...original_arg); if( is_conversation ) { var original_result = await original_promise; if( !original_result.ok ) { return original_result; } switch(convo_type) { case ConversationType.PROMPT: { last_response = null; is_blocked = false; mod_result = ModerationResult.SAFE; updateDeModMessageState(mod_result); startRedrawTests(); console.log("Processing basic prompted conversation. Scanning for moderation results..."); const stream = new ReadableStream({ async start(controller) { var reader = original_result.body.getReader(); var timestamp = Date.now(); var time_delay = 2000; // delay starts large as to not fill the controller too early var text_response; var buffer = []; function flushBuffer() { for( const entry of buffer ) { controller.enqueue(entry); } if( text_response !== null ) { controller.enqueue(text_response); } text_response = null; buffer.length = 0; } while (true) { const { done, value } = await reader.read(); var raw_chunk = value || new Uint8Array; var chunk = decoder.decode(raw_chunk); var response = new ChatResponse(payload, chunk, true); await response.process(is_blocked); if( mod_result < response.mod_result ) { mod_result = response.mod_result; updateDeModMessageState(mod_result); } is_blocked = response.is_blocked; payload = response.payload; for( const entry of response.queue ) { buffer.push( encoder.encode(entry) ); } if( response.last_text !== null ) { text_response = "data: "+JSON.stringify(response.last_text)+"\n\n"; text_response = encoder.encode(text_response); } if( has_redraw ) { has_redraw = false; var current_time = Date.now(); if( (current_time-timestamp) > time_delay ) { timestamp = current_time; time_delay = 1000; flushBuffer(); } } if( response.is_done || done ) { flushBuffer(); stopRedrawTests(); controller.close(); break; } } }, }); return new Response(stream, { status: original_result.status, statusText: original_result.statusText, headers: original_result.headers, }); break; } case ConversationType.INIT: { console.log("Processing conversation initialization. Checking if the conversation has existing moderation results."); var convo_init = await original_result.text(); convo_init = clearFlagging(convo_init); updateDeModMessageState(ModerationResult.UNKNOWN); return new Response(convo_init, { status: original_result.status, statusText: original_result.statusText, headers: original_result.headers, }); break; } } } return original_promise; } // Interceptor for new WebSocket communication (credit to WebSocket Logger for making this possible) var original_websocket = target_window.WebSocket; target_window.WebSocket = new Proxy(original_websocket, { construct: function (target, args, newTarget) { var ws = new target(...args); console.log("WebSocket interceptor created, connecting to: " + args[0]); var buffer = []; async function processMessage(original_onmessage, event) { var response = new ChatEvent(payload, event); if( response.is_valid ) { await response.process(is_blocked); if( mod_result < response.mod_result ) { mod_result = response.mod_result; updateDeModMessageState(mod_result); } var blocked = response.is_blocked; payload = response.payload; if(blocked) { if( response.is_done ) { console.log("Response finalized."); if( last_response != null ) { var latest = await redownloadLatest(); last_response.replace(latest); buffer.push(last_response); } buffer.push(response); } else { if( blocked != is_blocked ) { is_blocked = true; last_response = response; } else if( response.has_body ) { last_response = null; buffer.push(response); } else { buffer.push(response); } } } else { buffer.push(response); } var entry; try { for(entry of buffer) { original_onmessage(entry.event); } buffer.length = 0; } catch(e) { console.log("Failed to send parsed response: "+entry.response_data+"\n\nWith body: "+entry.response_body); } } else { original_onmessage(event); } }; var ws_proxy = { set: function (target, prop, v) { if (prop == 'onmessage') { var original_onmessage = v; v = (e) => processMessage(original_onmessage, e); } return target[prop] = v; } }; return new Proxy(ws, ws_proxy); } }); // Bonus functionality: blocking tracking calls XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { if( is_on && url.indexOf("/track/?") != -1 ) return; this.realOpen (method, url, async, user, password); } document.body.appendChild(demod_div); is_on = getDeModState(); updateDeModState(); console.log("DeMod interceptor is ready."); } // The script's core logic is being injected into the page to work around different JavaScript contexts var script = document.createElement('script'); script.appendChild(document.createTextNode('('+ main +')();')); (document.body || document.head || document.documentElement).appendChild(script); // Alternative method of adding DeMod to the chat in case the script injection fails var target_window = typeof(unsafeWindow)==='undefined' ? window : unsafeWindow; target_window.addEventListener("load", main); }; var target_window = typeof(unsafeWindow)==='undefined' ? window : unsafeWindow; var current_url = window.location.href; if( current_url.match("/c/") || current_url.match("/share/") ) { window.location.replace("https://chat.openai.com"); } else if( document.body == null ) { target_window.addEventListener("DOMContentLoaded", demod_init); } else { demod_init(); }