您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
当前为
- // ==UserScript==
- // @name NGA Filter
- // @namespace https://greasyfork.org/users/263018
- // @version 2.2.4
- // @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
- // ==/UserScript==
- (() => {
- // 声明泥潭主模块、主题模块、回复模块
- let commonui, 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";
- // TIPS
- const TIPS = {
- filterMode:
- "过滤顺序:用户 > 标记 > 关键字 > 属地<br/>过滤级别:显示 > 隐藏 > 遮罩 > 标记 > 继承",
- addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
- keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
- hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
- };
- // 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;
- }
- `);
- /**
- * 工具类
- */
- class Tools {
- /**
- * 返回当前值的类型
- * @param {*} value 值
- * @returns {String} 值的类型
- */
- static getType = (value) => {
- return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
- };
- /**
- * 返回当前值是否为指定的类型
- * @param {*} value 值
- * @param {Array<String>} types 类型名称集合
- * @returns {Boolean} 值是否为指定的类型
- */
- static isType = (value, ...types) => {
- return types.includes(this.getType(value));
- };
- /**
- * 拦截属性
- * @param {Object} target 目标对象
- * @param {String} property 属性或函数名称
- * @param {Function} beforeGet 获取属性前事件
- * @param {Function} beforeSet 设置属性前事件
- * @param {Function} afterGet 获取属性后事件
- * @param {Function} afterSet 设置属性前事件
- */
- static interceptProperty = (
- target,
- property,
- { beforeGet, beforeSet, afterGet, afterSet }
- ) => {
- // 缓存数据
- let source = target[property];
- // 如果已经有结果,则直接处理写入后操作
- if (Object.hasOwn(target, property)) {
- if (afterSet) {
- afterSet.apply(target, [source]);
- }
- }
- // 拦截
- Object.defineProperty(target, property, {
- get: () => {
- // 如果是函数
- if (this.isType(source, "function")) {
- return (...args) => {
- try {
- // 执行前操作
- // 可以在这一步修改参数
- // 可以通过在这一步抛出来阻止执行
- if (beforeGet) {
- args = beforeGet.apply(target, args);
- }
- // 执行函数
- const returnValue = source.apply(target, args);
- // 返回的可能是一个 Promise
- const result =
- returnValue instanceof Promise
- ? returnValue
- : Promise.resolve(returnValue);
- // 执行后操作
- if (afterGet) {
- result.then((value) => {
- afterGet.apply(target, [value, args, source]);
- });
- }
- } catch {}
- };
- }
- try {
- // 返回前操作
- // 可以在这一步修改返回结果
- // 可以通过在这一步抛出来返回 undefined
- const result = beforeGet
- ? beforeGet.apply(target, [source])
- : source;
- // 返回后操作
- // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 beforeGet 来操作处理后的数据
- if (afterGet) {
- afterGet.apply(target, [result, source]);
- }
- // 返回结果
- return result;
- } catch {
- return undefined;
- }
- },
- set: (value) => {
- try {
- // 写入前操作
- // 可以在这一步修改写入结果
- // 可以通过在这一步抛出来写入 undefined
- const result = beforeSet
- ? beforeSet.apply(target, [source, value])
- : value;
- // 写入结果
- source = result;
- // 写入后操作
- if (afterSet) {
- afterSet.apply(target, [result, value]);
- }
- } catch {
- source = undefined;
- }
- },
- });
- };
- /**
- * 合并数据
- * @param {*} target 目标对象
- * @param {Array} sources 来源对象集合
- * @returns 合并后的对象
- */
- static merge = (target, ...sources) => {
- for (const source of sources) {
- const targetType = this.getType(target);
- const sourceType = this.getType(source);
- // 如果来源对象的类型与目标对象不一致,替换为来源对象
- if (sourceType !== targetType) {
- target = source;
- continue;
- }
- // 如果来源对象是数组,直接合并
- if (targetType === "array") {
- target = [...target, ...source];
- continue;
- }
- // 如果来源对象是对象,合并对象
- if (sourceType === "object") {
- for (const key in source) {
- if (Object.hasOwn(target, key)) {
- target[key] = this.merge(target[key], source[key]);
- } else {
- target[key] = source[key];
- }
- }
- continue;
- }
- // 其他情况,更新值
- target = source;
- }
- return target;
- };
- /**
- * 数组排序
- * @param {Array} collection 数据集合
- * @param {Array<String | Function>} iterators 迭代器,要排序的属性名或排序函数
- */
- static sortBy = (collection, ...iterators) =>
- collection.slice().sort((a, b) => {
- for (let i = 0; i < iterators.length; i += 1) {
- const iteratee = iterators[i];
- const valueA = this.isType(iteratee, "function")
- ? iteratee(a)
- : a[iteratee];
- const valueB = this.isType(iteratee, "function")
- ? iteratee(b)
- : b[iteratee];
- if (valueA < valueB) {
- return -1;
- }
- if (valueA > valueB) {
- return 1;
- }
- }
- return 0;
- });
- /**
- * 读取论坛数据
- * @param {Response} response 请求响应
- * @param {Boolean} toJSON 是否转为 JSON 格式
- */
- static readForumData = async (response, toJSON = true) => {
- return new Promise(async (resolve) => {
- const blob = await response.blob();
- const reader = new FileReader();
- reader.onload = () => {
- const text = reader.result.replace(
- "window.script_muti_get_var_store=",
- ""
- );
- if (toJSON) {
- try {
- resolve(JSON.parse(text));
- } catch {
- resolve({});
- }
- return;
- }
- resolve(text);
- };
- reader.readAsText(blob, "GBK");
- });
- };
- /**
- * 获取成对括号的内容
- * @param {String} content 内容
- * @param {String} keyword 起始位置关键字
- * @param {String} start 左括号
- * @param {String} end 右括号
- * @returns {String} 包含括号的内容
- */
- static searchPair = (content, keyword, start = "{", end = "}") => {
- // 获取成对括号的位置
- const getLastIndex = (content, position, start = "{", end = "}") => {
- if (position >= 0) {
- let nextIndex = position + 1;
- while (nextIndex < content.length) {
- if (content[nextIndex] === end) {
- return nextIndex;
- }
- if (content[nextIndex] === start) {
- nextIndex = getLastIndex(content, nextIndex, start, end);
- if (nextIndex < 0) {
- break;
- }
- }
- nextIndex = nextIndex + 1;
- }
- }
- return -1;
- };
- // 起始位置
- const str = keyword + start;
- // 起始下标
- const index = content.indexOf(str) + str.length;
- // 结尾下标
- const lastIndex = getLastIndex(content, index, start, end);
- if (lastIndex >= 0) {
- return start + content.substring(index, lastIndex) + end;
- }
- return null;
- };
- /**
- * 计算字符串的颜色
- *
- * 采用的是泥潭的颜色方案,参见 commonui.htmlName
- * @param {String} value 字符串
- * @returns {String} RGB代码
- */
- static generateColor(value) {
- const hash = (() => {
- let h = 5381;
- for (var i = 0; i < value.length; i++) {
- h = ((h << 5) + h + value.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);
- });
- }
- }
- /**
- * IndexedDB
- *
- * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑
- */
- class DBStorage {
- /**
- * 数据库名称
- */
- name = "NGA_FILTER_CACHE";
- /**
- * 模块列表
- */
- modules = {};
- /**
- * 当前实例
- */
- instance = null;
- /**
- * 初始化
- * @param {*} modules 模块列表
- */
- constructor(modules) {
- this.modules = modules;
- }
- /**
- * 是否支持
- */
- isSupport() {
- return unsafeWindow.indexedDB !== undefined;
- }
- /**
- * 打开数据库并创建表
- * @returns {Promise<IDBDatabase>} 实例
- */
- async open() {
- // 创建实例
- if (this.instance === null) {
- // 声明一个数组,用于等待全部表处理完毕
- const queue = [];
- // 创建实例
- await new Promise((resolve, reject) => {
- // 版本
- const version = Object.values(this.modules)
- .map(({ version }) => version)
- .reduce((a, b) => Math.max(a, b), 0);
- // 创建请求
- const request = unsafeWindow.indexedDB.open(this.name, version);
- // 创建或者升级表
- request.onupgradeneeded = (event) => {
- this.instance = event.target.result;
- const transaction = event.target.transaction;
- const oldVersion = event.oldVersion;
- Object.entries(this.modules).forEach(([key, values]) => {
- if (values.version > oldVersion) {
- queue.push(this.createOrUpdateStore(key, values, transaction));
- }
- });
- };
- // 成功后处理
- request.onsuccess = (event) => {
- this.instance = event.target.result;
- resolve();
- };
- // 失败后处理
- request.onerror = () => {
- reject();
- };
- });
- // 等待全部表处理完毕
- await Promise.all(queue);
- }
- // 返回实例
- return this.instance;
- }
- /**
- * 获取表
- * @param {String} name 表名
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @param {String} mode 事务模式,默认为只读
- * @returns {Promise<IDBObjectStore>} 表
- */
- async getStore(name, transaction = null, mode = "readonly") {
- const db = await this.open();
- if (transaction === null) {
- transaction = db.transaction(name, mode);
- }
- return transaction.objectStore(name);
- }
- /**
- * 创建或升级表
- * @param {String} name 表名
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise}
- */
- async createOrUpdateStore(name, { keyPath, indexes }, transaction) {
- const db = transaction.db;
- const data = [];
- // 检查是否存在表,如果存在,缓存数据并删除旧表
- if (db.objectStoreNames.contains(name)) {
- // 获取并缓存全部数据
- const result = await this.bulkGet(name, [], transaction);
- if (result) {
- data.push(...result);
- }
- // 删除旧表
- db.deleteObjectStore(name);
- }
- // 创建表
- const store = db.createObjectStore(name, {
- keyPath,
- });
- // 创建索引
- if (indexes) {
- indexes.forEach((index) => {
- store.createIndex(index, index);
- });
- }
- // 迁移数据
- if (data.length > 0) {
- await this.bulkAdd(name, data, transaction);
- }
- }
- /**
- * 插入指定表的数据
- * @param {String} name 表名
- * @param {*} data 数据
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise}
- */
- async add(name, data, transaction = null) {
- // 获取表
- const store = await this.getStore(name, transaction, "readwrite");
- // 插入数据
- const result = await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.add(data);
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- // 返回结果
- return result;
- }
- /**
- * 删除指定表的数据
- * @param {String} name 表名
- * @param {String} key 主键
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise}
- */
- async delete(name, key, transaction = null) {
- // 获取表
- const store = await this.getStore(name, transaction, "readwrite");
- // 删除数据
- const result = await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.delete(key);
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- // 返回结果
- return result;
- }
- /**
- * 插入或修改指定表的数据
- * @param {String} name 表名
- * @param {*} data 数据
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise}
- */
- async put(name, data, transaction = null) {
- // 获取表
- const store = await this.getStore(name, transaction, "readwrite");
- // 插入或修改数据
- const result = await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.put(data);
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- // 返回结果
- return result;
- }
- /**
- * 获取指定表的数据
- * @param {String} name 表名
- * @param {String} key 主键
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise} 数据
- */
- async get(name, key, transaction = null) {
- // 获取表
- const store = await this.getStore(name, transaction);
- // 查询数据
- const result = await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.get(key);
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- // 返回结果
- return result;
- }
- /**
- * 批量插入指定表的数据
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise<number>} 成功数量
- */
- async bulkAdd(name, data, transaction = null) {
- // 等待操作结果
- const result = await Promise.all(
- data.map((item) =>
- this.add(name, item, transaction)
- .then(() => true)
- .catch(() => false)
- )
- );
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量删除指定表的数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则删除全部
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise<number>} 成功数量,删除全部时返回 -1
- */
- async bulkDelete(name, keys = [], transaction = null) {
- // 如果 keys 为空,删除全部数据
- if (keys.length === 0) {
- // 获取表
- const store = await this.getStore(name, transaction, "readwrite");
- // 清空数据
- await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.clear();
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- return -1;
- }
- // 等待操作结果
- const result = await Promise.all(
- data.map((item) =>
- this.delete(name, item, transaction)
- .then(() => true)
- .catch(() => false)
- )
- );
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量插入或修改指定表的数据
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise<number>} 成功数量
- */
- async bulkPut(name, data, transaction = null) {
- // 等待操作结果
- const result = await Promise.all(
- data.map((item) =>
- this.put(name, item, transaction)
- .then(() => true)
- .catch(() => false)
- )
- );
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量获取指定表的数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则获取全部
- * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
- * @returns {Promise<Array>} 数据集合
- */
- async bulkGet(name, keys = [], transaction = null) {
- // 如果 keys 为空,查询全部数据
- if (keys.length === 0) {
- // 获取表
- const store = await this.getStore(name, transaction);
- // 查询数据
- const result = await new Promise((resolve, reject) => {
- // 创建请求
- const request = store.getAll();
- // 成功后处理
- request.onsuccess = (event) => {
- resolve(event.target.result || []);
- };
- // 失败后处理
- request.onerror = (event) => {
- reject(event);
- };
- });
- // 返回结果
- return result;
- }
- // 返回符合的结果
- const result = [];
- await Promise.all(
- keys.map((key) =>
- this.get(name, key, transaction)
- .then((item) => {
- result.push(item);
- })
- .catch(() => {})
- )
- );
- return result;
- }
- }
- /**
- * 油猴存储
- *
- * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
- */
- class GMStorage extends DBStorage {
- /**
- * 初始化
- * @param {*} modules 模块列表
- */
- constructor(modules) {
- super(modules);
- }
- /**
- * 插入指定表的数据
- * @param {String} name 表名
- * @param {*} data 数据
- * @returns {Promise}
- */
- async add(name, data) {
- // 如果不在模块列表里,写入全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, data);
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.add(name, data);
- }
- // 获取对应的主键
- const keyPath = this.modules[name].keyPath;
- const key = data[keyPath];
- // 如果数据中不包含主键,抛出异常
- if (key === undefined) {
- throw new Error();
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 如果对应主键已存在,抛出异常
- if (Object.hasOwn(values, key)) {
- throw new Error();
- }
- // 插入数据
- values[key] = data;
- // 保存数据
- GM_setValue(name, values);
- }
- /**
- * 删除指定表的数据
- * @param {String} name 表名
- * @param {String} key 主键
- * @returns {Promise}
- */
- async delete(name, key) {
- // 如果不在模块列表里,忽略 key,删除全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, {});
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.delete(name, key);
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 如果对应主键不存在,抛出异常
- if (Object.hasOwn(values, key) === false) {
- throw new Error();
- }
- // 删除数据
- delete values[key];
- // 保存数据
- GM_setValue(name, values);
- }
- /**
- * 插入或修改指定表的数据
- * @param {String} name 表名
- * @param {*} data 数据
- * @returns {Promise}
- */
- async put(name, data) {
- // 如果不在模块列表里,写入全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, data);
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.put(name, data);
- }
- // 获取对应的主键
- const keyPath = this.modules[name].keyPath;
- const key = data[keyPath];
- // 如果数据中不包含主键,抛出异常
- if (key === undefined) {
- throw new Error();
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 插入或修改数据
- values[key] = data;
- // 保存数据
- GM_setValue(name, values);
- }
- /**
- * 获取指定表的数据
- * @param {String} name 表名
- * @param {String} key 主键
- * @returns {Promise} 数据
- */
- async get(name, key) {
- // 如果不在模块列表里,忽略 key,返回全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_getValue(name);
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.get(name, key);
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 如果对应主键不存在,抛出异常
- if (Object.hasOwn(values, key) === false) {
- throw new Error();
- }
- // 返回结果
- return values[key];
- }
- /**
- * 批量插入指定表的数据
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @returns {Promise<number>} 成功数量
- */
- async bulkAdd(name, data) {
- // 如果不在模块列表里,写入全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, {});
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.bulkAdd(name, data);
- }
- // 获取对应的主键
- const keyPath = this.modules[name].keyPath;
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 添加数据
- const result = data.map((item) => {
- const key = item[keyPath];
- // 如果数据中不包含主键,抛出异常
- if (key === undefined) {
- return false;
- }
- // 如果对应主键已存在,抛出异常
- if (Object.hasOwn(values, key)) {
- return false;
- }
- // 插入数据
- values[key] = item;
- return true;
- });
- // 保存数据
- GM_setValue(name, values);
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量删除指定表的数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则删除全部
- * @returns {Promise<number>} 成功数量,删除全部时返回 -1
- */
- async bulkDelete(name, keys = []) {
- // 如果不在模块列表里,忽略 keys,删除全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, {});
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.bulkDelete(name, keys);
- }
- // 如果 keys 为空,删除全部数据
- if (keys.length === 0) {
- GM_setValue(name, {});
- return -1;
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 删除数据
- const result = keys.map((key) => {
- // 如果对应主键不存在,抛出异常
- if (Object.hasOwn(values, key) === false) {
- return false;
- }
- // 删除数据
- delete values[key];
- return true;
- });
- // 保存数据
- GM_setValue(name, values);
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量插入或修改指定表的数据
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @returns {Promise<number>} 成功数量
- */
- async bulkPut(name, data) {
- // 如果不在模块列表里,写入全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_setValue(name, data);
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.bulkPut(name, keys);
- }
- // 获取对应的主键
- const keyPath = this.modules[name].keyPath;
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 添加数据
- const result = data.map((item) => {
- const key = item[keyPath];
- // 如果数据中不包含主键,抛出异常
- if (key === undefined) {
- return false;
- }
- // 插入数据
- values[key] = item;
- return true;
- });
- // 保存数据
- GM_setValue(name, values);
- // 返回受影响的数量
- return result.filter((item) => item).length;
- }
- /**
- * 批量获取指定表的数据,如果不在模块列表里,返回全部数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则获取全部
- * @returns {Promise<Array>} 数据集合
- */
- async bulkGet(name, keys = []) {
- // 如果不在模块列表里,忽略 keys,返回全部数据
- if (Object.hasOwn(this.modules, name) === false) {
- return GM_getValue(name);
- }
- // 如果支持 IndexedDB,使用 IndexedDB
- if (super.isSupport()) {
- return super.bulkGet(name, keys);
- }
- // 获取全部数据
- const values = GM_getValue(name, {});
- // 如果 keys 为空,返回全部数据
- if (keys.length === 0) {
- return Object.values(values);
- }
- // 返回符合的结果
- const result = [];
- keys.forEach((key) => {
- if (Object.hasOwn(values, key)) {
- result.push(values[key]);
- }
- });
- return result;
- }
- }
- /**
- * 缓存管理
- *
- * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
- */
- class Cache extends GMStorage {
- /**
- * 增加模块列表的 timestamp 索引
- * @param {*} modules 模块列表
- */
- constructor(modules) {
- Object.values(modules).forEach((item) => {
- item.indexes = item.indexes || [];
- if (item.indexes.includes("timestamp") === false) {
- item.indexes.push("timestamp");
- }
- });
- super(modules);
- this.autoClear();
- }
- /**
- * 插入指定表的数据,并增加 timestamp
- * @param {String} name 表名
- * @param {*} data 数据
- * @returns {Promise}
- */
- async add(name, data) {
- // 如果在模块里,增加 timestamp
- if (Object.hasOwn(this.modules, name)) {
- data.timestamp = data.timestamp || new Date().getTime();
- }
- return super.add(name, data);
- }
- /**
- * 插入或修改指定表的数据,并增加 timestamp
- * @param {String} name 表名
- * @param {*} data 数据
- * @returns {Promise}
- */
- async put(name, data) {
- // 如果在模块里,增加 timestamp
- if (Object.hasOwn(this.modules, name)) {
- data.timestamp = data.timestamp || new Date().getTime();
- }
- return super.put(name, data);
- }
- /**
- * 获取指定表的数据,并移除过期数据
- * @param {String} name 表名
- * @param {String} key 主键
- * @returns {Promise} 数据
- */
- async get(name, key) {
- // 获取数据
- const value = await super.get(name, key).catch(() => null);
- // 如果不在模块里,直接返回结果
- if (Object.hasOwn(this.modules, name) === false) {
- return value;
- }
- // 如果有结果的话,移除超时数据
- if (value) {
- // 读取模块配置
- const { expireTime, persistent } = this.modules[name];
- // 持久化或未超时
- if (persistent || value.timestamp + expireTime > new Date().getTime()) {
- return value;
- }
- // 移除超时数据
- await super.delete(name, key);
- }
- return null;
- }
- /**
- * 批量插入指定表的数据,并增加 timestamp
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @returns {Promise<number>} 成功数量
- */
- async bulkAdd(name, data) {
- // 如果在模块里,增加 timestamp
- if (Object.hasOwn(this.modules, name)) {
- data.forEach((item) => {
- item.timestamp = item.timestamp || new Date().getTime();
- });
- }
- return super.bulkAdd(name, data);
- }
- /**
- * 批量删除指定表的数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则删除全部
- * @param {boolean} force 是否强制删除,否则只删除过期数据
- * @returns {Promise<number>} 成功数量,删除全部时返回 -1
- */
- async bulkDelete(name, keys = [], force = false) {
- // 如果不在模块里,强制删除
- if (Object.hasOwn(this.modules, name) === false) {
- force = true;
- }
- // 强制删除
- if (force) {
- return super.bulkDelete(name, keys);
- }
- // 批量获取指定表的数据,并移除过期数据
- const result = this.bulkGet(name, keys);
- // 返回成功数量
- if (keys.length === 0) {
- return -1;
- }
- return keys.length - result.length;
- }
- /**
- * 批量插入或修改指定表的数据,并增加 timestamp
- * @param {String} name 表名
- * @param {Array} data 数据集合
- * @returns {Promise<number>} 成功数量
- */
- async bulkPut(name, data) {
- // 如果在模块里,增加 timestamp
- if (Object.hasOwn(this.modules, name)) {
- data.forEach((item) => {
- item.timestamp = item.timestamp || new Date().getTime();
- });
- }
- return super.bulkPut(name, data);
- }
- /**
- * 批量获取指定表的数据,并移除过期数据
- * @param {String} name 表名
- * @param {Array<String>} keys 主键集合,空则获取全部
- * @returns {Promise<Array>} 数据集合
- */
- async bulkGet(name, keys = []) {
- // 获取数据
- const values = await super.bulkGet(name, keys).catch(() => []);
- // 如果不在模块里,直接返回结果
- if (Object.hasOwn(this.modules, name) === false) {
- return values;
- }
- // 读取模块配置
- const { keyPath, expireTime, persistent } = this.modules[name];
- // 筛选出超时数据
- const result = [];
- const expired = [];
- values.forEach((value) => {
- // 持久化或未超时
- if (persistent || value.timestamp + expireTime > new Date().getTime()) {
- result.push(value);
- return;
- }
- // 记录超时数据
- expired.push(value[keyPath]);
- });
- // 移除超时数据
- await super.bulkDelete(name, expired);
- // 返回结果
- return result;
- }
- /**
- * 自动清理缓存
- */
- async autoClear() {
- const data = await this.get(CLEAR_TIME_KEY);
- const now = new Date();
- const clearTime = new Date(data || 0);
- const isToday =
- now.getDate() === clearTime.getDate() &&
- now.getMonth() === clearTime.getMonth() &&
- now.getFullYear() === clearTime.getFullYear();
- if (isToday) {
- return;
- }
- await Promise.all(
- Object.keys(this.modules).map((name) => this.bulkDelete(name))
- );
- await this.put(CLEAR_TIME_KEY, now.getTime());
- }
- }
- /**
- * 设置
- *
- * 暂时整体处理模块设置,后续再拆分
- */
- class Settings {
- /**
- * 缓存管理
- */
- cache;
- /**
- * 当前设置
- */
- data = null;
- /**
- * 初始化并绑定缓存管理
- * @param {Cache} cache 缓存管理
- */
- constructor(cache) {
- this.cache = cache;
- }
- /**
- * 读取设置
- */
- async load() {
- // 读取设置
- if (this.data === null) {
- // 默认配置
- const defaultData = {
- tags: {},
- users: {},
- keywords: {},
- locations: {},
- options: {
- filterRegdateLimit: 0,
- filterPostnumLimit: 0,
- filterTopicRateLimit: 100,
- filterReputationLimit: NaN,
- filterAnony: false,
- filterMode: "隐藏",
- },
- };
- // 读取数据
- const storedData = await this.cache
- .get(DATA_KEY)
- .then((values) => values || {});
- // 写入缓存
- this.data = Tools.merge({}, defaultData, storedData);
- // 写入默认模块选项
- if (Object.hasOwn(this.data, "modules") === false) {
- this.data.modules = ["user", "tag", "misc"];
- if (Object.keys(this.data.keywords).length > 0) {
- this.data.modules.push("keyword");
- }
- if (Object.keys(this.data.locations).length > 0) {
- this.data.modules.push("location");
- }
- }
- }
- // 返回设置
- return this.data;
- }
- /**
- * 写入设置
- */
- async save() {
- return this.cache.put(DATA_KEY, this.data);
- }
- /**
- * 获取模块列表
- */
- get modules() {
- return this.data.modules;
- }
- /**
- * 设置模块列表
- */
- set modules(values) {
- this.data.modules = values;
- this.save();
- }
- /**
- * 获取标签列表
- */
- get tags() {
- return this.data.tags;
- }
- /**
- * 设置标签列表
- */
- set tags(values) {
- this.data.tags = values;
- this.save();
- }
- /**
- * 获取用户列表
- */
- get users() {
- return this.data.users;
- }
- /**
- * 设置用户列表
- */
- set users(values) {
- this.data.users = values;
- this.save();
- }
- /**
- * 获取关键字列表
- */
- get keywords() {
- return this.data.keywords;
- }
- /**
- * 设置关键字列表
- */
- set keywords(values) {
- this.data.keywords = values;
- this.save();
- }
- /**
- * 获取属地列表
- */
- get locations() {
- return this.data.locations;
- }
- /**
- * 设置属地列表
- */
- set locations(values) {
- this.data.locations = values;
- this.save();
- }
- /**
- * 获取默认过滤模式
- */
- get defaultFilterMode() {
- return this.data.options.filterMode;
- }
- /**
- * 设置默认过滤模式
- */
- set defaultFilterMode(value) {
- this.data.options.filterMode = value;
- this.save();
- }
- /**
- * 获取注册时间限制
- */
- get filterRegdateLimit() {
- return this.data.options.filterRegdateLimit || 0;
- }
- /**
- * 设置注册时间限制
- */
- set filterRegdateLimit(value) {
- this.data.options.filterRegdateLimit = value;
- this.save();
- }
- /**
- * 获取发帖数量限制
- */
- get filterPostnumLimit() {
- return this.data.options.filterPostnumLimit || 0;
- }
- /**
- * 设置发帖数量限制
- */
- set filterPostnumLimit(value) {
- this.data.options.filterPostnumLimit = value;
- this.save();
- }
- /**
- * 获取发帖比例限制
- */
- get filterTopicRateLimit() {
- return this.data.options.filterTopicRateLimit || 100;
- }
- /**
- * 设置发帖比例限制
- */
- set filterTopicRateLimit(value) {
- this.data.options.filterTopicRateLimit = value;
- this.save();
- }
- /**
- * 获取版面声望限制
- */
- get filterReputationLimit() {
- return this.data.options.filterReputationLimit || NaN;
- }
- /**
- * 设置版面声望限制
- */
- set filterReputationLimit(value) {
- this.data.options.filterReputationLimit = value;
- this.save();
- }
- /**
- * 获取是否过滤匿名
- */
- get filterAnonymous() {
- return this.data.options.filterAnony || false;
- }
- /**
- * 设置是否过滤匿名
- */
- set filterAnonymous(value) {
- this.data.options.filterAnony = value;
- this.save();
- }
- /**
- * 获取代理设置
- */
- get userAgent() {
- return this.cache.get(USER_AGENT_KEY).then((value) => {
- if (value === undefined) {
- return "Nga_Official";
- }
- return value;
- });
- }
- /**
- * 修改代理设置
- */
- set userAgent(value) {
- this.cache.put(USER_AGENT_KEY, value).then(() => {
- location.reload();
- });
- }
- /**
- * 获取是否启用前置过滤
- */
- get preFilterEnabled() {
- return this.cache.get(PRE_FILTER_KEY).then((value) => {
- if (value === undefined) {
- return true;
- }
- return value;
- });
- }
- /**
- * 设置是否启用前置过滤
- */
- set preFilterEnabled(value) {
- this.cache.put(PRE_FILTER_KEY, value).then(() => {
- location.reload();
- });
- }
- /**
- * 获取过滤模式列表
- *
- * 模拟成从配置中获取
- */
- get filterModes() {
- return ["继承", "标记", "遮罩", "隐藏", "显示"];
- }
- /**
- * 获取指定下标过滤模式
- * @param {Number} index 下标
- */
- getNameByMode(index) {
- const modes = this.filterModes;
- return modes[index] || "";
- }
- /**
- * 获取指定过滤模式下标
- * @param {String} name 过滤模式
- */
- getModeByName(name) {
- const modes = this.filterModes;
- return modes.indexOf(name);
- }
- /**
- * 切换过滤模式
- * @param {String} value 过滤模式
- * @returns {String} 过滤模式
- */
- switchModeByName(value) {
- const index = this.getModeByName(value);
- const nextIndex = (index + 1) % this.filterModes.length;
- return this.filterModes[nextIndex];
- }
- }
- /**
- * API
- */
- class API {
- /**
- * 缓存模块
- */
- static modules = {
- TOPIC_NUM_CACHE: {
- keyPath: "uid",
- version: 1,
- expireTime: 1000 * 60 * 60,
- persistent: true,
- },
- USER_INFO_CACHE: {
- keyPath: "uid",
- version: 1,
- expireTime: 1000 * 60 * 60,
- persistent: false,
- },
- PAGE_CACHE: {
- keyPath: "url",
- version: 1,
- expireTime: 1000 * 60 * 10,
- persistent: false,
- },
- FORUM_POSTED_CACHE: {
- keyPath: "url",
- version: 2,
- expireTime: 1000 * 60 * 60 * 24,
- persistent: true,
- },
- };
- /**
- * 缓存管理
- */
- cache;
- /**
- * 设置
- */
- settings;
- /**
- * 初始化并绑定缓存管理、设置
- * @param {Cache} cache 缓存管理
- * @param {Settings} settings 设置
- */
- constructor(cache, settings) {
- this.cache = cache;
- this.settings = settings;
- }
- /**
- * 简单的统一请求
- * @param {String} url 请求地址
- * @param {Object} config 请求参数
- * @param {Boolean} toJSON 是否转为 JSON 格式
- */
- async request(url, config = {}, toJSON = true) {
- const userAgent = await this.settings.userAgent;
- const response = await fetch(url, {
- headers: {
- "X-User-Agent": userAgent,
- },
- ...config,
- });
- const result = await Tools.readForumData(response, toJSON);
- return result;
- }
- /**
- * 获取用户主题数量
- * @param {number} uid 用户 ID
- */
- async getTopicNum(uid) {
- const name = "TOPIC_NUM_CACHE";
- const expireTime = API.modules[name];
- const api = `/thread.php?lite=js&authorid=${uid}`;
- const cache = await this.cache.get(name, uid);
- // 仍在缓存期间内,直接返回
- if (cache) {
- const expired = cache.timestamp + expireTime < new Date().getTime();
- if (expired === false) {
- return cache.count;
- }
- }
- // 请求数据
- const result = await this.request(api);
- // 服务器可能返回错误,遇到这种情况下,需要保留缓存
- const count = (() => {
- if (result.data) {
- return result.data.__ROWS || 0;
- }
- if (cache) {
- return cache.count;
- }
- return 0;
- })();
- // 更新缓存
- this.cache.put(name, {
- uid,
- count,
- });
- return count;
- }
- /**
- * 获取用户信息
- * @param {number} uid 用户 ID
- */
- async getUserInfo(uid) {
- const name = "USER_INFO_CACHE";
- const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;
- const cache = await this.cache.get(name, uid);
- if (cache) {
- return cache.data;
- }
- const result = await this.request(api);
- const data = result.data ? result.data[0] : null;
- if (data) {
- this.cache.put(name, {
- uid,
- data,
- });
- }
- return data || {};
- }
- /**
- * 获取帖子内容、用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、版面声望
- * @param {number} tid 主题 ID
- * @param {number} pid 回复 ID
- */
- async getPostInfo(tid, pid) {
- const name = "PAGE_CACHE";
- const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
- const cache = await this.cache.get(name, api);
- if (cache) {
- return cache.data;
- }
- const result = await this.request(api, {}, false);
- const parser = new DOMParser();
- const doc = parser.parseFromString(result, "text/html");
- // 验证帖子正常
- const verify = doc.querySelector("#m_posts");
- if (verify === null) {
- return {};
- }
- // 声明返回值
- const data = {};
- // 取得顶楼 UID
- data.uid = (() => {
- const ele = doc.querySelector("#postauthor0");
- if (ele) {
- const res = ele.getAttribute("href").match(/uid=(\S+)/);
- if (res) {
- return res[1];
- }
- }
- return 0;
- })();
- // 取得顶楼标题
- data.subject = doc.querySelector("#postsubject0").innerHTML;
- // 取得顶楼内容
- data.content = doc.querySelector("#postcontent0").innerHTML;
- // 非匿名用户可以继续取得用户信息和版面声望
- if (data.uid > 0) {
- // 取得用户信息
- data.userInfo = (() => {
- const text = Tools.searchPair(result, `"${data.uid}":`);
- if (text) {
- try {
- return JSON.parse(text);
- } catch {
- return null;
- }
- }
- return null;
- })();
- // 取得用户声望
- data.reputation = (() => {
- const reputations = (() => {
- const text = Tools.searchPair(result, `"__REPUTATIONS":`);
- if (text) {
- try {
- return JSON.parse(text);
- } catch {
- return null;
- }
- }
- return null;
- })();
- if (reputations) {
- for (let fid in reputations) {
- return reputations[fid][data.uid] || 0;
- }
- }
- return NaN;
- })();
- }
- // 写入缓存
- this.cache.put(name, {
- url: api,
- data,
- });
- // 返回结果
- return data;
- }
- /**
- * 获取版面信息
- * @param {number} fid 版面 ID
- */
- async getForumInfo(fid) {
- if (Number.isNaN(fid)) {
- return null;
- }
- const api = `/thread.php?lite=js&fid=${fid}`;
- const result = await this.request(api);
- const info = result.data ? result.data.__F : null;
- return info;
- }
- /**
- * 获取版面发言记录
- * @param {number} fid 版面 ID
- * @param {number} uid 用户 ID
- */
- async getForumPosted(fid, uid) {
- const name = "FORUM_POSTED_CACHE";
- const expireTime = API.modules[name];
- const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`;
- const cache = await this.cache.get(name, api);
- if (cache) {
- // 发言是无法撤销的,只要有记录就永远不需要再获取
- // 手动处理没有记录的缓存数据
- const expired = cache.timestamp + expireTime < new Date().getTime();
- if (expired && cache.data === false) {
- await this.cache.delete(name, api);
- }
- return cache.data;
- }
- let isComplete = false;
- let isBusy = false;
- const func = async (url) => {
- if (isComplete || isBusy) {
- return;
- }
- const result = await this.request(url, {}, false);
- // 将所有匹配的 FID 写入缓存,即使并不在设置里
- const matched = result.match(/"fid":(-?\d+),/g);
- if (matched) {
- const list = [
- ...new Set(
- matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10))
- ),
- ];
- list.forEach((item) => {
- const key = api.replace(`&fid=${fid}`, `&fid=${item}`);
- // 写入缓存
- this.cache.put(name, {
- url: key,
- data: true,
- });
- // 已有结果,无需继续查询
- if (fid === item) {
- isComplete = true;
- }
- });
- }
- // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
- if (result.indexOf("服务器忙") > 0) {
- isBusy = true;
- }
- };
- // 先获取回复记录的第一页,顺便可以获取其他版面的记录
- // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
- 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 - 1000 * 60 * 60)
- : new Date().getTime();
- // 写入失败缓存
- this.cache.put(name, {
- url: api,
- data: false,
- timestamp,
- });
- }
- return isComplete;
- }
- }
- /**
- * UI
- */
- class UI {
- /**
- * 标签
- */
- static label = "屏蔽";
- /**
- * 设置
- */
- settings;
- /**
- * API
- */
- api;
- /**
- * 模块列表
- */
- modules = {};
- /**
- * 菜单元素
- */
- menu = null;
- /**
- * 视图元素
- */
- views = {};
- /**
- * 初始化并绑定设置、API,注册脚本菜单
- * @param {Settings} settings 设置
- * @param {API} api API
- */
- constructor(settings, api) {
- this.settings = settings;
- this.api = api;
- this.init();
- }
- /**
- * 初始化,创建基础视图,初始化通用设置
- */
- init() {
- const tabs = this.createTabs({
- className: "right_",
- });
- const content = this.createElement("DIV", [], {
- style: "width: 80vw;",
- });
- const container = this.createElement("DIV", [tabs, content]);
- this.views = {
- tabs,
- content,
- container,
- };
- this.initSettings();
- }
- /**
- * 初始化设置
- */
- initSettings() {
- // 创建基础视图
- const settings = this.createElement("DIV", []);
- // 添加设置项
- const add = (order, ...elements) => {
- const items = [...settings.childNodes];
- if (items.find((item) => item.order === order)) {
- return;
- }
- const item = this.createElement(
- "DIV",
- [...elements, this.createElement("BR", [])],
- {
- order,
- }
- );
- const anchor = items.find((item) => item.order > order);
- settings.insertBefore(item, anchor || null);
- return item;
- };
- // 绑定事件
- Object.assign(settings, {
- add,
- });
- // 合并视图
- Object.assign(this.views, {
- settings,
- });
- // 创建标签页
- const { tabs, content } = this.views;
- this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
- onclick: () => {
- content.innerHTML = "";
- content.appendChild(settings);
- },
- });
- }
- /**
- * 弹窗确认
- * @param {String} message 提示信息
- * @returns {Promise}
- */
- confirm(message = "是否确认?") {
- return new Promise((resolve, reject) => {
- const result = confirm(message);
- if (result) {
- resolve();
- return;
- }
- reject();
- });
- }
- /**
- * 折叠
- * @param {String | Number} key 标识
- * @param {HTMLElement} element 目标元素
- * @param {String} content 内容
- */
- collapse(key, element, content) {
- key = "collapsed_" + key;
- element.innerHTML = `
- <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
- <span class="crimson">Troll must die.</span>
- <a href="javascript:void(0)" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">点击查看</a>
- <div style="display: none;" name="${key}">
- ${content}
- </div>
- </div>`;
- }
- /**
- * 创建元素
- * @param {String} tagName 标签
- * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
- * @param {*} properties 额外属性
- * @returns {HTMLElement} 元素
- */
- createElement(tagName, content, properties = {}) {
- const element = document.createElement(tagName);
- // 写入内容
- if (typeof content === "string") {
- element.innerHTML = content;
- } else {
- if (Array.isArray(content) === false) {
- content = [content];
- }
- content.forEach((item) => {
- if (item === null) {
- return;
- }
- if (typeof item === "string") {
- element.append(item);
- return;
- }
- element.appendChild(item);
- });
- }
- // 对 A 标签的额外处理
- if (tagName.toUpperCase() === "A") {
- if (Object.hasOwn(properties, "href") === false) {
- properties.href = "javascript: void(0);";
- }
- }
- // 附加属性
- Object.entries(properties).forEach(([key, value]) => {
- element[key] = value;
- });
- return element;
- }
- /**
- * 创建按钮
- * @param {String} text 文字
- * @param {Function} onclick 点击事件
- * @param {*} properties 额外属性
- */
- createButton(text, onclick, properties = {}) {
- return this.createElement("BUTTON", text, {
- ...properties,
- onclick,
- });
- }
- /**
- * 创建按钮组
- * @param {Array} buttons 按钮集合
- */
- createButtonGroup(...buttons) {
- return this.createElement("DIV", buttons, {
- className: "filter-button-group",
- });
- }
- /**
- * 创建表格
- * @param {Array} headers 表头集合
- * @param {*} properties 额外属性
- * @returns {HTMLElement} 元素和相关函数
- */
- createTable(headers, properties = {}) {
- const rows = [];
- const ths = headers.map((item, index) =>
- this.createElement("TH", item.label, {
- ...item,
- className: `c${index + 1}`,
- })
- );
- const tr =
- ths.length > 0
- ? this.createElement("TR", ths, {
- className: "block_txt_c0",
- })
- : null;
- const thead = tr !== null ? this.createElement("THEAD", tr) : null;
- const tbody = this.createElement("TBODY", []);
- const table = this.createElement("TABLE", [thead, tbody], {
- ...properties,
- className: "filter-table forumbox",
- });
- const wrapper = this.createElement("DIV", table, {
- className: "filter-table-wrapper",
- });
- const intersectionObserver = new IntersectionObserver((entries) => {
- if (entries[0].intersectionRatio <= 0) return;
- const list = rows.splice(0, 10);
- if (list.length === 0) {
- return;
- }
- intersectionObserver.disconnect();
- tbody.append(...list);
- intersectionObserver.observe(tbody.lastElementChild);
- });
- const add = (...columns) => {
- const tds = columns.map((column, index) => {
- if (ths[index]) {
- const { center, ellipsis } = ths[index];
- const properties = {};
- if (center) {
- properties.style = "text-align: center;";
- }
- if (ellipsis) {
- properties.className = "filter-text-ellipsis";
- }
- column = this.createElement("DIV", column, properties);
- }
- return this.createElement("TD", column, {
- className: `c${index + 1}`,
- });
- });
- const tr = this.createElement("TR", tds, {
- className: `row${(rows.length % 2) + 1}`,
- });
- intersectionObserver.disconnect();
- rows.push(tr);
- intersectionObserver.observe(tbody.lastElementChild || tbody);
- };
- const update = (e, ...columns) => {
- const row = e.target.closest("TR");
- if (row) {
- const tds = row.querySelectorAll("TD");
- columns.map((column, index) => {
- if (ths[index]) {
- const { center, ellipsis } = ths[index];
- const properties = {};
- if (center) {
- properties.style = "text-align: center;";
- }
- if (ellipsis) {
- properties.className = "filter-text-ellipsis";
- }
- column = this.createElement("DIV", column, properties);
- }
- if (tds[index]) {
- tds[index].innerHTML = "";
- tds[index].append(column);
- }
- });
- }
- };
- const remove = (e) => {
- const row = e.target.closest("TR");
- if (row) {
- tbody.removeChild(row);
- }
- };
- const clear = () => {
- rows.splice(0);
- intersectionObserver.disconnect();
- tbody.innerHTML = "";
- };
- Object.assign(wrapper, {
- add,
- update,
- remove,
- clear,
- });
- return wrapper;
- }
- /**
- * 创建标签组
- * @param {*} properties 额外属性
- */
- createTabs(properties = {}) {
- const tabs = this.createElement(
- "DIV",
- `<table class="stdbtn" cellspacing="0">
- <tbody>
- <tr></tr>
- </tbody>
- </table>`,
- properties
- );
- return this.createElement(
- "DIV",
- [
- tabs,
- this.createElement("DIV", [], {
- className: "clear",
- }),
- ],
- {
- style: "display: none; margin-bottom: 5px;",
- }
- );
- }
- /**
- * 创建标签
- * @param {Element} tabs 标签组
- * @param {String} label 标签名称
- * @param {Number} order 标签顺序,重复则跳过
- * @param {*} properties 额外属性
- */
- createTab(tabs, label, order, properties = {}) {
- const group = tabs.querySelector("TR");
- const items = [...group.childNodes];
- if (items.find((item) => item.order === order)) {
- return;
- }
- if (items.length > 0) {
- tabs.style.removeProperty("display");
- }
- const tab = this.createElement("A", label, {
- ...properties,
- className: "nobr silver",
- onclick: () => {
- if (tab.className === "nobr") {
- return;
- }
- group.querySelectorAll("A").forEach((item) => {
- if (item === tab) {
- item.className = "nobr";
- } else {
- item.className = "nobr silver";
- }
- });
- if (properties.onclick) {
- properties.onclick();
- }
- },
- });
- const wrapper = this.createElement("TD", tab, {
- order,
- });
- const anchor = items.find((item) => item.order > order);
- group.insertBefore(wrapper, anchor || null);
- return wrapper;
- }
- /**
- * 创建对话框
- * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
- * @param {String} title 对话框的标题
- * @param {HTMLElement} content 对话框的内容
- */
- createDialog(anchor, title, content) {
- let window;
- const show = () => {
- if (window === undefined) {
- window = commonui.createCommmonWindow();
- }
- window._.addContent(null);
- window._.addTitle(title);
- window._.addContent(content);
- window._.show();
- };
- if (anchor) {
- anchor.onclick = show;
- } else {
- show();
- }
- return window;
- }
- /**
- * 渲染菜单
- */
- renderMenu() {
- // 如果泥潭的右上角菜单还没有加载完成,说明模块尚未加载完毕,跳过
- const anchor = document.querySelector("#mainmenu .td:last-child");
- if (anchor === null) {
- return;
- }
- const menu = this.createElement("A", this.constructor.label, {
- className: "mmdefault nobr",
- });
- const container = this.createElement("DIV", menu, {
- className: "td",
- });
- // 插入菜单
- anchor.before(container);
- // 绑定菜单元素
- this.menu = menu;
- }
- /**
- * 渲染视图
- */
- renderView() {
- // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
- if (this.menu === null) {
- return;
- }
- // 绑定菜单点击事件.
- this.createDialog(
- this.menu,
- this.constructor.label,
- this.views.container
- );
- // 启用第一个模块
- this.views.tabs.querySelector("A").click();
- }
- /**
- * 渲染
- */
- render() {
- this.renderMenu();
- this.renderView();
- }
- }
- /**
- * 基础模块
- */
- class Module {
- /**
- * 模块名称
- */
- static name;
- /**
- * 模块标签
- */
- static label;
- /**
- * 顺序
- */
- static order;
- /**
- * 依赖模块
- */
- static depends = [];
- /**
- * 附加模块
- */
- static addons = [];
- /**
- * 设置
- */
- settings;
- /**
- * API
- */
- api;
- /**
- * UI
- */
- ui;
- /**
- * 过滤列表
- */
- data = [];
- /**
- * 依赖模块
- */
- depends = {};
- /**
- * 附加模块
- */
- addons = {};
- /**
- * 视图元素
- */
- views = {};
- /**
- * 初始化并绑定设置、API、UI、过滤列表,注册 UI
- * @param {Settings} settings 设置
- * @param {API} api API
- * @param {UI} ui UI
- */
- constructor(settings, api, ui, data) {
- this.settings = settings;
- this.api = api;
- this.ui = ui;
- this.data = data;
- this.init();
- }
- /**
- * 创建实例
- * @param {Settings} settings 设置
- * @param {API} api API
- * @param {UI} ui UI
- * @param {Array} data 过滤列表
- * @returns {Module | null} 成功后返回模块实例
- */
- static create(settings, api, ui, data) {
- // 读取设置里的模块列表
- const modules = settings.modules;
- // 如果不包含自己或依赖的模块,则返回空
- const index = [this, ...this.depends].findIndex(
- (module) => modules.includes(module.name) === false
- );
- if (index >= 0) {
- return null;
- }
- // 创建实例
- const instance = new this(settings, api, ui, data);
- // 返回实例
- return instance;
- }
- /**
- * 判断指定附加模块是否启用
- * @param {typeof Module} module 模块
- */
- hasAddon(module) {
- return Object.hasOwn(this.addons, module.name);
- }
- /**
- * 初始化,创建基础视图和组件
- */
- init() {
- if (this.views.container) {
- this.destroy();
- }
- const { ui } = this;
- const container = ui.createElement("DIV", []);
- this.views = {
- container,
- };
- this.initComponents();
- }
- /**
- * 初始化组件
- */
- initComponents() {}
- /**
- * 销毁
- */
- destroy() {
- Object.values(this.views).forEach((view) => {
- if (view.parentNode) {
- view.parentNode.removeChild(view);
- }
- });
- this.views = {};
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- container.innerHTML = "";
- container.appendChild(this.views.container);
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {}
- /**
- * 通知
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async notify(item, result) {}
- }
- /**
- * 过滤器
- */
- class Filter {
- /**
- * 设置
- */
- settings;
- /**
- * API
- */
- api;
- /**
- * UI
- */
- ui;
- /**
- * 过滤列表
- */
- data = [];
- /**
- * 模块列表
- */
- modules = {};
- /**
- * 初始化并绑定设置、API、UI
- * @param {Settings} settings 设置
- * @param {API} api API
- * @param {UI} ui UI
- */
- constructor(settings, api, ui) {
- this.settings = settings;
- this.api = api;
- this.ui = ui;
- }
- /**
- * 绑定两个模块的互相关系
- * @param {Module} moduleA 模块A
- * @param {Module} moduleB 模块B
- */
- bindModule(moduleA, moduleB) {
- const nameA = moduleA.constructor.name;
- const nameB = moduleB.constructor.name;
- // A 依赖 B
- if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
- moduleA.depends[nameB] = moduleB;
- moduleA.init();
- }
- // B 依赖 A
- if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
- moduleB.depends[nameA] = moduleA;
- moduleB.init();
- }
- // A 附加 B
- if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
- moduleA.addons[nameB] = moduleB;
- moduleA.init();
- }
- // B 附加 A
- if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
- moduleB.addons[nameA] = moduleA;
- moduleB.init();
- }
- }
- /**
- * 加载模块
- * @param {typeof Module} module 模块
- */
- initModule(module) {
- // 如果已经加载过则跳过
- if (Object.hasOwn(this.modules, module.name)) {
- return;
- }
- // 创建模块
- const instance = module.create(
- this.settings,
- this.api,
- this.ui,
- this.data
- );
- // 如果创建失败则跳过
- if (instance === null) {
- return;
- }
- // 绑定依赖模块和附加模块
- Object.values(this.modules).forEach((item) => {
- this.bindModule(item, instance);
- });
- // 合并模块
- this.modules[module.name] = instance;
- // 按照顺序重新整理模块
- this.modules = Tools.sortBy(
- Object.values(this.modules),
- (item) => item.constructor.order
- ).reduce(
- (result, item) => ({
- ...result,
- [item.constructor.name]: item,
- }),
- {}
- );
- }
- /**
- * 加载模块列表
- * @param {typeof Module[]} modules 模块列表
- */
- initModules(...modules) {
- // 根据依赖和附加模块决定初始化的顺序
- Tools.sortBy(
- modules,
- (item) => item.depends.length,
- (item) => item.addons.length
- ).forEach((module) => {
- this.initModule(module);
- });
- }
- /**
- * 添加到过滤列表
- * @param {*} item 绑定的 nFilter
- */
- pushData(item) {
- // 清除掉无效数据
- for (let i = 0; i < this.data.length; ) {
- if (document.body.contains(this.data[i].container) === false) {
- this.data.splice(i, 1);
- continue;
- }
- i += 1;
- }
- // 加入过滤列表
- if (this.data.includes(item) === false) {
- this.data.push(item);
- }
- }
- /**
- * 判断指定 UID 是否是自己
- * @param {Number} uid 用户 ID
- */
- isSelf(uid) {
- return unsafeWindow.__CURRENT_UID === uid;
- }
- /**
- * 获取过滤模式
- * @param {*} item 绑定的 nFilter
- */
- async getFilterMode(item) {
- // 获取链接参数
- const params = new URLSearchParams(location.search);
- // 跳过屏蔽(插件自定义)
- if (params.has("nofilter")) {
- return;
- }
- // 收藏
- if (params.has("favor")) {
- return;
- }
- // 只看某人
- if (params.has("authorid")) {
- return;
- }
- // 跳过自己
- if (this.isSelf(item.uid)) {
- return;
- }
- // 声明结果
- const result = {
- mode: -1,
- reason: ``,
- };
- // 根据模块依次过滤
- for (const module of Object.values(this.modules)) {
- await module.filter(item, result);
- }
- // 写入过滤模式和过滤原因
- item.filterMode = this.settings.getNameByMode(result.mode);
- item.reason = result.reason;
- // 通知各模块过滤结果
- for (const module of Object.values(this.modules)) {
- await module.notify(item, result);
- }
- // 继承模式下返回默认过滤模式
- if (item.filterMode === "继承") {
- return this.settings.defaultFilterMode;
- }
- // 返回结果
- return item.filterMode;
- }
- /**
- * 过滤主题
- * @param {*} item 主题内容,见 commonui.topicArg.data
- */
- filterTopic(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 this.getFilterMode(item.nFilter);
- // 样式处理
- (() => {
- // 还原样式
- // TODO 应该整体采用 className 来实现
- (() => {
- // 标记模式
- title.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");
- }
- })();
- };
- // 绑定事件
- item.nFilter = {
- tid,
- pid: 0,
- uid,
- username,
- container,
- title,
- author,
- subject,
- action: null,
- tags: null,
- execute,
- };
- // 添加至列表
- this.pushData(item.nFilter);
- }
- // 开始过滤
- item.nFilter.execute();
- }
- /**
- * 过滤回复
- * @param {*} item 回复内容,见 commonui.postArg.data
- */
- filterReply(item) {
- // 绑定事件
- if (item.nFilter === undefined) {
- // 主题 ID
- const tid = item.tid;
- // 回复 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 this.getFilterMode(item.nFilter);
- // 样式处理
- (() => {
- // 还原样式
- // 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";
- }
- this.ui.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 = `<span class="crimson">Troll must die.</span>`;
- 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;
- }
- })();
- // 非隐藏模式下,恢复显示
- // 楼层的遮罩模式下仍需隐藏
- if (["遮罩", "隐藏"].includes(filterMode) === false) {
- container.style.removeProperty("display");
- }
- })();
- // 过滤引用
- this.filterQuote(item);
- };
- // 绑定事件
- item.nFilter = {
- tid,
- pid,
- uid,
- username,
- container,
- title,
- author,
- subject,
- content: content.innerText,
- action,
- tags,
- execute,
- };
- // 添加至列表
- this.pushData(item.nFilter);
- }
- // 开始过滤
- item.nFilter.execute();
- }
- /**
- * 过滤引用
- * @param {*} item 回复内容,见 commonui.postArg.data
- */
- filterQuote(item) {
- // 未绑定事件,直接跳过
- if (item.nFilter === undefined) {
- return;
- }
- // 回复内容
- const content = item.contentC;
- // 找到所有引用
- const quotes = content.querySelectorAll(".quote");
- // 处理引用
- [...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 parseInt(res[1], 10);
- }
- }
- 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 {};
- })();
- // 临时的 nFilter
- const nFilter = {
- uid,
- tid,
- pid,
- subject: "",
- content: quote.innerText,
- action: null,
- tags: null,
- };
- // 获取过滤模式
- const filterMode = await this.getFilterMode(nFilter);
- (() => {
- if (filterMode === "标记") {
- this.ui.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 = `<span class="crimson">Troll must die.</span>`;
- caption.onclick = () => {
- quote.removeChild(caption);
- source.style.display = "";
- };
- quote.innerHTML = "";
- quote.appendChild(source);
- quote.appendChild(caption);
- return;
- }
- if (filterMode === "隐藏") {
- quote.innerHTML = "";
- return;
- }
- })();
- // 绑定引用
- item.nFilter.quotes = item.nFilter.quotes || {};
- item.nFilter.quotes[uid] = nFilter.filterMode;
- });
- }
- }
- /**
- * 列表模块
- */
- class ListModule extends Module {
- /**
- * 模块名称
- */
- static name = "list";
- /**
- * 模块标签
- */
- static label = "列表";
- /**
- * 顺序
- */
- static order = 10;
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "内容", ellipsis: true },
- { label: "过滤模式", center: true, width: 1 },
- { label: "原因", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 绑定的 nFilter
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { tid, pid, filterMode, reason } = item;
- // 移除 BR 标签
- item.content = (item.content || "").replace(/<br>/g, "");
- // 内容
- const content = (() => {
- if (pid) {
- return ui.createElement("A", item.content, {
- href: `/read.php?pid=${pid}&nofilter`,
- });
- }
- // 如果有 TID 但没有标题,是引用,采用内容逻辑
- if (item.subject.length === 0) {
- return ui.createElement("A", item.content, {
- href: `/read.php?tid=${tid}&nofilter`,
- });
- }
- return ui.createElement("A", item.subject, {
- href: `/read.php?tid=${tid}&nofilter`,
- title: item.content,
- className: "b nobr",
- });
- })();
- return [content, filterMode, reason];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { tabs, content } = this.ui.views;
- const table = this.ui.createTable(this.columns());
- const tab = this.ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- const list = this.data.filter((item) => {
- return (item.filterMode || "显示") !== "显示";
- });
- Object.values(list).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- }
- }
- /**
- * 通知
- * @param {*} item 绑定的 nFilter
- */
- async notify() {
- // 获取过滤后的数量
- const count = this.data.filter((item) => {
- return (item.filterMode || "显示") !== "显示";
- }).length;
- // 更新菜单文字
- const { ui } = this;
- const { menu } = ui;
- if (menu === null) {
- return;
- }
- if (count) {
- menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
- } else {
- menu.innerHTML = `${ui.constructor.label}`;
- }
- // 重新渲染
- // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
- const { tab } = this.views;
- if (tab.querySelector("A").className === "nobr") {
- this.render(ui.views.content);
- }
- }
- }
- /**
- * 用户模块
- */
- class UserModule extends Module {
- /**
- * 模块名称
- */
- static name = "user";
- /**
- * 模块标签
- */
- static label = "用户";
- /**
- * 顺序
- */
- static order = 20;
- /**
- * 获取列表
- */
- get list() {
- return this.settings.users;
- }
- /**
- * 获取用户
- * @param {Number} uid 用户 ID
- */
- get(uid) {
- // 获取列表
- const list = this.list;
- // 如果存在,则返回信息
- if (list[uid]) {
- return list[uid];
- }
- return null;
- }
- /**
- * 添加用户
- * @param {Number} uid 用户 ID
- */
- add(uid, values) {
- // 获取列表
- const list = this.list;
- // 如果已存在,则返回信息
- if (list[uid]) {
- return list[uid];
- }
- // 写入用户信息
- list[uid] = values;
- // 保存数据
- this.settings.users = list;
- // 重新过滤
- this.reFilter(uid);
- // 返回添加的用户
- return values;
- }
- /**
- * 编辑用户
- * @param {Number} uid 用户 ID
- * @param {*} values 用户信息
- */
- update(uid, values) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, uid) === false) {
- return null;
- }
- // 获取用户
- const entity = list[uid];
- // 更新用户
- Object.assign(entity, values);
- // 保存数据
- this.settings.users = list;
- // 重新过滤
- this.reFilter(uid);
- // 返回编辑的用户
- return entity;
- }
- /**
- * 删除用户
- * @param {Number} uid 用户 ID
- * @returns {Object | null} 删除的用户
- */
- remove(uid) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, uid) === false) {
- return null;
- }
- // 获取用户
- const entity = list[uid];
- // 删除用户
- delete list[uid];
- // 保存数据
- this.settings.users = list;
- // 重新过滤
- this.reFilter(uid);
- // 返回删除的用户
- return entity;
- }
- /**
- * 格式化
- * @param {Number} uid 用户 ID
- * @param {String | undefined} name 用户名称
- */
- format(uid, name) {
- if (uid <= 0) {
- return null;
- }
- const { ui } = this;
- const user = this.get(uid);
- if (user) {
- name = user.name;
- }
- const username = name ? "@" + name : "#" + uid;
- return ui.createElement("A", `[${username}]`, {
- className: "b nobr",
- href: `/nuke.php?func=ucp&uid=${uid}`,
- });
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "昵称" },
- { label: "过滤模式", center: true, width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 用户信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { table } = this.views;
- const { id, name, filterMode } = item;
- // 昵称
- const user = this.format(id, name);
- // 切换过滤模式
- const switchMode = ui.createButton(
- filterMode || this.settings.filterModes[0],
- () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- this.update(id, {
- filterMode: newMode,
- });
- switchMode.innerText = newMode;
- }
- );
- // 操作
- const buttons = (() => {
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(() => {
- this.remove(id);
- table.remove(e);
- });
- });
- return ui.createButtonGroup(remove);
- })();
- return [user, switchMode, buttons];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { ui } = this;
- const { tabs, content, settings } = ui.views;
- const { add } = settings;
- const table = ui.createTable(this.columns());
- const tab = ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- // 删除非激活中的用户
- {
- const list = ui.createElement("DIV", [], {
- style: "white-space: normal;",
- });
- const button = ui.createButton("删除非激活中的用户", () => {
- ui.confirm().then(() => {
- list.innerHTML = "";
- const users = Object.values(this.list);
- const waitingQueue = users.map(
- ({ id }) =>
- () =>
- this.api.getUserInfo(id).then(({ bit }) => {
- const activeInfo = commonui.activeInfo(0, 0, bit);
- const activeType = activeInfo[1];
- if (["ACTIVED", "LINKED"].includes(activeType)) {
- return;
- }
- list.append(this.format(id));
- this.remove(id);
- })
- );
- const queueLength = waitingQueue.length;
- const execute = () => {
- if (waitingQueue.length) {
- const next = waitingQueue.shift();
- button.disabled = true;
- button.innerHTML = `删除非激活中的用户 (${
- queueLength - waitingQueue.length
- }/${queueLength})`;
- next().finally(execute);
- return;
- }
- button.disabled = false;
- };
- execute();
- });
- });
- const element = ui.createElement("DIV", [button, list]);
- add(this.constructor.order + 0, element);
- }
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- Object.values(this.list).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- }
- }
- /**
- * 渲染详情
- * @param {Number} uid 用户 ID
- * @param {String | undefined} name 用户名称
- * @param {Function} callback 回调函数
- */
- renderDetails(uid, name, callback = () => {}) {
- const { ui, settings } = this;
- // 只允许同时存在一个详情页
- if (this.views.details) {
- if (this.views.details.parentNode) {
- this.views.details.parentNode.removeChild(this.views.details);
- }
- }
- // 获取用户信息
- const user = this.get(uid);
- if (user) {
- name = user.name;
- }
- const title =
- (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
- const filterMode = user ? user.filterMode : settings.filterModes[0];
- const switchMode = ui.createButton(filterMode, () => {
- const newMode = settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- });
- const buttons = ui.createElement(
- "DIV",
- (() => {
- const remove = user
- ? ui.createButton("删除", () => {
- ui.confirm().then(() => {
- this.remove(uid);
- this.views.details._.hide();
- callback("REMOVE");
- });
- })
- : null;
- const save = ui.createButton("保存", () => {
- if (user === null) {
- const entity = this.add(uid, {
- id: uid,
- name,
- tags: [],
- filterMode: switchMode.innerText,
- });
- this.views.details._.hide();
- callback("ADD", entity);
- } else {
- const entity = this.update(uid, {
- name,
- filterMode: switchMode.innerText,
- });
- this.views.details._.hide();
- callback("UPDATE", entity);
- }
- });
- return ui.createButtonGroup(remove, save);
- })(),
- {
- className: "right_",
- }
- );
- const actions = ui.createElement(
- "DIV",
- [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
- {
- style: "margin-top: 10px;",
- }
- );
- const tips = ui.createElement("DIV", TIPS.filterMode, {
- className: "silver",
- style: "margin-top: 10px;",
- });
- const content = ui.createElement("DIV", [actions, tips], {
- style: "width: 80vw",
- });
- // 创建弹出框
- this.views.details = ui.createDialog(null, title, content);
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取用户信息
- const user = this.get(item.uid);
- // 没有则跳过
- if (user === null) {
- return;
- }
- // 获取用户过滤模式
- const mode = this.settings.getModeByName(user.filterMode);
- // 不高于当前过滤模式则跳过
- if (mode <= result.mode) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `用户模式: ${user.filterMode}`;
- }
- /**
- * 通知
- * @param {*} item 绑定的 nFilter
- */
- async notify(item) {
- const { uid, username, action } = item;
- // 如果没有 action 组件则跳过
- if (action === null) {
- return;
- }
- // 如果是匿名,隐藏组件
- if (uid <= 0) {
- action.style.display = "none";
- return;
- }
- // 获取当前用户
- const user = this.get(uid);
- // 修改操作按钮文字
- action.innerText = "屏蔽";
- // 修改操作按钮颜色
- if (user) {
- action.style.background = "#CB4042";
- } else {
- action.style.background = "#AAA";
- }
- // 绑定事件
- action.onclick = () => {
- this.renderDetails(uid, username);
- };
- }
- /**
- * 重新过滤
- * @param {Number} uid 用户 ID
- */
- reFilter(uid) {
- this.data.forEach((item) => {
- // 如果用户 ID 一致,则重新过滤
- if (item.uid === uid) {
- item.execute();
- return;
- }
- // 如果有引用,也重新过滤
- if (Object.hasOwn(item.quotes || {}, uid)) {
- item.execute();
- return;
- }
- });
- }
- }
- /**
- * 标记模块
- */
- class TagModule extends Module {
- /**
- * 模块名称
- */
- static name = "tag";
- /**
- * 模块标签
- */
- static label = "标记";
- /**
- * 顺序
- */
- static order = 30;
- /**
- * 依赖模块
- */
- static depends = [UserModule];
- /**
- * 依赖的用户模块
- * @returns {UserModule} 用户模块
- */
- get userModule() {
- return this.depends[UserModule.name];
- }
- /**
- * 获取列表
- */
- get list() {
- return this.settings.tags;
- }
- /**
- * 获取标记
- * @param {Number} id 标记 ID
- * @param {String} name 标记名称
- */
- get({ id, name }) {
- // 获取列表
- const list = this.list;
- // 通过 ID 获取标记
- if (list[id]) {
- return list[id];
- }
- // 通过名称获取标记
- if (name) {
- const tag = Object.values(list).find((item) => item.name === name);
- if (tag) {
- return tag;
- }
- }
- return null;
- }
- /**
- * 添加标记
- * @param {String} name 标记名称
- */
- add(name) {
- // 获取对应的标记
- const tag = this.get({ name });
- // 如果标记已存在,则返回标记信息,否则增加标记
- if (tag) {
- return tag;
- }
- // 获取列表
- const list = this.list;
- // ID 为最大值 + 1
- const id = Math.max(...Object.keys(list), 0) + 1;
- // 标记的颜色
- const color = Tools.generateColor(name);
- // 写入标记信息
- list[id] = {
- id,
- name,
- color,
- filterMode: this.settings.filterModes[0],
- };
- // 保存数据
- this.settings.tags = list;
- // 返回添加的标记
- return list[id];
- }
- /**
- * 编辑标记
- * @param {Number} id 标记 ID
- * @param {*} values 标记信息
- */
- update(id, values) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取标记
- const entity = list[id];
- // 获取相关的用户
- const users = Object.values(this.userModule.list).filter((user) =>
- user.tags.includes(id)
- );
- // 更新标记
- Object.assign(entity, values);
- // 保存数据
- this.settings.tags = list;
- // 重新过滤
- this.reFilter(users);
- }
- /**
- * 删除标记
- * @param {Number} id 标记 ID
- */
- remove(id) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取标记
- const entity = list[id];
- // 获取相关的用户
- const users = Object.values(this.userModule.list).filter((user) =>
- user.tags.includes(id)
- );
- // 删除标记
- delete list[id];
- // 删除相关的用户标记
- users.forEach((user) => {
- const index = user.tags.findIndex((item) => item === id);
- if (index >= 0) {
- user.tags.splice(index, 1);
- }
- });
- // 保存数据
- this.settings.tags = list;
- // 重新过滤
- this.reFilter(users);
- // 返回删除的标记
- return entity;
- }
- /**
- * 格式化
- * @param {Number} id 标记 ID
- * @param {String | undefined} name 标记名称
- * @param {String | undefined} name 标记颜色
- */
- format(id, name, color) {
- const { ui } = this;
- if (id >= 0) {
- const tag = this.get({ id });
- if (tag) {
- name = tag.name;
- color = tag.color;
- }
- }
- if (name && color) {
- return ui.createElement("B", name, {
- className: "block_txt nobr",
- style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
- });
- }
- return "";
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "标记", width: 1 },
- { label: "列表" },
- { label: "过滤模式", width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 标记信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { table } = this.views;
- const { id, filterMode } = item;
- // 标记
- const tag = this.format(id);
- // 用户列表
- const list = Object.values(this.userModule.list)
- .filter(({ tags }) => tags.includes(id))
- .map(({ id }) => this.userModule.format(id));
- const group = ui.createElement("DIV", list, {
- style: "white-space: normal; display: none;",
- });
- const switchButton = ui.createButton(list.length.toString(), () => {
- if (group.style.display === "none") {
- group.style.removeProperty("display");
- } else {
- group.style.display = "none";
- }
- });
- // 切换过滤模式
- const switchMode = ui.createButton(
- filterMode || this.settings.filterModes[0],
- () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- this.update(id, {
- filterMode: newMode,
- });
- switchMode.innerText = newMode;
- }
- );
- // 操作
- const buttons = (() => {
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(() => {
- this.remove(id);
- table.remove(e);
- });
- });
- return ui.createButtonGroup(remove);
- })();
- return [tag, [switchButton, group], switchMode, buttons];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { ui } = this;
- const { tabs, content, settings } = ui.views;
- const { add } = settings;
- const table = ui.createTable(this.columns());
- const tab = ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- // 删除没有标记的用户
- {
- const button = ui.createButton("删除没有标记的用户", () => {
- ui.confirm().then(() => {
- const users = Object.values(this.userModule.list);
- users.forEach(({ id, tags }) => {
- if (tags.length > 0) {
- return;
- }
- this.userModule.remove(id);
- });
- });
- });
- const element = ui.createElement("DIV", button);
- add(this.constructor.order + 0, element);
- }
- // 删除没有用户的标记
- {
- const button = ui.createButton("删除没有用户的标记", () => {
- ui.confirm().then(() => {
- const items = Object.values(this.list);
- const users = Object.values(this.userModule.list);
- items.forEach(({ id }) => {
- if (users.find(({ tags }) => tags.includes(id))) {
- return;
- }
- this.remove(id);
- });
- });
- });
- const element = ui.createElement("DIV", button);
- add(this.constructor.order + 1, element);
- }
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- Object.values(this.list).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- }
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取用户信息
- const user = this.userModule.get(item.uid);
- // 没有则跳过
- if (user === null) {
- return;
- }
- // 获取用户标记
- const tags = user.tags;
- // 取最高的过滤模式
- // 低于当前的过滤模式则跳过
- let max = result.mode;
- let tag = null;
- for (const id of tags) {
- const entity = this.get({ id });
- if (entity === null) {
- continue;
- }
- // 获取过滤模式
- const mode = this.settings.getModeByName(entity.filterMode);
- if (mode <= max) {
- continue;
- }
- max = mode;
- tag = entity;
- }
- // 没有匹配的则跳过
- if (tag === null) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = max;
- result.reason = `标记: ${tag.name}`;
- }
- /**
- * 通知
- * @param {*} item 绑定的 nFilter
- */
- async notify(item) {
- const { uid, tags } = item;
- // 如果没有 tags 组件则跳过
- if (tags === null) {
- return;
- }
- // 如果是匿名,隐藏组件
- if (uid <= 0) {
- tags.style.display = "none";
- return;
- }
- // 删除旧标记
- [...tags.querySelectorAll("[tid]")].forEach((item) => {
- tags.removeChild(item);
- });
- // 获取当前用户
- const user = this.userModule.get(uid);
- // 如果没有用户,则跳过
- if (user === null) {
- return;
- }
- // 格式化标记
- const items = user.tags.map((id) => {
- const item = this.format(id);
- if (item) {
- item.setAttribute("tid", id);
- }
- return item;
- });
- // 加入组件
- items.forEach((item) => {
- if (item) {
- tags.appendChild(item);
- }
- });
- }
- /**
- * 重新过滤
- * @param {Array} users 用户集合
- */
- reFilter(users) {
- users.forEach((user) => {
- this.userModule.reFilter(user.id);
- });
- }
- }
- /**
- * 关键字模块
- */
- class KeywordModule extends Module {
- /**
- * 模块名称
- */
- static name = "keyword";
- /**
- * 模块标签
- */
- static label = "关键字";
- /**
- * 顺序
- */
- static order = 40;
- /**
- * 获取列表
- */
- get list() {
- return this.settings.keywords;
- }
- /**
- * 获取关键字
- * @param {Number} id 关键字 ID
- */
- get(id) {
- // 获取列表
- const list = this.list;
- // 如果存在,则返回信息
- if (list[id]) {
- return list[id];
- }
- return null;
- }
- /**
- * 添加关键字
- * @param {String} keyword 关键字
- * @param {String} filterMode 过滤模式
- * @param {Number} filterLevel 过滤等级: 0 - 仅过滤标题; 1 - 过滤标题和内容
- */
- add(keyword, filterMode, filterLevel) {
- // 获取列表
- const list = this.list;
- // ID 为最大值 + 1
- const id = Math.max(...Object.keys(list), 0) + 1;
- // 写入关键字信息
- list[id] = {
- id,
- keyword,
- filterMode,
- filterLevel,
- };
- // 保存数据
- this.settings.keywords = list;
- // 重新过滤
- this.reFilter();
- // 返回添加的关键字
- return list[id];
- }
- /**
- * 编辑关键字
- * @param {Number} id 关键字 ID
- * @param {*} values 关键字信息
- */
- update(id, values) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取关键字
- const entity = list[id];
- // 更新关键字
- Object.assign(entity, values);
- // 保存数据
- this.settings.keywords = list;
- // 重新过滤
- this.reFilter();
- }
- /**
- * 删除关键字
- * @param {Number} id 关键字 ID
- */
- remove(id) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取关键字
- const entity = list[id];
- // 删除关键字
- delete list[id];
- // 保存数据
- this.settings.keywords = list;
- // 重新过滤
- this.reFilter();
- // 返回删除的关键字
- return entity;
- }
- /**
- * 获取帖子数据
- * @param {*} item 绑定的 nFilter
- */
- async getPostInfo(item) {
- const { tid, pid } = item;
- // 请求帖子数据
- const { subject, content, userInfo, reputation } =
- await this.api.getPostInfo(tid, pid);
- // 绑定用户信息和声望
- if (userInfo) {
- item.userInfo = userInfo;
- item.username = userInfo.username;
- item.reputation = reputation;
- }
- // 绑定标题和内容
- item.subject = subject;
- item.content = content;
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "关键字" },
- { label: "过滤模式", center: true, width: 1 },
- { label: "包括内容", center: true, width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 标记信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { table } = this.views;
- const { id, keyword, filterLevel, filterMode } = item;
- // 关键字
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: keyword,
- });
- const inputWrapper = ui.createElement("DIV", input, {
- className: "filter-input-wrapper",
- });
- // 切换过滤模式
- const switchMode = ui.createButton(
- filterMode || this.settings.filterModes[0],
- () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- }
- );
- // 包括内容
- const switchLevel = ui.createElement("INPUT", [], {
- type: "checkbox",
- checked: filterLevel > 0,
- });
- // 操作
- const buttons = (() => {
- const save = ui.createButton("保存", () => {
- this.update(id, {
- keyword: input.value,
- filterMode: switchMode.innerText,
- filterLevel: switchLevel.checked ? 1 : 0,
- });
- });
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(() => {
- this.remove(id);
- table.remove(e);
- });
- });
- return ui.createButtonGroup(save, remove);
- })();
- return [inputWrapper, switchMode, switchLevel, buttons];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { ui } = this;
- const { tabs, content } = ui.views;
- const table = ui.createTable(this.columns());
- const tips = ui.createElement("DIV", TIPS.keyword, {
- className: "silver",
- });
- const tab = ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- this.views.container.appendChild(tips);
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- Object.values(this.list).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- this.renderNewLine();
- }
- }
- /**
- * 渲染新行
- */
- renderNewLine() {
- const { ui } = this;
- const { table } = this.views;
- // 关键字
- const input = ui.createElement("INPUT", [], {
- type: "text",
- });
- const inputWrapper = ui.createElement("DIV", input, {
- className: "filter-input-wrapper",
- });
- // 切换过滤模式
- const switchMode = ui.createButton(this.settings.filterModes[0], () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- });
- // 包括内容
- const switchLevel = ui.createElement("INPUT", [], {
- type: "checkbox",
- });
- // 操作
- const buttons = (() => {
- const save = ui.createButton("添加", (e) => {
- const entity = this.add(
- input.value,
- switchMode.innerText,
- switchLevel.checked ? 1 : 0
- );
- table.update(e, ...this.column(entity));
- this.renderNewLine();
- });
- return ui.createButtonGroup(save);
- })();
- // 添加至列表
- table.add(inputWrapper, switchMode, switchLevel, buttons);
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取列表
- const list = this.list;
- // 跳过低于当前的过滤模式
- const filtered = Object.values(list).filter(
- (item) => this.settings.getModeByName(item.filterMode) > result.mode
- );
- // 没有则跳过
- if (filtered.length === 0) {
- return;
- }
- // 根据过滤模式依次判断
- const sorted = Tools.sortBy(filtered, (item) =>
- this.settings.getModeByName(item.filterMode)
- );
- for (let i = 0; i < sorted.length; i += 1) {
- const { keyword, filterMode } = sorted[i];
- // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
- const filterLevel = sorted[i].filterLevel || 0;
- // 过滤标题
- if (filterLevel >= 0) {
- const { subject } = item;
- const match = subject.match(keyword);
- if (match) {
- const mode = this.settings.getModeByName(filterMode);
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `关键字: ${match[0]}`;
- return;
- }
- }
- // 过滤内容
- if (filterLevel >= 1) {
- // 如果没有内容,则请求
- if (item.content === undefined) {
- await this.getPostInfo(item);
- }
- const { content } = item;
- const match = content.match(keyword);
- if (match) {
- const mode = this.settings.getModeByName(filterMode);
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `关键字: ${match[0]}`;
- return;
- }
- }
- }
- }
- /**
- * 重新过滤
- */
- reFilter() {
- // 实际上应该根据过滤模式来筛选要过滤的部分
- this.data.forEach((item) => {
- item.execute();
- });
- }
- }
- /**
- * 属地模块
- */
- class LocationModule extends Module {
- /**
- * 模块名称
- */
- static name = "location";
- /**
- * 模块标签
- */
- static label = "属地";
- /**
- * 顺序
- */
- static order = 50;
- /**
- * 请求缓存
- */
- cache = {};
- /**
- * 获取列表
- */
- get list() {
- return this.settings.locations;
- }
- /**
- * 获取属地
- * @param {Number} id 属地 ID
- */
- get(id) {
- // 获取列表
- const list = this.list;
- // 如果存在,则返回信息
- if (list[id]) {
- return list[id];
- }
- return null;
- }
- /**
- * 添加属地
- * @param {String} keyword 关键字
- * @param {String} filterMode 过滤模式
- */
- add(keyword, filterMode) {
- // 获取列表
- const list = this.list;
- // ID 为最大值 + 1
- const id = Math.max(...Object.keys(list), 0) + 1;
- // 写入属地信息
- list[id] = {
- id,
- keyword,
- filterMode,
- };
- // 保存数据
- this.settings.locations = list;
- // 重新过滤
- this.reFilter();
- // 返回添加的属地
- return list[id];
- }
- /**
- * 编辑属地
- * @param {Number} id 属地 ID
- * @param {*} values 属地信息
- */
- update(id, values) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取属地
- const entity = list[id];
- // 更新属地
- Object.assign(entity, values);
- // 保存数据
- this.settings.locations = list;
- // 重新过滤
- this.reFilter();
- }
- /**
- * 删除属地
- * @param {Number} id 属地 ID
- */
- remove(id) {
- // 获取列表
- const list = this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, id) === false) {
- return null;
- }
- // 获取属地
- const entity = list[id];
- // 删除属地
- delete list[id];
- // 保存数据
- this.settings.locations = list;
- // 重新过滤
- this.reFilter();
- // 返回删除的属地
- return entity;
- }
- /**
- * 获取 IP 属地
- * @param {*} item 绑定的 nFilter
- */
- async getIpLocation(item) {
- const { uid } = item;
- // 如果是匿名直接跳过
- if (uid <= 0) {
- return;
- }
- // 如果已有缓存,直接返回
- if (Object.hasOwn(this.cache, uid)) {
- return this.cache[uid];
- }
- // 请求属地
- const { ipLoc } = await this.api.getUserInfo(uid);
- // 写入缓存
- if (ipLoc) {
- this.cache[uid] = ipLoc;
- }
- // 返回结果
- return ipLoc;
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "关键字" },
- { label: "过滤模式", center: true, width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 标记信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { table } = this.views;
- const { id, keyword, filterMode } = item;
- // 关键字
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: keyword,
- });
- const inputWrapper = ui.createElement("DIV", input, {
- className: "filter-input-wrapper",
- });
- // 切换过滤模式
- const switchMode = ui.createButton(
- filterMode || this.settings.filterModes[0],
- () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- }
- );
- // 操作
- const buttons = (() => {
- const save = ui.createButton("保存", () => {
- this.update(id, {
- keyword: input.value,
- filterMode: switchMode.innerText,
- });
- });
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(() => {
- this.remove(id);
- table.remove(e);
- });
- });
- return ui.createButtonGroup(save, remove);
- })();
- return [inputWrapper, switchMode, buttons];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { ui } = this;
- const { tabs, content } = ui.views;
- const table = ui.createTable(this.columns());
- const tips = ui.createElement("DIV", TIPS.keyword, {
- className: "silver",
- });
- const tab = ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- this.views.container.appendChild(tips);
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- Object.values(this.list).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- this.renderNewLine();
- }
- }
- /**
- * 渲染新行
- */
- renderNewLine() {
- const { ui } = this;
- const { table } = this.views;
- // 关键字
- const input = ui.createElement("INPUT", [], {
- type: "text",
- });
- const inputWrapper = ui.createElement("DIV", input, {
- className: "filter-input-wrapper",
- });
- // 切换过滤模式
- const switchMode = ui.createButton(this.settings.filterModes[0], () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- });
- // 操作
- const buttons = (() => {
- const save = ui.createButton("添加", (e) => {
- const entity = this.add(input.value, switchMode.innerText);
- table.update(e, ...this.column(entity));
- this.renderNewLine();
- });
- return ui.createButtonGroup(save);
- })();
- // 添加至列表
- table.add(inputWrapper, switchMode, buttons);
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取列表
- const list = this.list;
- // 跳过低于当前的过滤模式
- const filtered = Object.values(list).filter(
- (item) => this.settings.getModeByName(item.filterMode) > result.mode
- );
- // 没有则跳过
- if (filtered.length === 0) {
- return;
- }
- // 获取当前属地
- const location = await this.getIpLocation(item);
- // 请求失败则跳过
- if (location === undefined) {
- return;
- }
- // 根据过滤模式依次判断
- const sorted = Tools.sortBy(filtered, (item) =>
- this.settings.getModeByName(item.filterMode)
- );
- for (let i = 0; i < sorted.length; i += 1) {
- const { keyword, filterMode } = sorted[i];
- const match = location.match(keyword);
- if (match) {
- const mode = this.settings.getModeByName(filterMode);
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `属地: ${match[0]}`;
- return;
- }
- }
- }
- /**
- * 重新过滤
- */
- reFilter() {
- // 实际上应该根据过滤模式来筛选要过滤的部分
- this.data.forEach((item) => {
- item.execute();
- });
- }
- }
- /**
- * 猎巫模块
- *
- * 其实是通过 Cache 模块读取配置,而非 Settings
- */
- class HunterModule extends Module {
- /**
- * 模块名称
- */
- static name = "hunter";
- /**
- * 模块标签
- */
- static label = "猎巫";
- /**
- * 顺序
- */
- static order = 60;
- /**
- * 请求缓存
- */
- cache = {};
- /**
- * 请求队列
- */
- queue = [];
- /**
- * 获取列表
- */
- get list() {
- return this.settings.cache
- .get("WITCH_HUNT")
- .then((values) => values || []);
- }
- /**
- * 获取猎巫
- * @param {Number} id 猎巫 ID
- */
- async get(id) {
- // 获取列表
- const list = await this.list;
- // 如果存在,则返回信息
- if (list[id]) {
- return list[id];
- }
- return null;
- }
- /**
- * 添加猎巫
- * @param {Number} fid 版面 ID
- * @param {String} label 标签
- * @param {String} filterMode 过滤模式
- * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
- */
- async add(fid, label, filterMode, filterLevel) {
- // FID 只能是数字
- fid = parseInt(fid, 10);
- // 获取列表
- const list = await this.list;
- // 如果版面 ID 已存在,则提示错误
- if (Object.keys(list).includes(fid)) {
- alert("已有相同版面ID");
- return;
- }
- // 请求版面信息
- const info = await this.api.getForumInfo(fid);
- // 如果版面不存在,则提示错误
- if (info === null) {
- alert("版面ID有误");
- return;
- }
- // 计算标记颜色
- const color = Tools.generateColor(info.name);
- // 写入猎巫信息
- list[fid] = {
- fid,
- name: info.name,
- label,
- color,
- filterMode,
- filterLevel,
- };
- // 保存数据
- this.settings.cache.put("WITCH_HUNT", list);
- // 重新过滤
- this.reFilter(true);
- // 返回添加的猎巫
- return list[fid];
- }
- /**
- * 编辑猎巫
- * @param {Number} fid 版面 ID
- * @param {*} values 猎巫信息
- */
- async update(fid, values) {
- // 获取列表
- const list = await this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, fid) === false) {
- return null;
- }
- // 获取猎巫
- const entity = list[fid];
- // 更新猎巫
- Object.assign(entity, values);
- // 保存数据
- this.settings.cache.put("WITCH_HUNT", list);
- // 重新过滤,更新样式即可
- this.reFilter(false);
- }
- /**
- * 删除猎巫
- * @param {Number} fid 版面 ID
- */
- async remove(fid) {
- // 获取列表
- const list = await this.list;
- // 如果不存在则跳过
- if (Object.hasOwn(list, fid) === false) {
- return null;
- }
- // 获取猎巫
- const entity = list[fid];
- // 删除猎巫
- delete list[fid];
- // 保存数据
- this.settings.cache.put("WITCH_HUNT", list);
- // 重新过滤
- this.reFilter(true);
- // 返回删除的属地
- return entity;
- }
- /**
- * 格式化版面
- * @param {Number} fid 版面 ID
- * @param {String} name 版面名称
- */
- formatForum(fid, name) {
- const { ui } = this;
- return ui.createElement("A", `[${name}]`, {
- className: "b nobr",
- href: `/thread.php?fid=${fid}`,
- });
- }
- /**
- * 格式化标签
- * @param {String} name 标签名称
- * @param {String} name 标签颜色
- */
- formatLabel(name, color) {
- const { ui } = this;
- return ui.createElement("B", name, {
- className: "block_txt nobr",
- style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
- });
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "版面", width: 200 },
- { label: "标签" },
- { label: "启用过滤", center: true, width: 1 },
- { label: "过滤模式", center: true, width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 标记信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
- const { table } = this.views;
- const { fid, name, label, color, filterMode, filterLevel } = item;
- // 版面
- const forum = this.formatForum(fid, name);
- // 标签
- const labelElement = this.formatLabel(label, color);
- // 启用过滤
- const switchLevel = ui.createElement("INPUT", [], {
- type: "checkbox",
- checked: filterLevel > 0,
- });
- // 切换过滤模式
- const switchMode = ui.createButton(
- filterMode || this.settings.filterModes[0],
- () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- }
- );
- // 操作
- const buttons = (() => {
- const save = ui.createButton("保存", () => {
- this.update(fid, {
- filterMode: switchMode.innerText,
- filterLevel: switchLevel.checked ? 1 : 0,
- });
- });
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(async () => {
- await this.remove(fid);
- table.remove(e);
- });
- });
- return ui.createButtonGroup(save, remove);
- })();
- return [forum, labelElement, switchLevel, switchMode, buttons];
- }
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
- const { ui } = this;
- const { tabs, content } = ui.views;
- const table = ui.createTable(this.columns());
- const tips = ui.createElement("DIV", TIPS.hunter, {
- className: "silver",
- });
- const tab = ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
- Object.assign(this.views, {
- tab,
- table,
- });
- this.views.container.appendChild(table);
- this.views.container.appendChild(tips);
- }
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
- const { table } = this.views;
- if (table) {
- const { add, clear } = table;
- clear();
- this.list.then((values) => {
- Object.values(values).forEach((item) => {
- const column = this.column(item);
- add(...column);
- });
- this.renderNewLine();
- });
- }
- }
- /**
- * 渲染新行
- */
- renderNewLine() {
- const { ui } = this;
- const { table } = this.views;
- // 版面 ID
- const forumInput = ui.createElement("INPUT", [], {
- type: "text",
- });
- const forumInputWrapper = ui.createElement("DIV", forumInput, {
- className: "filter-input-wrapper",
- });
- // 标签
- const labelInput = ui.createElement("INPUT", [], {
- type: "text",
- });
- const labelInputWrapper = ui.createElement("DIV", labelInput, {
- className: "filter-input-wrapper",
- });
- // 启用过滤
- const switchLevel = ui.createElement("INPUT", [], {
- type: "checkbox",
- });
- // 切换过滤模式
- const switchMode = ui.createButton(this.settings.filterModes[0], () => {
- const newMode = this.settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- });
- // 操作
- const buttons = (() => {
- const save = ui.createButton("添加", async (e) => {
- const entity = await this.add(
- forumInput.value,
- labelInput.value,
- switchMode.innerText,
- switchLevel.checked ? 1 : 0
- );
- table.update(e, ...this.column(entity));
- this.renderNewLine();
- });
- return ui.createButtonGroup(save);
- })();
- // 添加至列表
- table.add(
- forumInputWrapper,
- labelInputWrapper,
- switchLevel,
- switchMode,
- buttons
- );
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取当前猎巫结果
- const hunter = item.hunter || [];
- // 如果没有猎巫结果,则跳过
- if (hunter.length === 0) {
- return;
- }
- // 获取列表
- const items = await this.list;
- // 筛选出匹配的猎巫
- const list = Object.values(items).filter(({ fid }) =>
- hunter.includes(fid)
- );
- // 取最高的过滤模式
- // 低于当前的过滤模式则跳过
- let max = result.mode;
- let res = null;
- for (const entity of list) {
- const { filterLevel, filterMode } = entity;
- // 仅标记
- if (filterLevel === 0) {
- continue;
- }
- // 获取过滤模式
- const mode = this.settings.getModeByName(filterMode);
- if (mode <= max) {
- continue;
- }
- max = mode;
- res = entity;
- }
- // 没有匹配的则跳过
- if (res === null) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = max;
- result.reason = `猎巫: ${res.label}`;
- }
- /**
- * 通知
- * @param {*} item 绑定的 nFilter
- */
- async notify(item) {
- const { uid, tags } = item;
- // 如果没有 tags 组件则跳过
- if (tags === null) {
- return;
- }
- // 如果是匿名,隐藏组件
- if (uid <= 0) {
- tags.style.display = "none";
- return;
- }
- // 删除旧标签
- [...tags.querySelectorAll("[fid]")].forEach((item) => {
- tags.removeChild(item);
- });
- // 如果没有请求,开始请求
- if (Object.hasOwn(item, "hunter") === false) {
- this.execute(item);
- return;
- }
- // 获取当前猎巫结果
- const hunter = item.hunter;
- // 如果没有猎巫结果,则跳过
- if (hunter.length === 0) {
- return;
- }
- // 格式化标签
- const items = await Promise.all(
- hunter.map(async (fid) => {
- const item = await this.get(fid);
- if (item) {
- const element = this.formatLabel(item.label, item.color);
- element.setAttribute("fid", fid);
- return element;
- }
- return null;
- })
- );
- // 加入组件
- items.forEach((item) => {
- if (item) {
- tags.appendChild(item);
- }
- });
- }
- /**
- * 重新过滤
- * @param {Boolean} clear 是否清除缓存
- */
- reFilter(clear) {
- // 清除缓存
- if (clear) {
- this.cache = {};
- }
- // 重新过滤
- this.data.forEach((item) => {
- // 不需要清除缓存的话,只要重新加载标记
- if (clear === false) {
- item.hunter = [];
- }
- // 重新猎巫
- this.execute(item);
- });
- }
- /**
- * 猎巫
- * @param {*} item 绑定的 nFilter
- */
- async execute(item) {
- const { uid } = item;
- const { api, cache, queue, list } = this;
- // 如果是匿名,则跳过
- if (uid <= 0) {
- return;
- }
- // 初始化猎巫结果,用于标识正在猎巫
- item.hunter = item.hunter || [];
- // 获取列表
- const items = await list;
- // 没有设置且没有旧数据,直接跳过
- if (items.length === 0 && item.hunter.length === 0) {
- return;
- }
- // 重新过滤
- const reload = (newValue) => {
- const isEqual = newValue.sort().join() === item.hunter.sort().join();
- if (isEqual) {
- return;
- }
- item.hunter = newValue;
- item.execute();
- };
- // 创建任务
- const task = async () => {
- // 如果缓存里没有记录,请求数据并写入缓存
- if (Object.hasOwn(cache, uid) === false) {
- cache[uid] = [];
- await Promise.all(
- Object.keys(items).map(async (fid) => {
- // 转换为数字格式
- const id = parseInt(fid, 10);
- // 当前版面发言记录
- const result = await api.getForumPosted(id, uid);
- // 写入当前设置
- if (result) {
- cache[uid].push(id);
- }
- })
- );
- }
- // 重新过滤
- reload(cache[uid]);
- // 将当前任务移出队列
- queue.shift();
- // 如果还有任务,继续执行
- if (queue.length > 0) {
- queue[0]();
- }
- };
- // 队列里已经有任务
- const isRunning = queue.length > 0;
- // 加入队列
- queue.push(task);
- // 如果没有正在执行的任务,则立即执行
- if (isRunning === false) {
- task();
- }
- }
- }
- /**
- * 杂项模块
- */
- class MiscModule extends Module {
- /**
- * 模块名称
- */
- static name = "misc";
- /**
- * 模块标签
- */
- static label = "杂项";
- /**
- * 顺序
- */
- static order = 100;
- /**
- * 请求缓存
- */
- cache = {
- topicNums: {},
- };
- /**
- * 获取用户信息(从页面上)
- * @param {*} item 绑定的 nFilter
- */
- getUserInfo(item) {
- const { uid } = item;
- // 如果是匿名直接跳过
- if (uid <= 0) {
- return;
- }
- // 回复页面可以直接获取到用户信息和声望
- if (commonui.userInfo) {
- // 取得用户信息
- const userInfo = commonui.userInfo.users[uid];
- // 绑定用户信息和声望
- if (userInfo) {
- item.userInfo = userInfo;
- item.username = userInfo.username;
- item.reputation = (() => {
- const reputations = commonui.userInfo.reputations;
- if (reputations) {
- for (let fid in reputations) {
- return reputations[fid][uid] || 0;
- }
- }
- return NaN;
- })();
- }
- }
- }
- /**
- * 获取帖子数据
- * @param {*} item 绑定的 nFilter
- */
- async getPostInfo(item) {
- const { tid, pid } = item;
- // 请求帖子数据
- const { subject, content, userInfo, reputation } =
- await this.api.getPostInfo(tid, pid);
- // 绑定用户信息和声望
- if (userInfo) {
- item.userInfo = userInfo;
- item.username = userInfo.username;
- item.reputation = reputation;
- }
- // 绑定标题和内容
- item.subject = subject;
- item.content = content;
- }
- /**
- * 获取主题数量
- * @param {*} item 绑定的 nFilter
- */
- async getTopicNum(item) {
- const { uid } = item;
- // 如果是匿名直接跳过
- if (uid <= 0) {
- return;
- }
- // 如果已有缓存,直接返回
- if (Object.hasOwn(this.cache.topicNums, uid)) {
- return this.cache.topicNums[uid];
- }
- // 请求数量
- const number = await this.api.getTopicNum(uid);
- // 写入缓存
- this.cache.topicNums[uid] = number;
- // 返回结果
- return number;
- }
- /**
- * 初始化,增加设置
- */
- initComponents() {
- super.initComponents();
- const { settings, ui } = this;
- const { add } = ui.views.settings;
- // 小号过滤(注册时间)
- {
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: settings.filterRegdateLimit / 86400000,
- maxLength: 4,
- style: "width: 48px;",
- });
- const button = ui.createButton("确认", () => {
- const newValue = parseInt(input.value, 10) || 0;
- if (newValue < 0) {
- return;
- }
- settings.filterRegdateLimit = newValue * 86400000;
- this.reFilter();
- });
- const element = ui.createElement("DIV", [
- "隐藏注册时间小于",
- input,
- "天的用户",
- button,
- ]);
- add(this.constructor.order + 0, element);
- }
- // 小号过滤(发帖数)
- {
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: settings.filterPostnumLimit,
- maxLength: 5,
- style: "width: 48px;",
- });
- const button = ui.createButton("确认", () => {
- const newValue = parseInt(input.value, 10) || 0;
- if (newValue < 0) {
- return;
- }
- settings.filterPostnumLimit = newValue;
- this.reFilter();
- });
- const element = ui.createElement("DIV", [
- "隐藏发帖数量小于",
- input,
- "贴的用户",
- button,
- ]);
- add(this.constructor.order + 1, element);
- }
- // 流量号过滤(主题比例)
- {
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: settings.filterTopicRateLimit,
- maxLength: 3,
- style: "width: 48px;",
- });
- const button = ui.createButton("确认", () => {
- const newValue = parseInt(input.value, 10) || 100;
- if (newValue <= 0 || newValue > 100) {
- return;
- }
- settings.filterTopicRateLimit = newValue;
- this.reFilter();
- });
- const element = ui.createElement("DIV", [
- "隐藏发帖比例大于",
- input,
- "%的用户",
- button,
- ]);
- add(this.constructor.order + 2, element);
- }
- // 声望过滤
- {
- const input = ui.createElement("INPUT", [], {
- type: "text",
- value: settings.filterReputationLimit || "",
- maxLength: 4,
- style: "width: 48px;",
- });
- const button = ui.createButton("确认", () => {
- const newValue = parseInt(input.value, 10);
- settings.filterReputationLimit = newValue;
- this.reFilter();
- });
- const element = ui.createElement("DIV", [
- "隐藏版面声望低于",
- input,
- "点的用户",
- button,
- ]);
- add(this.constructor.order + 3, element);
- }
- // 匿名过滤
- {
- const input = ui.createElement("INPUT", [], {
- type: "checkbox",
- checked: settings.filterAnonymous,
- });
- const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
- style: "display: flex;",
- });
- const element = ui.createElement("DIV", label);
- input.onchange = () => {
- settings.filterAnonymous = input.checked;
- this.reFilter();
- };
- add(this.constructor.order + 4, element);
- }
- }
- /**
- * 过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filter(item, result) {
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 匿名过滤
- await this.filterByAnonymous(item, result);
- // 注册时间过滤
- await this.filterByRegdate(item, result);
- // 发帖数量过滤
- await this.filterByPostnum(item, result);
- // 发帖比例过滤
- await this.filterByTopicRate(item, result);
- // 版面声望过滤
- await this.filterByReputation(item, result);
- }
- /**
- * 根据匿名过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filterByAnonymous(item, result) {
- const { uid } = item;
- // 如果不是匿名,则跳过
- if (uid > 0) {
- return;
- }
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 获取过滤匿名设置
- const filterAnonymous = this.settings.filterAnonymous;
- if (filterAnonymous) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = "匿名";
- }
- }
- /**
- * 根据注册时间过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filterByRegdate(item, result) {
- const { uid } = item;
- // 如果是匿名,则跳过
- if (uid <= 0) {
- return;
- }
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 获取注册时间限制
- const filterRegdateLimit = this.settings.filterRegdateLimit;
- // 未启用则跳过
- if (filterRegdateLimit <= 0) {
- return;
- }
- // 没有用户信息,优先从页面上获取
- if (item.userInfo === undefined) {
- this.getUserInfo(item);
- }
- // 没有再从接口获取
- if (item.userInfo === undefined) {
- await this.getPostInfo(item);
- }
- // 获取注册时间
- const { regdate } = item.userInfo || {};
- // 获取失败则跳过
- if (regdate === undefined) {
- return;
- }
- // 转换时间格式,泥潭接口只精确到秒
- const date = new Date(regdate * 1000);
- // 判断是否符合条件
- if (Date.now() - date > filterRegdateLimit) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `注册时间: ${date.toLocaleDateString()}`;
- }
- /**
- * 根据发帖数量过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filterByPostnum(item, result) {
- const { uid } = item;
- // 如果是匿名,则跳过
- if (uid <= 0) {
- return;
- }
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 获取发帖数量限制
- const filterPostnumLimit = this.settings.filterPostnumLimit;
- // 未启用则跳过
- if (filterPostnumLimit <= 0) {
- return;
- }
- // 没有用户信息,优先从页面上获取
- if (item.userInfo === undefined) {
- this.getUserInfo(item);
- }
- // 没有再从接口获取
- if (item.userInfo === undefined) {
- await this.getPostInfo(item);
- }
- // 获取发帖数量
- const { postnum } = item.userInfo || {};
- // 获取失败则跳过
- if (postnum === undefined) {
- return;
- }
- // 判断是否符合条件
- if (postnum >= filterPostnumLimit) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `发帖数量: ${postnum}`;
- }
- /**
- * 根据发帖比例过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filterByTopicRate(item, result) {
- const { uid } = item;
- // 如果是匿名,则跳过
- if (uid <= 0) {
- return;
- }
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 获取发帖比例限制
- const filterTopicRateLimit = this.settings.filterTopicRateLimit;
- // 未启用则跳过
- if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
- return;
- }
- // 没有用户信息,优先从页面上获取
- if (item.userInfo === undefined) {
- this.getUserInfo(item);
- }
- // 没有再从接口获取
- if (item.userInfo === undefined) {
- await this.getPostInfo(item);
- }
- // 获取发帖数量
- const { postnum } = item.userInfo || {};
- // 获取失败则跳过
- if (postnum === undefined) {
- return;
- }
- // 获取主题数量
- const topicNum = await this.getTopicNum(item);
- // 计算发帖比例
- const topicRate = Math.ceil((topicNum / postnum) * 100);
- // 判断是否符合条件
- if (topicRate < filterTopicRateLimit) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
- }
- /**
- * 根据版面声望过滤
- * @param {*} item 绑定的 nFilter
- * @param {*} result 过滤结果
- */
- async filterByReputation(item, result) {
- const { uid } = item;
- // 如果是匿名,则跳过
- if (uid <= 0) {
- return;
- }
- // 获取隐藏模式下标
- const mode = this.settings.getModeByName("隐藏");
- // 如果当前模式不低于隐藏模式,则跳过
- if (result.mode >= mode) {
- return;
- }
- // 获取版面声望限制
- const filterReputationLimit = this.settings.filterReputationLimit;
- // 未启用则跳过
- if (Number.isNaN(filterReputationLimit)) {
- return;
- }
- // 没有声望信息,优先从页面上获取
- if (item.reputation === undefined) {
- this.getUserInfo(item);
- }
- // 没有再从接口获取
- if (item.reputation === undefined) {
- await this.getPostInfo(item);
- }
- // 获取版面声望
- const reputation = item.reputation || 0;
- // 判断是否符合条件
- if (reputation >= filterReputationLimit) {
- return;
- }
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `版面声望: ${reputation}`;
- }
- /**
- * 重新过滤
- */
- reFilter() {
- this.data.forEach((item) => {
- item.execute();
- });
- }
- }
- /**
- * 设置模块
- */
- class SettingsModule extends Module {
- /**
- * 模块名称
- */
- static name = "settings";
- /**
- * 顺序
- */
- static order = 0;
- /**
- * 创建实例
- * @param {Settings} settings 设置
- * @param {API} api API
- * @param {UI} ui UI
- * @param {Array} data 过滤列表
- * @returns {Module | null} 成功后返回模块实例
- */
- static create(settings, api, ui, data) {
- // 读取设置里的模块列表
- const modules = settings.modules;
- // 如果不包含自己,加入列表中,因为设置模块是必须的
- if (modules.includes(this.name) === false) {
- settings.modules = [...modules, this.name];
- }
- // 创建实例
- return super.create(settings, api, ui, data);
- }
- /**
- * 初始化,增加设置
- */
- initComponents() {
- super.initComponents();
- const { settings, ui } = this;
- const { add } = ui.views.settings;
- // 前置过滤
- {
- const input = ui.createElement("INPUT", [], {
- type: "checkbox",
- });
- const label = ui.createElement("LABEL", ["前置过滤", input], {
- style: "display: flex;",
- });
- settings.preFilterEnabled.then((checked) => {
- input.checked = checked;
- input.onchange = () => {
- settings.preFilterEnabled = !checked;
- };
- });
- add(this.constructor.order + 0, label);
- }
- // 模块选择
- {
- const modules = [
- ListModule,
- UserModule,
- TagModule,
- KeywordModule,
- LocationModule,
- HunterModule,
- MiscModule,
- ];
- const items = modules.map((item) => {
- const input = ui.createElement("INPUT", [], {
- type: "checkbox",
- value: item.name,
- checked: settings.modules.includes(item.name),
- onchange: () => {
- const checked = input.checked;
- modules.map((m, index) => {
- const isDepend = checked
- ? item.depends.find((i) => i.name === m.name)
- : m.depends.find((i) => i.name === item.name);
- if (isDepend) {
- const element = items[index].querySelector("INPUT");
- if (element) {
- element.checked = checked;
- }
- }
- });
- },
- });
- const label = ui.createElement("LABEL", [item.label, input], {
- style: "display: flex; margin-right: 10px;",
- });
- return label;
- });
- const button = ui.createButton("确认", () => {
- const checked = group.querySelectorAll("INPUT:checked");
- const values = [...checked].map((item) => item.value);
- settings.modules = values;
- location.reload();
- });
- const group = ui.createElement("DIV", [...items, button], {
- style: "display: flex;",
- });
- const label = ui.createElement("LABEL", "启用模块");
- add(this.constructor.order + 1, label, group);
- }
- // 默认过滤模式
- {
- const modes = ["标记", "遮罩", "隐藏"].map((item) => {
- const input = ui.createElement("INPUT", [], {
- type: "radio",
- name: "defaultFilterMode",
- value: item,
- checked: settings.defaultFilterMode === item,
- onchange: () => {
- settings.defaultFilterMode = item;
- this.reFilter();
- },
- });
- const label = ui.createElement("LABEL", [item, input], {
- style: "display: flex; margin-right: 10px;",
- });
- return label;
- });
- const group = ui.createElement("DIV", modes, {
- style: "display: flex;",
- });
- const label = ui.createElement("LABEL", "默认过滤模式");
- const tips = ui.createElement("DIV", TIPS.filterMode, {
- className: "silver",
- });
- add(this.constructor.order + 2, label, group, tips);
- }
- }
- /**
- * 重新过滤
- */
- reFilter() {
- // 目前仅在修改默认过滤模式时重新过滤
- this.data.forEach((item) => {
- // 如果过滤模式是继承,则重新过滤
- if (item.filterMode === "继承") {
- item.execute();
- }
- // 如果有引用,也重新过滤
- if (Object.values(item.quotes || {}).includes("继承")) {
- item.execute();
- return;
- }
- });
- }
- }
- /**
- * 增强的列表模块,增加了用户作为附加模块
- */
- class ListEnhancedModule extends ListModule {
- /**
- * 模块名称
- */
- static name = "list";
- /**
- * 附加模块
- */
- static addons = [UserModule];
- /**
- * 附加的用户模块
- * @returns {UserModule} 用户模块
- */
- get userModule() {
- return this.addons[UserModule.name];
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- const hasAddon = this.hasAddon(UserModule);
- if (hasAddon === false) {
- return super.columns();
- }
- return [
- { label: "用户", width: 1 },
- { label: "内容", ellipsis: true },
- { label: "过滤模式", center: true, width: 1 },
- { label: "原因", width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 绑定的 nFilter
- * @returns {Array} 表格项集合
- */
- column(item) {
- const column = super.column(item);
- const hasAddon = this.hasAddon(UserModule);
- if (hasAddon === false) {
- return column;
- }
- const { ui } = this;
- const { table } = this.views;
- const { uid, username } = item;
- const user = this.userModule.format(uid, username);
- const buttons = (() => {
- if (uid <= 0) {
- return null;
- }
- const block = ui.createButton("屏蔽", (e) => {
- this.userModule.renderDetails(uid, username, (type) => {
- // 删除失效数据,等待重新过滤
- table.remove(e);
- // 如果是新增,不会因为用户重新过滤,需要主动触发
- if (type === "ADD") {
- this.userModule.reFilter(uid);
- }
- });
- });
- return ui.createButtonGroup(block);
- })();
- return [user, ...column, buttons];
- }
- }
- /**
- * 增强的用户模块,增加了标记作为附加模块
- */
- class UserEnhancedModule extends UserModule {
- /**
- * 模块名称
- */
- static name = "user";
- /**
- * 附加模块
- */
- static addons = [TagModule];
- /**
- * 附加的标记模块
- * @returns {TagModule} 标记模块
- */
- get tagModule() {
- return this.addons[TagModule.name];
- }
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- const hasAddon = this.hasAddon(TagModule);
- if (hasAddon === false) {
- return super.columns();
- }
- return [
- { label: "昵称", width: 1 },
- { label: "标记" },
- { label: "过滤模式", center: true, width: 1 },
- { label: "操作", width: 1 },
- ];
- }
- /**
- * 表格项
- * @param {*} item 用户信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const column = super.column(item);
- const hasAddon = this.hasAddon(TagModule);
- if (hasAddon === false) {
- return column;
- }
- const { ui } = this;
- const { table } = this.views;
- const { id, name } = item;
- const tags = ui.createElement(
- "DIV",
- item.tags.map((id) => this.tagModule.format(id))
- );
- const newColumn = [...column];
- newColumn.splice(1, 0, tags);
- const buttons = column[column.length - 1];
- const update = ui.createButton("编辑", (e) => {
- this.renderDetails(id, name, (type, newValue) => {
- if (type === "UPDATE") {
- table.update(e, ...this.column(newValue));
- }
- if (type === "REMOVE") {
- table.remove(e);
- }
- });
- });
- buttons.insertBefore(update, buttons.firstChild);
- return newColumn;
- }
- /**
- * 渲染详情
- * @param {Number} uid 用户 ID
- * @param {String | undefined} name 用户名称
- * @param {Function} callback 回调函数
- */
- renderDetails(uid, name, callback = () => {}) {
- const hasAddon = this.hasAddon(TagModule);
- if (hasAddon === false) {
- return super.renderDetails(uid, name, callback);
- }
- const { ui, settings } = this;
- // 只允许同时存在一个详情页
- if (this.views.details) {
- if (this.views.details.parentNode) {
- this.views.details.parentNode.removeChild(this.views.details);
- }
- }
- // 获取用户信息
- const user = this.get(uid);
- if (user) {
- name = user.name;
- }
- // TODO 需要优化
- const title =
- (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
- const table = ui.createTable([]);
- {
- const size = Math.floor((screen.width * 0.8) / 200);
- const items = Object.values(this.tagModule.list).map(({ id }) => {
- const checked = user && user.tags.includes(id) ? "checked" : "";
- return `
- <td class="c1">
- <label for="s-tag-${id}" style="display: block; cursor: pointer;">
- ${this.tagModule.format(id).outerHTML}
- </label>
- </td>
- <td class="c2" width="1">
- <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
- </td>
- `;
- });
- const rows = [...new Array(Math.ceil(items.length / size))].map(
- (_, index) => `
- <tr class="row${(index % 2) + 1}">
- ${items.slice(size * index, size * (index + 1)).join("")}
- </tr>
- `
- );
- table.querySelector("TBODY").innerHTML = rows.join("");
- }
- const input = ui.createElement("INPUT", [], {
- type: "text",
- placeholder: TIPS.addTags,
- style: "width: -webkit-fill-available;",
- });
- const inputWrapper = ui.createElement("DIV", input, {
- style: "margin-top: 10px;",
- });
- const filterMode = user ? user.filterMode : settings.filterModes[0];
- const switchMode = ui.createButton(filterMode, () => {
- const newMode = settings.switchModeByName(switchMode.innerText);
- switchMode.innerText = newMode;
- });
- const buttons = ui.createElement(
- "DIV",
- (() => {
- const remove = user
- ? ui.createButton("删除", () => {
- ui.confirm().then(() => {
- this.remove(uid);
- this.views.details._.hide();
- callback("REMOVE");
- });
- })
- : null;
- const save = ui.createButton("保存", () => {
- const checked = [...table.querySelectorAll("INPUT:checked")].map(
- (input) => parseInt(input.value, 10)
- );
- const newTags = input.value
- .split("|")
- .filter((item) => item.length)
- .map((item) => this.tagModule.add(item))
- .filter((tag) => tag !== null)
- .map((tag) => tag.id);
- const tags = [...new Set([...checked, ...newTags])].sort();
- if (user === null) {
- const entity = this.add(uid, {
- id: uid,
- name,
- tags,
- filterMode: switchMode.innerText,
- });
- this.views.details._.hide();
- callback("ADD", entity);
- } else {
- const entity = this.update(uid, {
- name,
- tags,
- filterMode: switchMode.innerText,
- });
- this.views.details._.hide();
- callback("UPDATE", entity);
- }
- });
- return ui.createButtonGroup(remove, save);
- })(),
- {
- className: "right_",
- }
- );
- const actions = ui.createElement(
- "DIV",
- [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
- {
- style: "margin-top: 10px;",
- }
- );
- const tips = ui.createElement("DIV", TIPS.filterMode, {
- className: "silver",
- style: "margin-top: 10px;",
- });
- const content = ui.createElement(
- "DIV",
- [table, inputWrapper, actions, tips],
- {
- style: "width: 80vw",
- }
- );
- // 创建弹出框
- this.views.details = ui.createDialog(null, title, content);
- }
- }
- /**
- * 处理 topicArg 模块
- * @param {Filter} filter 过滤器
- * @param {*} value commonui.topicArg
- */
- const handleTopicModule = async (filter, value) => {
- // 绑定主题模块
- topicModule = value;
- // 是否启用前置过滤
- const preFilterEnabled = await filter.settings.preFilterEnabled;
- // 前置过滤
- // 先直接隐藏,等过滤完毕后再放出来
- const beforeGet = (...args) => {
- if (preFilterEnabled) {
- // 主题标题
- const title = document.getElementById(args[1]);
- // 主题容器
- const container = title.closest("tr");
- // 隐藏元素
- container.style.display = "none";
- }
- return args;
- };
- // 过滤
- const afterGet = (_, args) => {
- // 主题 ID
- const tid = args[8];
- // 找到对应数据
- const data = topicModule.data.find((item) => item[8] === tid);
- // 开始过滤
- if (data) {
- filter.filterTopic(data);
- }
- };
- // 如果已经有数据,则直接过滤
- Object.values(topicModule.data).forEach(filter.filterTopic);
- // 拦截 add 函数,这是泥潭的主题添加事件
- Tools.interceptProperty(topicModule, "add", {
- beforeGet,
- afterGet,
- });
- };
- /**
- * 处理 postArg 模块
- * @param {Filter} filter 过滤器
- * @param {*} value commonui.postArg
- */
- const handleReplyModule = async (filter, value) => {
- // 绑定回复模块
- replyModule = value;
- // 是否启用前置过滤
- const preFilterEnabled = await filter.settings.preFilterEnabled;
- // 前置过滤
- // 先直接隐藏,等过滤完毕后再放出来
- const beforeGet = (...args) => {
- if (preFilterEnabled) {
- // 楼层号
- const index = args[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";
- }
- return args;
- };
- // 过滤
- const afterGet = (_, args) => {
- // 楼层号
- const index = args[0];
- // 找到对应数据
- const data = replyModule.data[index];
- // 开始过滤
- if (data) {
- filter.filterReply(data);
- }
- };
- // 如果已经有数据,则直接过滤
- Object.values(replyModule.data).forEach(filter.filterReply);
- // 拦截 proc 函数,这是泥潭的回复添加事件
- Tools.interceptProperty(replyModule, "proc", {
- beforeGet,
- afterGet,
- });
- };
- /**
- * 处理 commonui 模块
- * @param {Filter} filter 过滤器
- * @param {*} value commonui
- */
- const handleCommonui = (filter, value) => {
- // 绑定主模块
- commonui = value;
- // 拦截 mainMenu 模块,UI 需要在 init 后加载
- Tools.interceptProperty(commonui, "mainMenu", {
- afterSet: (value) => {
- Tools.interceptProperty(value, "init", {
- afterGet: () => {
- filter.ui.render();
- },
- afterSet: () => {
- filter.ui.render();
- },
- });
- },
- });
- // 拦截 topicArg 模块,这是泥潭的主题入口
- Tools.interceptProperty(commonui, "topicArg", {
- afterSet: (value) => {
- handleTopicModule(filter, value);
- },
- });
- // 拦截 postArg 模块,这是泥潭的回复入口
- Tools.interceptProperty(commonui, "postArg", {
- afterSet: (value) => {
- handleReplyModule(filter, value);
- },
- });
- };
- /**
- * 注册脚本菜单
- * @param {Settings} settings 设置
- */
- const registerMenu = async (settings) => {
- // 修改 UA
- {
- const userAgent = await settings.userAgent;
- GM_registerMenuCommand(`修改UA:${userAgent}`, () => {
- const value = prompt("修改UA", userAgent);
- if (value) {
- settings.userAgent = value;
- }
- });
- }
- // 前置过滤
- {
- const enabled = await settings.preFilterEnabled;
- GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
- settings.preFilterEnabled = !enabled;
- });
- }
- };
- // 主函数
- (async () => {
- // 初始化缓存、设置
- const cache = new Cache(API.modules);
- const settings = new Settings(cache);
- // 读取设置
- await settings.load();
- // 初始化 API、UI
- const api = new API(cache, settings);
- const ui = new UI(settings, api);
- // 初始化过滤器
- const filter = new Filter(settings, api, ui);
- // 加载模块
- filter.initModules(
- SettingsModule,
- ListEnhancedModule,
- UserEnhancedModule,
- TagModule,
- KeywordModule,
- LocationModule,
- HunterModule,
- MiscModule
- );
- // 注册脚本菜单
- registerMenu(settings);
- // 处理 commonui 模块
- if (unsafeWindow.commonui) {
- handleCommonui(filter, unsafeWindow.commonui);
- return;
- }
- Tools.interceptProperty(unsafeWindow, "commonui", {
- afterSet: (value) => {
- handleCommonui(filter, value);
- },
- });
- })();
- })();