Grok DeMod

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

As of 2025-03-28. See the latest version.

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