NGA Filter

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址