NGA Filter

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

当前为 2024-04-26 提交的版本,查看 最新版本

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