NGA Filter

NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。

当前为 2024-01-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 2.2.0
  5. // @author snyssss
  6. // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
  7. // @license MIT
  8.  
  9. // @match *://bbs.nga.cn/*
  10. // @match *://ngabbs.com/*
  11. // @match *://nga.178.com/*
  12.  
  13. // @grant GM_addStyle
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_registerMenuCommand
  17. // @grant unsafeWindow
  18.  
  19. // @run-at document-start
  20. // @noframes
  21. // ==/UserScript==
  22.  
  23. (() => {
  24. // 声明泥潭主模块、主题模块、回复模块
  25. let commonui, topicModule, replyModule;
  26.  
  27. // KEY
  28. const DATA_KEY = "NGAFilter";
  29. const USER_AGENT_KEY = "USER_AGENT_KEY";
  30. const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  31. const CLEAR_TIME_KEY = "CLEAR_TIME_KEY";
  32.  
  33. // TIPS
  34. const TIPS = {
  35. filterMode:
  36. "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
  37. addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
  38. keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
  39. hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
  40. };
  41.  
  42. // STYLE
  43. GM_addStyle(`
  44. .filter-table-wrapper {
  45. max-height: 80vh;
  46. overflow-y: auto;
  47. }
  48. .filter-table {
  49. margin: 0;
  50. }
  51. .filter-table th,
  52. .filter-table td {
  53. position: relative;
  54. white-space: nowrap;
  55. }
  56. .filter-table th {
  57. position: sticky;
  58. top: 2px;
  59. z-index: 1;
  60. }
  61. .filter-table input:not([type]), .filter-table input[type="text"] {
  62. margin: 0;
  63. box-sizing: border-box;
  64. height: 100%;
  65. width: 100%;
  66. }
  67. .filter-input-wrapper {
  68. position: absolute;
  69. top: 6px;
  70. right: 6px;
  71. bottom: 6px;
  72. left: 6px;
  73. }
  74. .filter-text-ellipsis {
  75. display: flex;
  76. }
  77. .filter-text-ellipsis > * {
  78. flex: 1;
  79. width: 1px;
  80. overflow: hidden;
  81. text-overflow: ellipsis;
  82. }
  83. .filter-button-group {
  84. margin: -.1em -.2em;
  85. }
  86. .filter-tags {
  87. margin: 2px -0.2em 0;
  88. text-align: left;
  89. }
  90. .filter-mask {
  91. margin: 1px;
  92. color: #81C7D4;
  93. background: #81C7D4;
  94. }
  95. .filter-mask-block {
  96. display: block;
  97. border: 1px solid #66BAB7;
  98. text-align: center !important;
  99. }
  100. .filter-input-wrapper {
  101. position: absolute;
  102. top: 6px;
  103. right: 6px;
  104. bottom: 6px;
  105. left: 6px;
  106. }
  107. `);
  108.  
  109. /**
  110. * 工具类
  111. */
  112. class Tools {
  113. /**
  114. * 返回当前值的类型
  115. * @param {*} value 值
  116. * @returns {String} 值的类型
  117. */
  118. static getType = (value) => {
  119. return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
  120. };
  121.  
  122. /**
  123. * 返回当前值是否为指定的类型
  124. * @param {*} value 值
  125. * @param {Array<String>} types 类型名称集合
  126. * @returns {Boolean} 值是否为指定的类型
  127. */
  128. static isType = (value, ...types) => {
  129. return types.includes(this.getType(value));
  130. };
  131.  
  132. /**
  133. * 拦截属性
  134. * @param {Object} target 目标对象
  135. * @param {String} property 属性或函数名称
  136. * @param {Function} beforeGet 获取属性前事件
  137. * @param {Function} beforeSet 设置属性前事件
  138. * @param {Function} afterGet 获取属性后事件
  139. * @param {Function} afterSet 设置属性前事件
  140. */
  141. static interceptProperty = (
  142. target,
  143. property,
  144. { beforeGet, beforeSet, afterGet, afterSet }
  145. ) => {
  146. // 缓存数据
  147. let source = target[property];
  148.  
  149. // 如果已经有结果,则直接处理写入后操作
  150. if (Object.hasOwn(target, property)) {
  151. if (afterSet) {
  152. afterSet.apply(target, [source]);
  153. }
  154. }
  155.  
  156. // 拦截
  157. Object.defineProperty(target, property, {
  158. get: () => {
  159. // 如果是函数
  160. if (this.isType(source, "function")) {
  161. return (...args) => {
  162. try {
  163. // 执行前操作
  164. // 可以在这一步修改参数
  165. // 可以通过在这一步抛出来阻止执行
  166. if (beforeGet) {
  167. args = beforeGet.apply(target, args);
  168. }
  169.  
  170. // 执行函数
  171. const returnValue = source.apply(target, args);
  172.  
  173. // 返回的可能是一个 Promise
  174. const result =
  175. returnValue instanceof Promise
  176. ? returnValue
  177. : Promise.resolve(returnValue);
  178.  
  179. // 执行后操作
  180. if (afterGet) {
  181. result.then((value) => {
  182. afterGet.apply(target, [value, args, source]);
  183. });
  184. }
  185. } catch {}
  186. };
  187. }
  188.  
  189. try {
  190. // 返回前操作
  191. // 可以在这一步修改返回结果
  192. // 可以通过在这一步抛出来返回 undefined
  193. const result = beforeGet
  194. ? beforeGet.apply(target, [source])
  195. : source;
  196.  
  197. // 返回后操作
  198. // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 beforeGet 来操作处理后的数据
  199. if (afterGet) {
  200. afterGet.apply(target, [result, source]);
  201. }
  202.  
  203. // 返回结果
  204. return result;
  205. } catch {
  206. return undefined;
  207. }
  208. },
  209. set: (value) => {
  210. try {
  211. // 写入前操作
  212. // 可以在这一步修改写入结果
  213. // 可以通过在这一步抛出来写入 undefined
  214. const result = beforeSet
  215. ? beforeSet.apply(target, [source, value])
  216. : value;
  217.  
  218. // 写入结果
  219. source = result;
  220.  
  221. // 写入后操作
  222. if (afterSet) {
  223. afterSet.apply(target, [result, value]);
  224. }
  225. } catch {
  226. source = undefined;
  227. }
  228. },
  229. });
  230. };
  231.  
  232. /**
  233. * 合并数据
  234. * @param {*} target 目标对象
  235. * @param {Array} sources 来源对象集合
  236. * @returns 合并后的对象
  237. */
  238. static merge = (target, ...sources) => {
  239. for (const source of sources) {
  240. const targetType = this.getType(target);
  241. const sourceType = this.getType(source);
  242.  
  243. // 如果来源对象的类型与目标对象不一致,替换为来源对象
  244. if (sourceType !== targetType) {
  245. target = source;
  246. continue;
  247. }
  248.  
  249. // 如果来源对象是数组,直接合并
  250. if (targetType === "array") {
  251. target = [...target, ...source];
  252. continue;
  253. }
  254.  
  255. // 如果来源对象是对象,合并对象
  256. if (sourceType === "object") {
  257. for (const key in source) {
  258. if (Object.hasOwn(target, key)) {
  259. target[key] = this.merge(target[key], source[key]);
  260. } else {
  261. target[key] = source[key];
  262. }
  263. }
  264. continue;
  265. }
  266.  
  267. // 其他情况,更新值
  268. target = source;
  269. }
  270.  
  271. return target;
  272. };
  273.  
  274. /**
  275. * 数组排序
  276. * @param {Array} collection 数据集合
  277. * @param {Array<String | Function>} iterators 迭代器,要排序的属性名或排序函数
  278. */
  279. static sortBy = (collection, ...iterators) =>
  280. collection.slice().sort((a, b) => {
  281. for (let i = 0; i < iterators.length; i += 1) {
  282. const iteratee = iterators[i];
  283.  
  284. const valueA = this.isType(iteratee, "function")
  285. ? iteratee(a)
  286. : a[iteratee];
  287. const valueB = this.isType(iteratee, "function")
  288. ? iteratee(b)
  289. : b[iteratee];
  290.  
  291. if (valueA < valueB) {
  292. return -1;
  293. }
  294.  
  295. if (valueA > valueB) {
  296. return 1;
  297. }
  298. }
  299.  
  300. return 0;
  301. });
  302.  
  303. /**
  304. * 读取论坛数据
  305. * @param {Response} response 请求响应
  306. * @param {Boolean} toJSON 是否转为 JSON 格式
  307. */
  308. static readForumData = async (response, toJSON = true) => {
  309. return new Promise(async (resolve) => {
  310. const blob = await response.blob();
  311.  
  312. const reader = new FileReader();
  313.  
  314. reader.onload = () => {
  315. const text = reader.result.replace(
  316. "window.script_muti_get_var_store=",
  317. ""
  318. );
  319.  
  320. if (toJSON) {
  321. try {
  322. resolve(JSON.parse(text));
  323. } catch {
  324. resolve({});
  325. }
  326. return;
  327. }
  328.  
  329. resolve(text);
  330. };
  331.  
  332. reader.readAsText(blob, "GBK");
  333. });
  334. };
  335.  
  336. /**
  337. * 获取成对括号的内容
  338. * @param {String} content 内容
  339. * @param {String} keyword 起始位置关键字
  340. * @param {String} start 左括号
  341. * @param {String} end 右括号
  342. * @returns {String} 包含括号的内容
  343. */
  344. static searchPair = (content, keyword, start = "{", end = "}") => {
  345. // 获取成对括号的位置
  346. const getLastIndex = (content, position, start = "{", end = "}") => {
  347. if (position >= 0) {
  348. let nextIndex = position + 1;
  349.  
  350. while (nextIndex < content.length) {
  351. if (content[nextIndex] === end) {
  352. return nextIndex;
  353. }
  354.  
  355. if (content[nextIndex] === start) {
  356. nextIndex = getLastIndex(content, nextIndex, start, end);
  357.  
  358. if (nextIndex < 0) {
  359. break;
  360. }
  361. }
  362.  
  363. nextIndex = nextIndex + 1;
  364. }
  365. }
  366.  
  367. return -1;
  368. };
  369.  
  370. // 起始位置
  371. const str = keyword + start;
  372.  
  373. // 起始下标
  374. const index = content.indexOf(str) + str.length;
  375.  
  376. // 结尾下标
  377. const lastIndex = getLastIndex(content, index, start, end);
  378.  
  379. if (lastIndex >= 0) {
  380. return start + content.substring(index, lastIndex) + end;
  381. }
  382.  
  383. return null;
  384. };
  385.  
  386. /**
  387. * 计算字符串的颜色
  388. *
  389. * 采用的是泥潭的颜色方案,参见 commonui.htmlName
  390. * @param {String} value 字符串
  391. * @returns {String} RGB代码
  392. */
  393. static generateColor(value) {
  394. const hash = (() => {
  395. let h = 5381;
  396.  
  397. for (var i = 0; i < value.length; i++) {
  398. h = ((h << 5) + h + value.charCodeAt(i)) & 0xffffffff;
  399. }
  400.  
  401. return h;
  402. })();
  403.  
  404. const hex = Math.abs(hash).toString(16) + "000000";
  405.  
  406. const hsv = [
  407. `0x${hex.substring(2, 4)}` / 255,
  408. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  409. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  410. ];
  411.  
  412. const rgb = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  413.  
  414. return ["#", ...rgb].reduce((a, b) => {
  415. return a + ("0" + b.toString(16)).slice(-2);
  416. });
  417. }
  418. }
  419.  
  420. /**
  421. * IndexedDB
  422. *
  423. * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑
  424. */
  425.  
  426. class DBStorage {
  427. /**
  428. * 数据库名称
  429. */
  430. name = "NGA_FILTER_CACHE";
  431.  
  432. /**
  433. * 模块列表
  434. */
  435. modules = {};
  436.  
  437. /**
  438. * 当前实例
  439. */
  440. instance = null;
  441.  
  442. /**
  443. * 初始化
  444. * @param {*} modules 模块列表
  445. */
  446. constructor(modules) {
  447. this.modules = modules;
  448. }
  449.  
  450. /**
  451. * 是否支持
  452. */
  453. isSupport() {
  454. return unsafeWindow.indexedDB !== undefined;
  455. }
  456.  
  457. /**
  458. * 打开数据库并创建表
  459. * @returns {Promise<IDBDatabase>} 实例
  460. */
  461. async open() {
  462. // 创建实例
  463. if (this.instance === null) {
  464. // 声明一个数组,用于等待全部表处理完毕
  465. const queue = [];
  466.  
  467. // 创建实例
  468. await new Promise((resolve, reject) => {
  469. // 版本
  470. const version = Object.values(this.modules)
  471. .map(({ version }) => version)
  472. .reduce((a, b) => Math.max(a, b), 0);
  473.  
  474. // 创建请求
  475. const request = unsafeWindow.indexedDB.open(this.name, version);
  476.  
  477. // 创建或者升级表
  478. request.onupgradeneeded = (event) => {
  479. this.instance = event.target.result;
  480.  
  481. const transaction = event.target.transaction;
  482. const oldVersion = event.oldVersion;
  483.  
  484. Object.entries(this.modules).forEach(([key, values]) => {
  485. if (values.version > oldVersion) {
  486. queue.push(this.createOrUpdateStore(key, values, transaction));
  487. }
  488. });
  489. };
  490.  
  491. // 成功后处理
  492. request.onsuccess = (event) => {
  493. this.instance = event.target.result;
  494. resolve();
  495. };
  496.  
  497. // 失败后处理
  498. request.onerror = () => {
  499. reject();
  500. };
  501. });
  502.  
  503. // 等待全部表处理完毕
  504. await Promise.all(queue);
  505. }
  506.  
  507. // 返回实例
  508. return this.instance;
  509. }
  510.  
  511. /**
  512. * 获取表
  513. * @param {String} name 表名
  514. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  515. * @param {String} mode 事务模式,默认为只读
  516. * @returns {Promise<IDBObjectStore>} 表
  517. */
  518. async getStore(name, transaction = null, mode = "readonly") {
  519. const db = await this.open();
  520.  
  521. if (transaction === null) {
  522. transaction = db.transaction(name, mode);
  523. }
  524.  
  525. return transaction.objectStore(name);
  526. }
  527.  
  528. /**
  529. * 创建或升级表
  530. * @param {String} name 表名
  531. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  532. * @returns {Promise}
  533. */
  534. async createOrUpdateStore(name, { keyPath, indexes }, transaction) {
  535. const db = transaction.db;
  536. const data = [];
  537.  
  538. // 检查是否存在表,如果存在,缓存数据并删除旧表
  539. if (db.objectStoreNames.contains(name)) {
  540. // 获取并缓存全部数据
  541. const result = await this.bulkGet(name, [], transaction);
  542.  
  543. if (result) {
  544. data.push(...result);
  545. }
  546.  
  547. // 删除旧表
  548. db.deleteObjectStore(name);
  549. }
  550.  
  551. // 创建表
  552. const store = db.createObjectStore(name, {
  553. keyPath,
  554. });
  555.  
  556. // 创建索引
  557. if (indexes) {
  558. indexes.forEach((index) => {
  559. store.createIndex(index, index);
  560. });
  561. }
  562.  
  563. // 迁移数据
  564. if (data.length > 0) {
  565. await this.bulkAdd(name, data, transaction);
  566. }
  567. }
  568.  
  569. /**
  570. * 插入指定表的数据
  571. * @param {String} name 表名
  572. * @param {*} data 数据
  573. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  574. * @returns {Promise}
  575. */
  576. async add(name, data, transaction = null) {
  577. // 获取表
  578. const store = await this.getStore(name, transaction, "readwrite");
  579.  
  580. // 插入数据
  581. const result = await new Promise((resolve, reject) => {
  582. // 创建请求
  583. const request = store.add(data);
  584.  
  585. // 成功后处理
  586. request.onsuccess = (event) => {
  587. resolve(event.target.result);
  588. };
  589.  
  590. // 失败后处理
  591. request.onerror = (event) => {
  592. reject(event);
  593. };
  594. });
  595.  
  596. // 返回结果
  597. return result;
  598. }
  599.  
  600. /**
  601. * 删除指定表的数据
  602. * @param {String} name 表名
  603. * @param {String} key 主键
  604. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  605. * @returns {Promise}
  606. */
  607. async delete(name, key, transaction = null) {
  608. // 获取表
  609. const store = await this.getStore(name, transaction, "readwrite");
  610.  
  611. // 删除数据
  612. const result = await new Promise((resolve, reject) => {
  613. // 创建请求
  614. const request = store.delete(key);
  615.  
  616. // 成功后处理
  617. request.onsuccess = (event) => {
  618. resolve(event.target.result);
  619. };
  620.  
  621. // 失败后处理
  622. request.onerror = (event) => {
  623. reject(event);
  624. };
  625. });
  626.  
  627. // 返回结果
  628. return result;
  629. }
  630.  
  631. /**
  632. * 插入或修改指定表的数据
  633. * @param {String} name 表名
  634. * @param {*} data 数据
  635. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  636. * @returns {Promise}
  637. */
  638. async put(name, data, transaction = null) {
  639. // 获取表
  640. const store = await this.getStore(name, transaction, "readwrite");
  641.  
  642. // 插入或修改数据
  643. const result = await new Promise((resolve, reject) => {
  644. // 创建请求
  645. const request = store.put(data);
  646.  
  647. // 成功后处理
  648. request.onsuccess = (event) => {
  649. resolve(event.target.result);
  650. };
  651.  
  652. // 失败后处理
  653. request.onerror = (event) => {
  654. reject(event);
  655. };
  656. });
  657.  
  658. // 返回结果
  659. return result;
  660. }
  661.  
  662. /**
  663. * 获取指定表的数据
  664. * @param {String} name 表名
  665. * @param {String} key 主键
  666. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  667. * @returns {Promise} 数据
  668. */
  669. async get(name, key, transaction = null) {
  670. // 获取表
  671. const store = await this.getStore(name, transaction);
  672.  
  673. // 查询数据
  674. const result = await new Promise((resolve, reject) => {
  675. // 创建请求
  676. const request = store.get(key);
  677.  
  678. // 成功后处理
  679. request.onsuccess = (event) => {
  680. resolve(event.target.result);
  681. };
  682.  
  683. // 失败后处理
  684. request.onerror = (event) => {
  685. reject(event);
  686. };
  687. });
  688.  
  689. // 返回结果
  690. return result;
  691. }
  692.  
  693. /**
  694. * 批量插入指定表的数据
  695. * @param {String} name 表名
  696. * @param {Array} data 数据集合
  697. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  698. * @returns {Promise<number>} 成功数量
  699. */
  700. async bulkAdd(name, data, transaction = null) {
  701. // 等待操作结果
  702. const result = await Promise.all(
  703. data.map((item) =>
  704. this.add(name, item, transaction)
  705. .then(() => true)
  706. .catch(() => false)
  707. )
  708. );
  709.  
  710. // 返回受影响的数量
  711. return result.filter((item) => item).length;
  712. }
  713.  
  714. /**
  715. * 批量删除指定表的数据
  716. * @param {String} name 表名
  717. * @param {Array<String>} keys 主键集合,空则删除全部
  718. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  719. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  720. */
  721. async bulkDelete(name, keys = [], transaction = null) {
  722. // 如果 keys 为空,删除全部数据
  723. if (keys.length === 0) {
  724. // 获取表
  725. const store = await this.getStore(name, transaction, "readwrite");
  726.  
  727. // 清空数据
  728. await new Promise((resolve, reject) => {
  729. // 创建请求
  730. const request = store.clear();
  731.  
  732. // 成功后处理
  733. request.onsuccess = (event) => {
  734. resolve(event.target.result);
  735. };
  736.  
  737. // 失败后处理
  738. request.onerror = (event) => {
  739. reject(event);
  740. };
  741. });
  742.  
  743. return -1;
  744. }
  745.  
  746. // 等待操作结果
  747. const result = await Promise.all(
  748. data.map((item) =>
  749. this.delete(name, item, transaction)
  750. .then(() => true)
  751. .catch(() => false)
  752. )
  753. );
  754.  
  755. // 返回受影响的数量
  756. return result.filter((item) => item).length;
  757. }
  758.  
  759. /**
  760. * 批量插入或修改指定表的数据
  761. * @param {String} name 表名
  762. * @param {Array} data 数据集合
  763. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  764. * @returns {Promise<number>} 成功数量
  765. */
  766. async bulkPut(name, data, transaction = null) {
  767. // 等待操作结果
  768. const result = await Promise.all(
  769. data.map((item) =>
  770. this.put(name, item, transaction)
  771. .then(() => true)
  772. .catch(() => false)
  773. )
  774. );
  775.  
  776. // 返回受影响的数量
  777. return result.filter((item) => item).length;
  778. }
  779.  
  780. /**
  781. * 批量获取指定表的数据
  782. * @param {String} name 表名
  783. * @param {Array<String>} keys 主键集合,空则获取全部
  784. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  785. * @returns {Promise<Array>} 数据集合
  786. */
  787. async bulkGet(name, keys = [], transaction = null) {
  788. // 如果 keys 为空,查询全部数据
  789. if (keys.length === 0) {
  790. // 获取表
  791. const store = await this.getStore(name, transaction);
  792.  
  793. // 查询数据
  794. const result = await new Promise((resolve, reject) => {
  795. // 创建请求
  796. const request = store.getAll();
  797.  
  798. // 成功后处理
  799. request.onsuccess = (event) => {
  800. resolve(event.target.result || []);
  801. };
  802.  
  803. // 失败后处理
  804. request.onerror = (event) => {
  805. reject(event);
  806. };
  807. });
  808.  
  809. // 返回结果
  810. return result;
  811. }
  812.  
  813. // 返回符合的结果
  814. const result = [];
  815.  
  816. await Promise.all(
  817. keys.map((key) =>
  818. this.get(name, key, transaction)
  819. .then((item) => {
  820. result.push(item);
  821. })
  822. .catch(() => {})
  823. )
  824. );
  825.  
  826. return result;
  827. }
  828. }
  829.  
  830. /**
  831. * 油猴存储
  832. *
  833. * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
  834. */
  835. class GMStorage extends DBStorage {
  836. /**
  837. * 初始化
  838. * @param {*} modules 模块列表
  839. */
  840. constructor(modules) {
  841. super(modules);
  842. }
  843.  
  844. /**
  845. * 插入指定表的数据
  846. * @param {String} name 表名
  847. * @param {*} data 数据
  848. * @returns {Promise}
  849. */
  850. async add(name, data) {
  851. // 如果不在模块列表里,写入全部数据
  852. if (Object.hasOwn(this.modules, name) === false) {
  853. return GM_setValue(name, data);
  854. }
  855.  
  856. // 如果支持 IndexedDB,使用 IndexedDB
  857. if (super.isSupport()) {
  858. return super.add(name, data);
  859. }
  860.  
  861. // 获取对应的主键
  862. const keyPath = this.modules[name].keyPath;
  863. const key = data[keyPath];
  864.  
  865. // 如果数据中不包含主键,抛出异常
  866. if (key === undefined) {
  867. throw new Error();
  868. }
  869.  
  870. // 获取全部数据
  871. const values = GM_getValue(name, {});
  872.  
  873. // 如果对应主键已存在,抛出异常
  874. if (Object.hasOwn(values, key)) {
  875. throw new Error();
  876. }
  877.  
  878. // 插入数据
  879. values[key] = data;
  880.  
  881. // 保存数据
  882. GM_setValue(name, values);
  883. }
  884.  
  885. /**
  886. * 删除指定表的数据
  887. * @param {String} name 表名
  888. * @param {String} key 主键
  889. * @returns {Promise}
  890. */
  891. async delete(name, key) {
  892. // 如果不在模块列表里,忽略 key,删除全部数据
  893. if (Object.hasOwn(this.modules, name) === false) {
  894. return GM_setValue(name, {});
  895. }
  896.  
  897. // 如果支持 IndexedDB,使用 IndexedDB
  898. if (super.isSupport()) {
  899. return super.delete(name, key);
  900. }
  901.  
  902. // 获取全部数据
  903. const values = GM_getValue(name, {});
  904.  
  905. // 如果对应主键不存在,抛出异常
  906. if (Object.hasOwn(values, key) === false) {
  907. throw new Error();
  908. }
  909.  
  910. // 删除数据
  911. delete values[key];
  912.  
  913. // 保存数据
  914. GM_setValue(name, values);
  915. }
  916.  
  917. /**
  918. * 插入或修改指定表的数据
  919. * @param {String} name 表名
  920. * @param {*} data 数据
  921. * @returns {Promise}
  922. */
  923. async put(name, data) {
  924. // 如果不在模块列表里,写入全部数据
  925. if (Object.hasOwn(this.modules, name) === false) {
  926. return GM_setValue(name, data);
  927. }
  928.  
  929. // 如果支持 IndexedDB,使用 IndexedDB
  930. if (super.isSupport()) {
  931. return super.put(name, data);
  932. }
  933.  
  934. // 获取对应的主键
  935. const keyPath = this.modules[name].keyPath;
  936. const key = data[keyPath];
  937.  
  938. // 如果数据中不包含主键,抛出异常
  939. if (key === undefined) {
  940. throw new Error();
  941. }
  942.  
  943. // 获取全部数据
  944. const values = GM_getValue(name, {});
  945.  
  946. // 插入或修改数据
  947. values[key] = data;
  948.  
  949. // 保存数据
  950. GM_setValue(name, values);
  951. }
  952.  
  953. /**
  954. * 获取指定表的数据
  955. * @param {String} name 表名
  956. * @param {String} key 主键
  957. * @returns {Promise} 数据
  958. */
  959. async get(name, key) {
  960. // 如果不在模块列表里,忽略 key,返回全部数据
  961. if (Object.hasOwn(this.modules, name) === false) {
  962. return GM_getValue(name);
  963. }
  964.  
  965. // 如果支持 IndexedDB,使用 IndexedDB
  966. if (super.isSupport()) {
  967. return super.get(name, key);
  968. }
  969.  
  970. // 获取全部数据
  971. const values = GM_getValue(name, {});
  972.  
  973. // 如果对应主键不存在,抛出异常
  974. if (Object.hasOwn(values, key) === false) {
  975. throw new Error();
  976. }
  977.  
  978. // 返回结果
  979. return values[key];
  980. }
  981.  
  982. /**
  983. * 批量插入指定表的数据
  984. * @param {String} name 表名
  985. * @param {Array} data 数据集合
  986. * @returns {Promise<number>} 成功数量
  987. */
  988. async bulkAdd(name, data) {
  989. // 如果不在模块列表里,写入全部数据
  990. if (Object.hasOwn(this.modules, name) === false) {
  991. return GM_setValue(name, {});
  992. }
  993.  
  994. // 如果支持 IndexedDB,使用 IndexedDB
  995. if (super.isSupport()) {
  996. return super.bulkAdd(name, data);
  997. }
  998.  
  999. // 获取对应的主键
  1000. const keyPath = this.modules[name].keyPath;
  1001.  
  1002. // 获取全部数据
  1003. const values = GM_getValue(name, {});
  1004.  
  1005. // 添加数据
  1006. const result = data.map((item) => {
  1007. const key = item[keyPath];
  1008.  
  1009. // 如果数据中不包含主键,抛出异常
  1010. if (key === undefined) {
  1011. return false;
  1012. }
  1013.  
  1014. // 如果对应主键已存在,抛出异常
  1015. if (Object.hasOwn(values, key)) {
  1016. return false;
  1017. }
  1018.  
  1019. // 插入数据
  1020. values[key] = item;
  1021.  
  1022. return true;
  1023. });
  1024.  
  1025. // 保存数据
  1026. GM_setValue(name, values);
  1027.  
  1028. // 返回受影响的数量
  1029. return result.filter((item) => item).length;
  1030. }
  1031.  
  1032. /**
  1033. * 批量删除指定表的数据
  1034. * @param {String} name 表名
  1035. * @param {Array<String>} keys 主键集合,空则删除全部
  1036. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  1037. */
  1038. async bulkDelete(name, keys = []) {
  1039. // 如果不在模块列表里,忽略 keys,删除全部数据
  1040. if (Object.hasOwn(this.modules, name) === false) {
  1041. return GM_setValue(name, {});
  1042. }
  1043.  
  1044. // 如果支持 IndexedDB,使用 IndexedDB
  1045. if (super.isSupport()) {
  1046. return super.bulkDelete(name, keys);
  1047. }
  1048.  
  1049. // 如果 keys 为空,删除全部数据
  1050. if (keys.length === 0) {
  1051. GM_setValue(name, {});
  1052.  
  1053. return -1;
  1054. }
  1055.  
  1056. // 获取全部数据
  1057. const values = GM_getValue(name, {});
  1058.  
  1059. // 删除数据
  1060. const result = keys.map((key) => {
  1061. // 如果对应主键不存在,抛出异常
  1062. if (Object.hasOwn(values, key) === false) {
  1063. return false;
  1064. }
  1065.  
  1066. // 删除数据
  1067. delete values[key];
  1068.  
  1069. return true;
  1070. });
  1071.  
  1072. // 保存数据
  1073. GM_setValue(name, values);
  1074.  
  1075. // 返回受影响的数量
  1076. return result.filter((item) => item).length;
  1077. }
  1078.  
  1079. /**
  1080. * 批量插入或修改指定表的数据
  1081. * @param {String} name 表名
  1082. * @param {Array} data 数据集合
  1083. * @returns {Promise<number>} 成功数量
  1084. */
  1085. async bulkPut(name, data) {
  1086. // 如果不在模块列表里,写入全部数据
  1087. if (Object.hasOwn(this.modules, name) === false) {
  1088. return GM_setValue(name, data);
  1089. }
  1090.  
  1091. // 如果支持 IndexedDB,使用 IndexedDB
  1092. if (super.isSupport()) {
  1093. return super.bulkPut(name, keys);
  1094. }
  1095.  
  1096. // 获取对应的主键
  1097. const keyPath = this.modules[name].keyPath;
  1098.  
  1099. // 获取全部数据
  1100. const values = GM_getValue(name, {});
  1101.  
  1102. // 添加数据
  1103. const result = data.map((item) => {
  1104. const key = item[keyPath];
  1105.  
  1106. // 如果数据中不包含主键,抛出异常
  1107. if (key === undefined) {
  1108. return false;
  1109. }
  1110.  
  1111. // 插入数据
  1112. values[key] = item;
  1113.  
  1114. return true;
  1115. });
  1116.  
  1117. // 保存数据
  1118. GM_setValue(name, values);
  1119.  
  1120. // 返回受影响的数量
  1121. return result.filter((item) => item).length;
  1122. }
  1123.  
  1124. /**
  1125. * 批量获取指定表的数据,如果不在模块列表里,返回全部数据
  1126. * @param {String} name 表名
  1127. * @param {Array<String>} keys 主键集合,空则获取全部
  1128. * @returns {Promise<Array>} 数据集合
  1129. */
  1130. async bulkGet(name, keys = []) {
  1131. // 如果不在模块列表里,忽略 keys,返回全部数据
  1132. if (Object.hasOwn(this.modules, name) === false) {
  1133. return GM_getValue(name);
  1134. }
  1135.  
  1136. // 如果支持 IndexedDB,使用 IndexedDB
  1137. if (super.isSupport()) {
  1138. return super.bulkGet(name, keys);
  1139. }
  1140.  
  1141. // 获取全部数据
  1142. const values = GM_getValue(name, {});
  1143.  
  1144. // 如果 keys 为空,返回全部数据
  1145. if (keys.length === 0) {
  1146. return Object.values(values);
  1147. }
  1148.  
  1149. // 返回符合的结果
  1150. const result = [];
  1151.  
  1152. keys.forEach((key) => {
  1153. if (Object.hasOwn(values, key)) {
  1154. result.push(values[key]);
  1155. }
  1156. });
  1157.  
  1158. return result;
  1159. }
  1160. }
  1161.  
  1162. /**
  1163. * 缓存管理
  1164. *
  1165. * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
  1166. */
  1167. class Cache extends GMStorage {
  1168. /**
  1169. * 增加模块列表的 timestamp 索引
  1170. * @param {*} modules 模块列表
  1171. */
  1172. constructor(modules) {
  1173. Object.values(modules).forEach((item) => {
  1174. item.indexes = item.indexes || [];
  1175.  
  1176. if (item.indexes.includes("timestamp") === false) {
  1177. item.indexes.push("timestamp");
  1178. }
  1179. });
  1180.  
  1181. super(modules);
  1182.  
  1183. this.autoClear();
  1184. }
  1185.  
  1186. /**
  1187. * 插入指定表的数据,并增加 timestamp
  1188. * @param {String} name 表名
  1189. * @param {*} data 数据
  1190. * @returns {Promise}
  1191. */
  1192. async add(name, data) {
  1193. // 如果在模块里,增加 timestamp
  1194. if (Object.hasOwn(this.modules, name)) {
  1195. data.timestamp = data.timestamp || new Date().getTime();
  1196. }
  1197.  
  1198. return super.add(name, data);
  1199. }
  1200.  
  1201. /**
  1202. * 插入或修改指定表的数据,并增加 timestamp
  1203. * @param {String} name 表名
  1204. * @param {*} data 数据
  1205. * @returns {Promise}
  1206. */
  1207. async put(name, data) {
  1208. // 如果在模块里,增加 timestamp
  1209. if (Object.hasOwn(this.modules, name)) {
  1210. data.timestamp = data.timestamp || new Date().getTime();
  1211. }
  1212.  
  1213. return super.put(name, data);
  1214. }
  1215.  
  1216. /**
  1217. * 获取指定表的数据,并移除过期数据
  1218. * @param {String} name 表名
  1219. * @param {String} key 主键
  1220. * @returns {Promise} 数据
  1221. */
  1222. async get(name, key) {
  1223. // 获取数据
  1224. const value = await super.get(name, key).catch(() => null);
  1225.  
  1226. // 如果不在模块里,直接返回结果
  1227. if (Object.hasOwn(this.modules, name) === false) {
  1228. return value;
  1229. }
  1230.  
  1231. // 如果有结果的话,移除超时数据
  1232. if (value) {
  1233. // 读取模块配置
  1234. const { expireTime, persistent } = this.modules[name];
  1235.  
  1236. // 持久化或未超时
  1237. if (persistent || value.timestamp + expireTime > new Date().getTime()) {
  1238. return value;
  1239. }
  1240.  
  1241. // 移除超时数据
  1242. await super.delete(name, key);
  1243. }
  1244.  
  1245. return null;
  1246. }
  1247.  
  1248. /**
  1249. * 批量插入指定表的数据,并增加 timestamp
  1250. * @param {String} name 表名
  1251. * @param {Array} data 数据集合
  1252. * @returns {Promise<number>} 成功数量
  1253. */
  1254. async bulkAdd(name, data) {
  1255. // 如果在模块里,增加 timestamp
  1256. if (Object.hasOwn(this.modules, name)) {
  1257. data.forEach((item) => {
  1258. item.timestamp = item.timestamp || new Date().getTime();
  1259. });
  1260. }
  1261.  
  1262. return super.bulkAdd(name, data);
  1263. }
  1264.  
  1265. /**
  1266. * 批量删除指定表的数据
  1267. * @param {String} name 表名
  1268. * @param {Array<String>} keys 主键集合,空则删除全部
  1269. * @param {boolean} force 是否强制删除,否则只删除过期数据
  1270. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  1271. */
  1272. async bulkDelete(name, keys = [], force = false) {
  1273. // 如果不在模块里,强制删除
  1274. if (Object.hasOwn(this.modules, name) === false) {
  1275. force = true;
  1276. }
  1277.  
  1278. // 强制删除
  1279. if (force) {
  1280. return super.bulkDelete(name, keys);
  1281. }
  1282.  
  1283. // 批量获取指定表的数据,并移除过期数据
  1284. const result = this.bulkGet(name, keys);
  1285.  
  1286. // 返回成功数量
  1287. if (keys.length === 0) {
  1288. return -1;
  1289. }
  1290.  
  1291. return keys.length - result.length;
  1292. }
  1293.  
  1294. /**
  1295. * 批量插入或修改指定表的数据,并增加 timestamp
  1296. * @param {String} name 表名
  1297. * @param {Array} data 数据集合
  1298. * @returns {Promise<number>} 成功数量
  1299. */
  1300. async bulkPut(name, data) {
  1301. // 如果在模块里,增加 timestamp
  1302. if (Object.hasOwn(this.modules, name)) {
  1303. data.forEach((item) => {
  1304. item.timestamp = item.timestamp || new Date().getTime();
  1305. });
  1306. }
  1307.  
  1308. return super.bulkPut(name, data);
  1309. }
  1310.  
  1311. /**
  1312. * 批量获取指定表的数据,并移除过期数据
  1313. * @param {String} name 表名
  1314. * @param {Array<String>} keys 主键集合,空则获取全部
  1315. * @returns {Promise<Array>} 数据集合
  1316. */
  1317. async bulkGet(name, keys = []) {
  1318. // 获取数据
  1319. const values = await super.bulkGet(name, keys).catch(() => []);
  1320.  
  1321. // 如果不在模块里,直接返回结果
  1322. if (Object.hasOwn(this.modules, name) === false) {
  1323. return values;
  1324. }
  1325.  
  1326. // 读取模块配置
  1327. const { keyPath, expireTime, persistent } = this.modules[name];
  1328.  
  1329. // 筛选出超时数据
  1330. const result = [];
  1331. const expired = [];
  1332.  
  1333. values.forEach((value) => {
  1334. // 持久化或未超时
  1335. if (persistent || value.timestamp + expireTime > new Date().getTime()) {
  1336. result.push(value);
  1337. return;
  1338. }
  1339.  
  1340. // 记录超时数据
  1341. expired.push(value[keyPath]);
  1342. });
  1343.  
  1344. // 移除超时数据
  1345. await super.bulkDelete(name, expired);
  1346.  
  1347. // 返回结果
  1348. return result;
  1349. }
  1350.  
  1351. /**
  1352. * 自动清理缓存
  1353. */
  1354. async autoClear() {
  1355. const data = await this.get(CLEAR_TIME_KEY);
  1356.  
  1357. const now = new Date();
  1358. const clearTime = new Date(data || 0);
  1359.  
  1360. const isToday =
  1361. now.getDate() === clearTime.getDate() &&
  1362. now.getMonth() === clearTime.getMonth() &&
  1363. now.getFullYear() === clearTime.getFullYear();
  1364.  
  1365. if (isToday) {
  1366. return;
  1367. }
  1368.  
  1369. await Promise.all(
  1370. Object.keys(this.modules).map((name) => this.bulkDelete(name))
  1371. );
  1372.  
  1373. await this.put(CLEAR_TIME_KEY, now.getTime());
  1374. }
  1375. }
  1376.  
  1377. /**
  1378. * 设置
  1379. *
  1380. * 暂时整体处理模块设置,后续再拆分
  1381. */
  1382. class Settings {
  1383. /**
  1384. * 缓存管理
  1385. */
  1386. cache;
  1387.  
  1388. /**
  1389. * 当前设置
  1390. */
  1391. data = null;
  1392.  
  1393. /**
  1394. * 初始化并绑定缓存管理
  1395. * @param {Cache} cache 缓存管理
  1396. */
  1397. constructor(cache) {
  1398. this.cache = cache;
  1399. }
  1400.  
  1401. /**
  1402. * 读取设置
  1403. */
  1404. async load() {
  1405. // 读取设置
  1406. if (this.data === null) {
  1407. // 默认配置
  1408. const defaultData = {
  1409. tags: {},
  1410. users: {},
  1411. keywords: {},
  1412. locations: {},
  1413. options: {
  1414. filterRegdateLimit: 0,
  1415. filterPostnumLimit: 0,
  1416. filterTopicRateLimit: 100,
  1417. filterReputationLimit: NaN,
  1418. filterAnony: false,
  1419. filterMode: "隐藏",
  1420. },
  1421. };
  1422.  
  1423. // 读取数据
  1424. const storedData = await this.cache
  1425. .get(DATA_KEY)
  1426. .then((values) => values || {});
  1427.  
  1428. // 写入缓存
  1429. this.data = Tools.merge({}, defaultData, storedData);
  1430.  
  1431. // 写入默认模块选项
  1432. if (Object.hasOwn(this.data, "modules") === false) {
  1433. this.data.modules = [];
  1434.  
  1435. if (Object.keys(this.data.users).length > 0) {
  1436. this.data.modules.push("user");
  1437. }
  1438.  
  1439. if (Object.keys(this.data.tags).length > 0) {
  1440. this.data.modules.push("tag");
  1441. }
  1442.  
  1443. if (Object.keys(this.data.keywords).length > 0) {
  1444. this.data.modules.push("keyword");
  1445. }
  1446.  
  1447. if (Object.keys(this.data.locations).length > 0) {
  1448. this.data.modules.push("location");
  1449. }
  1450.  
  1451. this.data.modules.push("misc");
  1452. }
  1453. }
  1454.  
  1455. // 返回设置
  1456. return this.data;
  1457. }
  1458.  
  1459. /**
  1460. * 写入设置
  1461. */
  1462. async save() {
  1463. return this.cache.put(DATA_KEY, this.data);
  1464. }
  1465.  
  1466. /**
  1467. * 获取模块列表
  1468. */
  1469. get modules() {
  1470. return this.data.modules;
  1471. }
  1472.  
  1473. /**
  1474. * 设置模块列表
  1475. */
  1476. set modules(values) {
  1477. this.data.modules = values;
  1478. this.save();
  1479. }
  1480.  
  1481. /**
  1482. * 获取标签列表
  1483. */
  1484. get tags() {
  1485. return this.data.tags;
  1486. }
  1487.  
  1488. /**
  1489. * 设置标签列表
  1490. */
  1491. set tags(values) {
  1492. this.data.tags = values;
  1493. this.save();
  1494. }
  1495.  
  1496. /**
  1497. * 获取用户列表
  1498. */
  1499. get users() {
  1500. return this.data.users;
  1501. }
  1502.  
  1503. /**
  1504. * 设置用户列表
  1505. */
  1506. set users(values) {
  1507. this.data.users = values;
  1508. this.save();
  1509. }
  1510.  
  1511. /**
  1512. * 获取关键字列表
  1513. */
  1514. get keywords() {
  1515. return this.data.keywords;
  1516. }
  1517.  
  1518. /**
  1519. * 设置关键字列表
  1520. */
  1521. set keywords(values) {
  1522. this.data.keywords = values;
  1523. this.save();
  1524. }
  1525.  
  1526. /**
  1527. * 获取属地列表
  1528. */
  1529. get locations() {
  1530. return this.data.locations;
  1531. }
  1532.  
  1533. /**
  1534. * 设置属地列表
  1535. */
  1536. set locations(values) {
  1537. this.data.locations = values;
  1538. this.save();
  1539. }
  1540.  
  1541. /**
  1542. * 获取默认过滤模式
  1543. */
  1544. get defaultFilterMode() {
  1545. return this.data.options.filterMode;
  1546. }
  1547.  
  1548. /**
  1549. * 设置默认过滤模式
  1550. */
  1551. set defaultFilterMode(value) {
  1552. this.data.options.filterMode = value;
  1553. this.save();
  1554. }
  1555.  
  1556. /**
  1557. * 获取注册时间限制
  1558. */
  1559. get filterRegdateLimit() {
  1560. return this.data.options.filterRegdateLimit || 0;
  1561. }
  1562.  
  1563. /**
  1564. * 设置注册时间限制
  1565. */
  1566. set filterRegdateLimit(value) {
  1567. this.data.options.filterRegdateLimit = value;
  1568. this.save();
  1569. }
  1570.  
  1571. /**
  1572. * 获取发帖数量限制
  1573. */
  1574. get filterPostnumLimit() {
  1575. return this.data.options.filterPostnumLimit || 0;
  1576. }
  1577.  
  1578. /**
  1579. * 设置发帖数量限制
  1580. */
  1581. set filterPostnumLimit(value) {
  1582. this.data.options.filterPostnumLimit = value;
  1583. this.save();
  1584. }
  1585.  
  1586. /**
  1587. * 获取发帖比例限制
  1588. */
  1589. get filterTopicRateLimit() {
  1590. return this.data.options.filterTopicRateLimit || 100;
  1591. }
  1592.  
  1593. /**
  1594. * 设置发帖比例限制
  1595. */
  1596. set filterTopicRateLimit(value) {
  1597. this.data.options.filterTopicRateLimit = value;
  1598. this.save();
  1599. }
  1600.  
  1601. /**
  1602. * 获取版面声望限制
  1603. */
  1604. get filterReputationLimit() {
  1605. return this.data.options.filterReputationLimit || NaN;
  1606. }
  1607.  
  1608. /**
  1609. * 设置版面声望限制
  1610. */
  1611. set filterReputationLimit(value) {
  1612. this.data.options.filterReputationLimit = value;
  1613. this.save();
  1614. }
  1615.  
  1616. /**
  1617. * 获取是否过滤匿名
  1618. */
  1619. get filterAnonymous() {
  1620. return this.data.options.filterAnony || false;
  1621. }
  1622.  
  1623. /**
  1624. * 设置是否过滤匿名
  1625. */
  1626. set filterAnonymous(value) {
  1627. this.data.options.filterAnony = value;
  1628. this.save();
  1629. }
  1630.  
  1631. /**
  1632. * 获取代理设置
  1633. */
  1634. get userAgent() {
  1635. return this.cache.get(USER_AGENT_KEY).then((value) => {
  1636. if (value === undefined) {
  1637. return "Nga_Official";
  1638. }
  1639.  
  1640. return value;
  1641. });
  1642. }
  1643.  
  1644. /**
  1645. * 修改代理设置
  1646. */
  1647. set userAgent(value) {
  1648. this.cache.put(USER_AGENT_KEY, value).then(() => {
  1649. location.reload();
  1650. });
  1651. }
  1652.  
  1653. /**
  1654. * 获取是否启用前置过滤
  1655. */
  1656. get preFilterEnabled() {
  1657. return this.cache.get(PRE_FILTER_KEY).then((value) => {
  1658. if (value === undefined) {
  1659. return true;
  1660. }
  1661.  
  1662. return value;
  1663. });
  1664. }
  1665.  
  1666. /**
  1667. * 设置是否启用前置过滤
  1668. */
  1669. set preFilterEnabled(value) {
  1670. this.cache.put(PRE_FILTER_KEY, value).then(() => {
  1671. location.reload();
  1672. });
  1673. }
  1674.  
  1675. /**
  1676. * 获取过滤模式列表
  1677. *
  1678. * 模拟成从配置中获取
  1679. */
  1680. get filterModes() {
  1681. return ["继承", "标记", "遮罩", "隐藏", "显示"];
  1682. }
  1683.  
  1684. /**
  1685. * 获取指定下标过滤模式
  1686. * @param {Number} index 下标
  1687. */
  1688. getNameByMode(index) {
  1689. const modes = this.filterModes;
  1690.  
  1691. return modes[index] || "";
  1692. }
  1693.  
  1694. /**
  1695. * 获取指定过滤模式下标
  1696. * @param {String} name 过滤模式
  1697. */
  1698. getModeByName(name) {
  1699. const modes = this.filterModes;
  1700.  
  1701. return modes.indexOf(name);
  1702. }
  1703.  
  1704. /**
  1705. * 切换过滤模式
  1706. * @param {String} value 过滤模式
  1707. * @returns {String} 过滤模式
  1708. */
  1709. switchModeByName(value) {
  1710. const index = this.getModeByName(value);
  1711.  
  1712. const nextIndex = (index + 1) % this.filterModes.length;
  1713.  
  1714. return this.filterModes[nextIndex];
  1715. }
  1716. }
  1717.  
  1718. /**
  1719. * API
  1720. */
  1721. class API {
  1722. /**
  1723. * 缓存模块
  1724. */
  1725. static modules = {
  1726. TOPIC_NUM_CACHE: {
  1727. keyPath: "uid",
  1728. version: 1,
  1729. expireTime: 1000 * 60 * 60,
  1730. persistent: true,
  1731. },
  1732. USER_INFO_CACHE: {
  1733. keyPath: "uid",
  1734. version: 1,
  1735. expireTime: 1000 * 60 * 60,
  1736. persistent: false,
  1737. },
  1738. PAGE_CACHE: {
  1739. keyPath: "url",
  1740. version: 1,
  1741. expireTime: 1000 * 60 * 10,
  1742. persistent: false,
  1743. },
  1744. FORUM_POSTED_CACHE: {
  1745. keyPath: "url",
  1746. version: 2,
  1747. expireTime: 1000 * 60 * 60 * 24,
  1748. persistent: true,
  1749. },
  1750. };
  1751.  
  1752. /**
  1753. * 缓存管理
  1754. */
  1755. cache;
  1756.  
  1757. /**
  1758. * 设置
  1759. */
  1760. settings;
  1761.  
  1762. /**
  1763. * 初始化并绑定缓存管理、设置
  1764. * @param {Cache} cache 缓存管理
  1765. * @param {Settings} settings 设置
  1766. */
  1767. constructor(cache, settings) {
  1768. this.cache = cache;
  1769. this.settings = settings;
  1770. }
  1771.  
  1772. /**
  1773. * 简单的统一请求
  1774. * @param {String} url 请求地址
  1775. * @param {Object} config 请求参数
  1776. * @param {Boolean} toJSON 是否转为 JSON 格式
  1777. */
  1778. async request(url, config = {}, toJSON = true) {
  1779. const userAgent = await this.settings.userAgent;
  1780.  
  1781. const response = await fetch(url, {
  1782. headers: {
  1783. "X-User-Agent": userAgent,
  1784. },
  1785. ...config,
  1786. });
  1787.  
  1788. const result = await Tools.readForumData(response, toJSON);
  1789.  
  1790. return result;
  1791. }
  1792.  
  1793. /**
  1794. * 获取用户主题数量
  1795. * @param {number} uid 用户 ID
  1796. */
  1797. async getTopicNum(uid) {
  1798. const name = "TOPIC_NUM_CACHE";
  1799. const expireTime = API.modules[name];
  1800.  
  1801. const api = `/thread.php?lite=js&authorid=${uid}`;
  1802.  
  1803. const cache = await this.cache.get(name, uid);
  1804.  
  1805. // 仍在缓存期间内,直接返回
  1806. if (cache) {
  1807. const expired = cache.timestamp + expireTime < new Date().getTime();
  1808.  
  1809. if (expired === false) {
  1810. return cache.count;
  1811. }
  1812. }
  1813.  
  1814. // 请求数据
  1815. const result = await this.request(api);
  1816.  
  1817. // 服务器可能返回错误,遇到这种情况下,需要保留缓存
  1818. const count = (() => {
  1819. if (result.data) {
  1820. return result.data.__ROWS || 0;
  1821. }
  1822.  
  1823. if (cache) {
  1824. return cache.count;
  1825. }
  1826.  
  1827. return 0;
  1828. })();
  1829.  
  1830. // 更新缓存
  1831. this.cache.put(name, {
  1832. uid,
  1833. count,
  1834. });
  1835.  
  1836. return count;
  1837. }
  1838.  
  1839. /**
  1840. * 获取用户信息
  1841. * @param {number} uid 用户 ID
  1842. */
  1843. async getUserInfo(uid) {
  1844. const name = "USER_INFO_CACHE";
  1845.  
  1846. const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;
  1847.  
  1848. const cache = await this.cache.get(name, uid);
  1849.  
  1850. if (cache) {
  1851. return cache.data;
  1852. }
  1853.  
  1854. const result = await this.request(api);
  1855.  
  1856. const data = result.data ? result.data[0] : null;
  1857.  
  1858. if (data) {
  1859. this.cache.put(name, {
  1860. uid,
  1861. data,
  1862. });
  1863. }
  1864.  
  1865. return data || {};
  1866. }
  1867.  
  1868. /**
  1869. * 获取帖子内容、用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、版面声望
  1870. * @param {number} tid 主题 ID
  1871. * @param {number} pid 回复 ID
  1872. */
  1873. async getPostInfo(tid, pid) {
  1874. const name = "PAGE_CACHE";
  1875.  
  1876. const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
  1877.  
  1878. const cache = await this.cache.get(name, api);
  1879.  
  1880. if (cache) {
  1881. return cache.data;
  1882. }
  1883.  
  1884. const result = await this.request(api, {}, false);
  1885.  
  1886. const parser = new DOMParser();
  1887.  
  1888. const doc = parser.parseFromString(result, "text/html");
  1889.  
  1890. // 验证帖子正常
  1891. const verify = doc.querySelector("#m_posts");
  1892.  
  1893. if (verify === null) {
  1894. return {};
  1895. }
  1896.  
  1897. // 声明返回值
  1898. const data = {};
  1899.  
  1900. // 取得顶楼 UID
  1901. data.uid = (() => {
  1902. const ele = doc.querySelector("#postauthor0");
  1903.  
  1904. if (ele) {
  1905. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  1906.  
  1907. if (res) {
  1908. return res[1];
  1909. }
  1910. }
  1911.  
  1912. return 0;
  1913. })();
  1914.  
  1915. // 取得顶楼标题
  1916. data.subject = doc.querySelector("#postsubject0").innerHTML;
  1917.  
  1918. // 取得顶楼内容
  1919. data.content = doc.querySelector("#postcontent0").innerHTML;
  1920.  
  1921. // 非匿名用户可以继续取得用户信息和版面声望
  1922. if (data.uid > 0) {
  1923. // 取得用户信息
  1924. data.userInfo = (() => {
  1925. const text = Tools.searchPair(result, `"${data.uid}":`);
  1926.  
  1927. if (text) {
  1928. try {
  1929. return JSON.parse(text);
  1930. } catch {
  1931. return null;
  1932. }
  1933. }
  1934.  
  1935. return null;
  1936. })();
  1937.  
  1938. // 取得用户声望
  1939. data.reputation = (() => {
  1940. const reputations = (() => {
  1941. const text = Tools.searchPair(result, `"__REPUTATIONS":`);
  1942.  
  1943. if (text) {
  1944. try {
  1945. return JSON.parse(text);
  1946. } catch {
  1947. return null;
  1948. }
  1949. }
  1950.  
  1951. return null;
  1952. })();
  1953.  
  1954. if (reputations) {
  1955. for (let fid in reputations) {
  1956. return reputations[fid][data.uid] || 0;
  1957. }
  1958. }
  1959.  
  1960. return NaN;
  1961. })();
  1962. }
  1963.  
  1964. // 写入缓存
  1965. this.cache.put(name, {
  1966. url: api,
  1967. data,
  1968. });
  1969.  
  1970. // 返回结果
  1971. return data;
  1972. }
  1973.  
  1974. /**
  1975. * 获取版面信息
  1976. * @param {number} fid 版面 ID
  1977. */
  1978. async getForumInfo(fid) {
  1979. if (Number.isNaN(fid)) {
  1980. return null;
  1981. }
  1982.  
  1983. const api = `/thread.php?lite=js&fid=${fid}`;
  1984.  
  1985. const result = await this.request(api);
  1986.  
  1987. const info = result.data ? result.data.__F : null;
  1988.  
  1989. return info;
  1990. }
  1991.  
  1992. /**
  1993. * 获取版面发言记录
  1994. * @param {number} fid 版面 ID
  1995. * @param {number} uid 用户 ID
  1996. */
  1997. async getForumPosted(fid, uid) {
  1998. const name = "FORUM_POSTED_CACHE";
  1999. const expireTime = API.modules[name];
  2000.  
  2001. const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`;
  2002.  
  2003. const cache = await this.cache.get(name, api);
  2004.  
  2005. if (cache) {
  2006. // 发言是无法撤销的,只要有记录就永远不需要再获取
  2007. // 手动处理没有记录的缓存数据
  2008. const expired = cache.timestamp + expireTime < new Date().getTime();
  2009. if (expired && cache.data === false) {
  2010. await this.cache.delete(name, api);
  2011. }
  2012.  
  2013. return cache.data;
  2014. }
  2015.  
  2016. let isComplete = false;
  2017. let isBusy = false;
  2018.  
  2019. const func = async (url) => {
  2020. if (isComplete || isBusy) {
  2021. return;
  2022. }
  2023.  
  2024. const result = await this.request(url, {}, false);
  2025.  
  2026. // 将所有匹配的 FID 写入缓存,即使并不在设置里
  2027. const matched = result.match(/"fid":(-?\d+),/g);
  2028.  
  2029. if (matched) {
  2030. const list = [
  2031. ...new Set(
  2032. matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10))
  2033. ),
  2034. ];
  2035.  
  2036. list.forEach((item) => {
  2037. const key = api.replace(`&fid=${fid}`, `&fid=${item}`);
  2038.  
  2039. // 写入缓存
  2040. this.cache.put(name, {
  2041. url: key,
  2042. data: true,
  2043. });
  2044.  
  2045. // 已有结果,无需继续查询
  2046. if (fid === item) {
  2047. isComplete = true;
  2048. }
  2049. });
  2050. }
  2051.  
  2052. // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
  2053. if (result.indexOf("服务器忙") > 0) {
  2054. isBusy = true;
  2055. }
  2056. };
  2057.  
  2058. // 先获取回复记录的第一页,顺便可以获取其他版面的记录
  2059. // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
  2060. await func(api.replace(`&fid=${fid}`, `&searchpost=1`));
  2061. await func(api + "&searchpost=1");
  2062. await func(api);
  2063.  
  2064. // 无论成功与否都写入缓存
  2065. if (isComplete === false) {
  2066. // 遇到服务器忙的情况,手动调整缓存时间至 1 小时
  2067. const timestamp = isBusy
  2068. ? new Date().getTime() - (expireTime - 1000 * 60 * 60)
  2069. : new Date().getTime();
  2070.  
  2071. // 写入失败缓存
  2072. this.cache.put(name, {
  2073. url: api,
  2074. data: false,
  2075. timestamp,
  2076. });
  2077. }
  2078.  
  2079. return isComplete;
  2080. }
  2081. }
  2082.  
  2083. /**
  2084. * UI
  2085. */
  2086. class UI {
  2087. /**
  2088. * 标签
  2089. */
  2090. static label = "屏蔽";
  2091.  
  2092. /**
  2093. * 设置
  2094. */
  2095. settings;
  2096.  
  2097. /**
  2098. * API
  2099. */
  2100. api;
  2101.  
  2102. /**
  2103. * 模块列表
  2104. */
  2105. modules = {};
  2106.  
  2107. /**
  2108. * 菜单元素
  2109. */
  2110. menu = null;
  2111.  
  2112. /**
  2113. * 视图元素
  2114. */
  2115. views = {};
  2116.  
  2117. /**
  2118. * 初始化并绑定设置、API,注册脚本菜单
  2119. * @param {Settings} settings 设置
  2120. * @param {API} api API
  2121. */
  2122. constructor(settings, api) {
  2123. this.settings = settings;
  2124. this.api = api;
  2125.  
  2126. this.init();
  2127. }
  2128.  
  2129. /**
  2130. * 初始化,创建基础视图,初始化通用设置
  2131. */
  2132. init() {
  2133. const tabs = this.createTabs({
  2134. className: "right_",
  2135. });
  2136.  
  2137. const content = this.createElement("DIV", [], {
  2138. style: "width: 80vw;",
  2139. });
  2140.  
  2141. const container = this.createElement("DIV", [tabs, content]);
  2142.  
  2143. this.views = {
  2144. tabs,
  2145. content,
  2146. container,
  2147. };
  2148.  
  2149. this.initSettings();
  2150. }
  2151.  
  2152. /**
  2153. * 初始化设置
  2154. */
  2155. initSettings() {
  2156. // 创建基础视图
  2157. const settings = this.createElement("DIV", []);
  2158.  
  2159. // 添加设置项
  2160. const add = (order, ...elements) => {
  2161. const items = [...settings.childNodes];
  2162.  
  2163. if (items.find((item) => item.order === order)) {
  2164. return;
  2165. }
  2166.  
  2167. const item = this.createElement(
  2168. "DIV",
  2169. [...elements, this.createElement("BR", [])],
  2170. {
  2171. order,
  2172. }
  2173. );
  2174.  
  2175. const anchor = items.find((item) => item.order > order);
  2176.  
  2177. settings.insertBefore(item, anchor || null);
  2178.  
  2179. return item;
  2180. };
  2181.  
  2182. // 绑定事件
  2183. Object.assign(settings, {
  2184. add,
  2185. });
  2186.  
  2187. // 合并视图
  2188. Object.assign(this.views, {
  2189. settings,
  2190. });
  2191.  
  2192. // 创建标签页
  2193. const { tabs, content } = this.views;
  2194.  
  2195. this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
  2196. onclick: () => {
  2197. content.innerHTML = "";
  2198. content.appendChild(settings);
  2199. },
  2200. });
  2201. }
  2202.  
  2203. /**
  2204. * 弹窗确认
  2205. * @param {String} message 提示信息
  2206. * @returns {Promise}
  2207. */
  2208. confirm(message = "是否确认?") {
  2209. return new Promise((resolve, reject) => {
  2210. const result = confirm(message);
  2211.  
  2212. if (result) {
  2213. resolve();
  2214. return;
  2215. }
  2216.  
  2217. reject();
  2218. });
  2219. }
  2220.  
  2221. /**
  2222. * 折叠
  2223. * @param {String | Number} key 标识
  2224. * @param {HTMLElement} element 目标元素
  2225. * @param {String} content 内容
  2226. */
  2227. collapse(key, element, content) {
  2228. key = "collapsed_" + key;
  2229.  
  2230. element.innerHTML = `
  2231. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
  2232. <span class="crimson">Troll must die.</span>
  2233. <a href="javascript:void(0)" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">点击查看</a>
  2234. <div style="display: none;" name="${key}">
  2235. ${content}
  2236. </div>
  2237. </div>`;
  2238. }
  2239.  
  2240. /**
  2241. * 创建元素
  2242. * @param {String} tagName 标签
  2243. * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
  2244. * @param {*} properties 额外属性
  2245. * @returns {HTMLElement} 元素
  2246. */
  2247. createElement(tagName, content, properties = {}) {
  2248. const element = document.createElement(tagName);
  2249.  
  2250. // 写入内容
  2251. if (typeof content === "string") {
  2252. element.innerHTML = content;
  2253. } else {
  2254. if (Array.isArray(content) === false) {
  2255. content = [content];
  2256. }
  2257.  
  2258. content.forEach((item) => {
  2259. if (item === null) {
  2260. return;
  2261. }
  2262.  
  2263. if (typeof item === "string") {
  2264. element.append(item);
  2265. return;
  2266. }
  2267.  
  2268. element.appendChild(item);
  2269. });
  2270. }
  2271.  
  2272. // 对 A 标签的额外处理
  2273. if (tagName.toUpperCase() === "A") {
  2274. if (Object.hasOwn(properties, "href") === false) {
  2275. properties.href = "javascript: void(0);";
  2276. }
  2277. }
  2278.  
  2279. // 附加属性
  2280. Object.entries(properties).forEach(([key, value]) => {
  2281. element[key] = value;
  2282. });
  2283.  
  2284. return element;
  2285. }
  2286.  
  2287. /**
  2288. * 创建按钮
  2289. * @param {String} text 文字
  2290. * @param {Function} onclick 点击事件
  2291. * @param {*} properties 额外属性
  2292. */
  2293. createButton(text, onclick, properties = {}) {
  2294. return this.createElement("BUTTON", text, {
  2295. ...properties,
  2296. onclick,
  2297. });
  2298. }
  2299.  
  2300. /**
  2301. * 创建按钮组
  2302. * @param {Array} buttons 按钮集合
  2303. */
  2304. createButtonGroup(...buttons) {
  2305. return this.createElement("DIV", buttons, {
  2306. className: "filter-button-group",
  2307. });
  2308. }
  2309.  
  2310. /**
  2311. * 创建表格
  2312. * @param {Array} headers 表头集合
  2313. * @param {*} properties 额外属性
  2314. * @returns {HTMLElement} 元素和相关函数
  2315. */
  2316. createTable(headers, properties = {}) {
  2317. const rows = [];
  2318.  
  2319. const ths = headers.map((item, index) =>
  2320. this.createElement("TH", item.label, {
  2321. ...item,
  2322. className: `c${index + 1}`,
  2323. })
  2324. );
  2325.  
  2326. const tr =
  2327. ths.length > 0
  2328. ? this.createElement("TR", ths, {
  2329. className: "block_txt_c0",
  2330. })
  2331. : null;
  2332.  
  2333. const thead = tr !== null ? this.createElement("THEAD", tr) : null;
  2334.  
  2335. const tbody = this.createElement("TBODY", []);
  2336.  
  2337. const table = this.createElement("TABLE", [thead, tbody], {
  2338. ...properties,
  2339. className: "filter-table forumbox",
  2340. });
  2341.  
  2342. const wrapper = this.createElement("DIV", table, {
  2343. className: "filter-table-wrapper",
  2344. });
  2345.  
  2346. const intersectionObserver = new IntersectionObserver((entries) => {
  2347. if (entries[0].intersectionRatio <= 0) return;
  2348.  
  2349. const list = rows.splice(0, 10);
  2350.  
  2351. if (list.length === 0) {
  2352. return;
  2353. }
  2354.  
  2355. intersectionObserver.disconnect();
  2356.  
  2357. tbody.append(...list);
  2358.  
  2359. intersectionObserver.observe(tbody.lastElementChild);
  2360. });
  2361.  
  2362. const add = (...columns) => {
  2363. const tds = columns.map((column, index) => {
  2364. if (ths[index]) {
  2365. const { center, ellipsis } = ths[index];
  2366.  
  2367. const properties = {};
  2368.  
  2369. if (center) {
  2370. properties.style = "text-align: center;";
  2371. }
  2372.  
  2373. if (ellipsis) {
  2374. properties.className = "filter-text-ellipsis";
  2375. }
  2376.  
  2377. column = this.createElement("DIV", column, properties);
  2378. }
  2379.  
  2380. return this.createElement("TD", column, {
  2381. className: `c${index + 1}`,
  2382. });
  2383. });
  2384.  
  2385. const tr = this.createElement("TR", tds, {
  2386. className: `row${(rows.length % 2) + 1}`,
  2387. });
  2388.  
  2389. intersectionObserver.disconnect();
  2390.  
  2391. rows.push(tr);
  2392.  
  2393. intersectionObserver.observe(tbody.lastElementChild || tbody);
  2394. };
  2395.  
  2396. const update = (e, ...columns) => {
  2397. const row = e.target.closest("TR");
  2398.  
  2399. if (row) {
  2400. const tds = row.querySelectorAll("TD");
  2401.  
  2402. columns.map((column, index) => {
  2403. if (ths[index]) {
  2404. const { center, ellipsis } = ths[index];
  2405.  
  2406. const properties = {};
  2407.  
  2408. if (center) {
  2409. properties.style = "text-align: center;";
  2410. }
  2411.  
  2412. if (ellipsis) {
  2413. properties.className = "filter-text-ellipsis";
  2414. }
  2415.  
  2416. column = this.createElement("DIV", column, properties);
  2417. }
  2418.  
  2419. if (tds[index]) {
  2420. tds[index].innerHTML = "";
  2421. tds[index].append(column);
  2422. }
  2423. });
  2424. }
  2425. };
  2426.  
  2427. const remove = (e) => {
  2428. const row = e.target.closest("TR");
  2429.  
  2430. if (row) {
  2431. tbody.removeChild(row);
  2432. }
  2433. };
  2434.  
  2435. const clear = () => {
  2436. rows.splice(0);
  2437. intersectionObserver.disconnect();
  2438.  
  2439. tbody.innerHTML = "";
  2440. };
  2441.  
  2442. Object.assign(wrapper, {
  2443. add,
  2444. update,
  2445. remove,
  2446. clear,
  2447. });
  2448.  
  2449. return wrapper;
  2450. }
  2451.  
  2452. /**
  2453. * 创建标签组
  2454. * @param {*} properties 额外属性
  2455. */
  2456. createTabs(properties = {}) {
  2457. const tabs = this.createElement(
  2458. "DIV",
  2459. `<table class="stdbtn" cellspacing="0">
  2460. <tbody>
  2461. <tr></tr>
  2462. </tbody>
  2463. </table>`,
  2464. properties
  2465. );
  2466.  
  2467. return this.createElement(
  2468. "DIV",
  2469. [
  2470. tabs,
  2471. this.createElement("DIV", [], {
  2472. className: "clear",
  2473. }),
  2474. ],
  2475. {
  2476. style: "display: none; margin-bottom: 5px;",
  2477. }
  2478. );
  2479. }
  2480.  
  2481. /**
  2482. * 创建标签
  2483. * @param {Element} tabs 标签组
  2484. * @param {String} label 标签名称
  2485. * @param {Number} order 标签顺序,重复则跳过
  2486. * @param {*} properties 额外属性
  2487. */
  2488. createTab(tabs, label, order, properties = {}) {
  2489. const group = tabs.querySelector("TR");
  2490.  
  2491. const items = [...group.childNodes];
  2492.  
  2493. if (items.find((item) => item.order === order)) {
  2494. return;
  2495. }
  2496.  
  2497. if (items.length > 0) {
  2498. tabs.style.removeProperty("display");
  2499. }
  2500.  
  2501. const tab = this.createElement("A", label, {
  2502. ...properties,
  2503. className: "nobr silver",
  2504. onclick: () => {
  2505. if (tab.className === "nobr") {
  2506. return;
  2507. }
  2508.  
  2509. group.querySelectorAll("A").forEach((item) => {
  2510. if (item === tab) {
  2511. item.className = "nobr";
  2512. } else {
  2513. item.className = "nobr silver";
  2514. }
  2515. });
  2516.  
  2517. if (properties.onclick) {
  2518. properties.onclick();
  2519. }
  2520. },
  2521. });
  2522.  
  2523. const wrapper = this.createElement("TD", tab, {
  2524. order,
  2525. });
  2526.  
  2527. const anchor = items.find((item) => item.order > order);
  2528.  
  2529. group.insertBefore(wrapper, anchor || null);
  2530.  
  2531. return wrapper;
  2532. }
  2533.  
  2534. /**
  2535. * 创建对话框
  2536. * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
  2537. * @param {String} title 对话框的标题
  2538. * @param {HTMLElement} content 对话框的内容
  2539. */
  2540. createDialog(anchor, title, content) {
  2541. let window;
  2542.  
  2543. const show = () => {
  2544. if (window === undefined) {
  2545. window = commonui.createCommmonWindow();
  2546. }
  2547.  
  2548. window._.addContent(null);
  2549. window._.addTitle(title);
  2550. window._.addContent(content);
  2551. window._.show();
  2552. };
  2553.  
  2554. if (anchor) {
  2555. anchor.onclick = show;
  2556. } else {
  2557. show();
  2558. }
  2559.  
  2560. return window;
  2561. }
  2562.  
  2563. /**
  2564. * 渲染菜单
  2565. */
  2566. renderMenu() {
  2567. // 如果泥潭的右上角菜单还没有加载完成,说明模块尚未加载完毕,跳过
  2568. const anchor = document.querySelector("#mainmenu .td:last-child");
  2569.  
  2570. if (anchor === null) {
  2571. return;
  2572. }
  2573.  
  2574. const menu = this.createElement("A", this.constructor.label, {
  2575. className: "mmdefault nobr",
  2576. });
  2577.  
  2578. const container = this.createElement("DIV", menu, {
  2579. className: "td",
  2580. });
  2581.  
  2582. // 插入菜单
  2583. anchor.before(container);
  2584.  
  2585. // 绑定菜单元素
  2586. this.menu = menu;
  2587. }
  2588.  
  2589. /**
  2590. * 渲染视图
  2591. */
  2592. renderView() {
  2593. // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
  2594. if (this.menu === null) {
  2595. return;
  2596. }
  2597.  
  2598. // 绑定菜单点击事件.
  2599. this.createDialog(
  2600. this.menu,
  2601. this.constructor.label,
  2602. this.views.container
  2603. );
  2604.  
  2605. // 启用第一个模块
  2606. this.views.tabs.querySelector("A").click();
  2607. }
  2608.  
  2609. /**
  2610. * 渲染
  2611. */
  2612. render() {
  2613. this.renderMenu();
  2614. this.renderView();
  2615. }
  2616. }
  2617.  
  2618. /**
  2619. * 基础模块
  2620. */
  2621. class Module {
  2622. /**
  2623. * 模块名称
  2624. */
  2625. static name;
  2626.  
  2627. /**
  2628. * 模块标签
  2629. */
  2630. static label;
  2631.  
  2632. /**
  2633. * 顺序
  2634. */
  2635. static order;
  2636.  
  2637. /**
  2638. * 依赖模块
  2639. */
  2640. static depends = [];
  2641.  
  2642. /**
  2643. * 附加模块
  2644. */
  2645. static addons = [];
  2646.  
  2647. /**
  2648. * 设置
  2649. */
  2650. settings;
  2651.  
  2652. /**
  2653. * API
  2654. */
  2655. api;
  2656.  
  2657. /**
  2658. * UI
  2659. */
  2660. ui;
  2661.  
  2662. /**
  2663. * 过滤列表
  2664. */
  2665. data = [];
  2666.  
  2667. /**
  2668. * 依赖模块
  2669. */
  2670. depends = {};
  2671.  
  2672. /**
  2673. * 附加模块
  2674. */
  2675. addons = {};
  2676.  
  2677. /**
  2678. * 视图元素
  2679. */
  2680. views = {};
  2681.  
  2682. /**
  2683. * 初始化并绑定设置、API、UI、过滤列表,注册 UI
  2684. * @param {Settings} settings 设置
  2685. * @param {API} api API
  2686. * @param {UI} ui UI
  2687. */
  2688. constructor(settings, api, ui, data) {
  2689. this.settings = settings;
  2690. this.api = api;
  2691. this.ui = ui;
  2692.  
  2693. this.data = data;
  2694.  
  2695. this.init();
  2696. }
  2697.  
  2698. /**
  2699. * 创建实例
  2700. * @param {Settings} settings 设置
  2701. * @param {API} api API
  2702. * @param {UI} ui UI
  2703. * @param {Array} data 过滤列表
  2704. * @returns {Module | null} 成功后返回模块实例
  2705. */
  2706. static create(settings, api, ui, data) {
  2707. // 读取设置里的模块列表
  2708. const modules = settings.modules;
  2709.  
  2710. // 如果不包含自己或依赖的模块,则返回空
  2711. const index = [this, ...this.depends].findIndex(
  2712. (module) => modules.includes(module.name) === false
  2713. );
  2714.  
  2715. if (index >= 0) {
  2716. return null;
  2717. }
  2718.  
  2719. // 创建实例
  2720. const instance = new this(settings, api, ui, data);
  2721.  
  2722. // 返回实例
  2723. return instance;
  2724. }
  2725.  
  2726. /**
  2727. * 判断指定附加模块是否启用
  2728. * @param {typeof Module} module 模块
  2729. */
  2730. hasAddon(module) {
  2731. return Object.hasOwn(this.addons, module.name);
  2732. }
  2733.  
  2734. /**
  2735. * 初始化,创建基础视图和组件
  2736. */
  2737. init() {
  2738. if (this.views.container) {
  2739. this.destroy();
  2740. }
  2741.  
  2742. const { ui } = this;
  2743.  
  2744. const container = ui.createElement("DIV", []);
  2745.  
  2746. this.views = {
  2747. container,
  2748. };
  2749.  
  2750. this.initComponents();
  2751. }
  2752.  
  2753. /**
  2754. * 初始化组件
  2755. */
  2756. initComponents() {}
  2757.  
  2758. /**
  2759. * 销毁
  2760. */
  2761. destroy() {
  2762. Object.values(this.views).forEach((view) => {
  2763. if (view.parentNode) {
  2764. view.parentNode.removeChild(view);
  2765. }
  2766. });
  2767.  
  2768. this.views = {};
  2769. }
  2770.  
  2771. /**
  2772. * 渲染
  2773. * @param {HTMLElement} container 容器
  2774. */
  2775. render(container) {
  2776. container.innerHTML = "";
  2777. container.appendChild(this.views.container);
  2778. }
  2779.  
  2780. /**
  2781. * 过滤
  2782. * @param {*} item 绑定的 nFilter
  2783. * @param {*} result 过滤结果
  2784. */
  2785. async filter(item, result) {}
  2786.  
  2787. /**
  2788. * 通知
  2789. * @param {*} item 绑定的 nFilter
  2790. * @param {*} result 过滤结果
  2791. */
  2792. async notify(item, result) {}
  2793. }
  2794.  
  2795. /**
  2796. * 过滤器
  2797. */
  2798. class Filter {
  2799. /**
  2800. * 设置
  2801. */
  2802. settings;
  2803.  
  2804. /**
  2805. * API
  2806. */
  2807. api;
  2808.  
  2809. /**
  2810. * UI
  2811. */
  2812. ui;
  2813.  
  2814. /**
  2815. * 过滤列表
  2816. */
  2817. data = [];
  2818.  
  2819. /**
  2820. * 模块列表
  2821. */
  2822. modules = {};
  2823.  
  2824. /**
  2825. * 初始化并绑定设置、API、UI
  2826. * @param {Settings} settings 设置
  2827. * @param {API} api API
  2828. * @param {UI} ui UI
  2829. */
  2830. constructor(settings, api, ui) {
  2831. this.settings = settings;
  2832. this.api = api;
  2833. this.ui = ui;
  2834. }
  2835.  
  2836. /**
  2837. * 绑定两个模块的互相关系
  2838. * @param {Module} moduleA 模块A
  2839. * @param {Module} moduleB 模块B
  2840. */
  2841. bindModule(moduleA, moduleB) {
  2842. const nameA = moduleA.constructor.name;
  2843. const nameB = moduleB.constructor.name;
  2844.  
  2845. // A 依赖 B
  2846. if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
  2847. moduleA.depends[nameB] = moduleB;
  2848. moduleA.init();
  2849. }
  2850.  
  2851. // B 依赖 A
  2852. if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
  2853. moduleB.depends[nameA] = moduleA;
  2854. moduleB.init();
  2855. }
  2856.  
  2857. // A 附加 B
  2858. if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
  2859. moduleA.addons[nameB] = moduleB;
  2860. moduleA.init();
  2861. }
  2862.  
  2863. // B 附加 A
  2864. if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
  2865. moduleB.addons[nameA] = moduleA;
  2866. moduleB.init();
  2867. }
  2868. }
  2869.  
  2870. /**
  2871. * 加载模块
  2872. * @param {typeof Module} module 模块
  2873. */
  2874. initModule(module) {
  2875. // 如果已经加载过则跳过
  2876. if (Object.hasOwn(this.modules, module.name)) {
  2877. return;
  2878. }
  2879.  
  2880. // 创建模块
  2881. const instance = module.create(
  2882. this.settings,
  2883. this.api,
  2884. this.ui,
  2885. this.data
  2886. );
  2887.  
  2888. // 如果创建失败则跳过
  2889. if (instance === null) {
  2890. return;
  2891. }
  2892.  
  2893. // 绑定依赖模块和附加模块
  2894. Object.values(this.modules).forEach((item) => {
  2895. this.bindModule(item, instance);
  2896. });
  2897.  
  2898. // 合并模块
  2899. this.modules[module.name] = instance;
  2900.  
  2901. // 按照顺序重新整理模块
  2902. this.modules = Tools.sortBy(
  2903. Object.values(this.modules),
  2904. (item) => item.constructor.order
  2905. ).reduce(
  2906. (result, item) => ({
  2907. ...result,
  2908. [item.constructor.name]: item,
  2909. }),
  2910. {}
  2911. );
  2912. }
  2913.  
  2914. /**
  2915. * 加载模块列表
  2916. * @param {typeof Module[]} modules 模块列表
  2917. */
  2918. initModules(...modules) {
  2919. // 根据依赖和附加模块决定初始化的顺序
  2920. Tools.sortBy(
  2921. modules,
  2922. (item) => item.depends.length,
  2923. (item) => item.addons.length
  2924. ).forEach((module) => {
  2925. this.initModule(module);
  2926. });
  2927. }
  2928.  
  2929. /**
  2930. * 添加到过滤列表
  2931. * @param {*} item 绑定的 nFilter
  2932. */
  2933. pushData(item) {
  2934. // 清除掉无效数据
  2935. for (let i = 0; i < this.data.length; ) {
  2936. if (document.body.contains(this.data[i].container) === false) {
  2937. this.data.splice(i, 1);
  2938. continue;
  2939. }
  2940.  
  2941. i += 1;
  2942. }
  2943.  
  2944. // 加入过滤列表
  2945. if (this.data.includes(item) === false) {
  2946. this.data.push(item);
  2947. }
  2948. }
  2949.  
  2950. /**
  2951. * 判断指定 UID 是否是自己
  2952. * @param {Number} uid 用户 ID
  2953. */
  2954. isSelf(uid) {
  2955. return unsafeWindow.__CURRENT_UID === uid;
  2956. }
  2957.  
  2958. /**
  2959. * 获取过滤模式
  2960. * @param {*} item 绑定的 nFilter
  2961. */
  2962. async getFilterMode(item) {
  2963. // 获取链接参数
  2964. const params = new URLSearchParams(location.search);
  2965.  
  2966. // 跳过屏蔽(插件自定义)
  2967. if (params.has("nofilter")) {
  2968. return;
  2969. }
  2970.  
  2971. // 收藏
  2972. if (params.has("favor")) {
  2973. return;
  2974. }
  2975.  
  2976. // 只看某人
  2977. if (params.has("authorid")) {
  2978. return;
  2979. }
  2980.  
  2981. // 跳过自己
  2982. if (this.isSelf(item.uid)) {
  2983. return;
  2984. }
  2985.  
  2986. // 声明结果
  2987. const result = {
  2988. mode: -1,
  2989. reason: ``,
  2990. };
  2991.  
  2992. // 根据模块依次过滤
  2993. for (const module of Object.values(this.modules)) {
  2994. await module.filter(item, result);
  2995. }
  2996.  
  2997. // 写入过滤模式和过滤原因
  2998. item.filterMode = this.settings.getNameByMode(result.mode);
  2999. item.reason = result.reason;
  3000.  
  3001. // 通知各模块过滤结果
  3002. for (const module of Object.values(this.modules)) {
  3003. await module.notify(item, result);
  3004. }
  3005.  
  3006. // 继承模式下返回默认过滤模式
  3007. if (item.filterMode === "继承") {
  3008. return this.settings.defaultFilterMode;
  3009. }
  3010.  
  3011. // 返回结果
  3012. return item.filterMode;
  3013. }
  3014.  
  3015. /**
  3016. * 过滤主题
  3017. * @param {*} item 主题内容,见 commonui.topicArg.data
  3018. */
  3019. filterTopic(item) {
  3020. // 绑定事件
  3021. if (item.nFilter === undefined) {
  3022. // 主题 ID
  3023. const tid = item[8];
  3024.  
  3025. // 主题标题
  3026. const title = item[1];
  3027. const subject = title.innerText;
  3028.  
  3029. // 主题作者
  3030. const author = item[2];
  3031. const uid =
  3032. parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
  3033. const username = author.innerText;
  3034.  
  3035. // 主题容器
  3036. const container = title.closest("tr");
  3037.  
  3038. // 过滤函数
  3039. const execute = async () => {
  3040. // 获取过滤模式
  3041. const filterMode = await this.getFilterMode(item.nFilter);
  3042.  
  3043. // 样式处理
  3044. (() => {
  3045. // 还原样式
  3046. // TODO 应该整体采用 className 来实现
  3047. (() => {
  3048. // 标记模式
  3049. title.style.removeProperty("textDecoration");
  3050.  
  3051. // 遮罩模式
  3052. title.classList.remove("filter-mask");
  3053. author.classList.remove("filter-mask");
  3054. })();
  3055.  
  3056. // 样式处理
  3057. (() => {
  3058. // 标记模式下,主题标记会有删除线标识
  3059. if (filterMode === "标记") {
  3060. title.style.textDecoration = "line-through";
  3061. return;
  3062. }
  3063.  
  3064. // 遮罩模式下,主题和作者会有遮罩样式
  3065. if (filterMode === "遮罩") {
  3066. title.classList.add("filter-mask");
  3067. author.classList.add("filter-mask");
  3068. return;
  3069. }
  3070.  
  3071. // 隐藏模式下,容器会被隐藏
  3072. if (filterMode === "隐藏") {
  3073. container.style.display = "none";
  3074. return;
  3075. }
  3076. })();
  3077.  
  3078. // 非隐藏模式下,恢复显示
  3079. if (filterMode !== "隐藏") {
  3080. container.style.removeProperty("display");
  3081. }
  3082. })();
  3083. };
  3084.  
  3085. // 绑定事件
  3086. item.nFilter = {
  3087. tid,
  3088. pid: 0,
  3089. uid,
  3090. username,
  3091. container,
  3092. title,
  3093. author,
  3094. subject,
  3095. execute,
  3096. };
  3097.  
  3098. // 添加至列表
  3099. this.pushData(item.nFilter);
  3100. }
  3101.  
  3102. // 开始过滤
  3103. item.nFilter.execute();
  3104. }
  3105.  
  3106. /**
  3107. * 过滤回复
  3108. * @param {*} item 回复内容,见 commonui.postArg.data
  3109. */
  3110. filterReply(item) {
  3111. // 绑定事件
  3112. if (item.nFilter === undefined) {
  3113. // 主题 ID
  3114. const tid = item.tid;
  3115.  
  3116. // 回复 ID
  3117. const pid = item.pid;
  3118.  
  3119. // 判断是否是楼层
  3120. const isFloor = typeof item.i === "number";
  3121.  
  3122. // 回复容器
  3123. const container = isFloor
  3124. ? item.uInfoC.closest("tr")
  3125. : item.uInfoC.closest(".comment_c");
  3126.  
  3127. // 回复标题
  3128. const title = item.subjectC;
  3129. const subject = title.innerText;
  3130.  
  3131. // 回复内容
  3132. const content = item.contentC;
  3133. const contentBak = content.innerHTML;
  3134.  
  3135. // 回复作者
  3136. const author =
  3137. container.querySelector(".posterInfoLine") || item.uInfoC;
  3138. const uid = parseInt(item.pAid, 10) || 0;
  3139. const username = author.querySelector(".author").innerText;
  3140. const avatar = author.querySelector(".avatar");
  3141.  
  3142. // 找到用户 ID,将其视为操作按钮
  3143. const action = container.querySelector('[name="uid"]');
  3144.  
  3145. // 创建一个元素,用于展示标记列表
  3146. // 贴条和高赞不显示
  3147. const tags = (() => {
  3148. if (isFloor === false) {
  3149. return null;
  3150. }
  3151.  
  3152. const element = document.createElement("div");
  3153.  
  3154. element.className = "filter-tags";
  3155.  
  3156. author.appendChild(element);
  3157.  
  3158. return element;
  3159. })();
  3160.  
  3161. // 过滤函数
  3162. const execute = async () => {
  3163. // 获取过滤模式
  3164. const filterMode = await this.getFilterMode(item.nFilter);
  3165.  
  3166. // 样式处理
  3167. (() => {
  3168. // 还原样式
  3169. // TODO 应该整体采用 className 来实现
  3170. (() => {
  3171. // 标记模式
  3172. if (avatar) {
  3173. avatar.style.removeProperty("display");
  3174. }
  3175.  
  3176. content.innerHTML = contentBak;
  3177.  
  3178. // 遮罩模式
  3179. const caption = container.parentNode.querySelector("CAPTION");
  3180.  
  3181. if (caption) {
  3182. container.parentNode.removeChild(caption);
  3183. container.style.removeProperty("display");
  3184. }
  3185. })();
  3186.  
  3187. // 样式处理
  3188. (() => {
  3189. // 标记模式下,隐藏头像,采用泥潭的折叠样式
  3190. if (filterMode === "标记") {
  3191. if (avatar) {
  3192. avatar.style.display = "none";
  3193. }
  3194.  
  3195. this.ui.collapse(uid, content, contentBak);
  3196. return;
  3197. }
  3198.  
  3199. // 遮罩模式下,楼层会有遮罩样式
  3200. if (filterMode === "遮罩") {
  3201. const caption = document.createElement("CAPTION");
  3202.  
  3203. if (isFloor) {
  3204. caption.className = "filter-mask filter-mask-block";
  3205. } else {
  3206. caption.className = "filter-mask filter-mask-block left";
  3207. caption.style.width = "47%";
  3208. }
  3209.  
  3210. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  3211. caption.onclick = () => {
  3212. const caption = container.parentNode.querySelector("CAPTION");
  3213.  
  3214. if (caption) {
  3215. container.parentNode.removeChild(caption);
  3216. container.style.removeProperty("display");
  3217. }
  3218. };
  3219.  
  3220. container.parentNode.insertBefore(caption, container);
  3221. container.style.display = "none";
  3222. return;
  3223. }
  3224.  
  3225. // 隐藏模式下,容器会被隐藏
  3226. if (filterMode === "隐藏") {
  3227. container.style.display = "none";
  3228. return;
  3229. }
  3230. })();
  3231.  
  3232. // 非隐藏模式下,恢复显示
  3233. // 楼层的遮罩模式下仍需隐藏
  3234. if (["遮罩", "隐藏"].includes(filterMode) === false) {
  3235. container.style.removeProperty("display");
  3236. }
  3237. })();
  3238.  
  3239. // 过滤引用
  3240. this.filterQuote(item);
  3241. };
  3242.  
  3243. // 绑定事件
  3244. item.nFilter = {
  3245. tid,
  3246. pid,
  3247. uid,
  3248. username,
  3249. container,
  3250. title,
  3251. author,
  3252. subject,
  3253. content: content.innerText,
  3254. action,
  3255. tags,
  3256. execute,
  3257. };
  3258.  
  3259. // 添加至列表
  3260. this.pushData(item.nFilter);
  3261. }
  3262.  
  3263. // 开始过滤
  3264. item.nFilter.execute();
  3265. }
  3266.  
  3267. /**
  3268. * 过滤引用
  3269. * @param {*} item 回复内容,见 commonui.postArg.data
  3270. */
  3271. filterQuote(item) {
  3272. // 未绑定事件,直接跳过
  3273. if (item.nFilter === undefined) {
  3274. return;
  3275. }
  3276.  
  3277. // 回复内容
  3278. const content = item.contentC;
  3279.  
  3280. // 找到所有引用
  3281. const quotes = content.querySelectorAll(".quote");
  3282.  
  3283. // 处理引用
  3284. [...quotes].map(async (quote) => {
  3285. const uid = (() => {
  3286. const ele = quote.querySelector("a[href^='/nuke.php']");
  3287.  
  3288. if (ele) {
  3289. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  3290.  
  3291. if (res) {
  3292. return parseInt(res[1], 10);
  3293. }
  3294. }
  3295.  
  3296. return 0;
  3297. })();
  3298.  
  3299. const { tid, pid } = (() => {
  3300. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  3301.  
  3302. if (ele) {
  3303. const res = ele
  3304. .getAttribute("onclick")
  3305. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  3306.  
  3307. if (res) {
  3308. return {
  3309. tid: parseInt(res[2], 10),
  3310. pid: parseInt(res[3], 10) || 0,
  3311. };
  3312. }
  3313. }
  3314.  
  3315. return {};
  3316. })();
  3317.  
  3318. // 临时的 nFilter
  3319. const nFilter = {
  3320. uid,
  3321. tid,
  3322. pid,
  3323. subject: "",
  3324. content: quote.innerText,
  3325. };
  3326.  
  3327. // 获取过滤模式
  3328. const filterMode = await this.getFilterMode(nFilter);
  3329.  
  3330. (() => {
  3331. if (filterMode === "标记") {
  3332. this.ui.collapse(uid, quote, quote.innerHTML);
  3333. return;
  3334. }
  3335.  
  3336. if (filterMode === "遮罩") {
  3337. const source = document.createElement("DIV");
  3338.  
  3339. source.innerHTML = quote.innerHTML;
  3340. source.style.display = "none";
  3341.  
  3342. const caption = document.createElement("CAPTION");
  3343.  
  3344. caption.className = "filter-mask filter-mask-block";
  3345.  
  3346. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  3347. caption.onclick = () => {
  3348. quote.removeChild(caption);
  3349.  
  3350. source.style.display = "";
  3351. };
  3352.  
  3353. quote.innerHTML = "";
  3354. quote.appendChild(source);
  3355. quote.appendChild(caption);
  3356. return;
  3357. }
  3358.  
  3359. if (filterMode === "隐藏") {
  3360. quote.innerHTML = "";
  3361. return;
  3362. }
  3363. })();
  3364.  
  3365. // 绑定引用
  3366. item.nFilter.quotes = item.nFilter.quotes || {};
  3367. item.nFilter.quotes[uid] = nFilter.filterMode;
  3368. });
  3369. }
  3370. }
  3371.  
  3372. /**
  3373. * 列表模块
  3374. */
  3375. class ListModule extends Module {
  3376. /**
  3377. * 模块名称
  3378. */
  3379. static name = "list";
  3380.  
  3381. /**
  3382. * 模块标签
  3383. */
  3384. static label = "列表";
  3385.  
  3386. /**
  3387. * 顺序
  3388. */
  3389. static order = 10;
  3390.  
  3391. /**
  3392. * 表格列
  3393. * @returns {Array} 表格列集合
  3394. */
  3395. columns() {
  3396. return [
  3397. { label: "内容", ellipsis: true },
  3398. { label: "过滤模式", center: true, width: 1 },
  3399. { label: "原因", width: 1 },
  3400. ];
  3401. }
  3402.  
  3403. /**
  3404. * 表格项
  3405. * @param {*} item 绑定的 nFilter
  3406. * @returns {Array} 表格项集合
  3407. */
  3408. column(item) {
  3409. const { ui } = this;
  3410. const { tid, pid, filterMode, reason } = item;
  3411.  
  3412. // 移除 BR 标签
  3413. item.content = (item.content || "").replace(/<br>/g, "");
  3414.  
  3415. // 主题
  3416. const subject = (() => {
  3417. if (tid) {
  3418. // 如果有 TID 但没有标题,是引用,采用内容逻辑
  3419. if (item.subject.length === 0) {
  3420. return ui.createElement("A", item.content, {
  3421. href: `/read.php?tid=${tid}&nofilter`,
  3422. });
  3423. }
  3424.  
  3425. return ui.createElement("A", item.subject, {
  3426. href: `/read.php?tid=${tid}&nofilter`,
  3427. title: item.content,
  3428. className: "b nobr",
  3429. });
  3430. }
  3431.  
  3432. return item.subject;
  3433. })();
  3434.  
  3435. // 内容
  3436. const content = (() => {
  3437. if (subject) {
  3438. return subject;
  3439. }
  3440.  
  3441. if (pid) {
  3442. return ui.createElement("A", item.content, {
  3443. href: `/read.php?pid=${pid}&nofilter`,
  3444. });
  3445. }
  3446.  
  3447. return item.content;
  3448. })();
  3449.  
  3450. return [content, filterMode, reason];
  3451. }
  3452.  
  3453. /**
  3454. * 初始化组件
  3455. */
  3456. initComponents() {
  3457. super.initComponents();
  3458.  
  3459. const { tabs, content } = this.ui.views;
  3460.  
  3461. const table = this.ui.createTable(this.columns());
  3462.  
  3463. const tab = this.ui.createTab(
  3464. tabs,
  3465. this.constructor.label,
  3466. this.constructor.order,
  3467. {
  3468. onclick: () => {
  3469. this.render(content);
  3470. },
  3471. }
  3472. );
  3473.  
  3474. Object.assign(this.views, {
  3475. tab,
  3476. table,
  3477. });
  3478.  
  3479. this.views.container.appendChild(table);
  3480. }
  3481.  
  3482. /**
  3483. * 渲染
  3484. * @param {HTMLElement} container 容器
  3485. */
  3486. render(container) {
  3487. super.render(container);
  3488.  
  3489. const { table } = this.views;
  3490.  
  3491. if (table) {
  3492. const { add, clear } = table;
  3493.  
  3494. clear();
  3495.  
  3496. const list = this.data.filter((item) => {
  3497. return (item.filterMode || "显示") !== "显示";
  3498. });
  3499.  
  3500. Object.values(list).forEach((item) => {
  3501. const column = this.column(item);
  3502.  
  3503. add(...column);
  3504. });
  3505. }
  3506. }
  3507.  
  3508. /**
  3509. * 通知
  3510. * @param {*} item 绑定的 nFilter
  3511. */
  3512. async notify() {
  3513. // 获取过滤后的数量
  3514. const count = this.data.filter((item) => {
  3515. return (item.filterMode || "显示") !== "显示";
  3516. }).length;
  3517.  
  3518. // 更新菜单文字
  3519. const { ui } = this;
  3520. const { menu } = ui;
  3521.  
  3522. if (menu === null) {
  3523. return;
  3524. }
  3525.  
  3526. if (count) {
  3527. menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  3528. } else {
  3529. menu.innerHTML = `${ui.constructor.label}`;
  3530. }
  3531.  
  3532. // 重新渲染
  3533. // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
  3534. const { tab } = this.views;
  3535.  
  3536. if (tab.querySelector("A").className === "nobr") {
  3537. this.render(ui.views.content);
  3538. }
  3539. }
  3540. }
  3541.  
  3542. /**
  3543. * 用户模块
  3544. */
  3545. class UserModule extends Module {
  3546. /**
  3547. * 模块名称
  3548. */
  3549. static name = "user";
  3550.  
  3551. /**
  3552. * 模块标签
  3553. */
  3554. static label = "用户";
  3555.  
  3556. /**
  3557. * 顺序
  3558. */
  3559. static order = 20;
  3560.  
  3561. /**
  3562. * 获取列表
  3563. */
  3564. get list() {
  3565. return this.settings.users;
  3566. }
  3567.  
  3568. /**
  3569. * 获取用户
  3570. * @param {Number} uid 用户 ID
  3571. */
  3572. get(uid) {
  3573. // 获取列表
  3574. const list = this.list;
  3575.  
  3576. // 如果存在,则返回信息
  3577. if (list[uid]) {
  3578. return list[uid];
  3579. }
  3580.  
  3581. return null;
  3582. }
  3583.  
  3584. /**
  3585. * 添加用户
  3586. * @param {Number} uid 用户 ID
  3587. */
  3588. add(uid, values) {
  3589. // 获取列表
  3590. const list = this.list;
  3591.  
  3592. // 如果已存在,则返回信息
  3593. if (list[uid]) {
  3594. return list[uid];
  3595. }
  3596.  
  3597. // 写入用户信息
  3598. list[uid] = values;
  3599.  
  3600. // 保存数据
  3601. this.settings.users = list;
  3602.  
  3603. // 重新过滤
  3604. this.reFilter(uid);
  3605.  
  3606. // 返回添加的用户
  3607. return values;
  3608. }
  3609.  
  3610. /**
  3611. * 编辑用户
  3612. * @param {Number} uid 用户 ID
  3613. * @param {*} values 用户信息
  3614. */
  3615. update(uid, values) {
  3616. // 获取列表
  3617. const list = this.list;
  3618.  
  3619. // 如果不存在则跳过
  3620. if (Object.hasOwn(list, uid) === false) {
  3621. return null;
  3622. }
  3623.  
  3624. // 获取用户
  3625. const entity = list[uid];
  3626.  
  3627. // 更新用户
  3628. Object.assign(entity, values);
  3629.  
  3630. // 保存数据
  3631. this.settings.users = list;
  3632.  
  3633. // 重新过滤
  3634. this.reFilter(uid);
  3635.  
  3636. // 返回编辑的用户
  3637. return entity;
  3638. }
  3639.  
  3640. /**
  3641. * 删除用户
  3642. * @param {Number} uid 用户 ID
  3643. * @returns {Object | null} 删除的用户
  3644. */
  3645. remove(uid) {
  3646. // 获取列表
  3647. const list = this.list;
  3648.  
  3649. // 如果不存在则跳过
  3650. if (Object.hasOwn(list, uid) === false) {
  3651. return null;
  3652. }
  3653.  
  3654. // 获取用户
  3655. const entity = list[uid];
  3656.  
  3657. // 删除用户
  3658. delete list[uid];
  3659.  
  3660. // 保存数据
  3661. this.settings.users = list;
  3662.  
  3663. // 重新过滤
  3664. this.reFilter(uid);
  3665.  
  3666. // 返回删除的用户
  3667. return entity;
  3668. }
  3669.  
  3670. /**
  3671. * 格式化
  3672. * @param {Number} uid 用户 ID
  3673. * @param {String | undefined} name 用户名称
  3674. */
  3675. format(uid, name) {
  3676. if (uid <= 0) {
  3677. return null;
  3678. }
  3679.  
  3680. const { ui } = this;
  3681.  
  3682. const user = this.get(uid);
  3683.  
  3684. if (user) {
  3685. name = user.name;
  3686. }
  3687.  
  3688. const username = name ? "@" + name : "#" + uid;
  3689.  
  3690. return ui.createElement("A", `[${username}]`, {
  3691. className: "b nobr",
  3692. href: `/nuke.php?func=ucp&uid=${uid}`,
  3693. });
  3694. }
  3695.  
  3696. /**
  3697. * 表格列
  3698. * @returns {Array} 表格列集合
  3699. */
  3700. columns() {
  3701. return [
  3702. { label: "昵称" },
  3703. { label: "过滤模式", center: true, width: 1 },
  3704. { label: "操作", width: 1 },
  3705. ];
  3706. }
  3707.  
  3708. /**
  3709. * 表格项
  3710. * @param {*} item 用户信息
  3711. * @returns {Array} 表格项集合
  3712. */
  3713. column(item) {
  3714. const { ui } = this;
  3715. const { table } = this.views;
  3716. const { id, name, filterMode } = item;
  3717.  
  3718. // 昵称
  3719. const user = this.format(id, name);
  3720.  
  3721. // 切换过滤模式
  3722. const switchMode = ui.createButton(
  3723. filterMode || this.settings.filterModes[0],
  3724. () => {
  3725. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3726.  
  3727. this.update(id, {
  3728. filterMode: newMode,
  3729. });
  3730.  
  3731. switchMode.innerText = newMode;
  3732. }
  3733. );
  3734.  
  3735. // 操作
  3736. const buttons = (() => {
  3737. const remove = ui.createButton("删除", (e) => {
  3738. ui.confirm().then(() => {
  3739. this.remove(id);
  3740.  
  3741. table.remove(e);
  3742. });
  3743. });
  3744.  
  3745. return ui.createButtonGroup(remove);
  3746. })();
  3747.  
  3748. return [user, switchMode, buttons];
  3749. }
  3750.  
  3751. /**
  3752. * 初始化组件
  3753. */
  3754. initComponents() {
  3755. super.initComponents();
  3756.  
  3757. const { ui } = this;
  3758. const { tabs, content, settings } = ui.views;
  3759. const { add } = settings;
  3760.  
  3761. const table = ui.createTable(this.columns());
  3762.  
  3763. const tab = ui.createTab(
  3764. tabs,
  3765. this.constructor.label,
  3766. this.constructor.order,
  3767. {
  3768. onclick: () => {
  3769. this.render(content);
  3770. },
  3771. }
  3772. );
  3773.  
  3774. Object.assign(this.views, {
  3775. tab,
  3776. table,
  3777. });
  3778.  
  3779. this.views.container.appendChild(table);
  3780.  
  3781. // 删除非激活中的用户
  3782. {
  3783. const list = ui.createElement("DIV", [], {
  3784. style: "white-space: normal;",
  3785. });
  3786.  
  3787. const button = ui.createButton("删除非激活中的用户", () => {
  3788. ui.confirm().then(() => {
  3789. list.innerHTML = "";
  3790.  
  3791. const users = Object.values(this.list);
  3792.  
  3793. const waitingQueue = users.map(
  3794. ({ id }) =>
  3795. () =>
  3796. this.api.getUserInfo(id).then(({ bit }) => {
  3797. const activeInfo = commonui.activeInfo(0, 0, bit);
  3798. const activeType = activeInfo[1];
  3799.  
  3800. if (["ACTIVED", "LINKED"].includes(activeType)) {
  3801. return;
  3802. }
  3803.  
  3804. list.append(this.format(id));
  3805.  
  3806. this.remove(id);
  3807. })
  3808. );
  3809.  
  3810. const queueLength = waitingQueue.length;
  3811.  
  3812. const execute = () => {
  3813. if (waitingQueue.length) {
  3814. const next = waitingQueue.shift();
  3815.  
  3816. button.disabled = true;
  3817. button.innerHTML = `删除非激活中的用户 (${
  3818. queueLength - waitingQueue.length
  3819. }/${queueLength})`;
  3820.  
  3821. next().finally(execute);
  3822. return;
  3823. }
  3824.  
  3825. button.disabled = false;
  3826. };
  3827.  
  3828. execute();
  3829. });
  3830. });
  3831.  
  3832. const element = ui.createElement("DIV", [button, list]);
  3833.  
  3834. add(this.constructor.order + 0, element);
  3835. }
  3836. }
  3837.  
  3838. /**
  3839. * 渲染
  3840. * @param {HTMLElement} container 容器
  3841. */
  3842. render(container) {
  3843. super.render(container);
  3844.  
  3845. const { table } = this.views;
  3846.  
  3847. if (table) {
  3848. const { add, clear } = table;
  3849.  
  3850. clear();
  3851.  
  3852. Object.values(this.list).forEach((item) => {
  3853. const column = this.column(item);
  3854.  
  3855. add(...column);
  3856. });
  3857. }
  3858. }
  3859.  
  3860. /**
  3861. * 渲染详情
  3862. * @param {Number} uid 用户 ID
  3863. * @param {String | undefined} name 用户名称
  3864. * @param {Function} callback 回调函数
  3865. */
  3866. renderDetails(uid, name, callback = () => {}) {
  3867. const { ui, settings } = this;
  3868.  
  3869. // 只允许同时存在一个详情页
  3870. if (this.views.details) {
  3871. if (this.views.details.parentNode) {
  3872. this.views.details.parentNode.removeChild(this.views.details);
  3873. }
  3874. }
  3875.  
  3876. // 获取用户信息
  3877. const user = this.get(uid);
  3878.  
  3879. if (user) {
  3880. name = user.name;
  3881. }
  3882.  
  3883. const title =
  3884. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  3885.  
  3886. const filterMode = user ? user.filterMode : settings.filterModes[0];
  3887.  
  3888. const switchMode = ui.createButton(filterMode, () => {
  3889. const newMode = settings.switchModeByName(switchMode.innerText);
  3890.  
  3891. switchMode.innerText = newMode;
  3892. });
  3893.  
  3894. const buttons = ui.createElement(
  3895. "DIV",
  3896. (() => {
  3897. const remove = user
  3898. ? ui.createButton("删除", () => {
  3899. ui.confirm().then(() => {
  3900. this.remove(uid);
  3901.  
  3902. this.views.details._.hide();
  3903.  
  3904. callback("REMOVE");
  3905. });
  3906. })
  3907. : null;
  3908.  
  3909. const save = ui.createButton("保存", () => {
  3910. if (user === null) {
  3911. const entity = this.add(uid, {
  3912. id: uid,
  3913. name,
  3914. tags: [],
  3915. filterMode: switchMode.innerText,
  3916. });
  3917.  
  3918. this.views.details._.hide();
  3919.  
  3920. callback("ADD", entity);
  3921. } else {
  3922. const entity = this.update(uid, {
  3923. name,
  3924. filterMode: switchMode.innerText,
  3925. });
  3926.  
  3927. this.views.details._.hide();
  3928.  
  3929. callback("UPDATE", entity);
  3930. }
  3931. });
  3932.  
  3933. return ui.createButtonGroup(remove, save);
  3934. })(),
  3935. {
  3936. className: "right_",
  3937. }
  3938. );
  3939.  
  3940. const actions = ui.createElement(
  3941. "DIV",
  3942. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  3943. {
  3944. style: "margin-top: 10px;",
  3945. }
  3946. );
  3947.  
  3948. const tips = ui.createElement("DIV", TIPS.filterMode, {
  3949. className: "silver",
  3950. style: "margin-top: 10px;",
  3951. });
  3952.  
  3953. const content = ui.createElement("DIV", [actions, tips], {
  3954. style: "width: 80vw",
  3955. });
  3956.  
  3957. // 创建弹出框
  3958. this.views.details = ui.createDialog(null, title, content);
  3959. }
  3960.  
  3961. /**
  3962. * 过滤
  3963. * @param {*} item 绑定的 nFilter
  3964. * @param {*} result 过滤结果
  3965. */
  3966. async filter(item, result) {
  3967. // 获取用户信息
  3968. const user = this.get(item.uid);
  3969.  
  3970. // 没有则跳过
  3971. if (user === null) {
  3972. return;
  3973. }
  3974.  
  3975. // 获取用户过滤模式
  3976. const mode = this.settings.getModeByName(user.filterMode);
  3977.  
  3978. // 不高于当前过滤模式则跳过
  3979. if (mode <= result.mode) {
  3980. return;
  3981. }
  3982.  
  3983. // 更新过滤模式和原因
  3984. result.mode = mode;
  3985. result.reason = `用户模式: ${user.filterMode}`;
  3986. }
  3987.  
  3988. /**
  3989. * 通知
  3990. * @param {*} item 绑定的 nFilter
  3991. */
  3992. async notify(item) {
  3993. const { uid, username, action } = item;
  3994.  
  3995. // 如果没有 action 组件则跳过
  3996. if (action === undefined) {
  3997. return;
  3998. }
  3999.  
  4000. // 如果是匿名,隐藏组件
  4001. if (uid <= 0) {
  4002. action.style.display = "none";
  4003. return;
  4004. }
  4005.  
  4006. // 获取当前用户
  4007. const user = this.get(uid);
  4008.  
  4009. // 修改操作按钮文字
  4010. action.innerText = "屏蔽";
  4011.  
  4012. // 修改操作按钮颜色
  4013. if (user) {
  4014. action.style.background = "#CB4042";
  4015. } else {
  4016. action.style.background = "#AAA";
  4017. }
  4018.  
  4019. // 绑定事件
  4020. action.onclick = () => {
  4021. this.renderDetails(uid, username);
  4022. };
  4023. }
  4024.  
  4025. /**
  4026. * 重新过滤
  4027. * @param {Number} uid 用户 ID
  4028. */
  4029. reFilter(uid) {
  4030. this.data.forEach((item) => {
  4031. // 如果用户 ID 一致,则重新过滤
  4032. if (item.uid === uid) {
  4033. item.execute();
  4034. return;
  4035. }
  4036.  
  4037. // 如果有引用,也重新过滤
  4038. if (Object.hasOwn(item.quotes || {}, uid)) {
  4039. item.execute();
  4040. return;
  4041. }
  4042. });
  4043. }
  4044. }
  4045.  
  4046. /**
  4047. * 标记模块
  4048. */
  4049. class TagModule extends Module {
  4050. /**
  4051. * 模块名称
  4052. */
  4053. static name = "tag";
  4054.  
  4055. /**
  4056. * 模块标签
  4057. */
  4058. static label = "标记";
  4059.  
  4060. /**
  4061. * 顺序
  4062. */
  4063. static order = 30;
  4064.  
  4065. /**
  4066. * 依赖模块
  4067. */
  4068. static depends = [UserModule];
  4069.  
  4070. /**
  4071. * 依赖的用户模块
  4072. * @returns {UserModule} 用户模块
  4073. */
  4074. get userModule() {
  4075. return this.depends[UserModule.name];
  4076. }
  4077.  
  4078. /**
  4079. * 获取列表
  4080. */
  4081. get list() {
  4082. return this.settings.tags;
  4083. }
  4084.  
  4085. /**
  4086. * 获取标记
  4087. * @param {Number} id 标记 ID
  4088. * @param {String} name 标记名称
  4089. */
  4090. get({ id, name }) {
  4091. // 获取列表
  4092. const list = this.list;
  4093.  
  4094. // 通过 ID 获取标记
  4095. if (list[id]) {
  4096. return list[id];
  4097. }
  4098.  
  4099. // 通过名称获取标记
  4100. if (name) {
  4101. const tag = Object.values(list).find((item) => item.name === name);
  4102.  
  4103. if (tag) {
  4104. return tag;
  4105. }
  4106. }
  4107.  
  4108. return null;
  4109. }
  4110.  
  4111. /**
  4112. * 添加标记
  4113. * @param {String} name 标记名称
  4114. */
  4115. add(name) {
  4116. // 获取对应的标记
  4117. const tag = this.get({ name });
  4118.  
  4119. // 如果标记已存在,则返回标记信息,否则增加标记
  4120. if (tag) {
  4121. return tag;
  4122. }
  4123.  
  4124. // 获取列表
  4125. const list = this.list;
  4126.  
  4127. // ID 为最大值 + 1
  4128. const id = Math.max(...Object.keys(list), 0) + 1;
  4129.  
  4130. // 标记的颜色
  4131. const color = Tools.generateColor(name);
  4132.  
  4133. // 写入标记信息
  4134. list[id] = {
  4135. id,
  4136. name,
  4137. color,
  4138. filterMode: this.settings.filterModes[0],
  4139. };
  4140.  
  4141. // 保存数据
  4142. this.settings.tags = list;
  4143.  
  4144. // 返回添加的标记
  4145. return list[id];
  4146. }
  4147.  
  4148. /**
  4149. * 编辑标记
  4150. * @param {Number} id 标记 ID
  4151. * @param {*} values 标记信息
  4152. */
  4153. update(id, values) {
  4154. // 获取列表
  4155. const list = this.list;
  4156.  
  4157. // 如果不存在则跳过
  4158. if (Object.hasOwn(list, id) === false) {
  4159. return null;
  4160. }
  4161.  
  4162. // 获取标记
  4163. const entity = list[id];
  4164.  
  4165. // 获取相关的用户
  4166. const users = Object.values(this.userModule.list).filter((user) =>
  4167. user.tags.includes(id)
  4168. );
  4169.  
  4170. // 更新标记
  4171. Object.assign(entity, values);
  4172.  
  4173. // 保存数据
  4174. this.settings.tags = list;
  4175.  
  4176. // 重新过滤
  4177. this.reFilter(users);
  4178. }
  4179.  
  4180. /**
  4181. * 删除标记
  4182. * @param {Number} id 标记 ID
  4183. */
  4184. remove(id) {
  4185. // 获取列表
  4186. const list = this.list;
  4187.  
  4188. // 如果不存在则跳过
  4189. if (Object.hasOwn(list, id) === false) {
  4190. return null;
  4191. }
  4192.  
  4193. // 获取标记
  4194. const entity = list[id];
  4195.  
  4196. // 获取相关的用户
  4197. const users = Object.values(this.userModule.list).filter((user) =>
  4198. user.tags.includes(id)
  4199. );
  4200.  
  4201. // 删除标记
  4202. delete list[id];
  4203.  
  4204. // 删除相关的用户标记
  4205. users.forEach((user) => {
  4206. const index = user.tags.findIndex((item) => item === id);
  4207.  
  4208. if (index >= 0) {
  4209. user.tags.splice(index, 1);
  4210. }
  4211. });
  4212.  
  4213. // 保存数据
  4214. this.settings.tags = list;
  4215.  
  4216. // 重新过滤
  4217. this.reFilter(users);
  4218.  
  4219. // 返回删除的标记
  4220. return entity;
  4221. }
  4222.  
  4223. /**
  4224. * 格式化
  4225. * @param {Number} id 标记 ID
  4226. * @param {String | undefined} name 标记名称
  4227. * @param {String | undefined} name 标记颜色
  4228. */
  4229. format(id, name, color) {
  4230. const { ui } = this;
  4231.  
  4232. if (id >= 0) {
  4233. const tag = this.get({ id });
  4234.  
  4235. if (tag) {
  4236. name = tag.name;
  4237. color = tag.color;
  4238. }
  4239. }
  4240.  
  4241. if (name && color) {
  4242. return ui.createElement("B", name, {
  4243. className: "block_txt nobr",
  4244. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  4245. });
  4246. }
  4247.  
  4248. return "";
  4249. }
  4250.  
  4251. /**
  4252. * 表格列
  4253. * @returns {Array} 表格列集合
  4254. */
  4255. columns() {
  4256. return [
  4257. { label: "标记", width: 1 },
  4258. { label: "列表" },
  4259. { label: "过滤模式", width: 1 },
  4260. { label: "操作", width: 1 },
  4261. ];
  4262. }
  4263.  
  4264. /**
  4265. * 表格项
  4266. * @param {*} item 标记信息
  4267. * @returns {Array} 表格项集合
  4268. */
  4269. column(item) {
  4270. const { ui } = this;
  4271. const { table } = this.views;
  4272. const { id, filterMode } = item;
  4273.  
  4274. // 标记
  4275. const tag = this.format(id);
  4276.  
  4277. // 用户列表
  4278. const list = Object.values(this.userModule.list)
  4279. .filter(({ tags }) => tags.includes(id))
  4280. .map(({ id }) => this.userModule.format(id));
  4281.  
  4282. const group = ui.createElement("DIV", list, {
  4283. style: "white-space: normal; display: none;",
  4284. });
  4285.  
  4286. const switchButton = ui.createButton(list.length.toString(), () => {
  4287. if (group.style.display === "none") {
  4288. group.style.removeProperty("display");
  4289. } else {
  4290. group.style.display = "none";
  4291. }
  4292. });
  4293.  
  4294. // 切换过滤模式
  4295. const switchMode = ui.createButton(
  4296. filterMode || this.settings.filterModes[0],
  4297. () => {
  4298. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4299.  
  4300. this.update(id, {
  4301. filterMode: newMode,
  4302. });
  4303.  
  4304. switchMode.innerText = newMode;
  4305. }
  4306. );
  4307.  
  4308. // 操作
  4309. const buttons = (() => {
  4310. const remove = ui.createButton("删除", (e) => {
  4311. ui.confirm().then(() => {
  4312. this.remove(id);
  4313.  
  4314. table.remove(e);
  4315. });
  4316. });
  4317.  
  4318. return ui.createButtonGroup(remove);
  4319. })();
  4320.  
  4321. return [tag, [switchButton, group], switchMode, buttons];
  4322. }
  4323.  
  4324. /**
  4325. * 初始化组件
  4326. */
  4327. initComponents() {
  4328. super.initComponents();
  4329.  
  4330. const { ui } = this;
  4331. const { tabs, content, settings } = ui.views;
  4332. const { add } = settings;
  4333.  
  4334. const table = ui.createTable(this.columns());
  4335.  
  4336. const tab = ui.createTab(
  4337. tabs,
  4338. this.constructor.label,
  4339. this.constructor.order,
  4340. {
  4341. onclick: () => {
  4342. this.render(content);
  4343. },
  4344. }
  4345. );
  4346.  
  4347. Object.assign(this.views, {
  4348. tab,
  4349. table,
  4350. });
  4351.  
  4352. this.views.container.appendChild(table);
  4353.  
  4354. // 删除没有标记的用户
  4355. {
  4356. const button = ui.createButton("删除没有标记的用户", () => {
  4357. ui.confirm().then(() => {
  4358. const users = Object.values(this.userModule.list);
  4359.  
  4360. users.forEach(({ id, tags }) => {
  4361. if (tags.length > 0) {
  4362. return;
  4363. }
  4364.  
  4365. this.userModule.remove(id);
  4366. });
  4367. });
  4368. });
  4369.  
  4370. const element = ui.createElement("DIV", button);
  4371.  
  4372. add(this.constructor.order + 0, element);
  4373. }
  4374.  
  4375. // 删除没有用户的标记
  4376. {
  4377. const button = ui.createButton("删除没有用户的标记", () => {
  4378. ui.confirm().then(() => {
  4379. const items = Object.values(this.list);
  4380. const users = Object.values(this.userModule.list);
  4381.  
  4382. items.forEach(({ id }) => {
  4383. if (users.find(({ tags }) => tags.includes(id))) {
  4384. return;
  4385. }
  4386.  
  4387. this.remove(id);
  4388. });
  4389. });
  4390. });
  4391.  
  4392. const element = ui.createElement("DIV", button);
  4393.  
  4394. add(this.constructor.order + 1, element);
  4395. }
  4396. }
  4397.  
  4398. /**
  4399. * 渲染
  4400. * @param {HTMLElement} container 容器
  4401. */
  4402. render(container) {
  4403. super.render(container);
  4404.  
  4405. const { table } = this.views;
  4406.  
  4407. if (table) {
  4408. const { add, clear } = table;
  4409.  
  4410. clear();
  4411.  
  4412. Object.values(this.list).forEach((item) => {
  4413. const column = this.column(item);
  4414.  
  4415. add(...column);
  4416. });
  4417. }
  4418. }
  4419.  
  4420. /**
  4421. * 过滤
  4422. * @param {*} item 绑定的 nFilter
  4423. * @param {*} result 过滤结果
  4424. */
  4425. async filter(item, result) {
  4426. // 获取用户信息
  4427. const user = this.userModule.get(item.uid);
  4428.  
  4429. // 没有则跳过
  4430. if (user === null) {
  4431. return;
  4432. }
  4433.  
  4434. // 获取用户标记
  4435. const tags = user.tags;
  4436.  
  4437. // 取最高的过滤模式
  4438. // 低于当前的过滤模式则跳过
  4439. let max = result.mode;
  4440. let tag = null;
  4441.  
  4442. for (const id of tags) {
  4443. const entity = this.get({ id });
  4444.  
  4445. if (entity === null) {
  4446. continue;
  4447. }
  4448.  
  4449. // 获取过滤模式
  4450. const mode = this.settings.getModeByName(entity.filterMode);
  4451.  
  4452. if (mode <= max) {
  4453. continue;
  4454. }
  4455.  
  4456. max = mode;
  4457. tag = entity;
  4458. }
  4459.  
  4460. // 没有匹配的则跳过
  4461. if (tag === null) {
  4462. return;
  4463. }
  4464.  
  4465. // 更新过滤模式和原因
  4466. result.mode = max;
  4467. result.reason = `标记: ${tag.name}`;
  4468. }
  4469.  
  4470. /**
  4471. * 通知
  4472. * @param {*} item 绑定的 nFilter
  4473. */
  4474. async notify(item) {
  4475. const { uid, tags } = item;
  4476.  
  4477. // 如果没有 tags 组件则跳过
  4478. if (tags === undefined) {
  4479. return;
  4480. }
  4481.  
  4482. // 如果是匿名,隐藏组件
  4483. if (uid <= 0) {
  4484. tags.style.display = "none";
  4485. return;
  4486. }
  4487.  
  4488. // 删除旧标记
  4489. [...tags.querySelectorAll("[tid]")].forEach((item) => {
  4490. tags.removeChild(item);
  4491. });
  4492.  
  4493. // 获取当前用户
  4494. const user = this.userModule.get(uid);
  4495.  
  4496. // 如果没有用户,则跳过
  4497. if (user === null) {
  4498. return;
  4499. }
  4500.  
  4501. // 格式化标记
  4502. const items = user.tags.map((id) => {
  4503. const item = this.format(id);
  4504.  
  4505. if (item) {
  4506. item.setAttribute("tid", id);
  4507. }
  4508.  
  4509. return item;
  4510. });
  4511.  
  4512. // 加入组件
  4513. items.forEach((item) => {
  4514. if (item) {
  4515. tags.appendChild(item);
  4516. }
  4517. });
  4518. }
  4519.  
  4520. /**
  4521. * 重新过滤
  4522. * @param {Array} users 用户集合
  4523. */
  4524. reFilter(users) {
  4525. users.forEach((user) => {
  4526. this.userModule.reFilter(user.id);
  4527. });
  4528. }
  4529. }
  4530.  
  4531. /**
  4532. * 关键字模块
  4533. */
  4534. class KeywordModule extends Module {
  4535. /**
  4536. * 模块名称
  4537. */
  4538. static name = "keyword";
  4539.  
  4540. /**
  4541. * 模块标签
  4542. */
  4543. static label = "关键字";
  4544.  
  4545. /**
  4546. * 顺序
  4547. */
  4548. static order = 40;
  4549.  
  4550. /**
  4551. * 获取列表
  4552. */
  4553. get list() {
  4554. return this.settings.keywords;
  4555. }
  4556.  
  4557. /**
  4558. * 获取关键字
  4559. * @param {Number} id 关键字 ID
  4560. */
  4561. get(id) {
  4562. // 获取列表
  4563. const list = this.list;
  4564.  
  4565. // 如果存在,则返回信息
  4566. if (list[id]) {
  4567. return list[id];
  4568. }
  4569.  
  4570. return null;
  4571. }
  4572.  
  4573. /**
  4574. * 添加关键字
  4575. * @param {String} keyword 关键字
  4576. * @param {String} filterMode 过滤模式
  4577. * @param {Number} filterLevel 过滤等级: 0 - 仅过滤标题; 1 - 过滤标题和内容
  4578. */
  4579. add(keyword, filterMode, filterLevel) {
  4580. // 获取列表
  4581. const list = this.list;
  4582.  
  4583. // ID 为最大值 + 1
  4584. const id = Math.max(...Object.keys(list), 0) + 1;
  4585.  
  4586. // 写入关键字信息
  4587. list[id] = {
  4588. id,
  4589. keyword,
  4590. filterMode,
  4591. filterLevel,
  4592. };
  4593.  
  4594. // 保存数据
  4595. this.settings.keywords = list;
  4596.  
  4597. // 重新过滤
  4598. this.reFilter();
  4599.  
  4600. // 返回添加的关键字
  4601. return list[id];
  4602. }
  4603.  
  4604. /**
  4605. * 编辑关键字
  4606. * @param {Number} id 关键字 ID
  4607. * @param {*} values 关键字信息
  4608. */
  4609. update(id, values) {
  4610. // 获取列表
  4611. const list = this.list;
  4612.  
  4613. // 如果不存在则跳过
  4614. if (Object.hasOwn(list, id) === false) {
  4615. return null;
  4616. }
  4617.  
  4618. // 获取关键字
  4619. const entity = list[id];
  4620.  
  4621. // 更新关键字
  4622. Object.assign(entity, values);
  4623.  
  4624. // 保存数据
  4625. this.settings.keywords = list;
  4626.  
  4627. // 重新过滤
  4628. this.reFilter();
  4629. }
  4630.  
  4631. /**
  4632. * 删除关键字
  4633. * @param {Number} id 关键字 ID
  4634. */
  4635. remove(id) {
  4636. // 获取列表
  4637. const list = this.list;
  4638.  
  4639. // 如果不存在则跳过
  4640. if (Object.hasOwn(list, id) === false) {
  4641. return null;
  4642. }
  4643.  
  4644. // 获取关键字
  4645. const entity = list[id];
  4646.  
  4647. // 删除关键字
  4648. delete list[id];
  4649.  
  4650. // 保存数据
  4651. this.settings.keywords = list;
  4652.  
  4653. // 重新过滤
  4654. this.reFilter();
  4655.  
  4656. // 返回删除的关键字
  4657. return entity;
  4658. }
  4659.  
  4660. /**
  4661. * 获取帖子数据
  4662. * @param {*} item 绑定的 nFilter
  4663. */
  4664. async getPostInfo(item) {
  4665. const { tid, pid } = item;
  4666.  
  4667. // 请求帖子数据
  4668. const { subject, content, userInfo, reputation } =
  4669. await this.api.getPostInfo(tid, pid);
  4670.  
  4671. // 绑定用户信息和声望
  4672. if (userInfo) {
  4673. item.userInfo = userInfo;
  4674. item.username = userInfo.username;
  4675. item.reputation = reputation;
  4676. }
  4677.  
  4678. // 绑定标题和内容
  4679. item.subject = subject;
  4680. item.content = content;
  4681. }
  4682.  
  4683. /**
  4684. * 表格列
  4685. * @returns {Array} 表格列集合
  4686. */
  4687. columns() {
  4688. return [
  4689. { label: "关键字" },
  4690. { label: "过滤模式", center: true, width: 1 },
  4691. { label: "包括内容", center: true, width: 1 },
  4692. { label: "操作", width: 1 },
  4693. ];
  4694. }
  4695.  
  4696. /**
  4697. * 表格项
  4698. * @param {*} item 标记信息
  4699. * @returns {Array} 表格项集合
  4700. */
  4701. column(item) {
  4702. const { ui } = this;
  4703. const { table } = this.views;
  4704. const { id, keyword, filterLevel, filterMode } = item;
  4705.  
  4706. // 关键字
  4707. const input = ui.createElement("INPUT", [], {
  4708. type: "text",
  4709. value: keyword,
  4710. });
  4711.  
  4712. const inputWrapper = ui.createElement("DIV", input, {
  4713. className: "filter-input-wrapper",
  4714. });
  4715.  
  4716. // 切换过滤模式
  4717. const switchMode = ui.createButton(
  4718. filterMode || this.settings.filterModes[0],
  4719. () => {
  4720. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4721.  
  4722. switchMode.innerText = newMode;
  4723. }
  4724. );
  4725.  
  4726. // 包括内容
  4727. const switchLevel = ui.createElement("INPUT", [], {
  4728. type: "checkbox",
  4729. checked: filterLevel > 0,
  4730. });
  4731.  
  4732. // 操作
  4733. const buttons = (() => {
  4734. const save = ui.createButton("保存", () => {
  4735. this.update(id, {
  4736. keyword: input.value,
  4737. filterMode: switchMode.innerText,
  4738. filterLevel: switchLevel.checked ? 1 : 0,
  4739. });
  4740. });
  4741.  
  4742. const remove = ui.createButton("删除", (e) => {
  4743. ui.confirm().then(() => {
  4744. this.remove(id);
  4745.  
  4746. table.remove(e);
  4747. });
  4748. });
  4749.  
  4750. return ui.createButtonGroup(save, remove);
  4751. })();
  4752.  
  4753. return [inputWrapper, switchMode, switchLevel, buttons];
  4754. }
  4755.  
  4756. /**
  4757. * 初始化组件
  4758. */
  4759. initComponents() {
  4760. super.initComponents();
  4761.  
  4762. const { ui } = this;
  4763. const { tabs, content } = ui.views;
  4764.  
  4765. const table = ui.createTable(this.columns());
  4766.  
  4767. const tips = ui.createElement("DIV", TIPS.keyword, {
  4768. className: "silver",
  4769. });
  4770.  
  4771. const tab = ui.createTab(
  4772. tabs,
  4773. this.constructor.label,
  4774. this.constructor.order,
  4775. {
  4776. onclick: () => {
  4777. this.render(content);
  4778. },
  4779. }
  4780. );
  4781.  
  4782. Object.assign(this.views, {
  4783. tab,
  4784. table,
  4785. });
  4786.  
  4787. this.views.container.appendChild(table);
  4788. this.views.container.appendChild(tips);
  4789. }
  4790.  
  4791. /**
  4792. * 渲染
  4793. * @param {HTMLElement} container 容器
  4794. */
  4795. render(container) {
  4796. super.render(container);
  4797.  
  4798. const { table } = this.views;
  4799.  
  4800. if (table) {
  4801. const { add, clear } = table;
  4802.  
  4803. clear();
  4804.  
  4805. Object.values(this.list).forEach((item) => {
  4806. const column = this.column(item);
  4807.  
  4808. add(...column);
  4809. });
  4810.  
  4811. this.renderNewLine();
  4812. }
  4813. }
  4814.  
  4815. /**
  4816. * 渲染新行
  4817. */
  4818. renderNewLine() {
  4819. const { ui } = this;
  4820. const { table } = this.views;
  4821.  
  4822. // 关键字
  4823. const input = ui.createElement("INPUT", [], {
  4824. type: "text",
  4825. });
  4826.  
  4827. const inputWrapper = ui.createElement("DIV", input, {
  4828. className: "filter-input-wrapper",
  4829. });
  4830.  
  4831. // 切换过滤模式
  4832. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  4833. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4834.  
  4835. switchMode.innerText = newMode;
  4836. });
  4837.  
  4838. // 包括内容
  4839. const switchLevel = ui.createElement("INPUT", [], {
  4840. type: "checkbox",
  4841. });
  4842.  
  4843. // 操作
  4844. const buttons = (() => {
  4845. const save = ui.createButton("添加", (e) => {
  4846. const entity = this.add(
  4847. input.value,
  4848. switchMode.innerText,
  4849. switchLevel.checked ? 1 : 0
  4850. );
  4851.  
  4852. table.update(e, ...this.column(entity));
  4853.  
  4854. this.renderNewLine();
  4855. });
  4856.  
  4857. return ui.createButtonGroup(save);
  4858. })();
  4859.  
  4860. // 添加至列表
  4861. table.add(inputWrapper, switchMode, switchLevel, buttons);
  4862. }
  4863.  
  4864. /**
  4865. * 过滤
  4866. * @param {*} item 绑定的 nFilter
  4867. * @param {*} result 过滤结果
  4868. */
  4869. async filter(item, result) {
  4870. // 获取列表
  4871. const list = this.list;
  4872.  
  4873. // 跳过低于当前的过滤模式
  4874. const filtered = Object.values(list).filter(
  4875. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  4876. );
  4877.  
  4878. // 没有则跳过
  4879. if (filtered.length === 0) {
  4880. return;
  4881. }
  4882.  
  4883. // 根据过滤模式依次判断
  4884. const sorted = Tools.sortBy(filtered, (item) =>
  4885. this.settings.getModeByName(item.filterMode)
  4886. );
  4887.  
  4888. for (let i = 0; i < sorted.length; i += 1) {
  4889. const { keyword, filterMode } = sorted[i];
  4890.  
  4891. // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
  4892. const filterLevel = sorted[i].filterLevel || 0;
  4893.  
  4894. // 过滤标题
  4895. if (filterLevel >= 0) {
  4896. const { subject } = item;
  4897.  
  4898. const match = subject.match(keyword);
  4899.  
  4900. if (match) {
  4901. const mode = this.settings.getModeByName(filterMode);
  4902.  
  4903. // 更新过滤模式和原因
  4904. result.mode = mode;
  4905. result.reason = `关键字: ${match[0]}`;
  4906. return;
  4907. }
  4908. }
  4909.  
  4910. // 过滤内容
  4911. if (filterLevel >= 1) {
  4912. // 如果没有内容,则请求
  4913. if (item.content === undefined) {
  4914. await this.getPostInfo(item);
  4915. }
  4916.  
  4917. const { content } = item;
  4918.  
  4919. const match = content.match(keyword);
  4920.  
  4921. if (match) {
  4922. const mode = this.settings.getModeByName(filterMode);
  4923.  
  4924. // 更新过滤模式和原因
  4925. result.mode = mode;
  4926. result.reason = `关键字: ${match[0]}`;
  4927. return;
  4928. }
  4929. }
  4930. }
  4931. }
  4932.  
  4933. /**
  4934. * 重新过滤
  4935. */
  4936. reFilter() {
  4937. // 实际上应该根据过滤模式来筛选要过滤的部分
  4938. this.data.forEach((item) => {
  4939. item.execute();
  4940. });
  4941. }
  4942. }
  4943.  
  4944. /**
  4945. * 属地模块
  4946. */
  4947. class LocationModule extends Module {
  4948. /**
  4949. * 模块名称
  4950. */
  4951. static name = "location";
  4952.  
  4953. /**
  4954. * 模块标签
  4955. */
  4956. static label = "属地";
  4957.  
  4958. /**
  4959. * 顺序
  4960. */
  4961. static order = 50;
  4962.  
  4963. /**
  4964. * 请求缓存
  4965. */
  4966. cache = {};
  4967.  
  4968. /**
  4969. * 获取列表
  4970. */
  4971. get list() {
  4972. return this.settings.locations;
  4973. }
  4974.  
  4975. /**
  4976. * 获取属地
  4977. * @param {Number} id 属地 ID
  4978. */
  4979. get(id) {
  4980. // 获取列表
  4981. const list = this.list;
  4982.  
  4983. // 如果存在,则返回信息
  4984. if (list[id]) {
  4985. return list[id];
  4986. }
  4987.  
  4988. return null;
  4989. }
  4990.  
  4991. /**
  4992. * 添加属地
  4993. * @param {String} keyword 关键字
  4994. * @param {String} filterMode 过滤模式
  4995. */
  4996. add(keyword, filterMode) {
  4997. // 获取列表
  4998. const list = this.list;
  4999.  
  5000. // ID 为最大值 + 1
  5001. const id = Math.max(...Object.keys(list), 0) + 1;
  5002.  
  5003. // 写入属地信息
  5004. list[id] = {
  5005. id,
  5006. keyword,
  5007. filterMode,
  5008. };
  5009.  
  5010. // 保存数据
  5011. this.settings.locations = list;
  5012.  
  5013. // 重新过滤
  5014. this.reFilter();
  5015.  
  5016. // 返回添加的属地
  5017. return list[id];
  5018. }
  5019.  
  5020. /**
  5021. * 编辑属地
  5022. * @param {Number} id 属地 ID
  5023. * @param {*} values 属地信息
  5024. */
  5025. update(id, values) {
  5026. // 获取列表
  5027. const list = this.list;
  5028.  
  5029. // 如果不存在则跳过
  5030. if (Object.hasOwn(list, id) === false) {
  5031. return null;
  5032. }
  5033.  
  5034. // 获取属地
  5035. const entity = list[id];
  5036.  
  5037. // 更新属地
  5038. Object.assign(entity, values);
  5039.  
  5040. // 保存数据
  5041. this.settings.locations = list;
  5042.  
  5043. // 重新过滤
  5044. this.reFilter();
  5045. }
  5046.  
  5047. /**
  5048. * 删除属地
  5049. * @param {Number} id 属地 ID
  5050. */
  5051. remove(id) {
  5052. // 获取列表
  5053. const list = this.list;
  5054.  
  5055. // 如果不存在则跳过
  5056. if (Object.hasOwn(list, id) === false) {
  5057. return null;
  5058. }
  5059.  
  5060. // 获取属地
  5061. const entity = list[id];
  5062.  
  5063. // 删除属地
  5064. delete list[id];
  5065.  
  5066. // 保存数据
  5067. this.settings.locations = list;
  5068.  
  5069. // 重新过滤
  5070. this.reFilter();
  5071.  
  5072. // 返回删除的属地
  5073. return entity;
  5074. }
  5075.  
  5076. /**
  5077. * 获取 IP 属地
  5078. * @param {*} item 绑定的 nFilter
  5079. */
  5080. async getIpLocation(item) {
  5081. const { uid } = item;
  5082.  
  5083. // 如果是匿名直接跳过
  5084. if (uid <= 0) {
  5085. return;
  5086. }
  5087.  
  5088. // 如果已有缓存,直接返回
  5089. if (Object.hasOwn(this.cache, uid)) {
  5090. return this.cache[uid];
  5091. }
  5092.  
  5093. // 请求属地
  5094. const { ipLoc } = await this.api.getUserInfo(uid);
  5095.  
  5096. // 写入缓存
  5097. if (ipLoc) {
  5098. this.cache[uid] = ipLoc;
  5099. }
  5100.  
  5101. // 返回结果
  5102. return ipLoc;
  5103. }
  5104.  
  5105. /**
  5106. * 表格列
  5107. * @returns {Array} 表格列集合
  5108. */
  5109. columns() {
  5110. return [
  5111. { label: "关键字" },
  5112. { label: "过滤模式", center: true, width: 1 },
  5113. { label: "操作", width: 1 },
  5114. ];
  5115. }
  5116.  
  5117. /**
  5118. * 表格项
  5119. * @param {*} item 标记信息
  5120. * @returns {Array} 表格项集合
  5121. */
  5122. column(item) {
  5123. const { ui } = this;
  5124. const { table } = this.views;
  5125. const { id, keyword, filterMode } = item;
  5126.  
  5127. // 关键字
  5128. const input = ui.createElement("INPUT", [], {
  5129. type: "text",
  5130. value: keyword,
  5131. });
  5132.  
  5133. const inputWrapper = ui.createElement("DIV", input, {
  5134. className: "filter-input-wrapper",
  5135. });
  5136.  
  5137. // 切换过滤模式
  5138. const switchMode = ui.createButton(
  5139. filterMode || this.settings.filterModes[0],
  5140. () => {
  5141. const newMode = this.settings.switchModeByName(switchMode.innerText);
  5142.  
  5143. switchMode.innerText = newMode;
  5144. }
  5145. );
  5146.  
  5147. // 操作
  5148. const buttons = (() => {
  5149. const save = ui.createButton("保存", () => {
  5150. this.update(id, {
  5151. keyword: input.value,
  5152. filterMode: switchMode.innerText,
  5153. });
  5154. });
  5155.  
  5156. const remove = ui.createButton("删除", (e) => {
  5157. ui.confirm().then(() => {
  5158. this.remove(id);
  5159.  
  5160. table.remove(e);
  5161. });
  5162. });
  5163.  
  5164. return ui.createButtonGroup(save, remove);
  5165. })();
  5166.  
  5167. return [inputWrapper, switchMode, buttons];
  5168. }
  5169.  
  5170. /**
  5171. * 初始化组件
  5172. */
  5173. initComponents() {
  5174. super.initComponents();
  5175.  
  5176. const { ui } = this;
  5177. const { tabs, content } = ui.views;
  5178.  
  5179. const table = ui.createTable(this.columns());
  5180.  
  5181. const tips = ui.createElement("DIV", TIPS.keyword, {
  5182. className: "silver",
  5183. });
  5184.  
  5185. const tab = ui.createTab(
  5186. tabs,
  5187. this.constructor.label,
  5188. this.constructor.order,
  5189. {
  5190. onclick: () => {
  5191. this.render(content);
  5192. },
  5193. }
  5194. );
  5195.  
  5196. Object.assign(this.views, {
  5197. tab,
  5198. table,
  5199. });
  5200.  
  5201. this.views.container.appendChild(table);
  5202. this.views.container.appendChild(tips);
  5203. }
  5204.  
  5205. /**
  5206. * 渲染
  5207. * @param {HTMLElement} container 容器
  5208. */
  5209. render(container) {
  5210. super.render(container);
  5211.  
  5212. const { table } = this.views;
  5213.  
  5214. if (table) {
  5215. const { add, clear } = table;
  5216.  
  5217. clear();
  5218.  
  5219. Object.values(this.list).forEach((item) => {
  5220. const column = this.column(item);
  5221.  
  5222. add(...column);
  5223. });
  5224.  
  5225. this.renderNewLine();
  5226. }
  5227. }
  5228.  
  5229. /**
  5230. * 渲染新行
  5231. */
  5232. renderNewLine() {
  5233. const { ui } = this;
  5234. const { table } = this.views;
  5235.  
  5236. // 关键字
  5237. const input = ui.createElement("INPUT", [], {
  5238. type: "text",
  5239. });
  5240.  
  5241. const inputWrapper = ui.createElement("DIV", input, {
  5242. className: "filter-input-wrapper",
  5243. });
  5244.  
  5245. // 切换过滤模式
  5246. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  5247. const newMode = this.settings.switchModeByName(switchMode.innerText);
  5248.  
  5249. switchMode.innerText = newMode;
  5250. });
  5251.  
  5252. // 操作
  5253. const buttons = (() => {
  5254. const save = ui.createButton("添加", (e) => {
  5255. const entity = this.add(input.value, switchMode.innerText);
  5256.  
  5257. table.update(e, ...this.column(entity));
  5258.  
  5259. this.renderNewLine();
  5260. });
  5261.  
  5262. return ui.createButtonGroup(save);
  5263. })();
  5264.  
  5265. // 添加至列表
  5266. table.add(inputWrapper, switchMode, buttons);
  5267. }
  5268.  
  5269. /**
  5270. * 过滤
  5271. * @param {*} item 绑定的 nFilter
  5272. * @param {*} result 过滤结果
  5273. */
  5274. async filter(item, result) {
  5275. // 获取列表
  5276. const list = this.list;
  5277.  
  5278. // 跳过低于当前的过滤模式
  5279. const filtered = Object.values(list).filter(
  5280. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  5281. );
  5282.  
  5283. // 没有则跳过
  5284. if (filtered.length === 0) {
  5285. return;
  5286. }
  5287.  
  5288. // 获取当前属地
  5289. const location = await this.getIpLocation(item);
  5290.  
  5291. // 请求失败则跳过
  5292. if (location === undefined) {
  5293. return;
  5294. }
  5295.  
  5296. // 根据过滤模式依次判断
  5297. const sorted = Tools.sortBy(filtered, (item) =>
  5298. this.settings.getModeByName(item.filterMode)
  5299. );
  5300.  
  5301. for (let i = 0; i < sorted.length; i += 1) {
  5302. const { keyword, filterMode } = sorted[i];
  5303.  
  5304. const match = location.match(keyword);
  5305.  
  5306. if (match) {
  5307. const mode = this.settings.getModeByName(filterMode);
  5308.  
  5309. // 更新过滤模式和原因
  5310. result.mode = mode;
  5311. result.reason = `属地: ${match[0]}`;
  5312. return;
  5313. }
  5314. }
  5315. }
  5316.  
  5317. /**
  5318. * 重新过滤
  5319. */
  5320. reFilter() {
  5321. // 实际上应该根据过滤模式来筛选要过滤的部分
  5322. this.data.forEach((item) => {
  5323. item.execute();
  5324. });
  5325. }
  5326. }
  5327.  
  5328. /**
  5329. * 猎巫模块
  5330. *
  5331. * 其实是通过 Cache 模块读取配置,而非 Settings
  5332. */
  5333. class HunterModule extends Module {
  5334. /**
  5335. * 模块名称
  5336. */
  5337. static name = "hunter";
  5338.  
  5339. /**
  5340. * 模块标签
  5341. */
  5342. static label = "猎巫";
  5343.  
  5344. /**
  5345. * 顺序
  5346. */
  5347. static order = 60;
  5348.  
  5349. /**
  5350. * 请求缓存
  5351. */
  5352. cache = {};
  5353.  
  5354. /**
  5355. * 请求队列
  5356. */
  5357. queue = [];
  5358.  
  5359. /**
  5360. * 获取列表
  5361. */
  5362. get list() {
  5363. return this.settings.cache
  5364. .get("WITCH_HUNT")
  5365. .then((values) => values || []);
  5366. }
  5367.  
  5368. /**
  5369. * 获取猎巫
  5370. * @param {Number} id 猎巫 ID
  5371. */
  5372. async get(id) {
  5373. // 获取列表
  5374. const list = await this.list;
  5375.  
  5376. // 如果存在,则返回信息
  5377. if (list[id]) {
  5378. return list[id];
  5379. }
  5380.  
  5381. return null;
  5382. }
  5383.  
  5384. /**
  5385. * 添加猎巫
  5386. * @param {Number} fid 版面 ID
  5387. * @param {String} label 标签
  5388. * @param {String} filterMode 过滤模式
  5389. * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
  5390. */
  5391. async add(fid, label, filterMode, filterLevel) {
  5392. // FID 只能是数字
  5393. fid = parseInt(fid, 10);
  5394.  
  5395. // 获取列表
  5396. const list = await this.list;
  5397.  
  5398. // 如果版面 ID 已存在,则提示错误
  5399. if (Object.keys(list).includes(fid)) {
  5400. alert("已有相同版面ID");
  5401. return;
  5402. }
  5403.  
  5404. // 请求版面信息
  5405. const info = await this.api.getForumInfo(fid);
  5406.  
  5407. // 如果版面不存在,则提示错误
  5408. if (info === null) {
  5409. alert("版面ID有误");
  5410. return;
  5411. }
  5412.  
  5413. // 计算标记颜色
  5414. const color = Tools.generateColor(info.name);
  5415.  
  5416. // 写入猎巫信息
  5417. list[fid] = {
  5418. fid,
  5419. name: info.name,
  5420. label,
  5421. color,
  5422. filterMode,
  5423. filterLevel,
  5424. };
  5425.  
  5426. // 保存数据
  5427. this.settings.cache.put("WITCH_HUNT", list);
  5428.  
  5429. // 重新过滤
  5430. this.reFilter(true);
  5431.  
  5432. // 返回添加的猎巫
  5433. return list[fid];
  5434. }
  5435.  
  5436. /**
  5437. * 编辑猎巫
  5438. * @param {Number} fid 版面 ID
  5439. * @param {*} values 猎巫信息
  5440. */
  5441. async update(fid, values) {
  5442. // 获取列表
  5443. const list = await this.list;
  5444.  
  5445. // 如果不存在则跳过
  5446. if (Object.hasOwn(list, fid) === false) {
  5447. return null;
  5448. }
  5449.  
  5450. // 获取猎巫
  5451. const entity = list[fid];
  5452.  
  5453. // 更新猎巫
  5454. Object.assign(entity, values);
  5455.  
  5456. // 保存数据
  5457. this.settings.cache.put("WITCH_HUNT", list);
  5458.  
  5459. // 重新过滤,更新样式即可
  5460. this.reFilter(false);
  5461. }
  5462.  
  5463. /**
  5464. * 删除猎巫
  5465. * @param {Number} fid 版面 ID
  5466. */
  5467. async remove(fid) {
  5468. // 获取列表
  5469. const list = await this.list;
  5470.  
  5471. // 如果不存在则跳过
  5472. if (Object.hasOwn(list, fid) === false) {
  5473. return null;
  5474. }
  5475.  
  5476. // 获取猎巫
  5477. const entity = list[fid];
  5478.  
  5479. // 删除猎巫
  5480. delete list[fid];
  5481.  
  5482. // 保存数据
  5483. this.settings.cache.put("WITCH_HUNT", list);
  5484.  
  5485. // 重新过滤
  5486. this.reFilter(true);
  5487.  
  5488. // 返回删除的属地
  5489. return entity;
  5490. }
  5491.  
  5492. /**
  5493. * 格式化版面
  5494. * @param {Number} fid 版面 ID
  5495. * @param {String} name 版面名称
  5496. */
  5497. formatForum(fid, name) {
  5498. const { ui } = this;
  5499.  
  5500. return ui.createElement("A", `[${name}]`, {
  5501. className: "b nobr",
  5502. href: `/thread.php?fid=${fid}`,
  5503. });
  5504. }
  5505.  
  5506. /**
  5507. * 格式化标签
  5508. * @param {String} name 标签名称
  5509. * @param {String} name 标签颜色
  5510. */
  5511. formatLabel(name, color) {
  5512. const { ui } = this;
  5513.  
  5514. return ui.createElement("B", name, {
  5515. className: "block_txt nobr",
  5516. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  5517. });
  5518. }
  5519.  
  5520. /**
  5521. * 表格列
  5522. * @returns {Array} 表格列集合
  5523. */
  5524. columns() {
  5525. return [
  5526. { label: "版面", width: 200 },
  5527. { label: "标签" },
  5528. { label: "启用过滤", center: true, width: 1 },
  5529. { label: "过滤模式", center: true, width: 1 },
  5530. { label: "操作", width: 1 },
  5531. ];
  5532. }
  5533.  
  5534. /**
  5535. * 表格项
  5536. * @param {*} item 标记信息
  5537. * @returns {Array} 表格项集合
  5538. */
  5539. column(item) {
  5540. const { ui } = this;
  5541. const { table } = this.views;
  5542. const { fid, name, label, color, filterMode, filterLevel } = item;
  5543.  
  5544. // 版面
  5545. const forum = this.formatForum(fid, name);
  5546.  
  5547. // 标签
  5548. const labelElement = this.formatLabel(label, color);
  5549.  
  5550. // 启用过滤
  5551. const switchLevel = ui.createElement("INPUT", [], {
  5552. type: "checkbox",
  5553. checked: filterLevel > 0,
  5554. });
  5555.  
  5556. // 切换过滤模式
  5557. const switchMode = ui.createButton(
  5558. filterMode || this.settings.filterModes[0],
  5559. () => {
  5560. const newMode = this.settings.switchModeByName(switchMode.innerText);
  5561.  
  5562. switchMode.innerText = newMode;
  5563. }
  5564. );
  5565.  
  5566. // 操作
  5567. const buttons = (() => {
  5568. const save = ui.createButton("保存", () => {
  5569. this.update(fid, {
  5570. filterMode: switchMode.innerText,
  5571. filterLevel: switchLevel.checked ? 1 : 0,
  5572. });
  5573. });
  5574.  
  5575. const remove = ui.createButton("删除", (e) => {
  5576. ui.confirm().then(async () => {
  5577. await this.remove(fid);
  5578.  
  5579. table.remove(e);
  5580. });
  5581. });
  5582.  
  5583. return ui.createButtonGroup(save, remove);
  5584. })();
  5585.  
  5586. return [forum, labelElement, switchLevel, switchMode, buttons];
  5587. }
  5588.  
  5589. /**
  5590. * 初始化组件
  5591. */
  5592. initComponents() {
  5593. super.initComponents();
  5594.  
  5595. const { ui } = this;
  5596. const { tabs, content } = ui.views;
  5597.  
  5598. const table = ui.createTable(this.columns());
  5599.  
  5600. const tips = ui.createElement("DIV", TIPS.hunter, {
  5601. className: "silver",
  5602. });
  5603.  
  5604. const tab = ui.createTab(
  5605. tabs,
  5606. this.constructor.label,
  5607. this.constructor.order,
  5608. {
  5609. onclick: () => {
  5610. this.render(content);
  5611. },
  5612. }
  5613. );
  5614.  
  5615. Object.assign(this.views, {
  5616. tab,
  5617. table,
  5618. });
  5619.  
  5620. this.views.container.appendChild(table);
  5621. this.views.container.appendChild(tips);
  5622. }
  5623.  
  5624. /**
  5625. * 渲染
  5626. * @param {HTMLElement} container 容器
  5627. */
  5628. render(container) {
  5629. super.render(container);
  5630.  
  5631. const { table } = this.views;
  5632.  
  5633. if (table) {
  5634. const { add, clear } = table;
  5635.  
  5636. clear();
  5637.  
  5638. this.list.then((values) => {
  5639. Object.values(values).forEach((item) => {
  5640. const column = this.column(item);
  5641.  
  5642. add(...column);
  5643. });
  5644.  
  5645. this.renderNewLine();
  5646. });
  5647. }
  5648. }
  5649.  
  5650. /**
  5651. * 渲染新行
  5652. */
  5653. renderNewLine() {
  5654. const { ui } = this;
  5655. const { table } = this.views;
  5656.  
  5657. // 版面 ID
  5658. const forumInput = ui.createElement("INPUT", [], {
  5659. type: "text",
  5660. });
  5661.  
  5662. const forumInputWrapper = ui.createElement("DIV", forumInput, {
  5663. className: "filter-input-wrapper",
  5664. });
  5665.  
  5666. // 标签
  5667. const labelInput = ui.createElement("INPUT", [], {
  5668. type: "text",
  5669. });
  5670.  
  5671. const labelInputWrapper = ui.createElement("DIV", labelInput, {
  5672. className: "filter-input-wrapper",
  5673. });
  5674.  
  5675. // 启用过滤
  5676. const switchLevel = ui.createElement("INPUT", [], {
  5677. type: "checkbox",
  5678. });
  5679.  
  5680. // 切换过滤模式
  5681. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  5682. const newMode = this.settings.switchModeByName(switchMode.innerText);
  5683.  
  5684. switchMode.innerText = newMode;
  5685. });
  5686.  
  5687. // 操作
  5688. const buttons = (() => {
  5689. const save = ui.createButton("添加", async (e) => {
  5690. const entity = await this.add(
  5691. forumInput.value,
  5692. labelInput.value,
  5693. switchMode.innerText,
  5694. switchLevel.checked ? 1 : 0
  5695. );
  5696.  
  5697. table.update(e, ...this.column(entity));
  5698.  
  5699. this.renderNewLine();
  5700. });
  5701.  
  5702. return ui.createButtonGroup(save);
  5703. })();
  5704.  
  5705. // 添加至列表
  5706. table.add(
  5707. forumInputWrapper,
  5708. labelInputWrapper,
  5709. switchLevel,
  5710. switchMode,
  5711. buttons
  5712. );
  5713. }
  5714.  
  5715. /**
  5716. * 过滤
  5717. * @param {*} item 绑定的 nFilter
  5718. * @param {*} result 过滤结果
  5719. */
  5720. async filter(item, result) {
  5721. // 获取当前猎巫结果
  5722. const hunter = item.hunter || [];
  5723.  
  5724. // 如果没有猎巫结果,则跳过
  5725. if (hunter.length === 0) {
  5726. return;
  5727. }
  5728.  
  5729. // 获取列表
  5730. const items = await this.list;
  5731.  
  5732. // 筛选出匹配的猎巫
  5733. const list = Object.values(items).filter(({ fid }) =>
  5734. hunter.includes(fid)
  5735. );
  5736.  
  5737. // 取最高的过滤模式
  5738. // 低于当前的过滤模式则跳过
  5739. let max = result.mode;
  5740. let res = null;
  5741.  
  5742. for (const entity of list) {
  5743. const { filterLevel, filterMode } = entity;
  5744.  
  5745. // 仅标记
  5746. if (filterLevel === 0) {
  5747. continue;
  5748. }
  5749.  
  5750. // 获取过滤模式
  5751. const mode = this.settings.getModeByName(filterMode);
  5752.  
  5753. if (mode <= max) {
  5754. continue;
  5755. }
  5756.  
  5757. max = mode;
  5758. res = entity;
  5759. }
  5760.  
  5761. // 没有匹配的则跳过
  5762. if (res === null) {
  5763. return;
  5764. }
  5765.  
  5766. // 更新过滤模式和原因
  5767. result.mode = max;
  5768. result.reason = `猎巫: ${res.label}`;
  5769. }
  5770.  
  5771. /**
  5772. * 通知
  5773. * @param {*} item 绑定的 nFilter
  5774. */
  5775. async notify(item) {
  5776. const { uid, tags } = item;
  5777.  
  5778. // 如果没有 tags 组件则跳过
  5779. if (tags === undefined) {
  5780. return;
  5781. }
  5782.  
  5783. // 如果是匿名,隐藏组件
  5784. if (uid <= 0) {
  5785. tags.style.display = "none";
  5786. return;
  5787. }
  5788.  
  5789. // 删除旧标签
  5790. [...tags.querySelectorAll("[fid]")].forEach((item) => {
  5791. tags.removeChild(item);
  5792. });
  5793.  
  5794. // 如果没有请求,开始请求
  5795. if (Object.hasOwn(item, "hunter") === false) {
  5796. this.execute(item);
  5797. return;
  5798. }
  5799.  
  5800. // 获取当前猎巫结果
  5801. const hunter = item.hunter;
  5802.  
  5803. // 如果没有猎巫结果,则跳过
  5804. if (hunter.length === 0) {
  5805. return;
  5806. }
  5807.  
  5808. // 格式化标签
  5809. const items = await Promise.all(
  5810. hunter.map(async (fid) => {
  5811. const item = await this.get(fid);
  5812.  
  5813. if (item) {
  5814. const element = this.formatLabel(item.label, item.color);
  5815.  
  5816. element.setAttribute("fid", fid);
  5817.  
  5818. return element;
  5819. }
  5820.  
  5821. return null;
  5822. })
  5823. );
  5824.  
  5825. // 加入组件
  5826. items.forEach((item) => {
  5827. if (item) {
  5828. tags.appendChild(item);
  5829. }
  5830. });
  5831. }
  5832.  
  5833. /**
  5834. * 重新过滤
  5835. * @param {Boolean} clear 是否清除缓存
  5836. */
  5837. reFilter(clear) {
  5838. // 清除缓存
  5839. if (clear) {
  5840. this.cache = {};
  5841. }
  5842.  
  5843. // 重新过滤
  5844. this.data.forEach((item) => {
  5845. // 不需要清除缓存的话,只要重新加载标记
  5846. if (clear === false) {
  5847. item.hunter = [];
  5848. }
  5849.  
  5850. // 重新猎巫
  5851. this.execute(item);
  5852. });
  5853. }
  5854.  
  5855. /**
  5856. * 猎巫
  5857. * @param {*} item 绑定的 nFilter
  5858. */
  5859. async execute(item) {
  5860. const { uid } = item;
  5861. const { api, cache, queue, list } = this;
  5862.  
  5863. // 如果是匿名,则跳过
  5864. if (uid <= 0) {
  5865. return;
  5866. }
  5867.  
  5868. // 初始化猎巫结果,用于标识正在猎巫
  5869. item.hunter = item.hunter || [];
  5870.  
  5871. // 获取列表
  5872. const items = await list;
  5873.  
  5874. // 没有设置且没有旧数据,直接跳过
  5875. if (items.length === 0 && item.hunter.length === 0) {
  5876. return;
  5877. }
  5878.  
  5879. // 重新过滤
  5880. const reload = (newValue) => {
  5881. const isEqual = newValue.sort().join() === item.hunter.sort().join();
  5882.  
  5883. if (isEqual) {
  5884. return;
  5885. }
  5886.  
  5887. item.hunter = newValue;
  5888. item.execute();
  5889. };
  5890.  
  5891. // 创建任务
  5892. const task = async () => {
  5893. // 如果缓存里没有记录,请求数据并写入缓存
  5894. if (Object.hasOwn(cache, uid) === false) {
  5895. cache[uid] = [];
  5896.  
  5897. await Promise.all(
  5898. Object.keys(items).map(async (fid) => {
  5899. // 转换为数字格式
  5900. const id = parseInt(fid, 10);
  5901.  
  5902. // 当前版面发言记录
  5903. const result = await api.getForumPosted(id, uid);
  5904.  
  5905. // 写入当前设置
  5906. if (result) {
  5907. cache[uid].push(id);
  5908. }
  5909. })
  5910. );
  5911. }
  5912.  
  5913. // 重新过滤
  5914. reload(cache[uid]);
  5915.  
  5916. // 将当前任务移出队列
  5917. queue.shift();
  5918.  
  5919. // 如果还有任务,继续执行
  5920. if (queue.length > 0) {
  5921. queue[0]();
  5922. }
  5923. };
  5924.  
  5925. // 队列里已经有任务
  5926. const isRunning = queue.length > 0;
  5927.  
  5928. // 加入队列
  5929. queue.push(task);
  5930.  
  5931. // 如果没有正在执行的任务,则立即执行
  5932. if (isRunning === false) {
  5933. task();
  5934. }
  5935. }
  5936. }
  5937.  
  5938. /**
  5939. * 杂项模块
  5940. */
  5941. class MiscModule extends Module {
  5942. /**
  5943. * 模块名称
  5944. */
  5945. static name = "misc";
  5946.  
  5947. /**
  5948. * 模块标签
  5949. */
  5950. static label = "杂项";
  5951.  
  5952. /**
  5953. * 顺序
  5954. */
  5955. static order = 100;
  5956.  
  5957. /**
  5958. * 请求缓存
  5959. */
  5960. cache = {
  5961. topicNums: {},
  5962. };
  5963.  
  5964. /**
  5965. * 获取用户信息(从页面上)
  5966. * @param {*} item 绑定的 nFilter
  5967. */
  5968. getUserInfo(item) {
  5969. const { uid } = item;
  5970.  
  5971. // 如果是匿名直接跳过
  5972. if (uid <= 0) {
  5973. return;
  5974. }
  5975.  
  5976. // 回复页面可以直接获取到用户信息和声望
  5977. if (commonui.userInfo) {
  5978. // 取得用户信息
  5979. const userInfo = commonui.userInfo.users[uid];
  5980.  
  5981. // 绑定用户信息和声望
  5982. if (userInfo) {
  5983. item.userInfo = userInfo;
  5984. item.username = userInfo.username;
  5985.  
  5986. item.reputation = (() => {
  5987. const reputations = commonui.userInfo.reputations;
  5988.  
  5989. if (reputations) {
  5990. for (let fid in reputations) {
  5991. return reputations[fid][uid] || 0;
  5992. }
  5993. }
  5994.  
  5995. return NaN;
  5996. })();
  5997. }
  5998. }
  5999. }
  6000.  
  6001. /**
  6002. * 获取帖子数据
  6003. * @param {*} item 绑定的 nFilter
  6004. */
  6005. async getPostInfo(item) {
  6006. const { tid, pid } = item;
  6007.  
  6008. // 请求帖子数据
  6009. const { subject, content, userInfo, reputation } =
  6010. await this.api.getPostInfo(tid, pid);
  6011.  
  6012. // 绑定用户信息和声望
  6013. if (userInfo) {
  6014. item.userInfo = userInfo;
  6015. item.username = userInfo.username;
  6016. item.reputation = reputation;
  6017. }
  6018.  
  6019. // 绑定标题和内容
  6020. item.subject = subject;
  6021. item.content = content;
  6022. }
  6023.  
  6024. /**
  6025. * 获取主题数量
  6026. * @param {*} item 绑定的 nFilter
  6027. */
  6028. async getTopicNum(item) {
  6029. const { uid } = item;
  6030.  
  6031. // 如果是匿名直接跳过
  6032. if (uid <= 0) {
  6033. return;
  6034. }
  6035.  
  6036. // 如果已有缓存,直接返回
  6037. if (Object.hasOwn(this.cache.topicNums, uid)) {
  6038. return this.cache.topicNums[uid];
  6039. }
  6040.  
  6041. // 请求数量
  6042. const number = await this.api.getTopicNum(uid);
  6043.  
  6044. // 写入缓存
  6045. this.cache.topicNums[uid] = number;
  6046.  
  6047. // 返回结果
  6048. return number;
  6049. }
  6050.  
  6051. /**
  6052. * 初始化,增加设置
  6053. */
  6054. initComponents() {
  6055. super.initComponents();
  6056.  
  6057. const { settings, ui } = this;
  6058. const { add } = ui.views.settings;
  6059.  
  6060. // 小号过滤(注册时间)
  6061. {
  6062. const input = ui.createElement("INPUT", [], {
  6063. type: "text",
  6064. value: settings.filterRegdateLimit / 86400000,
  6065. maxLength: 4,
  6066. style: "width: 48px;",
  6067. });
  6068.  
  6069. const button = ui.createButton("确认", () => {
  6070. const newValue = parseInt(input.value, 10) || 0;
  6071.  
  6072. if (newValue < 0) {
  6073. return;
  6074. }
  6075.  
  6076. settings.filterRegdateLimit = newValue * 86400000;
  6077.  
  6078. this.reFilter();
  6079. });
  6080.  
  6081. const element = ui.createElement("DIV", [
  6082. "隐藏注册时间小于",
  6083. input,
  6084. "天的用户",
  6085. button,
  6086. ]);
  6087.  
  6088. add(this.constructor.order + 0, element);
  6089. }
  6090.  
  6091. // 小号过滤(发帖数)
  6092. {
  6093. const input = ui.createElement("INPUT", [], {
  6094. type: "text",
  6095. value: settings.filterPostnumLimit,
  6096. maxLength: 5,
  6097. style: "width: 48px;",
  6098. });
  6099.  
  6100. const button = ui.createButton("确认", () => {
  6101. const newValue = parseInt(input.value, 10) || 0;
  6102.  
  6103. if (newValue < 0) {
  6104. return;
  6105. }
  6106.  
  6107. settings.filterPostnumLimit = newValue;
  6108.  
  6109. this.reFilter();
  6110. });
  6111.  
  6112. const element = ui.createElement("DIV", [
  6113. "隐藏发帖数量小于",
  6114. input,
  6115. "贴的用户",
  6116. button,
  6117. ]);
  6118.  
  6119. add(this.constructor.order + 1, element);
  6120. }
  6121.  
  6122. // 流量号过滤(主题比例)
  6123. {
  6124. const input = ui.createElement("INPUT", [], {
  6125. type: "text",
  6126. value: settings.filterTopicRateLimit,
  6127. maxLength: 3,
  6128. style: "width: 48px;",
  6129. });
  6130.  
  6131. const button = ui.createButton("确认", () => {
  6132. const newValue = parseInt(input.value, 10) || 100;
  6133.  
  6134. if (newValue <= 0 || newValue > 100) {
  6135. return;
  6136. }
  6137.  
  6138. settings.filterTopicRateLimit = newValue;
  6139.  
  6140. this.reFilter();
  6141. });
  6142.  
  6143. const element = ui.createElement("DIV", [
  6144. "隐藏发帖比例大于",
  6145. input,
  6146. "%的用户",
  6147. button,
  6148. ]);
  6149.  
  6150. add(this.constructor.order + 2, element);
  6151. }
  6152.  
  6153. // 声望过滤
  6154. {
  6155. const input = ui.createElement("INPUT", [], {
  6156. type: "text",
  6157. value: settings.filterReputationLimit || "",
  6158. maxLength: 4,
  6159. style: "width: 48px;",
  6160. });
  6161.  
  6162. const button = ui.createButton("确认", () => {
  6163. const newValue = parseInt(input.value, 10);
  6164.  
  6165. settings.filterReputationLimit = newValue;
  6166.  
  6167. this.reFilter();
  6168. });
  6169.  
  6170. const element = ui.createElement("DIV", [
  6171. "隐藏版面声望低于",
  6172. input,
  6173. "点的用户",
  6174. button,
  6175. ]);
  6176.  
  6177. add(this.constructor.order + 3, element);
  6178. }
  6179.  
  6180. // 匿名过滤
  6181. {
  6182. const input = ui.createElement("INPUT", [], {
  6183. type: "checkbox",
  6184. checked: settings.filterAnonymous,
  6185. });
  6186.  
  6187. const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
  6188. style: "display: flex;",
  6189. });
  6190.  
  6191. const element = ui.createElement("DIV", label);
  6192.  
  6193. input.onchange = () => {
  6194. settings.filterAnonymous = input.checked;
  6195.  
  6196. this.reFilter();
  6197. };
  6198.  
  6199. add(this.constructor.order + 4, element);
  6200. }
  6201. }
  6202.  
  6203. /**
  6204. * 过滤
  6205. * @param {*} item 绑定的 nFilter
  6206. * @param {*} result 过滤结果
  6207. */
  6208. async filter(item, result) {
  6209. // 获取隐藏模式下标
  6210. const mode = this.settings.getModeByName("隐藏");
  6211.  
  6212. // 如果当前模式不低于隐藏模式,则跳过
  6213. if (result.mode >= mode) {
  6214. return;
  6215. }
  6216.  
  6217. // 匿名过滤
  6218. await this.filterByAnonymous(item, result);
  6219.  
  6220. // 注册时间过滤
  6221. await this.filterByRegdate(item, result);
  6222.  
  6223. // 发帖数量过滤
  6224. await this.filterByPostnum(item, result);
  6225.  
  6226. // 发帖比例过滤
  6227. await this.filterByTopicRate(item, result);
  6228.  
  6229. // 版面声望过滤
  6230. await this.filterByReputation(item, result);
  6231. }
  6232.  
  6233. /**
  6234. * 根据匿名过滤
  6235. * @param {*} item 绑定的 nFilter
  6236. * @param {*} result 过滤结果
  6237. */
  6238. async filterByAnonymous(item, result) {
  6239. const { uid } = item;
  6240.  
  6241. // 如果不是匿名,则跳过
  6242. if (uid > 0) {
  6243. return;
  6244. }
  6245.  
  6246. // 获取隐藏模式下标
  6247. const mode = this.settings.getModeByName("隐藏");
  6248.  
  6249. // 如果当前模式不低于隐藏模式,则跳过
  6250. if (result.mode >= mode) {
  6251. return;
  6252. }
  6253.  
  6254. // 获取过滤匿名设置
  6255. const filterAnonymous = this.settings.filterAnonymous;
  6256.  
  6257. if (filterAnonymous) {
  6258. // 更新过滤模式和原因
  6259. result.mode = mode;
  6260. result.reason = "匿名";
  6261. }
  6262. }
  6263.  
  6264. /**
  6265. * 根据注册时间过滤
  6266. * @param {*} item 绑定的 nFilter
  6267. * @param {*} result 过滤结果
  6268. */
  6269. async filterByRegdate(item, result) {
  6270. const { uid } = item;
  6271.  
  6272. // 如果是匿名,则跳过
  6273. if (uid <= 0) {
  6274. return;
  6275. }
  6276.  
  6277. // 获取隐藏模式下标
  6278. const mode = this.settings.getModeByName("隐藏");
  6279.  
  6280. // 如果当前模式不低于隐藏模式,则跳过
  6281. if (result.mode >= mode) {
  6282. return;
  6283. }
  6284.  
  6285. // 获取注册时间限制
  6286. const filterRegdateLimit = this.settings.filterRegdateLimit;
  6287.  
  6288. // 未启用则跳过
  6289. if (filterRegdateLimit <= 0) {
  6290. return;
  6291. }
  6292.  
  6293. // 没有用户信息,优先从页面上获取
  6294. if (item.userInfo === undefined) {
  6295. this.getUserInfo(item);
  6296. }
  6297.  
  6298. // 没有再从接口获取
  6299. if (item.userInfo === undefined) {
  6300. await this.getPostInfo(item);
  6301. }
  6302.  
  6303. // 获取注册时间
  6304. const { regdate } = item.userInfo || {};
  6305.  
  6306. // 获取失败则跳过
  6307. if (regdate === undefined) {
  6308. return;
  6309. }
  6310.  
  6311. // 转换时间格式,泥潭接口只精确到秒
  6312. const date = new Date(regdate * 1000);
  6313.  
  6314. // 判断是否符合条件
  6315. if (Date.now() - date > filterRegdateLimit) {
  6316. return;
  6317. }
  6318.  
  6319. // 更新过滤模式和原因
  6320. result.mode = mode;
  6321. result.reason = `注册时间: ${date.toLocaleDateString()}`;
  6322. }
  6323.  
  6324. /**
  6325. * 根据发帖数量过滤
  6326. * @param {*} item 绑定的 nFilter
  6327. * @param {*} result 过滤结果
  6328. */
  6329. async filterByPostnum(item, result) {
  6330. const { uid } = item;
  6331.  
  6332. // 如果是匿名,则跳过
  6333. if (uid <= 0) {
  6334. return;
  6335. }
  6336.  
  6337. // 获取隐藏模式下标
  6338. const mode = this.settings.getModeByName("隐藏");
  6339.  
  6340. // 如果当前模式不低于隐藏模式,则跳过
  6341. if (result.mode >= mode) {
  6342. return;
  6343. }
  6344.  
  6345. // 获取发帖数量限制
  6346. const filterPostnumLimit = this.settings.filterPostnumLimit;
  6347.  
  6348. // 未启用则跳过
  6349. if (filterPostnumLimit <= 0) {
  6350. return;
  6351. }
  6352.  
  6353. // 没有用户信息,优先从页面上获取
  6354. if (item.userInfo === undefined) {
  6355. this.getUserInfo(item);
  6356. }
  6357.  
  6358. // 没有再从接口获取
  6359. if (item.userInfo === undefined) {
  6360. await this.getPostInfo(item);
  6361. }
  6362.  
  6363. // 获取发帖数量
  6364. const { postnum } = item.userInfo || {};
  6365.  
  6366. // 获取失败则跳过
  6367. if (postnum === undefined) {
  6368. return;
  6369. }
  6370.  
  6371. // 判断是否符合条件
  6372. if (postnum >= filterPostnumLimit) {
  6373. return;
  6374. }
  6375.  
  6376. // 更新过滤模式和原因
  6377. result.mode = mode;
  6378. result.reason = `发帖数量: ${postnum}`;
  6379. }
  6380.  
  6381. /**
  6382. * 根据发帖比例过滤
  6383. * @param {*} item 绑定的 nFilter
  6384. * @param {*} result 过滤结果
  6385. */
  6386. async filterByTopicRate(item, result) {
  6387. const { uid } = item;
  6388.  
  6389. // 如果是匿名,则跳过
  6390. if (uid <= 0) {
  6391. return;
  6392. }
  6393.  
  6394. // 获取隐藏模式下标
  6395. const mode = this.settings.getModeByName("隐藏");
  6396.  
  6397. // 如果当前模式不低于隐藏模式,则跳过
  6398. if (result.mode >= mode) {
  6399. return;
  6400. }
  6401.  
  6402. // 获取发帖比例限制
  6403. const filterTopicRateLimit = this.settings.filterTopicRateLimit;
  6404.  
  6405. // 未启用则跳过
  6406. if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
  6407. return;
  6408. }
  6409.  
  6410. // 没有用户信息,优先从页面上获取
  6411. if (item.userInfo === undefined) {
  6412. this.getUserInfo(item);
  6413. }
  6414.  
  6415. // 没有再从接口获取
  6416. if (item.userInfo === undefined) {
  6417. await this.getPostInfo(item);
  6418. }
  6419.  
  6420. // 获取发帖数量
  6421. const { postnum } = item.userInfo || {};
  6422.  
  6423. // 获取失败则跳过
  6424. if (postnum === undefined) {
  6425. return;
  6426. }
  6427.  
  6428. // 获取主题数量
  6429. const topicNum = await this.getTopicNum(item);
  6430.  
  6431. // 计算发帖比例
  6432. const topicRate = Math.ceil((topicNum / postnum) * 100);
  6433.  
  6434. // 判断是否符合条件
  6435. if (topicRate < filterTopicRateLimit) {
  6436. return;
  6437. }
  6438.  
  6439. // 更新过滤模式和原因
  6440. result.mode = mode;
  6441. result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
  6442. }
  6443.  
  6444. /**
  6445. * 根据版面声望过滤
  6446. * @param {*} item 绑定的 nFilter
  6447. * @param {*} result 过滤结果
  6448. */
  6449. async filterByReputation(item, result) {
  6450. const { uid } = item;
  6451.  
  6452. // 如果是匿名,则跳过
  6453. if (uid <= 0) {
  6454. return;
  6455. }
  6456.  
  6457. // 获取隐藏模式下标
  6458. const mode = this.settings.getModeByName("隐藏");
  6459.  
  6460. // 如果当前模式不低于隐藏模式,则跳过
  6461. if (result.mode >= mode) {
  6462. return;
  6463. }
  6464.  
  6465. // 获取版面声望限制
  6466. const filterReputationLimit = this.settings.filterReputationLimit;
  6467.  
  6468. // 未启用则跳过
  6469. if (Number.isNaN(filterReputationLimit)) {
  6470. return;
  6471. }
  6472.  
  6473. // 没有声望信息,优先从页面上获取
  6474. if (item.reputation === undefined) {
  6475. this.getUserInfo(item);
  6476. }
  6477.  
  6478. // 没有再从接口获取
  6479. if (item.reputation === undefined) {
  6480. await this.getPostInfo(item);
  6481. }
  6482.  
  6483. // 获取版面声望
  6484. const reputation = item.reputation || 0;
  6485.  
  6486. // 判断是否符合条件
  6487. if (reputation >= filterReputationLimit) {
  6488. return;
  6489. }
  6490.  
  6491. // 更新过滤模式和原因
  6492. result.mode = mode;
  6493. result.reason = `版面声望: ${reputation}`;
  6494. }
  6495.  
  6496. /**
  6497. * 重新过滤
  6498. */
  6499. reFilter() {
  6500. this.data.forEach((item) => {
  6501. item.execute();
  6502. });
  6503. }
  6504. }
  6505.  
  6506. /**
  6507. * 设置模块
  6508. */
  6509. class SettingsModule extends Module {
  6510. /**
  6511. * 模块名称
  6512. */
  6513. static name = "settings";
  6514.  
  6515. /**
  6516. * 顺序
  6517. */
  6518. static order = 0;
  6519.  
  6520. /**
  6521. * 创建实例
  6522. * @param {Settings} settings 设置
  6523. * @param {API} api API
  6524. * @param {UI} ui UI
  6525. * @param {Array} data 过滤列表
  6526. * @returns {Module | null} 成功后返回模块实例
  6527. */
  6528. static create(settings, api, ui, data) {
  6529. // 读取设置里的模块列表
  6530. const modules = settings.modules;
  6531.  
  6532. // 如果不包含自己,加入列表中,因为设置模块是必须的
  6533. if (modules.includes(this.name) === false) {
  6534. settings.modules = [...modules, this.name];
  6535. }
  6536.  
  6537. // 创建实例
  6538. return super.create(settings, api, ui, data);
  6539. }
  6540.  
  6541. /**
  6542. * 初始化,增加设置
  6543. */
  6544. initComponents() {
  6545. super.initComponents();
  6546.  
  6547. const { settings, ui } = this;
  6548. const { add } = ui.views.settings;
  6549.  
  6550. // 前置过滤
  6551. {
  6552. const input = ui.createElement("INPUT", [], {
  6553. type: "checkbox",
  6554. });
  6555.  
  6556. const label = ui.createElement("LABEL", ["前置过滤", input], {
  6557. style: "display: flex;",
  6558. });
  6559.  
  6560. settings.preFilterEnabled.then((checked) => {
  6561. input.checked = checked;
  6562. input.onchange = () => {
  6563. settings.preFilterEnabled = !checked;
  6564. };
  6565. });
  6566.  
  6567. add(this.constructor.order + 0, label);
  6568. }
  6569.  
  6570. // 模块选择
  6571. {
  6572. const modules = [
  6573. ListModule,
  6574. UserModule,
  6575. TagModule,
  6576. KeywordModule,
  6577. LocationModule,
  6578. HunterModule,
  6579. MiscModule,
  6580. ];
  6581.  
  6582. const items = modules.map((item) => {
  6583. const input = ui.createElement("INPUT", [], {
  6584. type: "checkbox",
  6585. value: item.name,
  6586. checked: settings.modules.includes(item.name),
  6587. onchange: () => {
  6588. const checked = input.checked;
  6589.  
  6590. modules.map((m, index) => {
  6591. const isDepend = checked
  6592. ? item.depends.find((i) => i.name === m.name)
  6593. : m.depends.find((i) => i.name === item.name);
  6594.  
  6595. if (isDepend) {
  6596. const element = items[index].querySelector("INPUT");
  6597.  
  6598. if (element) {
  6599. element.checked = checked;
  6600. }
  6601. }
  6602. });
  6603. },
  6604. });
  6605.  
  6606. const label = ui.createElement("LABEL", [item.label, input], {
  6607. style: "display: flex; margin-right: 10px;",
  6608. });
  6609.  
  6610. return label;
  6611. });
  6612.  
  6613. const button = ui.createButton("确认", () => {
  6614. const checked = group.querySelectorAll("INPUT:checked");
  6615. const values = [...checked].map((item) => item.value);
  6616.  
  6617. settings.modules = values;
  6618.  
  6619. location.reload();
  6620. });
  6621.  
  6622. const group = ui.createElement("DIV", [...items, button], {
  6623. style: "display: flex;",
  6624. });
  6625.  
  6626. const label = ui.createElement("LABEL", "启用模块");
  6627.  
  6628. add(this.constructor.order + 1, label, group);
  6629. }
  6630.  
  6631. // 默认过滤模式
  6632. {
  6633. const modes = ["标记", "遮罩", "隐藏"].map((item) => {
  6634. const input = ui.createElement("INPUT", [], {
  6635. type: "radio",
  6636. name: "defaultFilterMode",
  6637. value: item,
  6638. checked: settings.defaultFilterMode === item,
  6639. onchange: () => {
  6640. settings.defaultFilterMode = item;
  6641.  
  6642. this.reFilter();
  6643. },
  6644. });
  6645.  
  6646. const label = ui.createElement("LABEL", [item, input], {
  6647. style: "display: flex; margin-right: 10px;",
  6648. });
  6649.  
  6650. return label;
  6651. });
  6652.  
  6653. const group = ui.createElement("DIV", modes, {
  6654. style: "display: flex;",
  6655. });
  6656.  
  6657. const label = ui.createElement("LABEL", "默认过滤模式");
  6658.  
  6659. const tips = ui.createElement("DIV", TIPS.filterMode, {
  6660. className: "silver",
  6661. });
  6662.  
  6663. add(this.constructor.order + 2, label, group, tips);
  6664. }
  6665. }
  6666.  
  6667. /**
  6668. * 重新过滤
  6669. */
  6670. reFilter() {
  6671. // 目前仅在修改默认过滤模式时重新过滤
  6672. this.data.forEach((item) => {
  6673. // 如果过滤模式是继承,则重新过滤
  6674. if (item.filterMode === "继承") {
  6675. item.execute();
  6676. }
  6677.  
  6678. // 如果有引用,也重新过滤
  6679. if (Object.values(item.quotes || {}).includes("继承")) {
  6680. item.execute();
  6681. return;
  6682. }
  6683. });
  6684. }
  6685. }
  6686.  
  6687. /**
  6688. * 增强的列表模块,增加了用户作为附加模块
  6689. */
  6690. class ListEnhancedModule extends ListModule {
  6691. /**
  6692. * 模块名称
  6693. */
  6694. static name = "list";
  6695.  
  6696. /**
  6697. * 附加模块
  6698. */
  6699. static addons = [UserModule];
  6700.  
  6701. /**
  6702. * 附加的用户模块
  6703. * @returns {UserModule} 用户模块
  6704. */
  6705. get userModule() {
  6706. return this.addons[UserModule.name];
  6707. }
  6708.  
  6709. /**
  6710. * 表格列
  6711. * @returns {Array} 表格列集合
  6712. */
  6713. columns() {
  6714. const hasAddon = this.hasAddon(UserModule);
  6715.  
  6716. if (hasAddon === false) {
  6717. return super.columns();
  6718. }
  6719.  
  6720. return [
  6721. { label: "用户", width: 1 },
  6722. { label: "内容", ellipsis: true },
  6723. { label: "过滤模式", center: true, width: 1 },
  6724. { label: "原因", width: 1 },
  6725. { label: "操作", width: 1 },
  6726. ];
  6727. }
  6728.  
  6729. /**
  6730. * 表格项
  6731. * @param {*} item 绑定的 nFilter
  6732. * @returns {Array} 表格项集合
  6733. */
  6734. column(item) {
  6735. const column = super.column(item);
  6736.  
  6737. const hasAddon = this.hasAddon(UserModule);
  6738.  
  6739. if (hasAddon === false) {
  6740. return column;
  6741. }
  6742.  
  6743. const { ui } = this;
  6744. const { table } = this.views;
  6745. const { uid, username } = item;
  6746.  
  6747. const user = this.userModule.format(uid, username);
  6748.  
  6749. const buttons = (() => {
  6750. if (uid <= 0) {
  6751. return null;
  6752. }
  6753.  
  6754. const block = ui.createButton("屏蔽", (e) => {
  6755. this.userModule.renderDetails(uid, username, (type) => {
  6756. // 删除失效数据,等待重新过滤
  6757. table.remove(e);
  6758.  
  6759. // 如果是新增,不会因为用户重新过滤,需要主动触发
  6760. if (type === "ADD") {
  6761. this.userModule.reFilter(uid);
  6762. }
  6763. });
  6764. });
  6765.  
  6766. return ui.createButtonGroup(block);
  6767. })();
  6768.  
  6769. return [user, ...column, buttons];
  6770. }
  6771. }
  6772.  
  6773. /**
  6774. * 增强的用户模块,增加了标记作为附加模块
  6775. */
  6776. class UserEnhancedModule extends UserModule {
  6777. /**
  6778. * 模块名称
  6779. */
  6780. static name = "user";
  6781.  
  6782. /**
  6783. * 附加模块
  6784. */
  6785. static addons = [TagModule];
  6786.  
  6787. /**
  6788. * 附加的标记模块
  6789. * @returns {TagModule} 标记模块
  6790. */
  6791. get tagModule() {
  6792. return this.addons[TagModule.name];
  6793. }
  6794.  
  6795. /**
  6796. * 表格列
  6797. * @returns {Array} 表格列集合
  6798. */
  6799. columns() {
  6800. const hasAddon = this.hasAddon(TagModule);
  6801.  
  6802. if (hasAddon === false) {
  6803. return super.columns();
  6804. }
  6805.  
  6806. return [
  6807. { label: "昵称", width: 1 },
  6808. { label: "标记" },
  6809. { label: "过滤模式", center: true, width: 1 },
  6810. { label: "操作", width: 1 },
  6811. ];
  6812. }
  6813.  
  6814. /**
  6815. * 表格项
  6816. * @param {*} item 用户信息
  6817. * @returns {Array} 表格项集合
  6818. */
  6819. column(item) {
  6820. const column = super.column(item);
  6821.  
  6822. const hasAddon = this.hasAddon(TagModule);
  6823.  
  6824. if (hasAddon === false) {
  6825. return column;
  6826. }
  6827.  
  6828. const { ui } = this;
  6829. const { table } = this.views;
  6830. const { id, name } = item;
  6831.  
  6832. const tags = ui.createElement(
  6833. "DIV",
  6834. item.tags.map((id) => this.tagModule.format(id))
  6835. );
  6836.  
  6837. const newColumn = [...column];
  6838.  
  6839. newColumn.splice(1, 0, tags);
  6840.  
  6841. const buttons = column[column.length - 1];
  6842.  
  6843. const update = ui.createButton("编辑", (e) => {
  6844. this.renderDetails(id, name, (type, newValue) => {
  6845. if (type === "UPDATE") {
  6846. table.update(e, ...this.column(newValue));
  6847. }
  6848.  
  6849. if (type === "REMOVE") {
  6850. table.remove(e);
  6851. }
  6852. });
  6853. });
  6854.  
  6855. buttons.insertBefore(update, buttons.firstChild);
  6856.  
  6857. return newColumn;
  6858. }
  6859.  
  6860. /**
  6861. * 渲染详情
  6862. * @param {Number} uid 用户 ID
  6863. * @param {String | undefined} name 用户名称
  6864. * @param {Function} callback 回调函数
  6865. */
  6866. renderDetails(uid, name, callback = () => {}) {
  6867. const hasAddon = this.hasAddon(TagModule);
  6868.  
  6869. if (hasAddon === false) {
  6870. return super.renderDetails(uid, name, callback);
  6871. }
  6872.  
  6873. const { ui, settings } = this;
  6874.  
  6875. // 只允许同时存在一个详情页
  6876. if (this.views.details) {
  6877. if (this.views.details.parentNode) {
  6878. this.views.details.parentNode.removeChild(this.views.details);
  6879. }
  6880. }
  6881.  
  6882. // 获取用户信息
  6883. const user = this.get(uid);
  6884.  
  6885. if (user) {
  6886. name = user.name;
  6887. }
  6888.  
  6889. // TODO 需要优化
  6890.  
  6891. const title =
  6892. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  6893.  
  6894. const table = ui.createTable([]);
  6895.  
  6896. {
  6897. const size = Math.floor((screen.width * 0.8) / 200);
  6898.  
  6899. const items = Object.values(this.tagModule.list).map(({ id }) => {
  6900. const checked = user && user.tags.includes(id) ? "checked" : "";
  6901.  
  6902. return `
  6903. <td class="c1">
  6904. <label for="s-tag-${id}" style="display: block; cursor: pointer;">
  6905. ${this.tagModule.format(id).outerHTML}
  6906. </label>
  6907. </td>
  6908. <td class="c2" width="1">
  6909. <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
  6910. </td>
  6911. `;
  6912. });
  6913.  
  6914. const rows = [...new Array(Math.ceil(items.length / size))].map(
  6915. (_, index) => `
  6916. <tr class="row${(index % 2) + 1}">
  6917. ${items.slice(size * index, size * (index + 1)).join("")}
  6918. </tr>
  6919. `
  6920. );
  6921.  
  6922. table.querySelector("TBODY").innerHTML = rows.join("");
  6923. }
  6924.  
  6925. const input = ui.createElement("INPUT", [], {
  6926. type: "text",
  6927. placeholder: TIPS.addTags,
  6928. style: "width: -webkit-fill-available;",
  6929. });
  6930.  
  6931. const inputWrapper = ui.createElement("DIV", input, {
  6932. style: "margin-top: 10px;",
  6933. });
  6934.  
  6935. const filterMode = user ? user.filterMode : settings.filterModes[0];
  6936.  
  6937. const switchMode = ui.createButton(filterMode, () => {
  6938. const newMode = settings.switchModeByName(switchMode.innerText);
  6939.  
  6940. switchMode.innerText = newMode;
  6941. });
  6942.  
  6943. const buttons = ui.createElement(
  6944. "DIV",
  6945. (() => {
  6946. const remove = user
  6947. ? ui.createButton("删除", () => {
  6948. ui.confirm().then(() => {
  6949. this.remove(uid);
  6950.  
  6951. this.views.details._.hide();
  6952.  
  6953. callback("REMOVE");
  6954. });
  6955. })
  6956. : null;
  6957.  
  6958. const save = ui.createButton("保存", () => {
  6959. const checked = [...table.querySelectorAll("INPUT:checked")].map(
  6960. (input) => parseInt(input.value, 10)
  6961. );
  6962.  
  6963. const newTags = input.value
  6964. .split("|")
  6965. .filter((item) => item.length)
  6966. .map((item) => this.tagModule.add(item))
  6967. .filter((tag) => tag !== null)
  6968. .map((tag) => tag.id);
  6969.  
  6970. const tags = [...new Set([...checked, ...newTags])].sort();
  6971.  
  6972. if (user === null) {
  6973. const entity = this.add(uid, {
  6974. id: uid,
  6975. name,
  6976. tags,
  6977. filterMode: switchMode.innerText,
  6978. });
  6979.  
  6980. this.views.details._.hide();
  6981.  
  6982. callback("ADD", entity);
  6983. } else {
  6984. const entity = this.update(uid, {
  6985. name,
  6986. tags,
  6987. filterMode: switchMode.innerText,
  6988. });
  6989.  
  6990. this.views.details._.hide();
  6991.  
  6992. callback("UPDATE", entity);
  6993. }
  6994. });
  6995.  
  6996. return ui.createButtonGroup(remove, save);
  6997. })(),
  6998. {
  6999. className: "right_",
  7000. }
  7001. );
  7002.  
  7003. const actions = ui.createElement(
  7004. "DIV",
  7005. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  7006. {
  7007. style: "margin-top: 10px;",
  7008. }
  7009. );
  7010.  
  7011. const tips = ui.createElement("DIV", TIPS.filterMode, {
  7012. className: "silver",
  7013. style: "margin-top: 10px;",
  7014. });
  7015.  
  7016. const content = ui.createElement(
  7017. "DIV",
  7018. [table, inputWrapper, actions, tips],
  7019. {
  7020. style: "width: 80vw",
  7021. }
  7022. );
  7023.  
  7024. // 创建弹出框
  7025. this.views.details = ui.createDialog(null, title, content);
  7026. }
  7027. }
  7028.  
  7029. /**
  7030. * 处理 topicArg 模块
  7031. * @param {Filter} filter 过滤器
  7032. * @param {*} value commonui.topicArg
  7033. */
  7034. const handleTopicModule = async (filter, value) => {
  7035. // 绑定主题模块
  7036. topicModule = value;
  7037.  
  7038. // 是否启用前置过滤
  7039. const preFilterEnabled = await filter.settings.preFilterEnabled;
  7040.  
  7041. // 前置过滤
  7042. // 先直接隐藏,等过滤完毕后再放出来
  7043. const beforeGet = (...args) => {
  7044. if (preFilterEnabled) {
  7045. // 主题标题
  7046. const title = document.getElementById(args[1]);
  7047.  
  7048. // 主题容器
  7049. const container = title.closest("tr");
  7050.  
  7051. // 隐藏元素
  7052. container.style.display = "none";
  7053. }
  7054.  
  7055. return args;
  7056. };
  7057.  
  7058. // 过滤
  7059. const afterGet = (_, args) => {
  7060. // 主题 ID
  7061. const tid = args[8];
  7062.  
  7063. // 找到对应数据
  7064. const data = topicModule.data.find((item) => item[8] === tid);
  7065.  
  7066. // 开始过滤
  7067. if (data) {
  7068. filter.filterTopic(data);
  7069. }
  7070. };
  7071.  
  7072. // 如果已经有数据,则直接过滤
  7073. Object.values(topicModule.data).forEach(filter.filterTopic);
  7074.  
  7075. // 拦截 add 函数,这是泥潭的主题添加事件
  7076. Tools.interceptProperty(topicModule, "add", {
  7077. beforeGet,
  7078. afterGet,
  7079. });
  7080. };
  7081.  
  7082. /**
  7083. * 处理 postArg 模块
  7084. * @param {Filter} filter 过滤器
  7085. * @param {*} value commonui.postArg
  7086. */
  7087. const handleReplyModule = async (filter, value) => {
  7088. // 绑定回复模块
  7089. replyModule = value;
  7090.  
  7091. // 是否启用前置过滤
  7092. const preFilterEnabled = await filter.settings.preFilterEnabled;
  7093.  
  7094. // 前置过滤
  7095. // 先直接隐藏,等过滤完毕后再放出来
  7096. const beforeGet = (...args) => {
  7097. if (preFilterEnabled) {
  7098. // 楼层号
  7099. const index = args[0];
  7100.  
  7101. // 判断是否是楼层
  7102. const isFloor = typeof index === "number";
  7103.  
  7104. // 评论额外标签
  7105. const prefix = isFloor ? "" : "comment";
  7106.  
  7107. // 用户容器
  7108. const uInfoC = document.querySelector(`#${prefix}posterinfo${index}`);
  7109.  
  7110. // 回复容器
  7111. const container = isFloor
  7112. ? uInfoC.closest("tr")
  7113. : uInfoC.closest(".comment_c");
  7114.  
  7115. // 隐藏元素
  7116. container.style.display = "none";
  7117. }
  7118.  
  7119. return args;
  7120. };
  7121.  
  7122. // 过滤
  7123. const afterGet = (_, args) => {
  7124. // 楼层号
  7125. const index = args[0];
  7126.  
  7127. // 找到对应数据
  7128. const data = replyModule.data[index];
  7129.  
  7130. // 开始过滤
  7131. if (data) {
  7132. filter.filterReply(data);
  7133. }
  7134. };
  7135.  
  7136. // 如果已经有数据,则直接过滤
  7137. Object.values(replyModule.data).forEach(filter.filterReply);
  7138.  
  7139. // 拦截 proc 函数,这是泥潭的回复添加事件
  7140. Tools.interceptProperty(replyModule, "proc", {
  7141. beforeGet,
  7142. afterGet,
  7143. });
  7144. };
  7145.  
  7146. /**
  7147. * 处理 commonui 模块
  7148. * @param {Filter} filter 过滤器
  7149. * @param {*} value commonui
  7150. */
  7151. const handleCommonui = (filter, value) => {
  7152. // 绑定主模块
  7153. commonui = value;
  7154.  
  7155. // 拦截 mainMenu 模块,UI 需要在 init 后加载
  7156. Tools.interceptProperty(commonui, "mainMenu", {
  7157. afterSet: (value) => {
  7158. Tools.interceptProperty(value, "init", {
  7159. afterGet: () => {
  7160. filter.ui.render();
  7161. },
  7162. afterSet: () => {
  7163. filter.ui.render();
  7164. },
  7165. });
  7166. },
  7167. });
  7168.  
  7169. // 拦截 topicArg 模块,这是泥潭的主题入口
  7170. Tools.interceptProperty(commonui, "topicArg", {
  7171. afterSet: (value) => {
  7172. handleTopicModule(filter, value);
  7173. },
  7174. });
  7175.  
  7176. // 拦截 postArg 模块,这是泥潭的回复入口
  7177. Tools.interceptProperty(commonui, "postArg", {
  7178. afterSet: (value) => {
  7179. handleReplyModule(filter, value);
  7180. },
  7181. });
  7182. };
  7183.  
  7184. /**
  7185. * 注册脚本菜单
  7186. * @param {Settings} settings 设置
  7187. */
  7188. const registerMenu = async (settings) => {
  7189. // 修改 UA
  7190. {
  7191. const userAgent = await settings.userAgent;
  7192.  
  7193. GM_registerMenuCommand(`修改UA${userAgent}`, () => {
  7194. const value = prompt("修改UA", userAgent);
  7195.  
  7196. if (value) {
  7197. settings.userAgent = value;
  7198. }
  7199. });
  7200. }
  7201.  
  7202. // 前置过滤
  7203. {
  7204. const enabled = await settings.preFilterEnabled;
  7205.  
  7206. GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
  7207. settings.preFilterEnabled = !enabled;
  7208. });
  7209. }
  7210. };
  7211.  
  7212. // 主函数
  7213. (async () => {
  7214. // 初始化缓存、设置
  7215. const cache = new Cache(API.modules);
  7216. const settings = new Settings(cache);
  7217.  
  7218. // 读取设置
  7219. await settings.load();
  7220.  
  7221. // 初始化 API、UI
  7222. const api = new API(cache, settings);
  7223. const ui = new UI(settings, api);
  7224.  
  7225. // 初始化过滤器
  7226. const filter = new Filter(settings, api, ui);
  7227.  
  7228. // 加载模块
  7229. filter.initModules(
  7230. SettingsModule,
  7231. ListEnhancedModule,
  7232. UserEnhancedModule,
  7233. TagModule,
  7234. KeywordModule,
  7235. LocationModule,
  7236. HunterModule,
  7237. MiscModule
  7238. );
  7239.  
  7240. // 注册脚本菜单
  7241. registerMenu(settings);
  7242.  
  7243. // 处理 commonui 模块
  7244. if (unsafeWindow.commonui) {
  7245. handleCommonui(filter, unsafeWindow.commonui);
  7246. return;
  7247. }
  7248.  
  7249. Tools.interceptProperty(unsafeWindow, "commonui", {
  7250. afterSet: (value) => {
  7251. handleCommonui(filter, value);
  7252. },
  7253. });
  7254. })();
  7255. })();