// ==UserScript== // @name 盒子IM - 防已读回执 // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description 拦截盒子IM的已读回执API请求,防止发送已读状态 // @author 归鸿 // @match https://www.boximchat.com/* // @grant none // @run-at document-start // @license AGPL-3.0 // ==/UserScript== (function() { 'use strict'; // 保存原始的 fetch 和 XMLHttpRequest const originalFetch = window.fetch; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; // 需要拦截的API路径特征 const BLOCKED_PATHS = [ '/api/message/private/readed', // 私聊已读回执 '/api/message/group/readed' // 群聊已读回执 ]; // 检查URL是否应该被拦截 function shouldBlockUrl(url) { if (!url) return false; try { // 处理相对路径或完整URL const urlObj = new URL(url, window.location.origin); const pathname = urlObj.pathname; return BLOCKED_PATHS.some(path => pathname.includes(path)); } catch (e) { // 如果URL解析失败,尝试字符串匹配 return BLOCKED_PATHS.some(path => url.includes(path)); } } // ----- 拦截 fetch ----- window.fetch = function(input, init) { let url = input; if (input && typeof input === 'object' && input.url) { url = input.url; } if (typeof input === 'string') { url = input; } // 检查是否是需要拦截的请求 if (shouldBlockUrl(url)) { console.log('[盒子IM防已读] 已拦截 fetch 请求:', url); // 返回一个空的成功响应,避免前端报错 return Promise.resolve(new Response(JSON.stringify({ code: 0, data: null }), { status: 200, headers: { 'Content-Type': 'application/json' } })); } // 其他请求正常发送 return originalFetch.apply(this, arguments); }; // ----- 拦截 XMLHttpRequest ----- // 拦截 open 方法,记录请求URL XMLHttpRequest.prototype.open = function(method, url, async, user, password) { this._blockedUrl = url; this._method = method; // 如果是被拦截的URL,标记为已拦截,但不阻止open执行(避免报错) if (shouldBlockUrl(url)) { this._shouldBlock = true; console.log('[盒子IM防已读] 已拦截 XHR 请求:', url); } else { this._shouldBlock = false; } // 调用原始open return originalXHROpen.apply(this, arguments); }; // 拦截 send 方法,如果请求被标记为拦截,则不发送真实请求 XMLHttpRequest.prototype.send = function(body) { if (this._shouldBlock) { // 模拟一个成功的响应,触发 load 和 readystatechange 事件 const xhr = this; // 使用 setTimeout 异步触发,模拟真实请求的异步行为 setTimeout(() => { // 设置状态 Object.defineProperty(xhr, 'readyState', { value: 4, writable: true }); Object.defineProperty(xhr, 'status', { value: 200, writable: true }); Object.defineProperty(xhr, 'responseText', { value: JSON.stringify({ code: 0, data: null }), writable: true }); Object.defineProperty(xhr, 'response', { value: JSON.stringify({ code: 0, data: null }), writable: true }); // 触发事件 if (xhr.onreadystatechange) { xhr.onreadystatechange.call(xhr); } if (xhr.onload) { xhr.onload.call(xhr); } if (xhr.onloadend) { xhr.onloadend.call(xhr); } }, 0); return; } // 其他请求正常发送 return originalXHRSend.apply(this, arguments); }; console.log('[盒子IM防已读] 脚本已启动,已拦截以下API:'); BLOCKED_PATHS.forEach(path => console.log(' -', path)); })();