NGA Filter

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

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

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

QingJ © 2025

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