NGA Filter

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

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

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