// ==UserScript== // @name NGA Filter // @namespace https://greasyfork.org/users/263018 // @version 2.1.2 // @author snyssss // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。 // @license MIT // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @noframes // @downloadURL none // ==/UserScript== (() => { // 声明泥潭主模块、菜单模块、主题模块、回复模块 let commonui, menuModule, topicModule, replyModule; // KEY const DATA_KEY = "NGAFilter"; const USER_AGENT_KEY = "USER_AGENT_KEY"; const PRE_FILTER_KEY = "PRE_FILTER_KEY"; const CLEAR_TIME_KEY = "CLEAR_TIME_KEY"; // User Agent const USER_AGENT = (() => { const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official"; GM_registerMenuCommand(`修改UA:${data}`, () => { const value = prompt("修改UA", data); if (value) { GM_setValue(USER_AGENT_KEY, value); location.reload(); } }); return data; })(); // 前置过滤 const preFilter = (() => { const data = GM_getValue(PRE_FILTER_KEY); const value = data === undefined ? true : data; GM_registerMenuCommand(`前置过滤:${value ? "是" : "否"}`, () => { GM_setValue(PRE_FILTER_KEY, !value); location.reload(); }); return value; })(); // STYLE GM_addStyle(` .filter-table-wrapper { max-height: 80vh; overflow-y: auto; } .filter-table { margin: 0; } .filter-table th, .filter-table td { position: relative; white-space: nowrap; } .filter-table th { position: sticky; top: 2px; z-index: 1; } .filter-table input:not([type]), .filter-table input[type="text"] { margin: 0; box-sizing: border-box; height: 100%; width: 100%; } .filter-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } .filter-text-ellipsis { display: flex; } .filter-text-ellipsis > * { flex: 1; width: 1px; overflow: hidden; text-overflow: ellipsis; } .filter-button-group { margin: -.1em -.2em; } .filter-tags { margin: 2px -0.2em 0; text-align: left; } .filter-mask { margin: 1px; color: #81C7D4; background: #81C7D4; } .filter-mask-block { display: block; border: 1px solid #66BAB7; text-align: center !important; } .filter-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } `); // 重新过滤 const reFilter = async (skip = () => false) => { // 清空列表 listModule.clear(); // 开始过滤 [ ...(topicModule ? Object.values(topicModule.data) : []), ...(replyModule ? Object.values(replyModule.data) : []), ].forEach((item) => { // 未绑定事件 if (item.nFilter === undefined) { return; } // 如果跳过过滤,直接添加列表 if (skip(item.nFilter)) { listModule.add(item.nFilter); return; } // 执行过滤 item.nFilter.execute(); }); }; // 缓存模块 const cacheModule = (() => { // 声明模块集合 const modules = {}; // IndexedDB 操作 const db = (() => { // 常量 const VERSION = 2; const DB_NAME = "NGA_FILTER_CACHE"; // 是否支持 const support = unsafeWindow.indexedDB !== undefined; // 不支持,直接返回 if (support === false) { return { support, }; } // 创建或获取数据库实例 const getInstance = (() => { let instance; return () => new Promise((resolve) => { // 如果已存在实例,直接返回 if (instance) { resolve(instance); return; } // 打开 IndexedDB 数据库 const request = unsafeWindow.indexedDB.open(DB_NAME, VERSION); // 如果数据库不存在则创建 request.onupgradeneeded = (event) => { // 获取旧版本号 var oldVersion = event.oldVersion; // 根据版本号创建表 Object.entries(modules).map(([name, { keyPath, version }]) => { if (version > oldVersion) { // 创建表 const store = event.target.result.createObjectStore(name, { keyPath, }); // 创建索引,用于清除过期数据 store.createIndex("timestamp", "timestamp"); } }); }; // 成功后写入实例并返回 request.onsuccess = (event) => { instance = event.target.result; resolve(instance); }; }); })(); return { support, getInstance, }; })(); // 删除缓存 const remove = async (name, key) => { // 不支持 IndexedDB,使用 GM_setValue if (db.support === false) { const cache = GM_getValue(name) || {}; delete cache[key]; GM_setValue(name, cache); return; } // 获取实例 const instance = await db.getInstance(); // 写入 IndexedDB await new Promise((resolve) => { // 创建事务 const transaction = instance.transaction([name], "readwrite"); // 获取对象仓库 const store = transaction.objectStore(name); // 删除数据 const r = store.delete(key); r.onsuccess = () => { resolve(); }; r.onerror = () => { resolve(); }; }); }; // 写入缓存 const save = async (name, key, value) => { // 不支持 IndexedDB,使用 GM_setValue if (db.support === false) { const cache = GM_getValue(name) || {}; cache[key] = value; GM_setValue(name, cache); return; } // 获取实例 const instance = await db.getInstance(); // 写入 IndexedDB await new Promise((resolve) => { // 创建事务 const transaction = instance.transaction([name], "readwrite"); // 获取对象仓库 const store = transaction.objectStore(name); // 插入数据 const r = store.put({ ...value, timestamp: Date.now(), }); r.onsuccess = () => { resolve(); }; r.onerror = () => { resolve(); }; }); }; // 读取缓存 const load = async (name, key, expireTime = 0) => { // 不支持 IndexedDB,使用 GM_getValue if (db.support === false) { const cache = GM_getValue(name) || {}; if (cache[key]) { const result = cache[key]; // 如果已超时则删除 if (expireTime > 0) { if (result.timestamp + expireTime < new Date().getTime()) { await remove(name, key); return null; } } return result; } return null; } // 获取实例 const instance = await db.getInstance(); // 查找 IndexedDB const result = await new Promise((resolve) => { // 创建事务 const transaction = instance.transaction([name], "readonly"); // 获取对象仓库 const store = transaction.objectStore(name); // 获取数据 const request = store.get(key); // 成功后处理数据 request.onsuccess = (event) => { const data = event.target.result; if (data) { resolve(data); return; } resolve(null); }; // 失败后处理 request.onerror = () => { resolve(null); }; }); // 没有数据 if (result === null) { return null; } // 如果已超时则删除 if (expireTime > 0) { if (result.timestamp + expireTime < new Date().getTime()) { await remove(name, key); return null; } } // 返回结果 return result; }; // 定时清理 const clear = async () => { // 获取实例 const instance = await db.getInstance(); // 清理 IndexedDB Object.entries(modules).map(([name, { persistent }]) => { // 持久化,不进行自动清理 if (persistent) { return; } // 创建事务 const transaction = instance.transaction([name], "readwrite"); // 获取对象仓库 const store = transaction.objectStore(name); // 清理数据 store.clear(); }); }; // 初始化,用于写入表信息 const init = (name, value) => { modules[name] = value; }; return { init, save, load, remove, clear, }; })(); // 过滤模块 const filterModule = (() => { // 过滤提示 const tips = "过滤顺序:用户 > 标记 > 关键字 > 属地
过滤级别:显示 > 隐藏 > 遮罩 > 标记 > 继承"; // 过滤方式 const modes = ["继承", "标记", "遮罩", "隐藏", "显示"]; // 默认过滤方式 const defaultMode = modes[0]; // 切换过滤方式 const switchModeByName = (value) => modes[modes.indexOf(value) + 1] || defaultMode; // 获取当前过滤方式下标 const getModeByName = (name, defaultValue = 0) => { const index = modes.indexOf(name); if (index < 0) { return defaultValue; } return index; }; // 获取指定下标过滤方式 const getNameByMode = (index) => modes[index] || ""; // 折叠样式 const collapse = (uid, element, content) => { element.innerHTML = `
Troll must die. 点击查看
${content}
`; }; return { tips, modes, defaultMode, collapse, getModeByName, getNameByMode, switchModeByName, }; })(); // 数据(及配置)模块 const dataModule = (() => { // 合并数据 const merge = (() => { const isObject = (value) => { return value !== null && typeof value === "object"; }; const deepClone = (value) => { if (isObject(value)) { const clone = Array.isArray(value) ? [] : {}; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { clone[key] = deepClone(value[key]); } } return clone; } return value; }; return (target, ...sources) => { for (const source of sources) { for (const key in source) { if (isObject(source[key])) { if (isObject(target[key])) { merge(target[key], source[key]); } else { target[key] = deepClone(source[key]); } } else { target[key] = source[key]; } } } return target; }; })(); // 初始化数据 const data = (() => { // 默认配置 const defaultData = { tags: {}, users: {}, keywords: {}, locations: {}, options: { filterRegdateLimit: 0, filterPostnumLimit: 0, filterTopicRateLimit: 100, filterReputationLimit: NaN, filterAnony: false, filterMode: "隐藏", }, }; // 读取数据 const storedData = GM_getValue(DATA_KEY); // 如果没有数据,则返回默认配置 if (typeof storedData !== "object") { return defaultData; } // 返回数据 return merge(defaultData, storedData); })(); // 保存数据 const save = (values) => { merge(data, values); GM_setValue(DATA_KEY, data); }; // 返回标记列表 const getTags = () => data.tags; // 返回用户列表 const getUsers = () => data.users; // 返回关键字列表 const getKeywords = () => data.keywords; // 返回属地列表 const getLocations = () => data.locations; // 获取默认过滤模式 const getDefaultFilterMode = () => data.options.filterMode; // 设置默认过滤模式 const setDefaultFilterMode = (value) => { save({ options: { filterMode: value, }, }); }; // 获取注册时间限制 const getFilterRegdateLimit = () => data.options.filterRegdateLimit || 0; // 设置注册时间限制 const setFilterRegdateLimit = (value) => { save({ options: { filterRegdateLimit: value, }, }); }; // 获取发帖数量限制 const getFilterPostnumLimit = () => data.options.filterPostnumLimit || 0; // 设置发帖数量限制 const setFilterPostnumLimit = (value) => { save({ options: { filterPostnumLimit: value, }, }); }; // 获取发帖比例限制 const getFilterTopicRateLimit = () => data.options.filterTopicRateLimit || 100; // 设置发帖比例限制 const setFilterTopicRateLimit = (value) => { save({ options: { filterTopicRateLimit: value, }, }); }; // 获取用户声望限制 const getFilterReputationLimit = () => data.options.filterReputationLimit || NaN; // 设置用户声望限制 const setFilterReputationLimit = (value) => { save({ options: { filterReputationLimit: value, }, }); }; // 获取是否过滤匿名 const getFilterAnony = () => data.options.filterAnony || false; // 设置是否过滤匿名 const setFilterAnony = (value) => { save({ options: { filterAnony: value, }, }); }; return { save, getTags, getUsers, getKeywords, getLocations, getDefaultFilterMode, setDefaultFilterMode, getFilterRegdateLimit, setFilterRegdateLimit, getFilterPostnumLimit, setFilterPostnumLimit, getFilterTopicRateLimit, setFilterTopicRateLimit, getFilterReputationLimit, setFilterReputationLimit, getFilterAnony, setFilterAnony, }; })(); // 列表模块 const listModule = (() => { const list = []; const callback = []; // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
用户 过滤方式 内容 原因
`; return element; })(); const tbody = content.querySelector("TBODY"); const load = (item) => { const { uid, username, tid, pid, filterMode, reason } = item; // 用户 const user = userModule.format(uid, username); // 移除 BR 标签 item.content = (item.content || "").replace(/
/g, ""); // 主题 const subject = (() => { if (tid) { // 如果有 TID 但没有标题,是引用,采用内容逻辑 if (item.subject.length === 0) { return `${ item.content }`; } return `${item.subject}`; } return item.subject; })(); // 内容 const content = (() => { if (pid) { return `${ item.content }`; } return item.content; })(); const row = document.createElement("TR"); row.className = `row${(tbody.querySelectorAll("TR").length % 2) + 1}`; row.innerHTML = ` ${user} ${filterMode}
${subject || content}
${reason} `; tbody.insertBefore(row, tbody.firstChild); }; const refresh = () => { tbody.innerHTML = ""; Object.values(list).forEach(load); }; return { content, refresh, load, }; })(); const add = (value) => { if ( list.find( (item) => item.tid === value.tid && item.pid === value.pid && item.subject === value.subject ) ) { return; } if ((value.filterMode || "显示") === "显示") { return; } list.push(value); view.load(value); callback.forEach((item) => item(list)); }; const clear = () => { list.splice(0, list.length); view.refresh(); callback.forEach((item) => item(list)); }; const bindCallback = (func) => { func(list); callback.push(func); }; return { add, clear, bindCallback, view, }; })(); // 用户模块 const userModule = (() => { // 获取用户列表 const list = () => dataModule.getUsers(); // 获取用户 const get = (uid) => { // 获取列表 const users = list(); // 如果已存在,则返回信息 if (users[uid]) { return users[uid]; } return null; }; // 增加用户 const add = (uid, username, tags, filterMode) => { // 获取对应的用户 const user = get(uid); // 如果用户已存在,则返回用户信息,否则增加用户 if (user) { return user; } // 保存用户 // TODO id 和 name 属于历史遗留问题,应该改为 uid 和 username 以便更好的理解 dataModule.save({ users: { [uid]: { id: uid, name: username, tags, filterMode, }, }, }); // 返回用户信息 return get(uid); }; // 编辑用户 const edit = (uid, values) => { dataModule.save({ users: { [uid]: values, }, }); }; // 删除用户 const remove = (uid) => { // TODO 这里不可避免的直接操作了原始数据 delete list()[uid]; // 保存数据 dataModule.save({}); }; // 格式化用户 const format = (uid, name) => { if (uid <= 0) { return ""; } const user = get(uid); if (user) { name = name || user.name; } const username = name ? "@" + name : "#" + uid; return `[${username}]`; }; // UI const view = (() => { const details = (() => { let window; return (uid, name, callback) => { if (window === undefined) { window = commonui.createCommmonWindow(); } const user = get(uid); const content = document.createElement("DIV"); const size = Math.floor((screen.width * 0.8) / 200); const items = Object.values(tagModule.list()).map((tag, index) => { const checked = user && user.tags.includes(tag.id) ? "checked" : ""; return ` `; }); const rows = [...new Array(Math.ceil(items.length / size))].map( (_, index) => ` ${items.slice(size * index, size * (index + 1)).join("")} ` ); content.className = "w100"; content.innerHTML = `
${rows.join("")}
过滤方式:
${ filterModule.tips }
`; const actions = content.querySelectorAll("BUTTON"); actions[0].onclick = () => { actions[0].innerText = filterModule.switchModeByName( actions[0].innerText ); }; actions[1].onclick = () => { if (confirm("是否确认?") === false) { return; } remove(uid); reFilter((item) => item.uid !== uid); if (callback) { callback({ id: null, }); } window._.hide(); }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } const filterMode = actions[0].innerText; const checked = [...content.querySelectorAll("INPUT:checked")].map( (input) => parseInt(input.value, 10) ); const newTags = content .querySelector("INPUT[type='text']") .value.split("|") .filter((item) => item.length) .map((item) => tagModule.add(item)); const tags = [...new Set([...checked, ...newTags])].sort(); if (user) { user.tags = tags; edit(uid, { filterMode, }); } else { add(uid, name, tags, filterMode); } reFilter((item) => item.uid !== uid); if (callback) { callback({ uid, name, tags, filterMode, }); } window._.hide(); }; if (user === null) { actions[1].style.display = "none"; } window._.addContent(null); window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`); window._.addContent(content); window._.show(); }; })(); const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
昵称 标记 过滤方式 操作
`; return element; })(); let index = 0; let size = 50; let hasNext = false; const box = content.querySelector("DIV"); const tbody = content.querySelector("TBODY"); const wrapper = content.querySelector(".filter-table-wrapper"); const load = ({ id, name, tags, filterMode }, anchor = null) => { if (id === null) { if (anchor) { tbody.removeChild(anchor); } return; } if (anchor === null) { anchor = document.createElement("TR"); anchor.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; tbody.appendChild(anchor); } anchor.innerHTML = ` ${format(id, name)} ${tags.map(tagModule.format).join("")}
`; const actions = anchor.querySelectorAll("BUTTON"); actions[0].onclick = () => { const filterMode = filterModule.switchModeByName( actions[0].innerHTML ); actions[0].innerHTML = filterMode; edit(id, { filterMode }); reFilter((item) => item.uid !== uid); }; actions[1].onclick = () => { details(id, name, (item) => { load(item, anchor); }); }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } tbody.removeChild(anchor); remove(id); reFilter((item) => item.uid !== uid); }; }; const loadNext = () => { hasNext = index + size < Object.keys(list()).length; Object.values(list()) .slice(index, index + size) .forEach((item) => load(item)); index += size; }; box.onscroll = () => { if (hasNext === false) { return; } if ( box.scrollHeight - box.scrollTop - box.clientHeight <= wrapper.clientHeight ) { loadNext(); } }; const refresh = () => { index = 0; tbody.innerHTML = ""; loadNext(); }; return { content, details, refresh, }; })(); return { list, get, add, edit, remove, format, view, }; })(); // 标记模块 const tagModule = (() => { // 获取标记列表 const list = () => dataModule.getTags(); // 计算标记颜色 // 采用的是泥潭的颜色方案,参见 commonui.htmlName const generateColor = (name) => { const hash = (() => { let h = 5381; for (var i = 0; i < name.length; i++) { h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff; } return h; })(); const hex = Math.abs(hash).toString(16) + "000000"; const hsv = [ `0x${hex.substring(2, 4)}` / 255, `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25, `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25, ]; const rgb = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]); return ["#", ...rgb].reduce((a, b) => { return a + ("0" + b.toString(16)).slice(-2); }); }; // 获取标记 const get = ({ id, name }) => { // 获取列表 const tags = list(); // 通过 ID 获取标记 if (tags[id]) { return tags[id]; } // 通过名称获取标记 if (name) { const tag = Object.values(tags).find((item) => item.name === name); if (tag) { return tag; } } return null; }; // 增加标记 const add = (name) => { // 获取对应的标记 const tag = get({ name }); // 如果标记已存在,则返回标记信息,否则增加标记 if (tag) { return tag; } // ID 为最大值 + 1 const id = Math.max(Object.keys(list()), 0) + 1; // 标记的颜色 const color = generateColor(name); // 保存标记 dataModule.save({ tags: { [id]: { id, name, color, filterMode: filterModule.defaultMode, }, }, }); // 返回标记信息 return get({ id }); }; // 编辑标记 const edit = (id, values) => { dataModule.save({ tags: { [id]: values, }, }); }; // 删除标记 const remove = (id) => { // TODO 这里不可避免的直接操作了原始数据 delete list()[id]; // 删除用户对应的标记 Object.values(userModule.list()).forEach((user) => { const index = user.tags.findIndex((tag) => tag === id); if (index >= 0) { user.tags.splice(index, 1); } }); // 保存数据 dataModule.save({}); }; // 格式化标记 const format = (id, name, color) => { if (id) { const tag = get({ id }); if (tag) { name = tag.name; color = tag.color; } } if (name && color) { return `${name}`; } return ""; }; // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
标记 列表 过滤方式 操作
`; return element; })(); let index = 0; let size = 50; let hasNext = false; const box = content.querySelector("DIV"); const tbody = content.querySelector("TBODY"); const wrapper = content.querySelector(".filter-table-wrapper"); const load = ({ id, filterMode }, anchor = null) => { if (id === null) { if (anchor) { tbody.removeChild(anchor); } return; } if (anchor === null) { anchor = document.createElement("TR"); anchor.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; tbody.appendChild(anchor); } const users = Object.values(userModule.list()); const filteredUsers = users.filter((user) => user.tags.includes(id)); anchor.innerHTML = ` ${format(id)}
${filteredUsers .map((user) => userModule.format(user.id)) .join("")}
`; const actions = anchor.querySelectorAll("BUTTON"); actions[0].onclick = (() => { let hide = true; return () => { hide = !hide; actions[0].nextElementSibling.style.display = hide ? "none" : "block"; }; })(); actions[1].onclick = () => { const filterMode = filterModule.switchModeByName( actions[1].innerHTML ); actions[1].innerHTML = filterMode; edit(id, { filterMode }); reFilter((item) => filteredUsers.find((user) => user.id === item.uid) ); }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } tbody.removeChild(anchor); remove(id); reFilter((item) => filteredUsers.find((user) => user.id === item.uid) ); }; }; const loadNext = () => { hasNext = index + size < Object.keys(list()).length; Object.values(list()) .slice(index, index + size) .forEach((item) => load(item)); index += size; }; box.onscroll = () => { if (hasNext === false) { return; } if ( box.scrollHeight - box.scrollTop - box.clientHeight <= wrapper.clientHeight ) { loadNext(); } }; const refresh = () => { index = 0; tbody.innerHTML = ""; loadNext(); }; return { content, refresh, }; })(); return { list, get, add, edit, remove, format, generateColor, view, }; })(); // 关键字模块 const keywordModule = (() => { // 获取关键字列表 const list = () => dataModule.getKeywords(); // 获取关键字 const get = (id) => { // 获取列表 const keywords = list(); // 如果已存在,则返回信息 if (keywords[id]) { return keywords[id]; } return null; }; // 编辑关键字 const edit = (id, values) => { dataModule.save({ keywords: { [id]: values, }, }); }; // 增加关键字 // filterLevel: 0 - 仅过滤标题; 1 - 过滤标题和内容 // 无需判重 const add = (keyword, filterMode, filterLevel) => { // ID 为最大值 + 1 const id = Math.max(Object.keys(list()), 0) + 1; // 保存关键字 dataModule.save({ keywords: { [id]: { id, keyword, filterMode, filterLevel, }, }, }); // 返回关键字信息 return get(id); }; // 删除关键字 const remove = (id) => { // TODO 这里不可避免的直接操作了原始数据 delete list()[id]; // 保存数据 dataModule.save({}); }; // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
列表 过滤方式 包括内容 操作
支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。
`; return element; })(); let index = 0; let size = 50; let hasNext = false; const box = content.querySelector("DIV"); const tbody = content.querySelector("TBODY"); const wrapper = content.querySelector(".filter-table-wrapper"); const load = ( { id, keyword, filterMode, filterLevel }, anchor = null ) => { if (id === null) { if (anchor) { tbody.removeChild(anchor); } return; } if (anchor === null) { anchor = document.createElement("TR"); anchor.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; tbody.appendChild(anchor); } const checked = filterLevel ? "checked" : ""; anchor.innerHTML = `
`; const actions = anchor.querySelectorAll("BUTTON"); actions[0].onclick = () => { actions[0].innerHTML = filterModule.switchModeByName( actions[0].innerHTML ); }; actions[1].onclick = () => { const keyword = anchor.querySelector("INPUT[type='text']").value; const filterMode = actions[0].innerHTML; const filterLevel = anchor.querySelector( `INPUT[type="checkbox"]:checked` ) ? 1 : 0; if (keyword) { edit(id, { keyword, filterMode, filterLevel, }); reFilter((item) => item.reason.indexOf("关键字") !== 0); } }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } tbody.removeChild(anchor); remove(id); reFilter((item) => item.reason.indexOf("关键字") !== 0); }; }; const loadNext = () => { hasNext = index + size < Object.keys(list()).length; Object.values(list()) .slice(index, index + size) .forEach((item) => load(item)); if (hasNext === false) { const loadNew = () => { const row = document.createElement("TR"); row.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; row.innerHTML = `
`; const actions = row.querySelectorAll("BUTTON"); actions[0].onclick = () => { const filterMode = filterModule.switchModeByName( actions[0].innerHTML ); actions[0].innerHTML = filterMode; }; actions[1].onclick = () => { const keyword = row.querySelector("INPUT[type='text']").value; const filterMode = actions[0].innerHTML; const filterLevel = row.querySelector( `INPUT[type="checkbox"]:checked` ) ? 1 : 0; if (keyword) { const item = add(keyword, filterMode, filterLevel); load(item, row); loadNew(); reFilter(); } }; tbody.appendChild(row); }; loadNew(); } index += size; }; box.onscroll = () => { if (hasNext === false) { return; } if ( box.scrollHeight - box.scrollTop - box.clientHeight <= wrapper.clientHeight ) { loadNext(); } }; const refresh = () => { index = 0; tbody.innerHTML = ""; loadNext(); }; return { content, refresh, }; })(); return { list, get, add, edit, remove, view, }; })(); // 属地模块 const locationModule = (() => { // 获取属地列表 const list = () => dataModule.getLocations(); // 获取属地 const get = (id) => { // 获取列表 const locations = list(); // 如果已存在,则返回信息 if (locations[id]) { return locations[id]; } return null; }; // 增加属地 // 无需判重 const add = (keyword, filterMode) => { // ID 为最大值 + 1 const id = Math.max(Object.keys(list()), 0) + 1; // 保存属地 dataModule.save({ locations: { [id]: { id, keyword, filterMode, }, }, }); // 返回属地信息 return get(id); }; // 编辑属地 const edit = (id, values) => { dataModule.save({ locations: { [id]: values, }, }); }; // 删除属地 const remove = (id) => { // TODO 这里不可避免的直接操作了原始数据 delete list()[id]; // 保存数据 dataModule.save({}); }; // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
列表 过滤方式 操作
支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。
属地过滤功能需要占用额外的资源,请谨慎开启
`; return element; })(); let index = 0; let size = 50; let hasNext = false; const box = content.querySelector("DIV"); const tbody = content.querySelector("TBODY"); const wrapper = content.querySelector(".filter-table-wrapper"); const load = ({ id, keyword, filterMode }, anchor = null) => { if (id === null) { if (anchor) { tbody.removeChild(anchor); } return; } if (anchor === null) { anchor = document.createElement("TR"); anchor.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; tbody.appendChild(anchor); } anchor.innerHTML = `
`; const actions = anchor.querySelectorAll("BUTTON"); actions[0].onclick = () => { actions[0].innerHTML = filterModule.switchModeByName( actions[0].innerHTML ); }; actions[1].onclick = () => { const keyword = anchor.querySelector("INPUT[type='text']").value; const filterMode = actions[0].innerHTML; if (keyword) { edit(id, { keyword, filterMode, }); reFilter((item) => item.reason.indexOf("属地") !== 0); } }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } tbody.removeChild(anchor); remove(id); reFilter((item) => item.reason.indexOf("属地") !== 0); }; }; const loadNext = () => { hasNext = index + size < Object.keys(list()).length; Object.values(list()) .slice(index, index + size) .forEach((item) => load(item)); if (hasNext === false) { const loadNew = () => { const row = document.createElement("TR"); row.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; row.innerHTML = `
`; const actions = row.querySelectorAll("BUTTON"); actions[0].onclick = () => { const filterMode = filterModule.switchModeByName( actions[0].innerHTML ); actions[0].innerHTML = filterMode; }; actions[1].onclick = () => { const keyword = row.querySelector("INPUT[type='text']").value; const filterMode = actions[0].innerHTML; if (keyword) { const item = add(keyword, filterMode); load(item, row); loadNew(); reFilter(); } }; tbody.appendChild(row); }; loadNew(); } index += size; }; box.onscroll = () => { if (hasNext === false) { return; } if ( box.scrollHeight - box.scrollTop - box.clientHeight <= wrapper.clientHeight ) { loadNext(); } }; const refresh = () => { index = 0; tbody.innerHTML = ""; loadNext(); }; return { content, refresh, }; })(); return { list, get, add, edit, remove, view, }; })(); // 猎巫模块 const witchHuntModule = (() => { const key = "WITCH_HUNT"; const queue = []; const cache = {}; // 获取设置列表 const list = () => GM_getValue(key) || {}; // 获取单条设置 const get = (fid) => { // 获取列表 const settings = list(); // 如果已存在,则返回信息 if (settings[fid]) { return settings[fid]; } return null; }; // 增加设置 // filterLevel: 0 - 仅标记; 1 - 标记并过滤 const add = async (fid, label, filterMode, filterLevel) => { // FID 只能是数字 fid = parseInt(fid, 10); // 获取列表 const settings = list(); // 如果版面 ID 已存在,则提示错误 if (Object.keys(settings).includes(fid)) { alert("已有相同版面ID"); return; } // 请求版面信息 const info = await fetchModule.getForumInfo(fid); // 如果版面不存在,则提示错误 if (info === null) { alert("版面ID有误"); return; } // 计算标记颜色 const color = tagModule.generateColor(info.name); // 保存设置 settings[fid] = { fid, name: info.name, label, color, filterMode, filterLevel, }; GM_setValue(key, settings); // 增加后需要清除缓存 Object.keys(cache).forEach((key) => { delete cache[key]; }); // 返回设置信息 return settings[fid]; }; // 编辑设置 const edit = (fid, values) => { // 获取列表 const settings = list(); // 没有则跳过 if (settings[fid] === undefined) { return; } // 保存设置 settings[fid] = { ...settings[fid], ...values, }; GM_setValue(key, settings); // 编辑后需要重新加载 reFilter((item) => { item.witchHunt = null; return true; }); }; // 删除设置 const remove = (fid) => { // 获取列表 const settings = list(); // 没有则跳过 if (settings[fid] === undefined) { return; } // 保存设置 delete settings[fid]; GM_setValue(key, settings); // 删除后需要清除缓存 Object.keys(cache).forEach((key) => { delete cache[key]; }); }; // 格式化版面 const format = (fid, name) => { return `[${name}]`; }; // 猎巫 const run = (item) => { item.witchHunt = item.witchHunt || []; // 重新过滤 const reload = (newValue) => { const isEqual = newValue.sort().join() === item.witchHunt.sort().join(); if (isEqual) { return; } item.witchHunt = newValue; item.execute(); }; // 获取列表 const settings = Object.keys(list()); // 没有设置且没有旧数据,直接跳过 if (settings.length === 0 && item.witchHunt.length === 0) { return; } // 猎巫任务 const task = async () => { // 请求版面发言记录 const result = cache[item.uid] ? cache[item.uid] : ( await Promise.all( settings.map(async (fid) => { // 当前版面发言记录 const result = await fetchModule.getForumPosted( fid, item.uid ); // 写入当前设置 if (result) { return parseInt(fid, 10); } return null; }) ) ).filter((i) => i !== null); // 写入缓存,同一个页面多次请求没意义 cache[item.uid] = result; // 执行完毕,如果结果有变,重新过滤 reload(result); // 将当前任务移出队列 queue.shift(); // 如果还有任务,继续执行 if (queue.length > 0) { queue[0](); } }; // 队列里已经有任务 const isRunning = queue.length > 0; // 加入队列 queue.push(task); // 如果没有正在执行的任务,则立即执行 if (isRunning === false) { task(); } }; // 重置 const clear = () => { reFilter((item) => { run(item); return true; }); }; // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; element.innerHTML = `
版面 标签 启用过滤 过滤方式 操作
猎巫模块需要占用额外的资源,请谨慎开启
`; return element; })(); let index = 0; let size = 50; let hasNext = false; const box = content.querySelector("DIV"); const tbody = content.querySelector("TBODY"); const wrapper = content.querySelector(".filter-table-wrapper"); const load = ( { fid, name, label, color, filterMode, filterLevel }, anchor = null ) => { if (fid === null) { if (anchor) { tbody.removeChild(anchor); } return; } if (anchor === null) { anchor = document.createElement("TR"); anchor.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; tbody.appendChild(anchor); } const checked = filterLevel ? "checked" : ""; anchor.innerHTML = ` ${format(fid, name)} ${tagModule.format(null, label, color)}
`; const actions = anchor.querySelectorAll("BUTTON"); actions[0].onclick = () => { actions[0].innerHTML = filterModule.switchModeByName( actions[0].innerHTML ); }; actions[1].onclick = () => { const filterMode = actions[0].innerHTML; const filterLevel = anchor.querySelector( `INPUT[type="checkbox"]:checked` ) ? 1 : 0; edit(fid, { filterMode, filterLevel, }); clear(); }; actions[2].onclick = () => { if (confirm("是否确认?") === false) { return; } tbody.removeChild(anchor); remove(fid); clear(); }; }; const loadNext = () => { hasNext = index + size < Object.keys(list()).length; Object.values(list()) .slice(index, index + size) .forEach((item) => load(item)); if (hasNext === false) { const loadNew = () => { const row = document.createElement("TR"); row.className = `row${ (tbody.querySelectorAll("TR").length % 2) + 1 }`; row.innerHTML = `
`; const actions = row.querySelectorAll("BUTTON"); actions[0].onclick = () => { const filterMode = filterModule.switchModeByName( actions[0].innerHTML ); actions[0].innerHTML = filterMode; }; actions[1].onclick = async () => { const inputs = row.querySelectorAll("INPUT[type='text']"); const fid = inputs[0].value; const label = inputs[1].value; const filterMode = actions[0].innerHTML; const filterLevel = row.querySelector( `INPUT[type="checkbox"]:checked` ) ? 1 : 0; if (fid && label) { const item = await add(fid, label, filterMode, filterLevel); if (item) { load(item, row); loadNew(); clear(); } } }; tbody.appendChild(row); }; loadNew(); } index += size; }; box.onscroll = () => { if (hasNext === false) { return; } if ( box.scrollHeight - box.scrollTop - box.clientHeight <= wrapper.clientHeight ) { loadNext(); } }; const refresh = () => { index = 0; tbody.innerHTML = ""; loadNext(); }; return { content, refresh, }; })(); return { list, get, add, edit, remove, run, view, }; })(); // 通用设置 const commonModule = (() => { // UI const view = (() => { const content = (() => { const element = document.createElement("DIV"); element.style = "display: none"; return element; })(); const refresh = () => { content.innerHTML = ""; // 前置过滤 (() => { const checked = preFilter ? "checked" : ""; const element = document.createElement("DIV"); element.innerHTML += `
`; const checkbox = element.querySelector("INPUT"); checkbox.onchange = () => { const newValue = checkbox.checked; GM_setValue(PRE_FILTER_KEY, newValue); location.reload(); }; content.appendChild(element); })(); // 默认过滤方式 (() => { const element = document.createElement("DIV"); element.innerHTML += `
默认过滤方式
${filterModule.tips}
`; ["标记", "遮罩", "隐藏"].forEach((item, index) => { const span = document.createElement("SPAN"); const checked = dataModule.getDefaultFilterMode() === item ? "checked" : ""; span.innerHTML += ` `; const input = span.querySelector("INPUT"); input.onchange = () => { if (input.checked) { dataModule.setDefaultFilterMode(item); reFilter((item) => item.filterMode === "继承"); } }; element.querySelectorAll("div")[1].append(span); }); content.appendChild(element); })(); // 小号过滤(时间) (() => { const value = dataModule.getFilterRegdateLimit() / 86400000; const element = document.createElement("DIV"); element.innerHTML += `
隐藏注册时间小于天的用户
`; const action = element.querySelector("BUTTON"); action.onclick = () => { const newValue = parseInt(element.querySelector("INPUT").value, 10) || 0; dataModule.setFilterRegdateLimit( newValue < 0 ? 0 : newValue * 86400000 ); reFilter((item) => item.reason.indexOf("注册时间") !== 0); }; content.appendChild(element); })(); // 小号过滤(发帖数) (() => { const value = dataModule.getFilterPostnumLimit(); const element = document.createElement("DIV"); element.innerHTML += `
隐藏发帖数量小于贴的用户
`; const action = element.querySelector("BUTTON"); action.onclick = () => { const newValue = parseInt(element.querySelector("INPUT").value, 10) || 0; dataModule.setFilterPostnumLimit(newValue < 0 ? 0 : newValue); reFilter((item) => item.reason.indexOf("发帖数量") !== 0); }; content.appendChild(element); })(); // 流量号过滤(主题比例) (() => { const value = dataModule.getFilterTopicRateLimit(); const element = document.createElement("DIV"); element.innerHTML += `
隐藏发帖比例大于%的用户
`; const action = element.querySelector("BUTTON"); action.onclick = () => { const newValue = parseInt(element.querySelector("INPUT").value, 10) || 100; if (newValue <= 0 || newValue > 100) { return; } dataModule.setFilterTopicRateLimit(newValue); reFilter((item) => item.reason.indexOf("发帖比例") !== 0); }; content.appendChild(element); })(); // 声望过滤 (() => { const value = dataModule.getFilterReputationLimit() || ""; const element = document.createElement("DIV"); element.innerHTML += `
隐藏版面声望低于点的用户
`; const action = element.querySelector("BUTTON"); action.onclick = () => { const newValue = parseInt(element.querySelector("INPUT").value, 10); dataModule.setFilterReputationLimit(newValue); reFilter((item) => item.reason.indexOf("版面声望") !== 0); }; content.appendChild(element); })(); // 匿名过滤 (() => { const checked = dataModule.getFilterAnony() ? "checked" : ""; const element = document.createElement("DIV"); element.innerHTML += `
`; const checkbox = element.querySelector("INPUT"); checkbox.onchange = () => { const newValue = checkbox.checked; dataModule.setFilterAnony(newValue); reFilter((item) => item.reason.indexOf("匿名") !== 0); }; content.appendChild(element); })(); // 删除没有标记的用户 (() => { const element = document.createElement("DIV"); element.innerHTML += `
`; const action = element.querySelector("BUTTON"); action.onclick = () => { if (confirm("是否确认?") === false) { return; } const filteredUsers = Object.values(userModule.list()).filter( ({ tags }) => tags.length === 0 ); filteredUsers.forEach(({ id }) => { userModule.remove(id); }); reFilter((item) => filteredUsers.find((user) => user.id === item.uid) ); }; content.appendChild(element); })(); // 删除没有用户的标记 (() => { const element = document.createElement("DIV"); element.innerHTML += `
`; const action = element.querySelector("BUTTON"); action.onclick = () => { if (confirm("是否确认?") === false) { return; } const users = Object.values(userModule.list()); Object.values(tagModule.list()).forEach(({ id }) => { if (users.find(({ tags }) => tags.includes(id))) { return; } tagModule.remove(id); }); }; content.appendChild(element); })(); // 删除非激活中的用户 (() => { const element = document.createElement("DIV"); element.innerHTML += `
`; const action = element.querySelector("BUTTON"); const list = action.nextElementSibling; action.onclick = () => { if (confirm("是否确认?") === false) { return; } const users = Object.values(userModule.list()); const filtered = []; const waitingQueue = users.map( ({ id }) => () => fetchModule.getUserInfo(id).then(({ bit }) => { const activeInfo = commonui.activeInfo(0, 0, bit); const activeType = activeInfo[1]; if (["ACTIVED", "LINKED"].includes(activeType)) { return; } list.innerHTML += userModule.format(id); filtered.push(id); userModule.remove(id); }) ); const queueLength = waitingQueue.length; const execute = () => { if (waitingQueue.length) { const next = waitingQueue.shift(); action.innerHTML = `删除非激活中的用户 (${ queueLength - waitingQueue.length }/${queueLength})`; action.disabled = true; next().finally(execute); } else { action.disabled = false; reFilter((item) => filtered.includes(item.uid)); } }; execute(); }; content.appendChild(element); })(); }; return { content, refresh, }; })(); return { view, }; })(); // 额外数据请求模块 // 临时的缓存写法 const fetchModule = (() => { // 简单的统一请求 const request = (url, config = {}) => fetch(url, { headers: { "X-User-Agent": USER_AGENT, }, ...config, }); // 获取主题数量 // 缓存 1 小时 const getTopicNum = (() => { const name = "TOPIC_NUM_CACHE"; const expireTime = 60 * 60 * 1000; cacheModule.init(name, { keyPath: "uid", version: 1, }); return async (uid) => { const cache = await cacheModule.load(name, uid, expireTime); if (cache) { return cache.count; } const api = `/thread.php?lite=js&authorid=${uid}`; const { __ROWS } = await new Promise((resolve) => { request(api) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { try { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); resolve(result.data); } catch { resolve({}); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve({}); }); }); cacheModule.save(name, uid, { uid, count: __ROWS, timestamp: new Date().getTime(), }); return __ROWS; }; })(); // 获取用户信息 // 缓存 1 小时 const getUserInfo = (() => { const name = "USER_INFO_CACHE"; const expireTime = 60 * 60 * 1000; cacheModule.init(name, { keyPath: "uid", version: 1, }); return async (uid) => { const cache = await cacheModule.load(name, uid, expireTime); if (cache) { return cache.data; } const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`; const data = await new Promise((resolve) => { request(api) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { try { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); resolve(result.data[0] || null); } catch { resolve(null); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve(null); }); }); if (data) { cacheModule.save(name, uid, { uid, data, timestamp: new Date().getTime(), }); } return data; }; })(); // 获取顶楼用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、声望 // 缓存 10 分钟 const getUserInfoAndReputation = (() => { const name = "PAGE_CACHE"; const expireTime = 10 * 60 * 1000; cacheModule.init(name, { keyPath: "url", version: 1, }); return async (tid, pid) => { if (tid === undefined && pid === undefined) { return; } const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`; const cache = await cacheModule.load(name, api, expireTime); if (cache) { return cache.data; } // 请求数据 const data = await new Promise((resolve) => { request(api) .then((res) => res.blob()) .then((blob) => { const getLastIndex = (content, position) => { if (position >= 0) { let nextIndex = position + 1; while (nextIndex < content.length) { if (content[nextIndex] === "}") { return nextIndex; } if (content[nextIndex] === "{") { nextIndex = getLastIndex(content, nextIndex); if (nextIndex < 0) { break; } } nextIndex = nextIndex + 1; } } return -1; }; const reader = new FileReader(); reader.onload = async () => { const parser = new DOMParser(); const doc = parser.parseFromString(reader.result, "text/html"); const html = doc.body.innerHTML; // 验证帖子正常 const verify = doc.querySelector("#m_posts"); if (verify) { // 取得顶楼 UID const uid = (() => { const ele = doc.querySelector("#postauthor0"); if (ele) { const res = ele.getAttribute("href").match(/uid=(\S+)/); if (res) { return res[1]; } } return 0; })(); // 取得顶楼标题 const subject = doc.querySelector("#postsubject0").innerHTML; // 取得顶楼内容 const content = doc.querySelector("#postcontent0").innerHTML; // 非匿名用户 if (uid && uid > 0) { // 取得用户信息 const userInfo = (() => { // 起始JSON const str = `"${uid}":{`; // 起始下标 const index = html.indexOf(str) + str.length; // 结尾下标 const lastIndex = getLastIndex(html, index); if (lastIndex >= 0) { try { return JSON.parse( `{${html.substring(index, lastIndex)}}` ); } catch {} } return null; })(); // 取得用户声望 const reputation = (() => { const reputations = (() => { // 起始JSON const str = `"__REPUTATIONS":{`; // 起始下标 const index = html.indexOf(str) + str.length; // 结尾下标 const lastIndex = getLastIndex(html, index); if (lastIndex >= 0) { return JSON.parse( `{${html.substring(index, lastIndex)}}` ); } return null; })(); if (reputations) { for (let fid in reputations) { return reputations[fid][uid] || 0; } } return NaN; })(); resolve({ uid, subject, content, userInfo, reputation, }); return; } resolve({ uid, subject, content, }); return; } resolve(null); }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve(null); }); }); if (data) { cacheModule.save(name, api, { url: api, data, timestamp: new Date().getTime(), }); } return data; }; })(); // 获取版面信息 // 不会频繁调用,无需缓存 const getForumInfo = async (fid) => { if (Number.isNaN(fid)) { return null; } const api = `/thread.php?lite=js&fid=${fid}`; const data = await new Promise((resolve) => { request(api) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { try { const text = reader.result; const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ); if (result.data) { resolve(result.data.__F || null); return; } resolve(null); } catch { resolve(null); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve(null); }); }); return data; }; // 获取版面发言记录 // 缓存 1 天 const getForumPosted = (() => { const name = "FORUM_POSTED_CACHE"; const expireTime = 24 * 60 * 60 * 1000; cacheModule.init(name, { keyPath: "url", persistent: true, version: 2, }); return async (fid, uid) => { if (uid <= 0) { return; } const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`; const cache = await cacheModule.load(name, api); if (cache) { // 发言是无法撤销的,只要有记录就永远不需要再获取 // 手动处理没有记录的缓存数据 if ( cache.data === false && cache.timestamp + expireTime < new Date().getTime() ) { await remove(name, api); } return cache.data; } let isComplete = false; let isBusy = false; const func = async (url) => await new Promise((resolve) => { if (isComplete || isBusy) { resolve(); return; } request(url) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const text = reader.result; // 将所有匹配的 FID 写入缓存,即使并不在设置里 const matched = text.match(/"fid":(-?\d+),/g); if (matched) { [ ...new Set( matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10) ) ), ].forEach((item) => { const key = api.replace(`&fid=${fid}`, `&fid=${item}`); // 直接写入缓存 cacheModule.save(name, key, { url: key, data: true, timestamp: new Date().getTime(), }); // 已有结果,无需继续查询 if (fid === item) { isComplete = true; } }); resolve(); return; } // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误 if (text.indexOf("服务器忙") > 0) { isBusy = true; } resolve(); }; reader.readAsText(blob, "GBK"); }) .catch(() => { resolve(); }); }); // 先获取回复记录的第一页,顺便可以获取其他版面的记录 // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误 await func(api.replace(`&fid=${fid}`, `&searchpost=1`)); await func(api + "&searchpost=1"); await func(api); // 无论成功与否都写入缓存 if (isComplete === false) { // 遇到服务器忙的情况,手动调整缓存时间至 1 小时 const timestamp = isBusy ? new Date().getTime() - (expireTime - 60 * 60 * 1000) : new Date().getTime(); // 写入失败缓存 cacheModule.save(name, api, { url: api, data: false, timestamp, }); } return isComplete; }; })(); // 每天清理缓存 (() => { const today = new Date(); const lastTime = new Date(GM_getValue(CLEAR_TIME_KEY) || 0); const isToday = lastTime.getDate() === today.getDate() && lastTime.getMonth() === today.getMonth() && lastTime.getFullYear() === today.getFullYear(); if (isToday === false) { cacheModule.clear(); GM_setValue(CLEAR_TIME_KEY, today.getTime()); } })(); return { getTopicNum, getUserInfo, getUserInfoAndReputation, getForumInfo, getForumPosted, }; })(); // UI const ui = (() => { const modules = {}; // 主界面 const view = (() => { const tabContainer = (() => { const element = document.createElement("DIV"); element.className = "w100"; element.innerHTML = `
`; return element; })(); const tabPanelContainer = (() => { const element = document.createElement("DIV"); element.style = "width: 80vw;"; return element; })(); const content = (() => { const element = document.createElement("DIV"); element.appendChild(tabContainer); element.appendChild(tabPanelContainer); return element; })(); const addModule = (() => { const tc = tabContainer.querySelector("TR"); const cc = tabPanelContainer; return (name, module) => { const tabBox = document.createElement("TD"); tabBox.innerHTML = `${name}`; const tab = tabBox.childNodes[0]; const toggle = () => { Object.values(modules).forEach((item) => { if (item.tab === tab) { item.tab.className = "nobr"; item.content.style = "display: block"; item.refresh(); } else { item.tab.className = "nobr silver"; item.content.style = "display: none"; } }); }; tc.append(tabBox); cc.append(module.content); tab.onclick = toggle; modules[name] = { ...module, tab, toggle, }; return modules[name]; }; })(); return { content, addModule, }; })(); // 右上角菜单 const menu = (() => { const container = document.createElement("DIV"); container.className = `td`; container.innerHTML = `屏蔽`; const content = container.querySelector("A"); const create = (onclick) => { const anchor = document.querySelector("#mainmenu .td:last-child"); if (anchor) { anchor.before(container); content.onclick = onclick; return true; } return false; }; const update = (list) => { const count = list.length; if (count) { content.innerHTML = `屏蔽 ${count}`; } else { content.innerHTML = `屏蔽`; } }; return { create, update, }; })(); return { ...view, ...menu, }; })(); // 判断是否为当前用户 UID const isCurrentUID = (uid) => { return unsafeWindow.__CURRENT_UID === parseInt(uid, 10); }; // 获取过滤方式 const getFilterMode = async (item) => { // 声明结果 const result = { mode: -1, reason: ``, }; // 获取 UID const uid = parseInt(item.uid, 10); // 获取链接参数 const params = new URLSearchParams(location.search); // 跳过屏蔽(插件自定义) if (params.has("nofilter")) { return; } // 收藏 if (params.has("favor")) { return; } // 只看某人 if (params.has("authorid")) { return; } // 跳过自己 if (isCurrentUID(uid)) { return ""; } // 用户过滤 (() => { // 获取屏蔽列表里匹配的用户 const user = userModule.get(uid); // 没有则跳过 if (user === null) { return; } const { filterMode } = user; const mode = filterModule.getModeByName(filterMode); // 低于当前的过滤模式则跳过 if (mode <= result.mode) { return; } // 更新过滤模式和原因 result.mode = mode; result.reason = `用户模式: ${filterMode}`; })(); // 标记过滤 (() => { // 获取屏蔽列表里匹配的用户 const user = userModule.get(uid); // 获取用户对应的标记,并跳过低于当前的过滤模式 const tags = user ? user.tags .map((id) => tagModule.get({ id })) .filter((i) => i !== null) .filter( (i) => filterModule.getModeByName(i.filterMode) > result.mode ) : []; // 没有则跳过 if (tags.length === 0) { return; } // 取最高的过滤模式 const { filterMode, name } = tags.sort( (a, b) => filterModule.getModeByName(b.filterMode) - filterModule.getModeByName(a.filterMode) )[0]; const mode = filterModule.getModeByName(filterMode); // 更新过滤模式和原因 result.mode = mode; result.reason = `标记: ${name}`; })(); // 关键字过滤 await (async () => { const { getContent } = item; // 获取设置里的关键字列表,并跳过低于当前的过滤模式 const keywords = Object.values(keywordModule.list()).filter( (i) => filterModule.getModeByName(i.filterMode) > result.mode ); // 没有则跳过 if (keywords.length === 0) { return; } // 根据过滤等级依次判断 const list = keywords.sort( (a, b) => filterModule.getModeByName(b.filterMode) - filterModule.getModeByName(a.filterMode) ); for (let i = 0; i < list.length; i += 1) { const { keyword, filterMode } = list[i]; // 过滤等级,0 为只过滤标题,1 为过滤标题和内容 const filterLevel = list[i].filterLevel || 0; // 过滤标题 if (filterLevel >= 0) { const { subject } = item; const match = subject.match(keyword); if (match) { const mode = filterModule.getModeByName(filterMode); // 更新过滤模式和原因 result.mode = mode; result.reason = `关键字: ${match[0]}`; return; } } // 过滤内容 if (filterLevel >= 1) { // 如果没有内容,则请求 const content = await (async () => { if (item.content === undefined) { await getContent().catch(() => {}); } return item.content || null; })(); if (content) { const match = content.match(keyword); if (match) { const mode = filterModule.getModeByName(filterMode); // 更新过滤模式和原因 result.mode = mode; result.reason = `关键字: ${match[0]}`; return; } } } } })(); // 杂项过滤 // 放在属地前是因为符合条件的过多,没必要再请求它们的属地 await (async () => { const { getUserInfo, getReputation } = item; // 如果当前模式是显示,则跳过 if (filterModule.getNameByMode(result.mode) === "显示") { return; } // 获取隐藏模式下标 const mode = filterModule.getModeByName("隐藏"); // 匿名 if (uid <= 0) { const filterAnony = dataModule.getFilterAnony(); if (filterAnony) { // 更新过滤模式和原因 result.mode = mode; result.reason = "匿名"; } return; } // 注册时间过滤 await (async () => { const filterRegdateLimit = dataModule.getFilterRegdateLimit(); // 如果没有用户信息,则请求 const userInfo = await (async () => { if (item.userInfo === undefined) { await getUserInfo().catch(() => {}); } return item.userInfo || {}; })(); const { regdate } = userInfo; if (regdate === undefined) { return; } if ( filterRegdateLimit > 0 && regdate * 1000 > new Date() - filterRegdateLimit ) { // 更新过滤模式和原因 result.mode = mode; result.reason = `注册时间: ${new Date( regdate * 1000 ).toLocaleDateString()}`; return; } })(); // 发帖数量过滤 await (async () => { const filterPostnumLimit = dataModule.getFilterPostnumLimit(); // 如果没有用户信息,则请求 const userInfo = await (async () => { if (item.userInfo === undefined) { await getUserInfo().catch(() => {}); } return item.userInfo || {}; })(); const { postnum } = userInfo; if (postnum === undefined) { return; } if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) { // 更新过滤模式和原因 result.mode = mode; result.reason = `发帖数量: ${postnum}`; return; } })(); // 发帖比例过滤 await (async () => { const filterTopicRateLimit = dataModule.getFilterTopicRateLimit(); // 如果没有用户信息,则请求 const userInfo = await (async () => { if (item.userInfo === undefined) { await getUserInfo().catch(() => {}); } return item.userInfo || {}; })(); const { postnum } = userInfo; if (postnum === undefined) { return; } if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) { // 获取主题数量 const topicNum = await fetchModule.getTopicNum(uid); // 计算发帖比例 const topicRate = (topicNum / postnum) * 100; if (topicRate > filterTopicRateLimit) { // 更新过滤模式和原因 result.mode = mode; result.reason = `发帖比例: ${topicRate.toFixed( 0 )}% (${topicNum}/${postnum})`; return; } } })(); // 版面声望过滤 await (async () => { const filterReputationLimit = dataModule.getFilterReputationLimit(); if (Number.isNaN(filterReputationLimit)) { return; } // 如果没有版面声望,则请求 const reputation = await (async () => { if (item.reputation === undefined) { await getReputation().catch(() => {}); } return item.reputation || NaN; })(); if (reputation < filterReputationLimit) { // 更新过滤模式和原因 result.mode = mode; result.reason = `版面声望: ${reputation}`; return; } })(); })(); // 属地过滤 await (async () => { // 匿名用户则跳过 if (uid <= 0) { return; } // 获取设置里的属地列表,并跳过低于当前的过滤模式 const locations = Object.values(locationModule.list()).filter( (i) => filterModule.getModeByName(i.filterMode) > result.mode ); // 没有则跳过 if (locations.length === 0) { return; } // 请求属地 const { ipLoc } = await fetchModule.getUserInfo(uid); // 请求失败则跳过 if (ipLoc === undefined) { return; } // 根据过滤等级依次判断 const list = locations.sort( (a, b) => filterModule.getModeByName(b.filterMode) - filterModule.getModeByName(a.filterMode) ); for (let i = 0; i < list.length; i += 1) { const { keyword, filterMode } = list[i]; const match = ipLoc.match(keyword); if (match) { const mode = filterModule.getModeByName(filterMode); // 更新过滤模式和原因 result.mode = mode; result.reason = `属地: ${ipLoc}`; return; } } })(); // 猎巫过滤 (() => { // 获取猎巫结果 const witchHunt = item.witchHunt; // 没有则跳过 if (witchHunt === undefined) { return; } // 获取设置 const list = Object.values(witchHuntModule.list()).filter(({ fid }) => witchHunt.includes(fid) ); // 筛选出匹配的猎巫 const filtered = Object.values(list) .filter(({ filterLevel }) => filterLevel > 0) .filter( ({ filterMode }) => filterModule.getModeByName(filterMode) > result.mode ); // 没有则跳过 if (filtered.length === 0) { return; } // 取最高的过滤模式 const { filterMode, label } = filtered.sort( (a, b) => filterModule.getModeByName(b.filterMode) - filterModule.getModeByName(a.filterMode) )[0]; const mode = filterModule.getModeByName(filterMode); // 更新过滤模式和原因 result.mode = mode; result.reason = `猎巫: ${label}`; })(); // 写入过滤模式和过滤原因 item.filterMode = filterModule.getNameByMode(result.mode); item.reason = result.reason; // 写入列表 listModule.add(item); // 继承模式下返回默认过滤模式 if (item.filterMode === "继承") { return dataModule.getDefaultFilterMode(); } // 返回结果 return item.filterMode; }; // 获取主题过滤方式 const getFilterModeByTopic = async (topic) => { const { tid } = topic; // 绑定额外的数据请求方式 if (topic.getContent === undefined) { // 获取帖子内容,按需调用 const getTopic = () => new Promise((resolve, reject) => { // 避免重复请求 if (topic.content || topic.userInfo || topic.reputation) { resolve(topic); return; } // 请求并写入数据 fetchModule .getUserInfoAndReputation(tid, undefined) .then(({ subject, content, userInfo, reputation }) => { // 写入用户名 if (userInfo) { topic.username = userInfo.username; } // 写入用户信息和声望 topic.userInfo = userInfo; topic.reputation = reputation; // 写入帖子标题和内容 topic.subject = subject; topic.content = content; // 返回结果 resolve(topic); }) .catch(reject); }); // 绑定请求方式 topic.getContent = getTopic; topic.getUserInfo = getTopic; topic.getReputation = getTopic; } // 获取过滤模式 const filterMode = await getFilterMode(topic); // 返回结果 return filterMode; }; // 获取回复过滤方式 const getFilterModeByReply = async (reply) => { const { tid, pid, uid } = reply; // 回复页面可以直接获取到用户信息和声望 if (uid > 0) { // 取得用户信息 const userInfo = commonui.userInfo.users[uid]; // 取得用户声望 const reputation = (() => { const reputations = commonui.userInfo.reputations; if (reputations) { for (let fid in reputations) { return reputations[fid][uid] || 0; } } return NaN; })(); // 写入用户名 if (userInfo) { reply.username = userInfo.username; } // 写入用户信息和声望 reply.userInfo = userInfo; reply.reputation = reputation; } // 绑定额外的数据请求方式 if (reply.getContent === undefined) { // 获取帖子内容,按需调用 const getReply = () => new Promise((resolve, reject) => { // 避免重复请求 if (reply.userInfo || reply.reputation) { resolve(reply); return; } // 请求并写入数据 fetchModule .getUserInfoAndReputation(tid, pid) .then(({ subject, content, userInfo, reputation }) => { // 写入用户名 if (userInfo) { reply.username = userInfo.username; } // 写入用户信息和声望 reply.userInfo = userInfo; reply.reputation = reputation; // 写入帖子标题和内容 reply.subject = subject; reply.content = content; // 返回结果 resolve(reply); }) .catch(reject); }); // 绑定请求方式 reply.getContent = getReply; reply.getUserInfo = getReply; reply.getReputation = getReply; } // 获取过滤模式 const filterMode = await getFilterMode(reply); // 返回结果 return filterMode; }; // 处理引用 const handleQuote = async (content) => { const quotes = content.querySelectorAll(".quote"); await Promise.all( [...quotes].map(async (quote) => { const uid = (() => { const ele = quote.querySelector("a[href^='/nuke.php']"); if (ele) { const res = ele.getAttribute("href").match(/uid=(\S+)/); if (res) { return res[1]; } } return 0; })(); const { tid, pid } = (() => { const ele = quote.querySelector("[title='快速浏览这个帖子']"); if (ele) { const res = ele .getAttribute("onclick") .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/); if (res) { return { tid: parseInt(res[2], 10), pid: parseInt(res[3], 10) || 0, }; } } return {}; })(); // 获取过滤方式 const filterMode = await getFilterModeByReply({ uid, tid, pid, subject: "", content: quote.innerText, }); (() => { if (filterMode === "标记") { filterModule.collapse(uid, quote, quote.innerHTML); return; } if (filterMode === "遮罩") { const source = document.createElement("DIV"); source.innerHTML = quote.innerHTML; source.style.display = "none"; const caption = document.createElement("CAPTION"); caption.className = "filter-mask filter-mask-block"; caption.innerHTML = `Troll must die.`; caption.onclick = () => { quote.removeChild(caption); source.style.display = ""; }; quote.innerHTML = ""; quote.appendChild(source); quote.appendChild(caption); return; } if (filterMode === "隐藏") { quote.innerHTML = ""; return; } })(); }) ); }; // 过滤主题 const filterTopic = async (item) => { // 绑定事件 if (item.nFilter === undefined) { // 主题 ID const tid = item[8]; // 主题标题 const title = item[1]; const subject = title.innerText; // 主题作者 const author = item[2]; const uid = parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0; const username = author.innerText; // 主题容器 const container = title.closest("tr"); // 过滤函数 const execute = async () => { // 获取过滤方式 const filterMode = await getFilterModeByTopic(item.nFilter); // 样式处理 (() => { // 还原样式 // TODO 应该整体采用 className 来实现 (() => { // 标记模式 container.style.removeProperty("textDecoration"); // 遮罩模式 title.classList.remove("filter-mask"); author.classList.remove("filter-mask"); })(); // 样式处理 (() => { // 标记模式下,主题标记会有删除线标识 if (filterMode === "标记") { title.style.textDecoration = "line-through"; return; } // 遮罩模式下,主题和作者会有遮罩样式 if (filterMode === "遮罩") { title.classList.add("filter-mask"); author.classList.add("filter-mask"); return; } // 隐藏模式下,容器会被隐藏 if (filterMode === "隐藏") { container.style.display = "none"; return; } })(); // 非隐藏模式下,恢复显示 if (filterMode !== "隐藏") { container.style.removeProperty("display"); } })(); // 猎巫会影响效率,待猎巫结果出来后再次过滤 witchHuntModule.run(item.nFilter); }; // 绑定事件 item.nFilter = { tid, uid, username, container, title, author, subject, execute, }; } // 等待过滤完成 await item.nFilter.execute(); }; // 过滤回复 const filterReply = async (item) => { // 绑定事件 if (item.nFilter === undefined) { // 回复 ID const pid = item.pid; // 判断是否是楼层 const isFloor = typeof item.i === "number"; // 回复容器 const container = isFloor ? item.uInfoC.closest("tr") : item.uInfoC.closest(".comment_c"); // 回复标题 const title = item.subjectC; const subject = title.innerText; // 回复内容 const content = item.contentC; const contentBak = content.innerHTML; // 回复作者 const author = container.querySelector(".posterInfoLine") || item.uInfoC; const uid = parseInt(item.pAid, 10) || 0; const username = author.querySelector(".author").innerText; const avatar = author.querySelector(".avatar"); // 找到用户 ID,将其视为操作按钮 const action = container.querySelector('[name="uid"]'); // 创建一个元素,用于展示标记列表 // 贴条和高赞不显示 const tags = (() => { if (isFloor === false) { return null; } const element = document.createElement("div"); element.className = "filter-tags"; author.appendChild(element); return element; })(); // 过滤函数 const execute = async () => { // 获取过滤方式 const filterMode = await getFilterModeByReply(item.nFilter); // 样式处理 await (async () => { // 还原样式 // TODO 应该整体采用 className 来实现 (() => { // 标记模式 if (avatar) { avatar.style.removeProperty("display"); } content.innerHTML = contentBak; // 遮罩模式 const caption = container.parentNode.querySelector("CAPTION"); if (caption) { container.parentNode.removeChild(caption); container.style.removeProperty("display"); } })(); // 样式处理 (() => { // 标记模式下,隐藏头像,采用泥潭的折叠样式 if (filterMode === "标记") { if (avatar) { avatar.style.display = "none"; } filterModule.collapse(uid, content, contentBak); return; } // 遮罩模式下,楼层会有遮罩样式 if (filterMode === "遮罩") { const caption = document.createElement("CAPTION"); if (isFloor) { caption.className = "filter-mask filter-mask-block"; } else { caption.className = "filter-mask filter-mask-block left"; caption.style.width = "47%"; } caption.innerHTML = `Troll must die.`; caption.onclick = () => { const caption = container.parentNode.querySelector("CAPTION"); if (caption) { container.parentNode.removeChild(caption); container.style.removeProperty("display"); } }; container.parentNode.insertBefore(caption, container); container.style.display = "none"; return; } // 隐藏模式下,容器会被隐藏 if (filterMode === "隐藏") { container.style.display = "none"; return; } })(); // 处理引用 await handleQuote(content); // 非隐藏模式下,恢复显示 // 如果是隐藏模式,没必要再加载按钮和标记 if (filterMode !== "隐藏") { // 获取当前用户 const user = userModule.get(uid); // 修改操作按钮颜色 if (action) { if (user) { action.style.background = "#CB4042"; } else { action.style.background = "#AAA"; } } // 加载标记和猎巫 if (tags) { const witchHunt = item.nFilter.witchHunt || []; const list = [ ...(user ? user.tags .map((id) => tagModule.get({ id })) .map((tag) => tagModule.format(tag.id)) || [] : []), ...Object.values(witchHuntModule.list()) .filter(({ fid }) => witchHunt.includes(fid)) .map(({ label, color }) => tagModule.format(null, label, color) ), ]; tags.style.display = list.length ? "" : "none"; tags.innerHTML = list.join(""); } // 恢复显示 // 楼层的遮罩模式下仍需隐藏 if (filterMode !== "遮罩") { container.style.removeProperty("display"); } } })(); // 猎巫会影响效率,待猎巫结果出来后再次过滤 witchHuntModule.run(item.nFilter); }; // 绑定操作按钮事件 (() => { if (action) { // 隐藏匿名操作按钮 if (uid <= 0) { action.style.display = "none"; return; } action.innerHTML = `屏蔽`; action.onclick = (e) => { const user = userModule.get(uid); if (e.ctrlKey === false) { userModule.view.details(uid, username, execute); return; } if (user) { userModule.remove(uid); } else { userModule.add(uid, username, [], filterModule.defaultMode); } execute(); }; } })(); // 绑定事件 item.nFilter = { pid, uid, username, container, title, author, subject, content: content.innerText, execute, }; } // 等待过滤完成 await item.nFilter.execute(); }; // 加载 UI const loadUI = () => { // 右上角菜单 const result = (() => { let window; return ui.create(() => { if (window === undefined) { window = commonui.createCommmonWindow(); } window._.addContent(null); window._.addTitle(`屏蔽`); window._.addContent(ui.content); window._.show(); }); })(); // 加载失败 if (result === false) { return; } // 模块 ui.addModule("列表", listModule.view).toggle(); ui.addModule("用户", userModule.view); ui.addModule("标记", tagModule.view); ui.addModule("关键字", keywordModule.view); ui.addModule("属地", locationModule.view); ui.addModule("猎巫", witchHuntModule.view); ui.addModule("通用设置", commonModule.view); // 绑定列表更新回调 listModule.bindCallback(ui.update); }; // 处理 mainMenu 模块 const handleMenu = () => { let init = menuModule.init; // 劫持 init 函数,这个函数完成后才能添加 UI Object.defineProperty(menuModule, "init", { get: () => { return (...arguments) => { // 等待执行完毕 init.apply(menuModule, arguments); // 加载 UI loadUI(); }; }, set: (value) => { init = value; }, }); // 如果已经有模块,则直接加载 UI if (init) { loadUI(); } }; // 处理 topicArg 模块 const handleTopicModule = async () => { let add = topicModule.add; // 劫持 add 函数,这是泥潭的主题添加事件 Object.defineProperty(topicModule, "add", { get: () => { return async (...arguments) => { // 主题 ID const tid = arguments[8]; // 先直接隐藏,等过滤完毕后再放出来 (() => { // 主题标题 const title = document.getElementById(arguments[1]); // 主题容器 const container = title.closest("tr"); // 隐藏元素 container.style.display = "none"; })(); // 加入列表 add.apply(topicModule, arguments); // 找到对应数据 const topic = topicModule.data.find((item) => item[8] === tid); // 开始过滤 await filterTopic(topic); }; }, set: (value) => { add = value; }, }); // 如果已经有数据,则直接过滤 if (topicModule.data) { await Promise.all(Object.values(topicModule.data).map(filterTopic)); } }; // 处理 postArg 模块 const handleReplyModule = async () => { let proc = replyModule.proc; // 劫持 proc 函数,这是泥潭的回复添加事件 Object.defineProperty(replyModule, "proc", { get: () => { return async (...arguments) => { // 楼层号 const index = arguments[0]; // 先直接隐藏,等过滤完毕后再放出来 (() => { // 判断是否是楼层 const isFloor = typeof index === "number"; // 评论额外标签 const prefix = isFloor ? "" : "comment"; // 用户容器 const uInfoC = document.querySelector( `#${prefix}posterinfo${index}` ); // 回复容器 const container = isFloor ? uInfoC.closest("tr") : uInfoC.closest(".comment_c"); // 隐藏元素 container.style.display = "none"; })(); // 加入列表 proc.apply(replyModule, arguments); // 找到对应数据 const reply = replyModule.data[index]; // 开始过滤 await filterReply(reply); }; }, set: (value) => { proc = value; }, }); // 如果已经有数据,则直接过滤 if (replyModule.data) { await Promise.all(Object.values(replyModule.data).map(filterReply)); } }; // 处理 commonui 模块 const handleCommonui = () => { // 监听 mainMenu 模块,UI 需要等待这个模块加载完成 (() => { if (commonui.mainMenu) { menuModule = commonui.mainMenu; handleMenu(); return; } Object.defineProperty(commonui, "mainMenu", { get: () => menuModule, set: (value) => { menuModule = value; handleMenu(); }, }); })(); // 监听 topicArg 模块,这是泥潭的主题入口 (() => { if (commonui.topicArg) { topicModule = commonui.topicArg; handleTopicModule(); return; } Object.defineProperty(commonui, "topicArg", { get: () => topicModule, set: (value) => { topicModule = value; handleTopicModule(); }, }); })(); // 监听 postArg 模块,这是泥潭的回复入口 (() => { if (commonui.postArg) { replyModule = commonui.postArg; handleReplyModule(); return; } Object.defineProperty(commonui, "postArg", { get: () => replyModule, set: (value) => { replyModule = value; handleReplyModule(); }, }); })(); }; // 前置过滤 const handlePreFilter = () => { // 监听 commonui 模块,这是泥潭的主入口 (() => { if (unsafeWindow.commonui) { commonui = unsafeWindow.commonui; handleCommonui(); return; } Object.defineProperty(unsafeWindow, "commonui", { get: () => commonui, set: (value) => { commonui = value; handleCommonui(); }, }); })(); }; // 普通过滤 const handleFilter = () => { const runFilter = async () => { if (topicModule) { await Promise.all( Object.values(topicModule.data).map((item) => { if (item.executed) { return; } item.executed = true; filterTopic(item); }) ); } if (replyModule) { await Promise.all( Object.values(replyModule.data).map((item) => { if (item.executed) { return; } item.executed = true; filterReply(item); }) ); } }; const hookFunction = (object, functionName, callback) => { ((originalFunction) => { object[functionName] = function () { const returnValue = originalFunction.apply(this, arguments); callback.apply(this, [returnValue, originalFunction, arguments]); return returnValue; }; })(object[functionName]); }; const hook = () => { (() => { if (topicModule) { return; } if (commonui.topicArg) { topicModule = commonui.topicArg; hookFunction(topicModule, "add", runFilter); } })(); (() => { if (replyModule) { return; } if (commonui.postArg) { replyModule = commonui.postArg; hookFunction(replyModule, "add", runFilter); } })(); }; hook(); runFilter(); hookFunction(commonui, "eval", hook); }; // 主函数 (() => { // 前置过滤 if (preFilter) { handlePreFilter(); return; } // 等待页面加载完毕后过滤 unsafeWindow.addEventListener("load", () => { if (unsafeWindow.commonui === undefined) { return; } commonui = unsafeWindow.commonui; menuModule = commonui.mainMenu; loadUI(); handleFilter(); }); })(); })();