NGA Filter

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

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

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