NGA Filter

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

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

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