ChatGPT DeMod

Hides moderation results during conversations with ChatGPT

Versión del día 15/08/2024. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name         ChatGPT DeMod
// @namespace    pl.4as.chatgpt
// @version      4.6
// @description  Hides moderation results during conversations with ChatGPT
// @author       4as
// @match        *://chatgpt.com/*
// @match        *://chat.openai.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @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 hasFlagged(text) {
            return text.match(/(\"blocked\"|\"flagged\"): ?true/ig);
        }
        function hasBlocked(text) {
            return text.match(/(\"blocked\"): ?true/ig);
        }

        function clearFlagging(text) {
            // repeated replacement to ensure the style stays consistant
            text = text.replaceAll(/\"flagged\": true/ig, "\"flagged\": false");
            text = text.replaceAll(/\"blocked\": true/ig, "\"blocked\": false");
            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);
        }


        // Interceptors shared data
        const DONE = "[DONE]";
        var init_cache = null;
        var original_fetch = target_window.fetch;
        var response_blocked = false;
        var payload = null;
        var last_response = null;
        var mod_result = ModerationResult.UNKNOWN;
        var sequence_shift = 0; //each sequence in the generated response has an id and all ids need to match, so DeMod shifts them to insert own messages if needed.

        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;
            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(current_blocked) {
                this.is_blocked = current_blocked;

                if( this.chunk_start == -1 ) {
                    this.queue.push(this.chunk);
                    return;
                }

                if( hasFlagged(this.chunk) || this.payload === null || this.payload.message === null || (this.is_blocked && this.chunk.indexOf(DONE) !== -1) ) {
                    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");
                                }
                            }

                        }
                        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;
                                        }
                                    }
                                }
                            }
                        }

                        this.chunk_start = this.chunk.indexOf("data: ", chunk_end+1);
                    }

                    var cleaned = clearFlagging(this.chunk);
                    this.queue.push(cleaned);
                }
                else {
                    this.queue.push(this.chunk);
                }

                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;
            sequence_id;
            constructor(current_payload, event) {
                this.event = event;
                this.response_data = decodeData(event.data);
                this.response_object = JSON.parse(this.response_data);
                this.sequence_id = this.response_object.sequenceId;
                if( this.has_body ) {
                    this.response_body = atob(this.response_object.data.body);
                    this.response = new ChatResponse(current_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; }
            get payload() {
                if( this.is_valid ) return this.response.payload;
                else return null;
            }

            async process(current_blocked) {
                await this.response.process(current_blocked);

                var data = "";
                for(const entry of this.response.queue) {
                    data += entry;
                }

                this.response_body = data;
            }

            replace(latest) {
                if( this.response == null ) {
                    this.response = new ChatResponse(this.payload, "", false);
                }
                var data = this.response.replace(latest);
                this.response_body = data;
            }

            getEvent() {
                this.response_object.sequenceId = this.sequence_id;
                this.response_object.data.body = btoa(this.response_body);
                var updated_data = JSON.stringify(this.response_object);
                return cloneEvent(this.event, updated_data);
            }

            clone() {
                var copy = new ChatEvent(this.payload, this.event);
                copy.response_body = this.response_body;
                return copy;
            }
        }

        // 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: {

                        sequence_shift = 0;
                        last_response = null;
                        response_blocked = false;
                        mod_result = ModerationResult.SAFE;
                        updateDeModMessageState(mod_result);

                        console.log("Processing basic prompted conversation. Scanning for moderation results...");
                        const stream = new ReadableStream({
                            async start(controller) {
                                var reader = original_result.body.getReader();

                                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(response_blocked);

                                    if( mod_result < response.mod_result ) {
                                        mod_result = response.mod_result;
                                        updateDeModMessageState(mod_result);
                                    }
                                    response_blocked = response.is_blocked;
                                    payload = response.payload;

                                    for( const entry of response.queue ) {
                                        const encoded_chunk = encoder.encode(entry);
                                        controller.enqueue(encoded_chunk);
                                    }

                                    if( response.is_done || done ) {
                                        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(response_blocked);

                        if( mod_result < response.mod_result ) {
                            mod_result = response.mod_result;
                            updateDeModMessageState(mod_result);
                        }

                        response_blocked = response.is_blocked;
                        payload = response.payload;

                        if( response.has_body ) {
                            last_response = response.clone();
                            last_response.sequence_id += sequence_shift;
                        }

                        if(response_blocked) {
                           if( response.is_done ) {
                               if( last_response != null ) {
                                   var latest = await redownloadLatest();
                                   last_response.replace(latest);
                                   buffer.push(last_response);
                                   sequence_shift ++;
                               }
                            }
                        }

                        response.sequence_id += sequence_shift;
                        buffer.push(response);

                        var entry;
                        try {
                            for(entry of buffer) {
                                var entry_event = entry.getEvent();
                                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();
}