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