NGA Filter

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

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

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