Grok DeMod

Hides moderation results in Grok conversations, auto-recovers blocked messages.

Size

36.3 KB

Version

1.0

Created

Dec 1, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name         Grok DeMod
3// @license      GPL-3.0-or-later
4// @namespace    http://tampermonkey.net/
5// @version      1.0
6// @description  Hides moderation results in Grok conversations, auto-recovers blocked messages.
7// @author       UniverseDev
8// @license      MIT
9// @icon         https://www.google.com/s2/favicons?sz=64&domain=grok.com
10// @match        https://grok.com/*
11// @grant        unsafeWindow
12// @downloadURL https://update.greasyfork.org/scripts/531147/Grok%20DeMod.user.js
13// @updateURL https://update.greasyfork.org/scripts/531147/Grok%20DeMod.meta.js
14// ==/UserScript==
15
16(function() {
17    'use strict';
18
19    const CONFIG = {
20        defaultFlags: [
21            'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
22        ],
23        messageKeys: ['message', 'content', 'text', 'error'],
24        moderationMessagePatterns: [
25            /this content has been moderated/i,
26            /sorry, i cannot assist/i,
27            /policy violation/i,
28            /blocked/i,
29            /moderated/i,
30            /restricted/i,
31            /content restricted/i,
32            /unable to process/i,
33            /cannot help/i,
34            /(sorry|apologies).*?(cannot|unable|help|assist)/i,
35        ],
36        clearedMessageText: '[Content cleared by Grok DeMod]',
37        recoveryTimeoutMs: 5000,
38        lsKeys: {
39            enabled: 'GrokDeModEnabled',
40            debug: 'GrokDeModDebug',
41            flags: 'GrokDeModFlags',
42
43        },
44        styles: {
45
46            uiContainer: `
47                position: fixed;
48                bottom: 10px;
49                right: 10px;
50                z-index: 10000;
51                background: #2d2d2d;
52                padding: 10px;
53                border-radius: 8px;
54                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
55                display: flex;
56                flex-direction: column;
57                gap: 8px;
58                font-family: Arial, sans-serif;
59                color: #e0e0e0;
60                min-width: 170px;
61
62            `,
63            button: `
64                padding: 6px 12px;
65                border-radius: 5px;
66                border: none;
67                cursor: pointer;
68                color: #fff;
69                font-size: 13px;
70                transition: background-color 0.2s ease;
71            `,
72            status: `
73                padding: 5px;
74                font-size: 12px;
75                color: #a0a0a0;
76                text-align: center;
77                border-top: 1px solid #444;
78                margin-top: 5px;
79                min-height: 16px;
80            `,
81            logContainer: `
82                max-height: 100px;
83                overflow-y: auto;
84                font-size: 11px;
85                color: #c0c0c0;
86                background-color: #333;
87                padding: 5px;
88                border-radius: 4px;
89                line-height: 1.4;
90                margin-top: 5px;
91            `,
92            logEntry: `
93                padding-bottom: 3px;
94                border-bottom: 1px dashed #555;
95                margin-bottom: 3px;
96                word-break: break-word;
97            `,
98
99            colors: {
100                enabled: '#388E3C',
101                disabled: '#D32F2F',
102                debugEnabled: '#1976D2',
103                debugDisabled: '#555555',
104                safe: '#66ff66',
105                flagged: '#ffa500',
106                blocked: '#ff6666',
107                recovering: '#ffcc00'
108            }
109        }
110    };
111
112
113    let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
114    let debug = getState(CONFIG.lsKeys.debug, false);
115    let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
116    let initCache = null;
117    let currentConversationId = null;
118    const encoder = new TextEncoder();
119    const decoder = new TextDecoder();
120    const uiLogBuffer = [];
121    const MAX_LOG_ENTRIES = 50;
122
123
124    const ModerationResult = Object.freeze({
125        SAFE: 0,
126        FLAGGED: 1,
127        BLOCKED: 2,
128    });
129
130
131
132    function logDebug(...args) {
133        if (debug) {
134            console.log('[Grok DeMod]', ...args);
135        }
136    }
137
138    function logError(...args) {
139        console.error('[Grok DeMod]', ...args);
140    }
141
142
143    function getState(key, defaultValue) {
144        try {
145            const value = localStorage.getItem(key);
146            if (value === null) return defaultValue;
147            if (value === 'true') return true;
148            if (value === 'false') return false;
149            return JSON.parse(value);
150        } catch (e) {
151            logError(`Error reading ${key} from localStorage:`, e);
152            return defaultValue;
153        }
154    }
155
156
157    function setState(key, value) {
158        try {
159            const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
160            localStorage.setItem(key, valueToStore);
161        } catch (e) {
162            logError(`Error writing ${key} to localStorage:`, e);
163        }
164    }
165
166
167    function timeoutPromise(ms, promise, description = 'Promise') {
168        return new Promise((resolve, reject) => {
169            const timer = setTimeout(() => {
170                logDebug(`${description} timed out after ${ms}ms`);
171                reject(new Error(`Timeout (${description})`));
172            }, ms);
173            promise.then(
174                (value) => { clearTimeout(timer); resolve(value); },
175                (error) => { clearTimeout(timer); reject(error); }
176            );
177        });
178    }
179
180
181    function getModerationResult(obj, path = '') {
182        if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
183
184        let result = ModerationResult.SAFE;
185
186        for (const key in obj) {
187            if (!obj.hasOwnProperty(key)) continue;
188
189            const currentPath = path ? `${path}.${key}` : key;
190            const value = obj[key];
191
192
193            if (key === 'isBlocked' && value === true) {
194                logDebug(`Blocked detected via flag '${currentPath}'`);
195                return ModerationResult.BLOCKED;
196            }
197
198
199            if (moderationFlags.includes(key) && value === true) {
200                logDebug(`Flagged detected via flag '${currentPath}'`);
201                result = Math.max(result, ModerationResult.FLAGGED);
202            }
203
204
205            if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
206                const content = value.toLowerCase();
207                for (const pattern of CONFIG.moderationMessagePatterns) {
208                    if (pattern.test(content)) {
209                        logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
210
211                        if (/blocked|moderated|restricted/i.test(pattern.source)) {
212                             return ModerationResult.BLOCKED;
213                        }
214                        result = Math.max(result, ModerationResult.FLAGGED);
215                        break;
216                    }
217                }
218
219                if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
220                     logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
221                     result = Math.max(result, ModerationResult.FLAGGED);
222                }
223            }
224
225
226            if (typeof value === 'object') {
227                const childResult = getModerationResult(value, currentPath);
228                if (childResult === ModerationResult.BLOCKED) {
229                    return ModerationResult.BLOCKED;
230                }
231                result = Math.max(result, childResult);
232            }
233        }
234        return result;
235    }
236
237
238    function clearFlagging(obj) {
239        if (typeof obj !== 'object' || obj === null) return obj;
240
241        if (Array.isArray(obj)) {
242            return obj.map(item => clearFlagging(item));
243        }
244
245        const newObj = {};
246        for (const key in obj) {
247            if (!obj.hasOwnProperty(key)) continue;
248
249            const value = obj[key];
250
251
252            if (moderationFlags.includes(key) && value === true) {
253                newObj[key] = false;
254                logDebug(`Cleared flag '${key}'`);
255            }
256
257            else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
258                let replaced = false;
259                for (const pattern of CONFIG.moderationMessagePatterns) {
260                    if (pattern.test(value)) {
261                        newObj[key] = CONFIG.clearedMessageText;
262                        logDebug(`Replaced moderated message in '${key}' using pattern`);
263                        replaced = true;
264                        break;
265                    }
266                }
267
268                 if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
269
270                     if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
271                        newObj[key] = CONFIG.clearedMessageText;
272                        logDebug(`Replaced heuristic moderated message in '${key}'`);
273                        replaced = true;
274                     }
275                 }
276
277                if (!replaced) {
278                    newObj[key] = value;
279                }
280            }
281
282            else if (typeof value === 'object') {
283                newObj[key] = clearFlagging(value);
284            }
285
286            else {
287                newObj[key] = value;
288            }
289        }
290        return newObj;
291    }
292
293
294
295    let uiContainer, toggleButton, debugButton, statusEl, logContainer;
296
297    function addLog(message) {
298        if (!logContainer) return;
299        const timestamp = new Date().toLocaleTimeString();
300        const logEntry = document.createElement('div');
301        logEntry.textContent = `[${timestamp}] ${message}`;
302        logEntry.style.cssText = CONFIG.styles.logEntry;
303
304
305        uiLogBuffer.push(logEntry);
306        if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
307            const removed = uiLogBuffer.shift();
308
309            if (removed && removed.parentNode === logContainer) {
310                logContainer.removeChild(removed);
311            }
312        }
313
314        logContainer.appendChild(logEntry);
315
316        logContainer.scrollTop = logContainer.scrollHeight;
317    }
318
319    function updateStatus(modResult, isRecovering = false) {
320         if (!statusEl) return;
321        let text = 'Status: ';
322        let color = CONFIG.styles.colors.safe;
323
324        if (isRecovering) {
325            text += 'Recovering...';
326            color = CONFIG.styles.colors.recovering;
327        } else if (modResult === ModerationResult.BLOCKED) {
328            text += 'Blocked (Recovered/Cleared)';
329            color = CONFIG.styles.colors.blocked;
330        } else if (modResult === ModerationResult.FLAGGED) {
331            text += 'Flagged (Cleared)';
332            color = CONFIG.styles.colors.flagged;
333        } else {
334            text += 'Safe';
335            color = CONFIG.styles.colors.safe;
336        }
337        statusEl.textContent = text;
338        statusEl.style.color = color;
339    }
340
341
342    function setupUI() {
343        uiContainer = document.createElement('div');
344        uiContainer.id = 'grok-demod-ui';
345        uiContainer.style.cssText = CONFIG.styles.uiContainer;
346
347
348
349        toggleButton = document.createElement('button');
350        debugButton = document.createElement('button');
351        statusEl = document.createElement('div');
352        logContainer = document.createElement('div');
353
354
355        toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
356        toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
357        toggleButton.style.cssText = CONFIG.styles.button;
358        toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
359        toggleButton.onclick = (e) => {
360
361            demodEnabled = !demodEnabled;
362            setState(CONFIG.lsKeys.enabled, demodEnabled);
363            toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
364            toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
365            addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
366            console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
367        };
368
369
370        debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
371        debugButton.title = 'Toggle debug mode (logs verbose details to console)';
372        debugButton.style.cssText = CONFIG.styles.button;
373        debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
374        debugButton.onclick = (e) => {
375
376            debug = !debug;
377            setState(CONFIG.lsKeys.debug, debug);
378            debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
379            debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
380            addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
381            logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
382        };
383
384
385        statusEl.id = 'grok-demod-status';
386        statusEl.style.cssText = CONFIG.styles.status;
387        updateStatus(ModerationResult.SAFE);
388
389
390        logContainer.id = 'grok-demod-log';
391        logContainer.style.cssText = CONFIG.styles.logContainer;
392
393        uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
394        logContainer.scrollTop = logContainer.scrollHeight;
395
396
397        uiContainer.appendChild(toggleButton);
398        uiContainer.appendChild(debugButton);
399        uiContainer.appendChild(statusEl);
400        uiContainer.appendChild(logContainer);
401        document.body.appendChild(uiContainer);
402
403        addLog("Grok DeMod Initialized.");
404        if (debug) addLog("Debug mode is ON.");
405
406
407    }
408
409
410
411    async function redownloadLatestMessage() {
412        if (!currentConversationId) {
413            logDebug('Recovery skipped: Missing conversationId');
414            addLog('Recovery failed: No conversation ID.');
415            return null;
416        }
417         if (!initCache || !initCache.headers) {
418
419            logDebug('Recovery cache missing, attempting fresh fetch for headers...');
420            try {
421                const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
422                const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
423                if (tempResp.ok) {
424
425                    logDebug('Fresh header fetch successful (status OK).');
426
427                    initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
428                } else {
429                     logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
430                     addLog('Recovery failed: Cannot get request data.');
431                     return null;
432                }
433            } catch (e) {
434                 logError('Error during fresh header fetch:', e);
435                 addLog('Recovery failed: Error getting request data.');
436                 return null;
437            }
438
439        }
440
441        const url = `/rest/app-chat/conversation/${currentConversationId}`;
442        logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
443        addLog('Attempting content recovery...');
444
445
446        const headers = new Headers(initCache.headers);
447
448        if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
449
450
451
452        const requestOptions = {
453            method: 'GET',
454            headers: headers,
455            credentials: initCache.credentials || 'include',
456        };
457
458        try {
459            const response = await timeoutPromise(
460                CONFIG.recoveryTimeoutMs,
461                fetch(url, requestOptions),
462                'Recovery Fetch'
463            );
464
465            if (!response.ok) {
466                logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
467                addLog(`Recovery failed: HTTP ${response.status}`);
468
469                try {
470                    const errorBody = await response.text();
471                    logDebug('Recovery error body:', errorBody.substring(0, 500));
472                } catch (e) { }
473                return null;
474            }
475
476            const data = await response.json();
477            const messages = data?.messages;
478
479            if (!Array.isArray(messages) || messages.length === 0) {
480                logDebug('Recovery failed: No messages found in conversation data', data);
481                addLog('Recovery failed: No messages found.');
482                return null;
483            }
484
485
486             messages.sort((a, b) => {
487                const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
488                const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
489                return tsB - tsA;
490             });
491
492            const latestMessage = messages[0];
493
494
495            if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
496                 logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
497                 addLog('Recovery failed: Invalid latest message.');
498                 return null;
499            }
500
501            logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
502            addLog('Recovery seems successful.');
503            return { content: latestMessage.content };
504
505        } catch (e) {
506            logError('Recovery fetch/parse error:', e);
507            addLog(`Recovery error: ${e.message}`);
508            return null;
509        }
510    }
511
512
513    function extractConversationIdFromUrl(url) {
514
515        const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
516        return match ? match[1] : null;
517    }
518
519
520    async function processPotentialModeration(json, source) {
521        const modResult = getModerationResult(json);
522        let finalJson = json;
523
524        if (modResult !== ModerationResult.SAFE) {
525            if (modResult === ModerationResult.BLOCKED) {
526                logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
527                addLog(`Blocked content from ${source}.`);
528                updateStatus(modResult, true);
529
530                const recoveredData = await redownloadLatestMessage();
531
532                if (recoveredData && recoveredData.content) {
533                    addLog(`Recovery successful (${source}).`);
534                    logDebug(`Recovered content applied (${source})`);
535
536
537                    let replaced = false;
538                    const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
539                    for (const key of keysToTry) {
540                         if (typeof finalJson[key] === 'string') {
541                            finalJson[key] = recoveredData.content;
542                            logDebug(`Injected recovered content into key '${key}'`);
543                            replaced = true;
544                            break;
545                         }
546                    }
547
548                    if (!replaced) {
549                        logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
550                         finalJson.recovered_content = recoveredData.content;
551                    }
552
553
554                    finalJson = clearFlagging(finalJson);
555                    updateStatus(modResult, false);
556
557                } else {
558
559                    addLog(`Recovery failed (${source}). Content may be lost.`);
560                    logDebug(`Recovery failed (${source}), applying standard clearing.`);
561                    finalJson = clearFlagging(json);
562                    updateStatus(modResult, false);
563                }
564            } else {
565                logDebug(`Flagged content detected and cleared from ${source}.`);
566                addLog(`Flagged content cleared (${source}).`);
567                finalJson = clearFlagging(json);
568                updateStatus(modResult);
569            }
570        } else {
571
572
573            if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
574                 updateStatus(modResult);
575            } else if (statusEl && statusEl.textContent.includes('Recovering')) {
576
577                logDebug("Recovery attempt finished (next message safe). Resetting status.");
578                updateStatus(ModerationResult.SAFE);
579            }
580        }
581        return finalJson;
582    }
583
584
585    async function handleFetchResponse(original_response, url, requestArgs) {
586
587        const response = original_response.clone();
588
589
590        if (!response.ok) {
591            logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
592            return original_response;
593        }
594
595        const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
596        logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
597
598
599
600        const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
601        if (conversationGetMatch && requestArgs?.method === 'GET') {
602            logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
603
604            initCache = {
605                headers: new Headers(requestArgs.headers),
606                credentials: requestArgs.credentials || 'include'
607            };
608
609             if (!currentConversationId) {
610                 currentConversationId = conversationGetMatch[1];
611                 logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
612             }
613        }
614
615         if (!currentConversationId) {
616             const idFromUrl = extractConversationIdFromUrl(url);
617             if (idFromUrl) {
618                 currentConversationId = idFromUrl;
619                 logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
620             }
621         }
622
623
624
625
626        if (contentType.includes('text/event-stream')) {
627            logDebug(`Processing SSE stream for ${url}`);
628            const reader = response.body.getReader();
629            const stream = new ReadableStream({
630                async start(controller) {
631                    let buffer = '';
632                    let currentEvent = { data: '', type: 'message', id: null };
633
634                    try {
635                        while (true) {
636                            const { done, value } = await reader.read();
637                            if (done) {
638
639                                if (buffer.trim()) {
640                                     logDebug("SSE stream ended, processing final buffer:", buffer);
641
642                                     if (buffer.startsWith('{') || buffer.startsWith('[')) {
643                                          try {
644                                             let json = JSON.parse(buffer);
645                                             json = await processPotentialModeration(json, 'SSE-Final');
646                                             controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
647                                          } catch(e) {
648                                             logDebug("Error parsing final SSE buffer, sending as is:", e);
649                                             controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
650                                          }
651                                      } else {
652                                          controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
653                                      }
654                                } else if (currentEvent.data) {
655
656                                    logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
657                                    try {
658                                        let json = JSON.parse(currentEvent.data);
659                                        json = await processPotentialModeration(json, 'SSE-Event');
660                                        controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
661                                    } catch (e) {
662                                        logDebug("Error parsing trailing SSE data, sending as is:", e);
663                                        controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
664                                    }
665                                }
666                                controller.close();
667                                break;
668                            }
669
670
671                            buffer += decoder.decode(value, { stream: true });
672                            let lines = buffer.split('\n');
673
674                            buffer = lines.pop() || '';
675
676
677                            for (const line of lines) {
678                                if (line.trim() === '') {
679                                    if (currentEvent.data) {
680                                        logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
681                                        if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
682                                             try {
683                                                let json = JSON.parse(currentEvent.data);
684
685                                                if (json.conversation_id && !currentConversationId) {
686                                                    currentConversationId = json.conversation_id;
687                                                    logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
688                                                }
689
690                                                json = await processPotentialModeration(json, 'SSE');
691
692                                                controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
693                                             } catch(e) {
694                                                logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
695
696                                                controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
697                                             }
698                                        } else {
699                                             logDebug("SSE data is not JSON, forwarding as is.");
700                                             controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
701                                        }
702                                    }
703
704                                    currentEvent = { data: '', type: 'message', id: null };
705                                } else if (line.startsWith('data:')) {
706
707                                    currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
708                                } else if (line.startsWith('event:')) {
709                                    currentEvent.type = line.substring(6).trim();
710                                } else if (line.startsWith('id:')) {
711                                    currentEvent.id = line.substring(3).trim();
712                                } else if (line.startsWith(':')) {
713
714                                } else {
715                                    logDebug("Unknown SSE line:", line);
716
717                                }
718                            }
719                        }
720                    } catch (e) {
721                        logError('Error reading/processing SSE stream:', e);
722                        controller.error(e);
723                    } finally {
724                        reader.releaseLock();
725                    }
726                }
727            });
728
729            const newHeaders = new Headers(response.headers);
730            return new Response(stream, {
731                status: response.status,
732                statusText: response.statusText,
733                headers: newHeaders
734            });
735        }
736
737
738        if (contentType.includes('application/json')) {
739             logDebug(`Processing JSON response for ${url}`);
740            try {
741                const text = await response.text();
742                let json = JSON.parse(text);
743
744
745                 if (json.conversation_id && !currentConversationId) {
746                     currentConversationId = json.conversation_id;
747                     logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
748                 }
749
750
751                json = await processPotentialModeration(json, 'Fetch');
752
753
754                const newBody = JSON.stringify(json);
755                const newHeaders = new Headers(response.headers);
756
757                if (newHeaders.has('content-length')) {
758                    newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
759                }
760
761
762                return new Response(newBody, {
763                    status: response.status,
764                    statusText: response.statusText,
765                    headers: newHeaders
766                });
767            } catch (e) {
768                logError('Fetch JSON processing error:', e, 'URL:', url);
769
770                return original_response;
771            }
772        }
773
774
775        logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
776        return original_response;
777    }
778
779
780
781    const originalFetch = unsafeWindow.fetch;
782
783
784    unsafeWindow.fetch = async function(input, init) {
785
786        if (!demodEnabled) {
787            return originalFetch.apply(this, arguments);
788        }
789
790        let url;
791        let requestArgs = init || {};
792
793        try {
794            url = (input instanceof Request) ? input.url : String(input);
795        } catch (e) {
796
797            logDebug('Invalid fetch input, passing through:', input, e);
798            return originalFetch.apply(this, arguments);
799        }
800
801
802        if (!url.includes('/rest/app-chat/')) {
803            return originalFetch.apply(this, arguments);
804        }
805
806
807         if (requestArgs.method === 'POST') {
808             logDebug(`Observing POST request: ${url}`);
809             const idFromUrl = extractConversationIdFromUrl(url);
810             if (idFromUrl) {
811                 if (!currentConversationId) {
812                     currentConversationId = idFromUrl;
813                     logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
814                 }
815
816                 if (!initCache && requestArgs.headers) {
817                      logDebug(`Caching headers from POST request to ${idFromUrl}`);
818                      initCache = {
819                          headers: new Headers(requestArgs.headers),
820                          credentials: requestArgs.credentials || 'include'
821                      };
822                 }
823             }
824
825             return originalFetch.apply(this, arguments);
826         }
827
828
829        logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
830
831        try {
832
833            const original_response = await originalFetch.apply(this, arguments);
834
835            return await handleFetchResponse(original_response, url, requestArgs);
836        } catch (error) {
837
838            logError(`Fetch interception failed for ${url}:`, error);
839
840            throw error;
841        }
842    };
843
844
845    const OriginalWebSocket = unsafeWindow.WebSocket;
846
847
848    unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
849        construct(target, args) {
850            const url = args[0];
851            logDebug('WebSocket connection attempt:', url);
852
853
854            const ws = new target(...args);
855
856
857
858            let originalOnMessageHandler = null;
859
860
861             Object.defineProperty(ws, 'onmessage', {
862                configurable: true,
863                enumerable: true,
864                get() {
865                    return originalOnMessageHandler;
866                },
867                async set(handler) {
868                    logDebug('WebSocket onmessage handler assigned');
869                    originalOnMessageHandler = handler;
870
871
872                    ws.onmessageinternal = async function(event) {
873
874                        if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
875                            if (originalOnMessageHandler) {
876                                 try {
877                                     originalOnMessageHandler.call(ws, event);
878                                 } catch (e) {
879                                     logError("Error in original WebSocket onmessage handler:", e);
880                                 }
881                            }
882                            return;
883                        }
884
885                        logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
886                        try {
887                            let json = JSON.parse(event.data);
888
889
890                            if (json.conversation_id && json.conversation_id !== currentConversationId) {
891                                currentConversationId = json.conversation_id;
892                                logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
893                            }
894
895
896                            const processedJson = await processPotentialModeration(json, 'WebSocket');
897
898
899                             const newEvent = new MessageEvent('message', {
900                                 data: JSON.stringify(processedJson),
901                                 origin: event.origin,
902                                 lastEventId: event.lastEventId,
903                                 source: event.source,
904                                 ports: event.ports,
905                             });
906
907
908                            if (originalOnMessageHandler) {
909                                 try {
910                                    originalOnMessageHandler.call(ws, newEvent);
911                                 } catch (e) {
912                                     logError("Error calling original WebSocket onmessage handler after modification:", e);
913
914                                 }
915                            } else {
916                                logDebug("Original WebSocket onmessage handler not found when message received.");
917                            }
918
919                        } catch (e) {
920                            logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
921
922                            if (originalOnMessageHandler) {
923                                 try {
924                                    originalOnMessageHandler.call(ws, event);
925                                 } catch (eInner) {
926                                     logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
927                                 }
928                            }
929                        }
930                    };
931
932
933                    ws.addEventListener('message', ws.onmessageinternal);
934                }
935             });
936
937
938
939             const wrapHandler = (eventName) => {
940                let originalHandler = null;
941                Object.defineProperty(ws, `on${eventName}`, {
942                    configurable: true,
943                    enumerable: true,
944                    get() { return originalHandler; },
945                    set(handler) {
946                        logDebug(`WebSocket on${eventName} handler assigned`);
947                        originalHandler = handler;
948                        ws.addEventListener(eventName, (event) => {
949                             if (eventName === 'message') return;
950                             logDebug(`WebSocket event: ${eventName}`, event);
951                             if (originalHandler) {
952                                 try {
953                                     originalHandler.call(ws, event);
954                                 } catch (e) {
955                                     logError(`Error in original WebSocket on${eventName} handler:`, e);
956                                 }
957                             }
958                        });
959                    }
960                });
961             };
962
963             wrapHandler('close');
964             wrapHandler('error');
965
966
967             ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
968
969            return ws;
970        }
971    });
972
973
974
975
976    if (window.location.hostname !== 'grok.com') {
977        console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
978        return;
979    }
980
981
982    if (document.readyState === 'loading') {
983        document.addEventListener('DOMContentLoaded', setupUI);
984    } else {
985
986        setupUI();
987    }
988
989    console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
990
991})();
Grok DeMod | Robomonkey