Grok DeMod

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

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