NGA Filter

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

当前为 2024-07-22 提交的版本,查看 最新版本

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