NGA Filter

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

目前為 2024-01-04 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 2.1.3
  5. // @author snyssss
  6. // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
  7. // @license MIT
  8.  
  9. // @match *://bbs.nga.cn/*
  10. // @match *://ngabbs.com/*
  11. // @match *://nga.178.com/*
  12.  
  13. // @grant GM_addStyle
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_registerMenuCommand
  17. // @grant unsafeWindow
  18.  
  19. // @run-at document-start
  20. // @noframes
  21. // ==/UserScript==
  22.  
  23. (() => {
  24. // 声明泥潭主模块、菜单模块、主题模块、回复模块
  25. let commonui, menuModule, topicModule, replyModule;
  26.  
  27. // KEY
  28. const DATA_KEY = "NGAFilter";
  29. const USER_AGENT_KEY = "USER_AGENT_KEY";
  30. const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  31. const CLEAR_TIME_KEY = "CLEAR_TIME_KEY";
  32.  
  33. // User Agent
  34. const USER_AGENT = (() => {
  35. const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official";
  36.  
  37. GM_registerMenuCommand(`修改UA${data}`, () => {
  38. const value = prompt("修改UA", data);
  39.  
  40. if (value) {
  41. GM_setValue(USER_AGENT_KEY, value);
  42.  
  43. location.reload();
  44. }
  45. });
  46.  
  47. return data;
  48. })();
  49.  
  50. // 前置过滤
  51. const preFilter = (() => {
  52. const data = GM_getValue(PRE_FILTER_KEY);
  53.  
  54. const value = data === undefined ? true : data;
  55.  
  56. GM_registerMenuCommand(`前置过滤:${value ? "是" : "否"}`, () => {
  57. GM_setValue(PRE_FILTER_KEY, !value);
  58.  
  59. location.reload();
  60. });
  61.  
  62. return value;
  63. })();
  64.  
  65. // STYLE
  66. GM_addStyle(`
  67. .filter-table-wrapper {
  68. max-height: 80vh;
  69. overflow-y: auto;
  70. }
  71. .filter-table {
  72. margin: 0;
  73. }
  74. .filter-table th,
  75. .filter-table td {
  76. position: relative;
  77. white-space: nowrap;
  78. }
  79. .filter-table th {
  80. position: sticky;
  81. top: 2px;
  82. z-index: 1;
  83. }
  84. .filter-table input:not([type]), .filter-table input[type="text"] {
  85. margin: 0;
  86. box-sizing: border-box;
  87. height: 100%;
  88. width: 100%;
  89. }
  90. .filter-input-wrapper {
  91. position: absolute;
  92. top: 6px;
  93. right: 6px;
  94. bottom: 6px;
  95. left: 6px;
  96. }
  97. .filter-text-ellipsis {
  98. display: flex;
  99. }
  100. .filter-text-ellipsis > * {
  101. flex: 1;
  102. width: 1px;
  103. overflow: hidden;
  104. text-overflow: ellipsis;
  105. }
  106. .filter-button-group {
  107. margin: -.1em -.2em;
  108. }
  109. .filter-tags {
  110. margin: 2px -0.2em 0;
  111. text-align: left;
  112. }
  113. .filter-mask {
  114. margin: 1px;
  115. color: #81C7D4;
  116. background: #81C7D4;
  117. }
  118. .filter-mask-block {
  119. display: block;
  120. border: 1px solid #66BAB7;
  121. text-align: center !important;
  122. }
  123. .filter-input-wrapper {
  124. position: absolute;
  125. top: 6px;
  126. right: 6px;
  127. bottom: 6px;
  128. left: 6px;
  129. }
  130. `);
  131.  
  132. // 重新过滤
  133. const reFilter = async (skip = () => false) => {
  134. // 清空列表
  135. listModule.clear();
  136.  
  137. // 开始过滤
  138. [
  139. ...(topicModule ? Object.values(topicModule.data) : []),
  140. ...(replyModule ? Object.values(replyModule.data) : []),
  141. ].forEach((item) => {
  142. // 未绑定事件
  143. if (item.nFilter === undefined) {
  144. return;
  145. }
  146.  
  147. // 如果跳过过滤,直接添加列表
  148. if (skip(item.nFilter)) {
  149. listModule.add(item.nFilter);
  150. return;
  151. }
  152.  
  153. // 执行过滤
  154. item.nFilter.execute();
  155. });
  156. };
  157.  
  158. // 缓存模块
  159. const cacheModule = (() => {
  160. // 声明模块集合
  161. const modules = {};
  162.  
  163. // IndexedDB 操作
  164. const db = (() => {
  165. // 常量
  166. const VERSION = 2;
  167. const DB_NAME = "NGA_FILTER_CACHE";
  168.  
  169. // 是否支持
  170. const support = unsafeWindow.indexedDB !== undefined;
  171.  
  172. // 不支持,直接返回
  173. if (support === false) {
  174. return {
  175. support,
  176. };
  177. }
  178.  
  179. // 创建或获取数据库实例
  180. const getInstance = (() => {
  181. let instance;
  182.  
  183. return () =>
  184. new Promise((resolve) => {
  185. // 如果已存在实例,直接返回
  186. if (instance) {
  187. resolve(instance);
  188. return;
  189. }
  190.  
  191. // 打开 IndexedDB 数据库
  192. const request = unsafeWindow.indexedDB.open(DB_NAME, VERSION);
  193.  
  194. // 如果数据库不存在则创建
  195. request.onupgradeneeded = (event) => {
  196. // 获取旧版本号
  197. var oldVersion = event.oldVersion;
  198.  
  199. // 根据版本号创建表
  200. Object.entries(modules).map(([name, { keyPath, version }]) => {
  201. if (version > oldVersion) {
  202. // 创建表
  203. const store = event.target.result.createObjectStore(name, {
  204. keyPath,
  205. });
  206.  
  207. // 创建索引,用于清除过期数据
  208. store.createIndex("timestamp", "timestamp");
  209. }
  210. });
  211. };
  212.  
  213. // 成功后写入实例并返回
  214. request.onsuccess = (event) => {
  215. instance = event.target.result;
  216.  
  217. resolve(instance);
  218. };
  219. });
  220. })();
  221.  
  222. return {
  223. support,
  224. getInstance,
  225. };
  226. })();
  227.  
  228. // 删除缓存
  229. const remove = async (name, key) => {
  230. // 不支持 IndexedDB,使用 GM_setValue
  231. if (db.support === false) {
  232. const cache = GM_getValue(name) || {};
  233.  
  234. delete cache[key];
  235.  
  236. GM_setValue(name, cache);
  237. return;
  238. }
  239.  
  240. // 获取实例
  241. const instance = await db.getInstance();
  242.  
  243. // 写入 IndexedDB
  244. await new Promise((resolve) => {
  245. // 创建事务
  246. const transaction = instance.transaction([name], "readwrite");
  247.  
  248. // 获取对象仓库
  249. const store = transaction.objectStore(name);
  250.  
  251. // 删除数据
  252. const r = store.delete(key);
  253.  
  254. r.onsuccess = () => {
  255. resolve();
  256. };
  257.  
  258. r.onerror = () => {
  259. resolve();
  260. };
  261. });
  262. };
  263.  
  264. // 写入缓存
  265. const save = async (name, key, value) => {
  266. // 不支持 IndexedDB,使用 GM_setValue
  267. if (db.support === false) {
  268. const cache = GM_getValue(name) || {};
  269.  
  270. cache[key] = value;
  271.  
  272. GM_setValue(name, cache);
  273. return;
  274. }
  275.  
  276. // 获取实例
  277. const instance = await db.getInstance();
  278.  
  279. // 写入 IndexedDB
  280. await new Promise((resolve) => {
  281. // 创建事务
  282. const transaction = instance.transaction([name], "readwrite");
  283.  
  284. // 获取对象仓库
  285. const store = transaction.objectStore(name);
  286.  
  287. // 插入数据
  288. const r = store.put({
  289. ...value,
  290. timestamp: Date.now(),
  291. });
  292.  
  293. r.onsuccess = () => {
  294. resolve();
  295. };
  296.  
  297. r.onerror = () => {
  298. resolve();
  299. };
  300. });
  301. };
  302.  
  303. // 读取缓存
  304. const load = async (name, key, expireTime = 0) => {
  305. // 不支持 IndexedDB,使用 GM_getValue
  306. if (db.support === false) {
  307. const cache = GM_getValue(name) || {};
  308.  
  309. if (cache[key]) {
  310. const result = cache[key];
  311.  
  312. // 如果已超时则删除
  313. if (expireTime > 0) {
  314. if (result.timestamp + expireTime < new Date().getTime()) {
  315. await remove(name, key);
  316.  
  317. return null;
  318. }
  319. }
  320.  
  321. return result;
  322. }
  323.  
  324. return null;
  325. }
  326.  
  327. // 获取实例
  328. const instance = await db.getInstance();
  329.  
  330. // 查找 IndexedDB
  331. const result = await new Promise((resolve) => {
  332. // 创建事务
  333. const transaction = instance.transaction([name], "readonly");
  334.  
  335. // 获取对象仓库
  336. const store = transaction.objectStore(name);
  337.  
  338. // 获取数据
  339. const request = store.get(key);
  340.  
  341. // 成功后处理数据
  342. request.onsuccess = (event) => {
  343. const data = event.target.result;
  344.  
  345. if (data) {
  346. resolve(data);
  347. return;
  348. }
  349.  
  350. resolve(null);
  351. };
  352.  
  353. // 失败后处理
  354. request.onerror = () => {
  355. resolve(null);
  356. };
  357. });
  358.  
  359. // 没有数据
  360. if (result === null) {
  361. return null;
  362. }
  363.  
  364. // 如果已超时则删除
  365. if (expireTime > 0) {
  366. if (result.timestamp + expireTime < new Date().getTime()) {
  367. await remove(name, key);
  368.  
  369. return null;
  370. }
  371. }
  372.  
  373. // 返回结果
  374. return result;
  375. };
  376.  
  377. // 定时清理
  378. const clear = async () => {
  379. // 获取实例
  380. const instance = await db.getInstance();
  381.  
  382. // 清理 IndexedDB
  383. Object.entries(modules).map(([name, { persistent }]) => {
  384. // 持久化,不进行自动清理
  385. if (persistent) {
  386. return;
  387. }
  388.  
  389. // 创建事务
  390. const transaction = instance.transaction([name], "readwrite");
  391.  
  392. // 获取对象仓库
  393. const store = transaction.objectStore(name);
  394.  
  395. // 清理数据
  396. store.clear();
  397. });
  398. };
  399.  
  400. // 初始化,用于写入表信息
  401. const init = (name, value) => {
  402. modules[name] = value;
  403. };
  404.  
  405. return {
  406. init,
  407. save,
  408. load,
  409. remove,
  410. clear,
  411. };
  412. })();
  413.  
  414. // 过滤模块
  415. const filterModule = (() => {
  416. // 过滤提示
  417. const tips =
  418. "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承";
  419.  
  420. // 过滤方式
  421. const modes = ["继承", "标记", "遮罩", "隐藏", "显示"];
  422.  
  423. // 默认过滤方式
  424. const defaultMode = modes[0];
  425.  
  426. // 切换过滤方式
  427. const switchModeByName = (value) =>
  428. modes[modes.indexOf(value) + 1] || defaultMode;
  429.  
  430. // 获取当前过滤方式下标
  431. const getModeByName = (name, defaultValue = 0) => {
  432. const index = modes.indexOf(name);
  433.  
  434. if (index < 0) {
  435. return defaultValue;
  436. }
  437.  
  438. return index;
  439. };
  440.  
  441. // 获取指定下标过滤方式
  442. const getNameByMode = (index) => modes[index] || "";
  443.  
  444. // 折叠样式
  445. const collapse = (uid, element, content) => {
  446. element.innerHTML = `
  447. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
  448. <span class="crimson">Troll must die.</span>
  449. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  450. <div style="display: none;" name="troll_${uid}">
  451. ${content}
  452. </div>
  453. </div>`;
  454. };
  455.  
  456. return {
  457. tips,
  458. modes,
  459. defaultMode,
  460. collapse,
  461. getModeByName,
  462. getNameByMode,
  463. switchModeByName,
  464. };
  465. })();
  466.  
  467. // 数据(及配置)模块
  468. const dataModule = (() => {
  469. // 合并数据
  470. const merge = (() => {
  471. const isObject = (value) => {
  472. return value !== null && typeof value === "object";
  473. };
  474.  
  475. const deepClone = (value) => {
  476. if (isObject(value)) {
  477. const clone = Array.isArray(value) ? [] : {};
  478.  
  479. for (const key in value) {
  480. if (Object.hasOwn(value, key)) {
  481. clone[key] = deepClone(value[key]);
  482. }
  483. }
  484.  
  485. return clone;
  486. }
  487.  
  488. return value;
  489. };
  490.  
  491. return (target, ...sources) => {
  492. for (const source of sources) {
  493. for (const key in source) {
  494. if (isObject(source[key])) {
  495. if (isObject(target[key])) {
  496. merge(target[key], source[key]);
  497. } else {
  498. target[key] = deepClone(source[key]);
  499. }
  500. } else {
  501. target[key] = source[key];
  502. }
  503. }
  504. }
  505.  
  506. return target;
  507. };
  508. })();
  509.  
  510. // 初始化数据
  511. const data = (() => {
  512. // 默认配置
  513. const defaultData = {
  514. tags: {},
  515. users: {},
  516. keywords: {},
  517. locations: {},
  518. options: {
  519. filterRegdateLimit: 0,
  520. filterPostnumLimit: 0,
  521. filterTopicRateLimit: 100,
  522. filterReputationLimit: NaN,
  523. filterAnony: false,
  524. filterMode: "隐藏",
  525. },
  526. };
  527.  
  528. // 读取数据
  529. const storedData = GM_getValue(DATA_KEY);
  530.  
  531. // 如果没有数据,则返回默认配置
  532. if (typeof storedData !== "object") {
  533. return defaultData;
  534. }
  535.  
  536. // 返回数据
  537. return merge(defaultData, storedData);
  538. })();
  539.  
  540. // 保存数据
  541. const save = (values) => {
  542. merge(data, values);
  543.  
  544. GM_setValue(DATA_KEY, data);
  545. };
  546.  
  547. // 返回标记列表
  548. const getTags = () => data.tags;
  549.  
  550. // 返回用户列表
  551. const getUsers = () => data.users;
  552.  
  553. // 返回关键字列表
  554. const getKeywords = () => data.keywords;
  555.  
  556. // 返回属地列表
  557. const getLocations = () => data.locations;
  558.  
  559. // 获取默认过滤模式
  560. const getDefaultFilterMode = () => data.options.filterMode;
  561.  
  562. // 设置默认过滤模式
  563. const setDefaultFilterMode = (value) => {
  564. save({
  565. options: {
  566. filterMode: value,
  567. },
  568. });
  569. };
  570.  
  571. // 获取注册时间限制
  572. const getFilterRegdateLimit = () => data.options.filterRegdateLimit || 0;
  573.  
  574. // 设置注册时间限制
  575. const setFilterRegdateLimit = (value) => {
  576. save({
  577. options: {
  578. filterRegdateLimit: value,
  579. },
  580. });
  581. };
  582.  
  583. // 获取发帖数量限制
  584. const getFilterPostnumLimit = () => data.options.filterPostnumLimit || 0;
  585.  
  586. // 设置发帖数量限制
  587. const setFilterPostnumLimit = (value) => {
  588. save({
  589. options: {
  590. filterPostnumLimit: value,
  591. },
  592. });
  593. };
  594.  
  595. // 获取发帖比例限制
  596. const getFilterTopicRateLimit = () =>
  597. data.options.filterTopicRateLimit || 100;
  598.  
  599. // 设置发帖比例限制
  600. const setFilterTopicRateLimit = (value) => {
  601. save({
  602. options: {
  603. filterTopicRateLimit: value,
  604. },
  605. });
  606. };
  607.  
  608. // 获取用户声望限制
  609. const getFilterReputationLimit = () =>
  610. data.options.filterReputationLimit || NaN;
  611.  
  612. // 设置用户声望限制
  613. const setFilterReputationLimit = (value) => {
  614. save({
  615. options: {
  616. filterReputationLimit: value,
  617. },
  618. });
  619. };
  620.  
  621. // 获取是否过滤匿名
  622. const getFilterAnony = () => data.options.filterAnony || false;
  623.  
  624. // 设置是否过滤匿名
  625. const setFilterAnony = (value) => {
  626. save({
  627. options: {
  628. filterAnony: value,
  629. },
  630. });
  631. };
  632.  
  633. return {
  634. save,
  635. getTags,
  636. getUsers,
  637. getKeywords,
  638. getLocations,
  639. getDefaultFilterMode,
  640. setDefaultFilterMode,
  641. getFilterRegdateLimit,
  642. setFilterRegdateLimit,
  643. getFilterPostnumLimit,
  644. setFilterPostnumLimit,
  645. getFilterTopicRateLimit,
  646. setFilterTopicRateLimit,
  647. getFilterReputationLimit,
  648. setFilterReputationLimit,
  649. getFilterAnony,
  650. setFilterAnony,
  651. };
  652. })();
  653.  
  654. // 列表模块
  655. const listModule = (() => {
  656. const list = [];
  657.  
  658. const callback = [];
  659.  
  660. // UI
  661. const view = (() => {
  662. const content = (() => {
  663. const element = document.createElement("DIV");
  664.  
  665. element.style = "display: none";
  666. element.innerHTML = `
  667. <div class="filter-table-wrapper">
  668. <table class="filter-table forumbox">
  669. <thead>
  670. <tr class="block_txt_c0">
  671. <th class="c1" width="1">用户</th>
  672. <th class="c2" width="1">过滤方式</th>
  673. <th class="c3">内容</th>
  674. <th class="c4" width="1">原因</th>
  675. </tr>
  676. </thead>
  677. <tbody></tbody>
  678. </table>
  679. </div>
  680. `;
  681.  
  682. return element;
  683. })();
  684.  
  685. const tbody = content.querySelector("TBODY");
  686.  
  687. const load = (item) => {
  688. const { uid, username, tid, pid, filterMode, reason } = item;
  689.  
  690. // 用户
  691. const user = userModule.format(uid, username);
  692.  
  693. // 移除 BR 标签
  694. item.content = (item.content || "").replace(/<br>/g, "");
  695.  
  696. // 主题
  697. const subject = (() => {
  698. if (tid) {
  699. // 如果有 TID 但没有标题,是引用,采用内容逻辑
  700. if (item.subject.length === 0) {
  701. return `<a href="${`/read.php?tid=${tid}`}&nofilter">${
  702. item.content
  703. }</a>`;
  704. }
  705.  
  706. return `<a href="${`/read.php?tid=${tid}`}&nofilter" title="${
  707. item.content
  708. }" class="b nobr">${item.subject}</a>`;
  709. }
  710.  
  711. return item.subject;
  712. })();
  713.  
  714. // 内容
  715. const content = (() => {
  716. if (pid) {
  717. return `<a href="${`/read.php?pid=${pid}`}&nofilter">${
  718. item.content
  719. }</a>`;
  720. }
  721.  
  722. return item.content;
  723. })();
  724.  
  725. const row = document.createElement("TR");
  726.  
  727. row.className = `row${(tbody.querySelectorAll("TR").length % 2) + 1}`;
  728.  
  729. row.innerHTML = `
  730. <td class="c1">${user}</td>
  731. <td class="c2">${filterMode}</td>
  732. <td class="c3">
  733. <div class="filter-text-ellipsis">
  734. ${subject || content}
  735. </div>
  736. </td>
  737. <td class="c4">${reason}</td>
  738. `;
  739.  
  740. tbody.insertBefore(row, tbody.firstChild);
  741. };
  742.  
  743. const refresh = () => {
  744. tbody.innerHTML = "";
  745.  
  746. Object.values(list).forEach(load);
  747. };
  748.  
  749. return {
  750. content,
  751. refresh,
  752. load,
  753. };
  754. })();
  755.  
  756. const add = (value) => {
  757. if (
  758. list.find(
  759. (item) =>
  760. item.tid === value.tid &&
  761. item.pid === value.pid &&
  762. item.subject === value.subject
  763. )
  764. ) {
  765. return;
  766. }
  767.  
  768. if ((value.filterMode || "显示") === "显示") {
  769. return;
  770. }
  771.  
  772. list.push(value);
  773.  
  774. view.load(value);
  775.  
  776. callback.forEach((item) => item(list));
  777. };
  778.  
  779. const clear = () => {
  780. list.splice(0, list.length);
  781.  
  782. view.refresh();
  783.  
  784. callback.forEach((item) => item(list));
  785. };
  786.  
  787. const bindCallback = (func) => {
  788. func(list);
  789.  
  790. callback.push(func);
  791. };
  792.  
  793. return {
  794. add,
  795. clear,
  796. bindCallback,
  797. view,
  798. };
  799. })();
  800.  
  801. // 用户模块
  802. const userModule = (() => {
  803. // 获取用户列表
  804. const list = () => dataModule.getUsers();
  805.  
  806. // 获取用户
  807. const get = (uid) => {
  808. // 获取列表
  809. const users = list();
  810.  
  811. // 如果已存在,则返回信息
  812. if (users[uid]) {
  813. return users[uid];
  814. }
  815.  
  816. return null;
  817. };
  818.  
  819. // 增加用户
  820. const add = (uid, username, tags, filterMode) => {
  821. // 获取对应的用户
  822. const user = get(uid);
  823.  
  824. // 如果用户已存在,则返回用户信息,否则增加用户
  825. if (user) {
  826. return user;
  827. }
  828.  
  829. // 保存用户
  830. // TODO id 和 name 属于历史遗留问题,应该改为 uid 和 username 以便更好的理解
  831. dataModule.save({
  832. users: {
  833. [uid]: {
  834. id: uid,
  835. name: username,
  836. tags,
  837. filterMode,
  838. },
  839. },
  840. });
  841.  
  842. // 重新过滤
  843. refresh(uid);
  844.  
  845. // 返回用户信息
  846. return get(uid);
  847. };
  848.  
  849. // 编辑用户
  850. const edit = (uid, values) => {
  851. // 保存用户
  852. dataModule.save({
  853. users: {
  854. [uid]: values,
  855. },
  856. });
  857.  
  858. // 重新过滤
  859. refresh(uid);
  860. };
  861.  
  862. // 删除用户
  863. const remove = (uid) => {
  864. // TODO 这里不可避免的直接操作了原始数据
  865. delete list()[uid];
  866.  
  867. // 保存数据
  868. dataModule.save({});
  869.  
  870. // 重新过滤
  871. refresh(uid);
  872. };
  873.  
  874. // 格式化用户
  875. const format = (uid, name) => {
  876. if (uid <= 0) {
  877. return "";
  878. }
  879.  
  880. const user = get(uid);
  881.  
  882. if (user) {
  883. name = name || user.name;
  884. }
  885.  
  886. const username = name ? "@" + name : "#" + uid;
  887.  
  888. return `<a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[${username}]</a>`;
  889. };
  890.  
  891. // 重新过滤
  892. const refresh = (uid) => {
  893. reFilter((item) => {
  894. if (item.uid === uid) {
  895. return false;
  896. }
  897.  
  898. if (Object.hasOwn(item.quotes || {}, uid)) {
  899. return false;
  900. }
  901.  
  902. return true;
  903. });
  904. };
  905.  
  906. // UI
  907. const view = (() => {
  908. const details = (() => {
  909. let window;
  910.  
  911. return (uid, name, callback) => {
  912. if (window === undefined) {
  913. window = commonui.createCommmonWindow();
  914. }
  915.  
  916. const user = get(uid);
  917.  
  918. const content = document.createElement("DIV");
  919.  
  920. const size = Math.floor((screen.width * 0.8) / 200);
  921.  
  922. const items = Object.values(tagModule.list()).map((tag, index) => {
  923. const checked = user && user.tags.includes(tag.id) ? "checked" : "";
  924.  
  925. return `
  926. <td class="c1">
  927. <label for="s-tag-${index}" style="display: block; cursor: pointer;">
  928. ${tagModule.format(tag.id)}
  929. </label>
  930. </td>
  931. <td class="c2" width="1">
  932. <input id="s-tag-${index}" type="checkbox" value="${
  933. tag.id
  934. }" ${checked}/>
  935. </td>
  936. `;
  937. });
  938.  
  939. const rows = [...new Array(Math.ceil(items.length / size))].map(
  940. (_, index) => `
  941. <tr class="row${(index % 2) + 1}">
  942. ${items.slice(size * index, size * (index + 1)).join("")}
  943. </tr>
  944. `
  945. );
  946.  
  947. content.className = "w100";
  948. content.innerHTML = `
  949. <div class="filter-table-wrapper" style="width: 80vw;">
  950. <table class="filter-table forumbox">
  951. <tbody>
  952. ${rows.join("")}
  953. </tbody>
  954. </table>
  955. </div>
  956. <div style="margin: 10px 0;">
  957. <input type="text" placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
  958. </div>
  959. <div style="margin: 10px 0;">
  960. <span>过滤方式:</span>
  961. <button>${
  962. (user && user.filterMode) || filterModule.defaultMode
  963. }</button>
  964. <div class="right_">
  965. <button>删除</button>
  966. <button>保存</button>
  967. </div>
  968. </div>
  969. <div class="silver" style="margin-top: 5px;">${
  970. filterModule.tips
  971. }</div>
  972. `;
  973.  
  974. const actions = content.querySelectorAll("BUTTON");
  975.  
  976. actions[0].onclick = () => {
  977. actions[0].innerText = filterModule.switchModeByName(
  978. actions[0].innerText
  979. );
  980. };
  981.  
  982. actions[1].onclick = () => {
  983. if (confirm("是否确认?") === false) {
  984. return;
  985. }
  986.  
  987. remove(uid);
  988.  
  989. if (callback) {
  990. callback({
  991. id: null,
  992. });
  993. }
  994.  
  995. window._.hide();
  996. };
  997.  
  998. actions[2].onclick = () => {
  999. if (confirm("是否确认?") === false) {
  1000. return;
  1001. }
  1002.  
  1003. const filterMode = actions[0].innerText;
  1004.  
  1005. const checked = [...content.querySelectorAll("INPUT:checked")].map(
  1006. (input) => parseInt(input.value, 10)
  1007. );
  1008.  
  1009. const newTags = content
  1010. .querySelector("INPUT[type='text']")
  1011. .value.split("|")
  1012. .filter((item) => item.length)
  1013. .map((item) => tagModule.add(item))
  1014. .filter((tag) => tag !== null)
  1015. .map((tag) => tag.id);
  1016.  
  1017. const tags = [...new Set([...checked, ...newTags])].sort();
  1018.  
  1019. if (user) {
  1020. user.tags = tags;
  1021.  
  1022. edit(uid, {
  1023. filterMode,
  1024. });
  1025. } else {
  1026. add(uid, name, tags, filterMode);
  1027. }
  1028.  
  1029. if (callback) {
  1030. callback({
  1031. uid,
  1032. name,
  1033. tags,
  1034. filterMode,
  1035. });
  1036. }
  1037.  
  1038. window._.hide();
  1039. };
  1040.  
  1041. if (user === null) {
  1042. actions[1].style.display = "none";
  1043. }
  1044.  
  1045. window._.addContent(null);
  1046. window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
  1047. window._.addContent(content);
  1048. window._.show();
  1049. };
  1050. })();
  1051.  
  1052. const content = (() => {
  1053. const element = document.createElement("DIV");
  1054.  
  1055. element.style = "display: none";
  1056. element.innerHTML = `
  1057. <div class="filter-table-wrapper">
  1058. <table class="filter-table forumbox">
  1059. <thead>
  1060. <tr class="block_txt_c0">
  1061. <th class="c1" width="1">昵称</th>
  1062. <th class="c2">标记</th>
  1063. <th class="c3" width="1">过滤方式</th>
  1064. <th class="c4" width="1">操作</th>
  1065. </tr>
  1066. </thead>
  1067. <tbody></tbody>
  1068. </table>
  1069. </div>
  1070. `;
  1071.  
  1072. return element;
  1073. })();
  1074.  
  1075. let index = 0;
  1076. let size = 50;
  1077. let hasNext = false;
  1078.  
  1079. const box = content.querySelector("DIV");
  1080.  
  1081. const tbody = content.querySelector("TBODY");
  1082.  
  1083. const wrapper = content.querySelector(".filter-table-wrapper");
  1084.  
  1085. const load = ({ id, name, tags, filterMode }, anchor = null) => {
  1086. if (id === null) {
  1087. if (anchor) {
  1088. tbody.removeChild(anchor);
  1089. }
  1090. return;
  1091. }
  1092.  
  1093. if (anchor === null) {
  1094. anchor = document.createElement("TR");
  1095.  
  1096. anchor.className = `row${
  1097. (tbody.querySelectorAll("TR").length % 2) + 1
  1098. }`;
  1099.  
  1100. tbody.appendChild(anchor);
  1101. }
  1102.  
  1103. anchor.innerHTML = `
  1104. <td class="c1">
  1105. ${format(id, name)}
  1106. </td>
  1107. <td class="c2">
  1108. ${tags.map(tagModule.format).join("")}
  1109. </td>
  1110. <td class="c3">
  1111. <div class="filter-table-button-group">
  1112. <button>${filterMode || filterModule.defaultMode}</button>
  1113. </div>
  1114. </td>
  1115. <td class="c4">
  1116. <div class="filter-table-button-group">
  1117. <button>编辑</button>
  1118. <button>删除</button>
  1119. </div>
  1120. </td>
  1121. `;
  1122.  
  1123. const actions = anchor.querySelectorAll("BUTTON");
  1124.  
  1125. actions[0].onclick = () => {
  1126. const filterMode = filterModule.switchModeByName(
  1127. actions[0].innerHTML
  1128. );
  1129.  
  1130. actions[0].innerHTML = filterMode;
  1131.  
  1132. edit(id, { filterMode });
  1133. };
  1134.  
  1135. actions[1].onclick = () => {
  1136. details(id, name, (item) => {
  1137. load(item, anchor);
  1138. });
  1139. };
  1140.  
  1141. actions[2].onclick = () => {
  1142. if (confirm("是否确认?") === false) {
  1143. return;
  1144. }
  1145.  
  1146. tbody.removeChild(anchor);
  1147.  
  1148. remove(id);
  1149. };
  1150. };
  1151.  
  1152. const loadNext = () => {
  1153. hasNext = index + size < Object.keys(list()).length;
  1154.  
  1155. Object.values(list())
  1156. .slice(index, index + size)
  1157. .forEach((item) => load(item));
  1158.  
  1159. index += size;
  1160. };
  1161.  
  1162. box.onscroll = () => {
  1163. if (hasNext === false) {
  1164. return;
  1165. }
  1166.  
  1167. if (
  1168. box.scrollHeight - box.scrollTop - box.clientHeight <=
  1169. wrapper.clientHeight
  1170. ) {
  1171. loadNext();
  1172. }
  1173. };
  1174.  
  1175. const refresh = () => {
  1176. index = 0;
  1177.  
  1178. tbody.innerHTML = "";
  1179.  
  1180. loadNext();
  1181. };
  1182.  
  1183. return {
  1184. content,
  1185. details,
  1186. refresh,
  1187. };
  1188. })();
  1189.  
  1190. return {
  1191. list,
  1192. get,
  1193. add,
  1194. edit,
  1195. remove,
  1196. format,
  1197. refresh,
  1198. view,
  1199. };
  1200. })();
  1201.  
  1202. // 标记模块
  1203. const tagModule = (() => {
  1204. // 获取标记列表
  1205. const list = () => dataModule.getTags();
  1206.  
  1207. // 计算标记颜色
  1208. // 采用的是泥潭的颜色方案,参见 commonui.htmlName
  1209. const generateColor = (name) => {
  1210. const hash = (() => {
  1211. let h = 5381;
  1212.  
  1213. for (var i = 0; i < name.length; i++) {
  1214. h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
  1215. }
  1216.  
  1217. return h;
  1218. })();
  1219.  
  1220. const hex = Math.abs(hash).toString(16) + "000000";
  1221.  
  1222. const hsv = [
  1223. `0x${hex.substring(2, 4)}` / 255,
  1224. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  1225. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  1226. ];
  1227.  
  1228. const rgb = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  1229.  
  1230. return ["#", ...rgb].reduce((a, b) => {
  1231. return a + ("0" + b.toString(16)).slice(-2);
  1232. });
  1233. };
  1234.  
  1235. // 获取标记
  1236. const get = ({ id, name }) => {
  1237. // 获取列表
  1238. const tags = list();
  1239.  
  1240. // 通过 ID 获取标记
  1241. if (tags[id]) {
  1242. return tags[id];
  1243. }
  1244.  
  1245. // 通过名称获取标记
  1246. if (name) {
  1247. const tag = Object.values(tags).find((item) => item.name === name);
  1248.  
  1249. if (tag) {
  1250. return tag;
  1251. }
  1252. }
  1253.  
  1254. return null;
  1255. };
  1256.  
  1257. // 增加标记
  1258. const add = (name) => {
  1259. // 获取对应的标记
  1260. const tag = get({ name });
  1261.  
  1262. // 如果标记已存在,则返回标记信息,否则增加标记
  1263. if (tag) {
  1264. return tag;
  1265. }
  1266.  
  1267. // ID 为最大值 + 1
  1268. const id = Math.max(Object.keys(list()), 0) + 1;
  1269.  
  1270. // 标记的颜色
  1271. const color = generateColor(name);
  1272.  
  1273. // 保存标记
  1274. dataModule.save({
  1275. tags: {
  1276. [id]: {
  1277. id,
  1278. name,
  1279. color,
  1280. filterMode: filterModule.defaultMode,
  1281. },
  1282. },
  1283. });
  1284.  
  1285. // 返回标记信息
  1286. return get({ id });
  1287. };
  1288.  
  1289. // 编辑标记
  1290. const edit = (id, values) => {
  1291. // 保存标记
  1292. dataModule.save({
  1293. tags: {
  1294. [id]: values,
  1295. },
  1296. });
  1297.  
  1298. // 关联的用户
  1299. const users = Object.values(userModule.list())
  1300. .filter((user) => user.tags.includes(id))
  1301. .map((user) => user.id);
  1302.  
  1303. // 重新过滤
  1304. users.forEach((uid) => {
  1305. userModule.refresh(uid);
  1306. });
  1307. };
  1308.  
  1309. // 删除标记
  1310. const remove = (id) => {
  1311. // TODO 这里不可避免的直接操作了原始数据
  1312. delete list()[id];
  1313.  
  1314. // 关联的用户
  1315. const users = [];
  1316.  
  1317. // 删除用户对应的标记
  1318. Object.values(userModule.list()).forEach((user) => {
  1319. const index = user.tags.findIndex((tag) => tag === id);
  1320.  
  1321. if (index >= 0) {
  1322. user.tags.splice(index, 1);
  1323.  
  1324. users.push(user.id);
  1325. }
  1326. });
  1327.  
  1328. // 保存数据
  1329. dataModule.save({});
  1330.  
  1331. // 重新过滤
  1332. users.forEach((uid) => {
  1333. userModule.refresh(uid);
  1334. });
  1335. };
  1336.  
  1337. // 格式化标记
  1338. const format = (id, name, color) => {
  1339. if (id) {
  1340. const tag = get({ id });
  1341.  
  1342. if (tag) {
  1343. name = tag.name;
  1344. color = tag.color;
  1345. }
  1346. }
  1347.  
  1348. if (name && color) {
  1349. return `<b class="block_txt nobr" style="background: ${color}; color: #FFF; margin: 0.1em 0.2em;">${name}</b>`;
  1350. }
  1351.  
  1352. return "";
  1353. };
  1354.  
  1355. // UI
  1356. const view = (() => {
  1357. const content = (() => {
  1358. const element = document.createElement("DIV");
  1359.  
  1360. element.style = "display: none";
  1361. element.innerHTML = `
  1362. <div class="filter-table-wrapper">
  1363. <table class="filter-table forumbox">
  1364. <thead>
  1365. <tr class="block_txt_c0">
  1366. <th class="c1" width="1">标记</th>
  1367. <th class="c2">列表</th>
  1368. <th class="c3" width="1">过滤方式</th>
  1369. <th class="c4" width="1">操作</th>
  1370. </tr>
  1371. </thead>
  1372. <tbody></tbody>
  1373. </table>
  1374. </div>
  1375. `;
  1376.  
  1377. return element;
  1378. })();
  1379.  
  1380. let index = 0;
  1381. let size = 50;
  1382. let hasNext = false;
  1383.  
  1384. const box = content.querySelector("DIV");
  1385.  
  1386. const tbody = content.querySelector("TBODY");
  1387.  
  1388. const wrapper = content.querySelector(".filter-table-wrapper");
  1389.  
  1390. const load = ({ id, filterMode }, anchor = null) => {
  1391. if (id === null) {
  1392. if (anchor) {
  1393. tbody.removeChild(anchor);
  1394. }
  1395. return;
  1396. }
  1397.  
  1398. if (anchor === null) {
  1399. anchor = document.createElement("TR");
  1400.  
  1401. anchor.className = `row${
  1402. (tbody.querySelectorAll("TR").length % 2) + 1
  1403. }`;
  1404.  
  1405. tbody.appendChild(anchor);
  1406. }
  1407.  
  1408. const users = Object.values(userModule.list());
  1409.  
  1410. const filteredUsers = users.filter((user) => user.tags.includes(id));
  1411.  
  1412. anchor.innerHTML = `
  1413. <td class="c1">
  1414. ${format(id)}
  1415. </td>
  1416. <td class="c2">
  1417. <button>${filteredUsers.length}</button>
  1418. <div style="white-space: normal; display: none;">
  1419. ${filteredUsers
  1420. .map((user) => userModule.format(user.id))
  1421. .join("")}
  1422. </div>
  1423. </td>
  1424. <td class="c3">
  1425. <div class="filter-table-button-group">
  1426. <button>${filterMode || filterModule.defaultMode}</button>
  1427. </div>
  1428. </td>
  1429. <td class="c4">
  1430. <div class="filter-table-button-group">
  1431. <button>删除</button>
  1432. </div>
  1433. </td>
  1434. `;
  1435.  
  1436. const actions = anchor.querySelectorAll("BUTTON");
  1437.  
  1438. actions[0].onclick = (() => {
  1439. let hide = true;
  1440.  
  1441. return () => {
  1442. hide = !hide;
  1443.  
  1444. actions[0].nextElementSibling.style.display = hide
  1445. ? "none"
  1446. : "block";
  1447. };
  1448. })();
  1449.  
  1450. actions[1].onclick = () => {
  1451. const filterMode = filterModule.switchModeByName(
  1452. actions[1].innerHTML
  1453. );
  1454.  
  1455. actions[1].innerHTML = filterMode;
  1456.  
  1457. edit(id, { filterMode });
  1458. };
  1459.  
  1460. actions[2].onclick = () => {
  1461. if (confirm("是否确认?") === false) {
  1462. return;
  1463. }
  1464.  
  1465. tbody.removeChild(anchor);
  1466.  
  1467. remove(id);
  1468. };
  1469. };
  1470.  
  1471. const loadNext = () => {
  1472. hasNext = index + size < Object.keys(list()).length;
  1473.  
  1474. Object.values(list())
  1475. .slice(index, index + size)
  1476. .forEach((item) => load(item));
  1477.  
  1478. index += size;
  1479. };
  1480.  
  1481. box.onscroll = () => {
  1482. if (hasNext === false) {
  1483. return;
  1484. }
  1485.  
  1486. if (
  1487. box.scrollHeight - box.scrollTop - box.clientHeight <=
  1488. wrapper.clientHeight
  1489. ) {
  1490. loadNext();
  1491. }
  1492. };
  1493.  
  1494. const refresh = () => {
  1495. index = 0;
  1496.  
  1497. tbody.innerHTML = "";
  1498.  
  1499. loadNext();
  1500. };
  1501.  
  1502. return {
  1503. content,
  1504. refresh,
  1505. };
  1506. })();
  1507.  
  1508. return {
  1509. list,
  1510. get,
  1511. add,
  1512. edit,
  1513. remove,
  1514. format,
  1515. generateColor,
  1516. view,
  1517. };
  1518. })();
  1519.  
  1520. // 关键字模块
  1521. const keywordModule = (() => {
  1522. // 获取关键字列表
  1523. const list = () => dataModule.getKeywords();
  1524.  
  1525. // 获取关键字
  1526. const get = (id) => {
  1527. // 获取列表
  1528. const keywords = list();
  1529.  
  1530. // 如果已存在,则返回信息
  1531. if (keywords[id]) {
  1532. return keywords[id];
  1533. }
  1534.  
  1535. return null;
  1536. };
  1537.  
  1538. // 增加关键字
  1539. // filterLevel: 0 - 仅过滤标题; 1 - 过滤标题和内容
  1540. // 无需判重
  1541. const add = (keyword, filterMode, filterLevel) => {
  1542. // ID 为最大值 + 1
  1543. const id = Math.max(Object.keys(list()), 0) + 1;
  1544.  
  1545. // 保存关键字
  1546. dataModule.save({
  1547. keywords: {
  1548. [id]: {
  1549. id,
  1550. keyword,
  1551. filterMode,
  1552. filterLevel,
  1553. },
  1554. },
  1555. });
  1556.  
  1557. // 重新过滤所有数据
  1558. reFilter();
  1559.  
  1560. // 返回关键字信息
  1561. return get(id);
  1562. };
  1563.  
  1564. // 编辑关键字
  1565. const edit = (id, values) => {
  1566. // 保存关键字
  1567. dataModule.save({
  1568. keywords: {
  1569. [id]: values,
  1570. },
  1571. });
  1572.  
  1573. // 重新过滤所有数据
  1574. reFilter();
  1575. };
  1576.  
  1577. // 删除关键字
  1578. const remove = (id) => {
  1579. // TODO 这里不可避免的直接操作了原始数据
  1580. delete list()[id];
  1581.  
  1582. // 保存数据
  1583. dataModule.save({});
  1584.  
  1585. // 重新过滤相关数据
  1586. reFilter((item) => item.reason.indexOf("关键字") !== 0);
  1587. };
  1588.  
  1589. // UI
  1590. const view = (() => {
  1591. const content = (() => {
  1592. const element = document.createElement("DIV");
  1593.  
  1594. element.style = "display: none";
  1595. element.innerHTML = `
  1596. <div class="filter-table-wrapper">
  1597. <table class="filter-table forumbox">
  1598. <thead>
  1599. <tr class="block_txt_c0">
  1600. <th class="c1">列表</th>
  1601. <th class="c2" width="1">过滤方式</th>
  1602. <th class="c3" width="1">包括内容</th>
  1603. <th class="c4" width="1">操作</th>
  1604. </tr>
  1605. </thead>
  1606. <tbody></tbody>
  1607. </table>
  1608. </div>
  1609. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
  1610. `;
  1611.  
  1612. return element;
  1613. })();
  1614.  
  1615. let index = 0;
  1616. let size = 50;
  1617. let hasNext = false;
  1618.  
  1619. const box = content.querySelector("DIV");
  1620.  
  1621. const tbody = content.querySelector("TBODY");
  1622.  
  1623. const wrapper = content.querySelector(".filter-table-wrapper");
  1624.  
  1625. const load = (
  1626. { id, keyword, filterMode, filterLevel },
  1627. anchor = null
  1628. ) => {
  1629. if (id === null) {
  1630. if (anchor) {
  1631. tbody.removeChild(anchor);
  1632. }
  1633. return;
  1634. }
  1635.  
  1636. if (anchor === null) {
  1637. anchor = document.createElement("TR");
  1638.  
  1639. anchor.className = `row${
  1640. (tbody.querySelectorAll("TR").length % 2) + 1
  1641. }`;
  1642.  
  1643. tbody.appendChild(anchor);
  1644. }
  1645.  
  1646. const checked = filterLevel ? "checked" : "";
  1647.  
  1648. anchor.innerHTML = `
  1649. <td class="c1">
  1650. <div class="filter-input-wrapper">
  1651. <input type="text" value="${keyword || ""}" />
  1652. </div>
  1653. </td>
  1654. <td class="c2">
  1655. <div class="filter-table-button-group">
  1656. <button>${filterMode || filterModule.defaultMode}</button>
  1657. </div>
  1658. </td>
  1659. <td class="c3">
  1660. <div style="text-align: center;">
  1661. <input type="checkbox" ${checked} />
  1662. </div>
  1663. </td>
  1664. <td class="c4">
  1665. <div class="filter-table-button-group">
  1666. <button>保存</button>
  1667. <button>删除</button>
  1668. </div>
  1669. </td>
  1670. `;
  1671.  
  1672. const actions = anchor.querySelectorAll("BUTTON");
  1673.  
  1674. actions[0].onclick = () => {
  1675. actions[0].innerHTML = filterModule.switchModeByName(
  1676. actions[0].innerHTML
  1677. );
  1678. };
  1679.  
  1680. actions[1].onclick = () => {
  1681. const keyword = anchor.querySelector("INPUT[type='text']").value;
  1682.  
  1683. const filterMode = actions[0].innerHTML;
  1684.  
  1685. const filterLevel = anchor.querySelector(
  1686. `INPUT[type="checkbox"]:checked`
  1687. )
  1688. ? 1
  1689. : 0;
  1690.  
  1691. if (keyword) {
  1692. edit(id, {
  1693. keyword,
  1694. filterMode,
  1695. filterLevel,
  1696. });
  1697. }
  1698. };
  1699.  
  1700. actions[2].onclick = () => {
  1701. if (confirm("是否确认?") === false) {
  1702. return;
  1703. }
  1704.  
  1705. tbody.removeChild(anchor);
  1706.  
  1707. remove(id);
  1708. };
  1709. };
  1710.  
  1711. const loadNext = () => {
  1712. hasNext = index + size < Object.keys(list()).length;
  1713.  
  1714. Object.values(list())
  1715. .slice(index, index + size)
  1716. .forEach((item) => load(item));
  1717.  
  1718. if (hasNext === false) {
  1719. const loadNew = () => {
  1720. const row = document.createElement("TR");
  1721.  
  1722. row.className = `row${
  1723. (tbody.querySelectorAll("TR").length % 2) + 1
  1724. }`;
  1725.  
  1726. row.innerHTML = `
  1727. <td class="c1">
  1728. <div class="filter-input-wrapper">
  1729. <input type="text" value="" />
  1730. </div>
  1731. </td>
  1732. <td class="c2">
  1733. <div class="filter-table-button-group">
  1734. <button>${filterModule.defaultMode}</button>
  1735. </div>
  1736. </td>
  1737. <td class="c3">
  1738. <div style="text-align: center;">
  1739. <input type="checkbox" />
  1740. </div>
  1741. </td>
  1742. <td class="c4">
  1743. <div class="filter-table-button-group">
  1744. <button>添加</button>
  1745. </div>
  1746. </td>
  1747. `;
  1748.  
  1749. const actions = row.querySelectorAll("BUTTON");
  1750.  
  1751. actions[0].onclick = () => {
  1752. const filterMode = filterModule.switchModeByName(
  1753. actions[0].innerHTML
  1754. );
  1755.  
  1756. actions[0].innerHTML = filterMode;
  1757. };
  1758.  
  1759. actions[1].onclick = () => {
  1760. const keyword = row.querySelector("INPUT[type='text']").value;
  1761.  
  1762. const filterMode = actions[0].innerHTML;
  1763.  
  1764. const filterLevel = row.querySelector(
  1765. `INPUT[type="checkbox"]:checked`
  1766. )
  1767. ? 1
  1768. : 0;
  1769.  
  1770. if (keyword) {
  1771. const item = add(keyword, filterMode, filterLevel);
  1772.  
  1773. load(item, row);
  1774. loadNew();
  1775. }
  1776. };
  1777.  
  1778. tbody.appendChild(row);
  1779. };
  1780.  
  1781. loadNew();
  1782. }
  1783.  
  1784. index += size;
  1785. };
  1786.  
  1787. box.onscroll = () => {
  1788. if (hasNext === false) {
  1789. return;
  1790. }
  1791.  
  1792. if (
  1793. box.scrollHeight - box.scrollTop - box.clientHeight <=
  1794. wrapper.clientHeight
  1795. ) {
  1796. loadNext();
  1797. }
  1798. };
  1799.  
  1800. const refresh = () => {
  1801. index = 0;
  1802.  
  1803. tbody.innerHTML = "";
  1804.  
  1805. loadNext();
  1806. };
  1807.  
  1808. return {
  1809. content,
  1810. refresh,
  1811. };
  1812. })();
  1813.  
  1814. return {
  1815. list,
  1816. get,
  1817. add,
  1818. edit,
  1819. remove,
  1820. view,
  1821. };
  1822. })();
  1823.  
  1824. // 属地模块
  1825. const locationModule = (() => {
  1826. // 获取属地列表
  1827. const list = () => dataModule.getLocations();
  1828.  
  1829. // 获取属地
  1830. const get = (id) => {
  1831. // 获取列表
  1832. const locations = list();
  1833.  
  1834. // 如果已存在,则返回信息
  1835. if (locations[id]) {
  1836. return locations[id];
  1837. }
  1838.  
  1839. return null;
  1840. };
  1841.  
  1842. // 增加属地
  1843. // 无需判重
  1844. const add = (keyword, filterMode) => {
  1845. // ID 为最大值 + 1
  1846. const id = Math.max(Object.keys(list()), 0) + 1;
  1847.  
  1848. // 保存属地
  1849. dataModule.save({
  1850. locations: {
  1851. [id]: {
  1852. id,
  1853. keyword,
  1854. filterMode,
  1855. },
  1856. },
  1857. });
  1858.  
  1859. // 重新过滤所有数据
  1860. reFilter();
  1861.  
  1862. // 返回属地信息
  1863. return get(id);
  1864. };
  1865.  
  1866. // 编辑属地
  1867. const edit = (id, values) => {
  1868. // 保存属地
  1869. dataModule.save({
  1870. locations: {
  1871. [id]: values,
  1872. },
  1873. });
  1874.  
  1875. // 重新过滤所有数据
  1876. reFilter();
  1877. };
  1878.  
  1879. // 删除属地
  1880. const remove = (id) => {
  1881. // TODO 这里不可避免的直接操作了原始数据
  1882. delete list()[id];
  1883.  
  1884. // 保存数据
  1885. dataModule.save({});
  1886.  
  1887. // 重新过滤相关数据
  1888. reFilter((item) => item.reason.indexOf("属地") !== 0);
  1889. };
  1890.  
  1891. // UI
  1892. const view = (() => {
  1893. const content = (() => {
  1894. const element = document.createElement("DIV");
  1895.  
  1896. element.style = "display: none";
  1897. element.innerHTML = `
  1898. <div class="filter-table-wrapper">
  1899. <table class="filter-table forumbox">
  1900. <thead>
  1901. <tr class="block_txt_c0">
  1902. <th class="c1">列表</th>
  1903. <th class="c2" width="1">过滤方式</th>
  1904. <th class="c3" width="1">操作</th>
  1905. </tr>
  1906. </thead>
  1907. <tbody></tbody>
  1908. </table>
  1909. </div>
  1910. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
  1911. `;
  1912.  
  1913. return element;
  1914. })();
  1915.  
  1916. let index = 0;
  1917. let size = 50;
  1918. let hasNext = false;
  1919.  
  1920. const box = content.querySelector("DIV");
  1921.  
  1922. const tbody = content.querySelector("TBODY");
  1923.  
  1924. const wrapper = content.querySelector(".filter-table-wrapper");
  1925.  
  1926. const load = ({ id, keyword, filterMode }, anchor = null) => {
  1927. if (id === null) {
  1928. if (anchor) {
  1929. tbody.removeChild(anchor);
  1930. }
  1931. return;
  1932. }
  1933.  
  1934. if (anchor === null) {
  1935. anchor = document.createElement("TR");
  1936.  
  1937. anchor.className = `row${
  1938. (tbody.querySelectorAll("TR").length % 2) + 1
  1939. }`;
  1940.  
  1941. tbody.appendChild(anchor);
  1942. }
  1943.  
  1944. anchor.innerHTML = `
  1945. <td class="c1">
  1946. <div class="filter-input-wrapper">
  1947. <input type="text" value="${keyword || ""}" />
  1948. </div>
  1949. </td>
  1950. <td class="c2">
  1951. <div class="filter-table-button-group">
  1952. <button>${filterMode || filterModule.defaultMode}</button>
  1953. </div>
  1954. </td>
  1955. <td class="c3">
  1956. <div class="filter-table-button-group">
  1957. <button>保存</button>
  1958. <button>删除</button>
  1959. </div>
  1960. </td>
  1961. `;
  1962.  
  1963. const actions = anchor.querySelectorAll("BUTTON");
  1964.  
  1965. actions[0].onclick = () => {
  1966. actions[0].innerHTML = filterModule.switchModeByName(
  1967. actions[0].innerHTML
  1968. );
  1969. };
  1970.  
  1971. actions[1].onclick = () => {
  1972. const keyword = anchor.querySelector("INPUT[type='text']").value;
  1973.  
  1974. const filterMode = actions[0].innerHTML;
  1975.  
  1976. if (keyword) {
  1977. edit(id, {
  1978. keyword,
  1979. filterMode,
  1980. });
  1981. }
  1982. };
  1983.  
  1984. actions[2].onclick = () => {
  1985. if (confirm("是否确认?") === false) {
  1986. return;
  1987. }
  1988.  
  1989. tbody.removeChild(anchor);
  1990.  
  1991. remove(id);
  1992. };
  1993. };
  1994.  
  1995. const loadNext = () => {
  1996. hasNext = index + size < Object.keys(list()).length;
  1997.  
  1998. Object.values(list())
  1999. .slice(index, index + size)
  2000. .forEach((item) => load(item));
  2001.  
  2002. if (hasNext === false) {
  2003. const loadNew = () => {
  2004. const row = document.createElement("TR");
  2005.  
  2006. row.className = `row${
  2007. (tbody.querySelectorAll("TR").length % 2) + 1
  2008. }`;
  2009.  
  2010. row.innerHTML = `
  2011. <td class="c1">
  2012. <div class="filter-input-wrapper">
  2013. <input type="text" value="" />
  2014. </div>
  2015. </td>
  2016. <td class="c2">
  2017. <div class="filter-table-button-group">
  2018. <button>${filterModule.defaultMode}</button>
  2019. </div>
  2020. </td>
  2021. <td class="c3">
  2022. <div class="filter-table-button-group">
  2023. <button>添加</button>
  2024. </div>
  2025. </td>
  2026. `;
  2027.  
  2028. const actions = row.querySelectorAll("BUTTON");
  2029.  
  2030. actions[0].onclick = () => {
  2031. const filterMode = filterModule.switchModeByName(
  2032. actions[0].innerHTML
  2033. );
  2034.  
  2035. actions[0].innerHTML = filterMode;
  2036. };
  2037.  
  2038. actions[1].onclick = () => {
  2039. const keyword = row.querySelector("INPUT[type='text']").value;
  2040.  
  2041. const filterMode = actions[0].innerHTML;
  2042.  
  2043. if (keyword) {
  2044. const item = add(keyword, filterMode);
  2045.  
  2046. load(item, row);
  2047. loadNew();
  2048. }
  2049. };
  2050.  
  2051. tbody.appendChild(row);
  2052. };
  2053.  
  2054. loadNew();
  2055. }
  2056.  
  2057. index += size;
  2058. };
  2059.  
  2060. box.onscroll = () => {
  2061. if (hasNext === false) {
  2062. return;
  2063. }
  2064.  
  2065. if (
  2066. box.scrollHeight - box.scrollTop - box.clientHeight <=
  2067. wrapper.clientHeight
  2068. ) {
  2069. loadNext();
  2070. }
  2071. };
  2072.  
  2073. const refresh = () => {
  2074. index = 0;
  2075.  
  2076. tbody.innerHTML = "";
  2077.  
  2078. loadNext();
  2079. };
  2080.  
  2081. return {
  2082. content,
  2083. refresh,
  2084. };
  2085. })();
  2086.  
  2087. return {
  2088. list,
  2089. get,
  2090. add,
  2091. edit,
  2092. remove,
  2093. view,
  2094. };
  2095. })();
  2096.  
  2097. // 猎巫模块
  2098. const witchHuntModule = (() => {
  2099. const key = "WITCH_HUNT";
  2100.  
  2101. const queue = [];
  2102.  
  2103. const cache = {};
  2104.  
  2105. // 获取设置列表
  2106. const list = () => GM_getValue(key) || {};
  2107.  
  2108. // 获取单条设置
  2109. const get = (fid) => {
  2110. // 获取列表
  2111. const settings = list();
  2112.  
  2113. // 如果已存在,则返回信息
  2114. if (settings[fid]) {
  2115. return settings[fid];
  2116. }
  2117.  
  2118. return null;
  2119. };
  2120.  
  2121. // 增加设置
  2122. // filterLevel: 0 - 仅标记; 1 - 标记并过滤
  2123. const add = async (fid, label, filterMode, filterLevel) => {
  2124. // FID 只能是数字
  2125. fid = parseInt(fid, 10);
  2126.  
  2127. // 获取列表
  2128. const settings = list();
  2129.  
  2130. // 如果版面 ID 已存在,则提示错误
  2131. if (Object.keys(settings).includes(fid)) {
  2132. alert("已有相同版面ID");
  2133. return;
  2134. }
  2135.  
  2136. // 请求版面信息
  2137. const info = await fetchModule.getForumInfo(fid);
  2138.  
  2139. // 如果版面不存在,则提示错误
  2140. if (info === null) {
  2141. alert("版面ID有误");
  2142. return;
  2143. }
  2144.  
  2145. // 计算标记颜色
  2146. const color = tagModule.generateColor(info.name);
  2147.  
  2148. // 保存设置
  2149. settings[fid] = {
  2150. fid,
  2151. name: info.name,
  2152. label,
  2153. color,
  2154. filterMode,
  2155. filterLevel,
  2156. };
  2157.  
  2158. GM_setValue(key, settings);
  2159.  
  2160. // 增加后需要清除缓存
  2161. Object.keys(cache).forEach((key) => {
  2162. delete cache[key];
  2163. });
  2164.  
  2165. // 重新猎巫
  2166. reFilter((item) => {
  2167. run(item);
  2168.  
  2169. return true;
  2170. });
  2171.  
  2172. // 返回设置信息
  2173. return settings[fid];
  2174. };
  2175.  
  2176. // 编辑设置
  2177. const edit = (fid, values) => {
  2178. // 获取列表
  2179. const settings = list();
  2180.  
  2181. // 没有则跳过
  2182. if (settings[fid] === undefined) {
  2183. return;
  2184. }
  2185.  
  2186. // 保存设置
  2187. settings[fid] = {
  2188. ...settings[fid],
  2189. ...values,
  2190. };
  2191.  
  2192. GM_setValue(key, settings);
  2193.  
  2194. // 重新加载缓存,更新样式即可
  2195. reFilter((item) => {
  2196. item.witchHunt = null;
  2197.  
  2198. run(item);
  2199.  
  2200. return true;
  2201. });
  2202. };
  2203.  
  2204. // 删除设置
  2205. const remove = (fid) => {
  2206. // 获取列表
  2207. const settings = list();
  2208.  
  2209. // 没有则跳过
  2210. if (settings[fid] === undefined) {
  2211. return;
  2212. }
  2213.  
  2214. // 保存设置
  2215. delete settings[fid];
  2216.  
  2217. GM_setValue(key, settings);
  2218.  
  2219. // 删除后需要清除缓存
  2220. Object.keys(cache).forEach((key) => {
  2221. delete cache[key];
  2222. });
  2223.  
  2224. // 重新猎巫
  2225. reFilter((item) => {
  2226. run(item);
  2227.  
  2228. return true;
  2229. });
  2230. };
  2231.  
  2232. // 格式化版面
  2233. const format = (fid, name) => {
  2234. return `<a href="/thread.php?fid=${fid}" class="b nobr">[${name}]</a>`;
  2235. };
  2236.  
  2237. // 猎巫
  2238. const run = (item) => {
  2239. item.witchHunt = item.witchHunt || [];
  2240.  
  2241. // 重新过滤
  2242. const reload = (newValue) => {
  2243. const isEqual = newValue.sort().join() === item.witchHunt.sort().join();
  2244.  
  2245. if (isEqual) {
  2246. return;
  2247. }
  2248.  
  2249. item.witchHunt = newValue;
  2250. item.execute();
  2251. };
  2252.  
  2253. // 获取列表
  2254. const settings = Object.keys(list());
  2255.  
  2256. // 没有设置且没有旧数据,直接跳过
  2257. if (settings.length === 0 && item.witchHunt.length === 0) {
  2258. return;
  2259. }
  2260.  
  2261. // 猎巫任务
  2262. const task = async () => {
  2263. // 请求版面发言记录
  2264. const result = cache[item.uid]
  2265. ? cache[item.uid]
  2266. : (
  2267. await Promise.all(
  2268. settings.map(async (fid) => {
  2269. // 当前版面发言记录
  2270. const result = await fetchModule.getForumPosted(
  2271. fid,
  2272. item.uid
  2273. );
  2274.  
  2275. // 写入当前设置
  2276. if (result) {
  2277. return parseInt(fid, 10);
  2278. }
  2279.  
  2280. return null;
  2281. })
  2282. )
  2283. ).filter((i) => i !== null);
  2284.  
  2285. // 写入缓存,同一个页面多次请求没意义
  2286. cache[item.uid] = result;
  2287.  
  2288. // 执行完毕,如果结果有变,重新过滤
  2289. reload(result);
  2290.  
  2291. // 将当前任务移出队列
  2292. queue.shift();
  2293.  
  2294. // 如果还有任务,继续执行
  2295. if (queue.length > 0) {
  2296. queue[0]();
  2297. }
  2298. };
  2299.  
  2300. // 队列里已经有任务
  2301. const isRunning = queue.length > 0;
  2302.  
  2303. // 加入队列
  2304. queue.push(task);
  2305.  
  2306. // 如果没有正在执行的任务,则立即执行
  2307. if (isRunning === false) {
  2308. task();
  2309. }
  2310. };
  2311.  
  2312. // UI
  2313. const view = (() => {
  2314. const content = (() => {
  2315. const element = document.createElement("DIV");
  2316.  
  2317. element.style = "display: none";
  2318. element.innerHTML = `
  2319. <div class="filter-table-wrapper">
  2320. <table class="filter-table forumbox">
  2321. <thead>
  2322. <tr class="block_txt_c0">
  2323. <th class="c1" width="1">版面</th>
  2324. <th class="c2">标签</th>
  2325. <th class="c3" width="1">启用过滤</th>
  2326. <th class="c4" width="1">过滤方式</th>
  2327. <th class="c5" width="1">操作</th>
  2328. </tr>
  2329. </thead>
  2330. <tbody></tbody>
  2331. </table>
  2332. </div>
  2333. <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启</div>
  2334. `;
  2335.  
  2336. return element;
  2337. })();
  2338.  
  2339. let index = 0;
  2340. let size = 50;
  2341. let hasNext = false;
  2342.  
  2343. const box = content.querySelector("DIV");
  2344.  
  2345. const tbody = content.querySelector("TBODY");
  2346.  
  2347. const wrapper = content.querySelector(".filter-table-wrapper");
  2348.  
  2349. const load = (
  2350. { fid, name, label, color, filterMode, filterLevel },
  2351. anchor = null
  2352. ) => {
  2353. if (fid === null) {
  2354. if (anchor) {
  2355. tbody.removeChild(anchor);
  2356. }
  2357. return;
  2358. }
  2359.  
  2360. if (anchor === null) {
  2361. anchor = document.createElement("TR");
  2362.  
  2363. anchor.className = `row${
  2364. (tbody.querySelectorAll("TR").length % 2) + 1
  2365. }`;
  2366.  
  2367. tbody.appendChild(anchor);
  2368. }
  2369.  
  2370. const checked = filterLevel ? "checked" : "";
  2371.  
  2372. anchor.innerHTML = `
  2373. <td class="c1">
  2374. ${format(fid, name)}
  2375. </td>
  2376. <td class="c2">
  2377. ${tagModule.format(null, label, color)}
  2378. </td>
  2379. <td class="c3">
  2380. <div style="text-align: center;">
  2381. <input type="checkbox" ${checked} />
  2382. </div>
  2383. </td>
  2384. <td class="c4">
  2385. <div class="filter-table-button-group">
  2386. <button>${filterMode || filterModule.defaultMode}</button>
  2387. </div>
  2388. </td>
  2389. <td class="c5">
  2390. <div class="filter-table-button-group">
  2391. <button>保存</button>
  2392. <button>删除</button>
  2393. </div>
  2394. </td>
  2395. `;
  2396.  
  2397. const actions = anchor.querySelectorAll("BUTTON");
  2398.  
  2399. actions[0].onclick = () => {
  2400. actions[0].innerHTML = filterModule.switchModeByName(
  2401. actions[0].innerHTML
  2402. );
  2403. };
  2404.  
  2405. actions[1].onclick = () => {
  2406. const filterMode = actions[0].innerHTML;
  2407.  
  2408. const filterLevel = anchor.querySelector(
  2409. `INPUT[type="checkbox"]:checked`
  2410. )
  2411. ? 1
  2412. : 0;
  2413.  
  2414. edit(fid, {
  2415. filterMode,
  2416. filterLevel,
  2417. });
  2418. };
  2419.  
  2420. actions[2].onclick = () => {
  2421. if (confirm("是否确认?") === false) {
  2422. return;
  2423. }
  2424.  
  2425. tbody.removeChild(anchor);
  2426.  
  2427. remove(fid);
  2428. };
  2429. };
  2430.  
  2431. const loadNext = () => {
  2432. hasNext = index + size < Object.keys(list()).length;
  2433.  
  2434. Object.values(list())
  2435. .slice(index, index + size)
  2436. .forEach((item) => load(item));
  2437.  
  2438. if (hasNext === false) {
  2439. const loadNew = () => {
  2440. const row = document.createElement("TR");
  2441.  
  2442. row.className = `row${
  2443. (tbody.querySelectorAll("TR").length % 2) + 1
  2444. }`;
  2445.  
  2446. row.innerHTML = `
  2447. <td class="c1" style="min-width: 200px;">
  2448. <div class="filter-input-wrapper">
  2449. <input type="text" value="" placeholder="版面ID" />
  2450. </div>
  2451. </td>
  2452. <td class="c2">
  2453. <div class="filter-input-wrapper">
  2454. <input type="text" value="" placeholder="标签" />
  2455. </div>
  2456. </td>
  2457. <td class="c3">
  2458. <div style="text-align: center;">
  2459. <input type="checkbox" />
  2460. </div>
  2461. </td>
  2462. <td class="c4">
  2463. <div class="filter-table-button-group">
  2464. <button>${filterModule.defaultMode}</button>
  2465. </div>
  2466. </td>
  2467. <td class="c5">
  2468. <div class="filter-table-button-group">
  2469. <button>添加</button>
  2470. </div>
  2471. </td>
  2472. `;
  2473.  
  2474. const actions = row.querySelectorAll("BUTTON");
  2475.  
  2476. actions[0].onclick = () => {
  2477. const filterMode = filterModule.switchModeByName(
  2478. actions[0].innerHTML
  2479. );
  2480.  
  2481. actions[0].innerHTML = filterMode;
  2482. };
  2483.  
  2484. actions[1].onclick = async () => {
  2485. const inputs = row.querySelectorAll("INPUT[type='text']");
  2486.  
  2487. const fid = inputs[0].value;
  2488. const label = inputs[1].value;
  2489.  
  2490. const filterMode = actions[0].innerHTML;
  2491.  
  2492. const filterLevel = row.querySelector(
  2493. `INPUT[type="checkbox"]:checked`
  2494. )
  2495. ? 1
  2496. : 0;
  2497.  
  2498. if (fid && label) {
  2499. const item = await add(fid, label, filterMode, filterLevel);
  2500.  
  2501. if (item) {
  2502. load(item, row);
  2503. loadNew();
  2504. }
  2505. }
  2506. };
  2507.  
  2508. tbody.appendChild(row);
  2509. };
  2510.  
  2511. loadNew();
  2512. }
  2513.  
  2514. index += size;
  2515. };
  2516.  
  2517. box.onscroll = () => {
  2518. if (hasNext === false) {
  2519. return;
  2520. }
  2521.  
  2522. if (
  2523. box.scrollHeight - box.scrollTop - box.clientHeight <=
  2524. wrapper.clientHeight
  2525. ) {
  2526. loadNext();
  2527. }
  2528. };
  2529.  
  2530. const refresh = () => {
  2531. index = 0;
  2532.  
  2533. tbody.innerHTML = "";
  2534.  
  2535. loadNext();
  2536. };
  2537.  
  2538. return {
  2539. content,
  2540. refresh,
  2541. };
  2542. })();
  2543.  
  2544. return {
  2545. list,
  2546. get,
  2547. add,
  2548. edit,
  2549. remove,
  2550. run,
  2551. view,
  2552. };
  2553. })();
  2554.  
  2555. // 通用设置
  2556. const commonModule = (() => {
  2557. // UI
  2558. const view = (() => {
  2559. const content = (() => {
  2560. const element = document.createElement("DIV");
  2561.  
  2562. element.style = "display: none";
  2563.  
  2564. return element;
  2565. })();
  2566.  
  2567. const refresh = () => {
  2568. content.innerHTML = "";
  2569.  
  2570. // 前置过滤
  2571. (() => {
  2572. const checked = preFilter ? "checked" : "";
  2573.  
  2574. const element = document.createElement("DIV");
  2575.  
  2576. element.innerHTML += `
  2577. <div>
  2578. <label>
  2579. 前置过滤
  2580. <input type="checkbox" ${checked} />
  2581. </label>
  2582. </div>
  2583. `;
  2584.  
  2585. const checkbox = element.querySelector("INPUT");
  2586.  
  2587. checkbox.onchange = () => {
  2588. const newValue = checkbox.checked;
  2589.  
  2590. GM_setValue(PRE_FILTER_KEY, newValue);
  2591.  
  2592. location.reload();
  2593. };
  2594.  
  2595. content.appendChild(element);
  2596. })();
  2597.  
  2598. // 默认过滤方式
  2599. (() => {
  2600. const element = document.createElement("DIV");
  2601.  
  2602. element.innerHTML += `
  2603. <br/>
  2604. <div>默认过滤方式</div>
  2605. <div></div>
  2606. <div class="silver" style="margin-top: 10px;">${filterModule.tips}</div>
  2607. `;
  2608.  
  2609. ["标记", "遮罩", "隐藏"].forEach((item, index) => {
  2610. const span = document.createElement("SPAN");
  2611.  
  2612. const checked =
  2613. dataModule.getDefaultFilterMode() === item ? "checked" : "";
  2614.  
  2615. span.innerHTML += `
  2616. <input id="s-fm-${index}" type="radio" name="filterType" ${checked}>
  2617. <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
  2618. `;
  2619.  
  2620. const input = span.querySelector("INPUT");
  2621.  
  2622. input.onchange = () => {
  2623. if (input.checked) {
  2624. dataModule.setDefaultFilterMode(item);
  2625.  
  2626. reFilter((item) => item.filterMode !== "继承");
  2627. }
  2628. };
  2629.  
  2630. element.querySelectorAll("div")[1].append(span);
  2631. });
  2632.  
  2633. content.appendChild(element);
  2634. })();
  2635.  
  2636. // 小号过滤(时间)
  2637. (() => {
  2638. const value = dataModule.getFilterRegdateLimit() / 86400000;
  2639.  
  2640. const element = document.createElement("DIV");
  2641.  
  2642. element.innerHTML += `
  2643. <br/>
  2644. <div>
  2645. 隐藏注册时间小于<input value="${value}" maxLength="4" style="width: 48px;" />天的用户
  2646. <button>确认</button>
  2647. </div>
  2648. `;
  2649.  
  2650. const action = element.querySelector("BUTTON");
  2651.  
  2652. action.onclick = () => {
  2653. const newValue =
  2654. parseInt(element.querySelector("INPUT").value, 10) || 0;
  2655.  
  2656. dataModule.setFilterRegdateLimit(
  2657. newValue < 0 ? 0 : newValue * 86400000
  2658. );
  2659.  
  2660. reFilter((item) => item.filterMode === "显示");
  2661. };
  2662.  
  2663. content.appendChild(element);
  2664. })();
  2665.  
  2666. // 小号过滤(发帖数)
  2667. (() => {
  2668. const value = dataModule.getFilterPostnumLimit();
  2669.  
  2670. const element = document.createElement("DIV");
  2671.  
  2672. element.innerHTML += `
  2673. <br/>
  2674. <div>
  2675. 隐藏发帖数量小于<input value="${value}" maxLength="5" style="width: 48px;" />贴的用户
  2676. <button>确认</button>
  2677. </div>
  2678. `;
  2679.  
  2680. const action = element.querySelector("BUTTON");
  2681.  
  2682. action.onclick = () => {
  2683. const newValue =
  2684. parseInt(element.querySelector("INPUT").value, 10) || 0;
  2685.  
  2686. dataModule.setFilterPostnumLimit(newValue < 0 ? 0 : newValue);
  2687.  
  2688. reFilter((item) => item.filterMode === "显示");
  2689. };
  2690.  
  2691. content.appendChild(element);
  2692. })();
  2693.  
  2694. // 流量号过滤(主题比例)
  2695. (() => {
  2696. const value = dataModule.getFilterTopicRateLimit();
  2697.  
  2698. const element = document.createElement("DIV");
  2699.  
  2700. element.innerHTML += `
  2701. <br/>
  2702. <div>
  2703. 隐藏发帖比例大于<input value="${value}" maxLength="3" style="width: 48px;" />%的用户
  2704. <button>确认</button>
  2705. </div>
  2706. `;
  2707.  
  2708. const action = element.querySelector("BUTTON");
  2709.  
  2710. action.onclick = () => {
  2711. const newValue =
  2712. parseInt(element.querySelector("INPUT").value, 10) || 100;
  2713.  
  2714. if (newValue <= 0 || newValue > 100) {
  2715. return;
  2716. }
  2717.  
  2718. dataModule.setFilterTopicRateLimit(newValue);
  2719.  
  2720. reFilter((item) => item.filterMode === "显示");
  2721. };
  2722.  
  2723. content.appendChild(element);
  2724. })();
  2725.  
  2726. // 声望过滤
  2727. (() => {
  2728. const value = dataModule.getFilterReputationLimit() || "";
  2729.  
  2730. const element = document.createElement("DIV");
  2731.  
  2732. element.innerHTML += `
  2733. <br/>
  2734. <div>
  2735. 隐藏版面声望低于<input value="${value}" maxLength="5" style="width: 48px;" />点的用户
  2736. <button>确认</button>
  2737. </div>
  2738. `;
  2739.  
  2740. const action = element.querySelector("BUTTON");
  2741.  
  2742. action.onclick = () => {
  2743. const newValue = parseInt(element.querySelector("INPUT").value, 10);
  2744.  
  2745. dataModule.setFilterReputationLimit(newValue);
  2746.  
  2747. reFilter((item) => item.filterMode === "显示");
  2748. };
  2749.  
  2750. content.appendChild(element);
  2751. })();
  2752.  
  2753. // 匿名过滤
  2754. (() => {
  2755. const checked = dataModule.getFilterAnony() ? "checked" : "";
  2756.  
  2757. const element = document.createElement("DIV");
  2758.  
  2759. element.innerHTML += `
  2760. <br/>
  2761. <div>
  2762. <label>
  2763. 隐藏匿名的用户
  2764. <input type="checkbox" ${checked} />
  2765. </label>
  2766. </div>
  2767. `;
  2768.  
  2769. const checkbox = element.querySelector("INPUT");
  2770.  
  2771. checkbox.onchange = () => {
  2772. const newValue = checkbox.checked;
  2773.  
  2774. dataModule.setFilterAnony(newValue);
  2775.  
  2776. reFilter((item) => item.filterMode === "显示");
  2777. };
  2778.  
  2779. content.appendChild(element);
  2780. })();
  2781.  
  2782. // 删除没有标记的用户
  2783. (() => {
  2784. const element = document.createElement("DIV");
  2785.  
  2786. element.innerHTML += `
  2787. <br/>
  2788. <div>
  2789. <button>删除没有标记的用户</button>
  2790. </div>
  2791. `;
  2792.  
  2793. const action = element.querySelector("BUTTON");
  2794.  
  2795. action.onclick = () => {
  2796. if (confirm("是否确认?") === false) {
  2797. return;
  2798. }
  2799.  
  2800. const filteredUsers = Object.values(userModule.list()).filter(
  2801. ({ tags }) => tags.length === 0
  2802. );
  2803.  
  2804. filteredUsers.forEach(({ id }) => {
  2805. userModule.remove(id);
  2806. });
  2807. };
  2808.  
  2809. content.appendChild(element);
  2810. })();
  2811.  
  2812. // 删除没有用户的标记
  2813. (() => {
  2814. const element = document.createElement("DIV");
  2815.  
  2816. element.innerHTML += `
  2817. <br/>
  2818. <div>
  2819. <button>删除没有用户的标记</button>
  2820. </div>
  2821. `;
  2822.  
  2823. const action = element.querySelector("BUTTON");
  2824.  
  2825. action.onclick = () => {
  2826. if (confirm("是否确认?") === false) {
  2827. return;
  2828. }
  2829.  
  2830. const users = Object.values(userModule.list());
  2831.  
  2832. Object.values(tagModule.list()).forEach(({ id }) => {
  2833. if (users.find(({ tags }) => tags.includes(id))) {
  2834. return;
  2835. }
  2836.  
  2837. tagModule.remove(id);
  2838. });
  2839. };
  2840.  
  2841. content.appendChild(element);
  2842. })();
  2843.  
  2844. // 删除非激活中的用户
  2845. (() => {
  2846. const element = document.createElement("DIV");
  2847.  
  2848. element.innerHTML += `
  2849. <br/>
  2850. <div>
  2851. <button>删除非激活中的用户</button>
  2852. <div style="white-space: normal;"></div>
  2853. </div>
  2854. `;
  2855.  
  2856. const action = element.querySelector("BUTTON");
  2857. const list = action.nextElementSibling;
  2858.  
  2859. action.onclick = () => {
  2860. if (confirm("是否确认?") === false) {
  2861. return;
  2862. }
  2863.  
  2864. const users = Object.values(userModule.list());
  2865.  
  2866. const filtered = [];
  2867.  
  2868. const waitingQueue = users.map(
  2869. ({ id }) =>
  2870. () =>
  2871. fetchModule.getUserInfo(id).then(({ bit }) => {
  2872. const activeInfo = commonui.activeInfo(0, 0, bit);
  2873.  
  2874. const activeType = activeInfo[1];
  2875.  
  2876. if (["ACTIVED", "LINKED"].includes(activeType)) {
  2877. return;
  2878. }
  2879.  
  2880. list.innerHTML += userModule.format(id);
  2881.  
  2882. filtered.push(id);
  2883.  
  2884. userModule.remove(id);
  2885. })
  2886. );
  2887.  
  2888. const queueLength = waitingQueue.length;
  2889.  
  2890. const execute = () => {
  2891. if (waitingQueue.length) {
  2892. const next = waitingQueue.shift();
  2893.  
  2894. action.innerHTML = `删除非激活中的用户 (${
  2895. queueLength - waitingQueue.length
  2896. }/${queueLength})`;
  2897.  
  2898. action.disabled = true;
  2899.  
  2900. next().finally(execute);
  2901. return;
  2902. }
  2903.  
  2904. action.disabled = false;
  2905.  
  2906. filtered.forEach((uid) => {
  2907. userModule.refresh(uid);
  2908. });
  2909. };
  2910.  
  2911. execute();
  2912. };
  2913.  
  2914. content.appendChild(element);
  2915. })();
  2916. };
  2917.  
  2918. return {
  2919. content,
  2920. refresh,
  2921. };
  2922. })();
  2923.  
  2924. return {
  2925. view,
  2926. };
  2927. })();
  2928.  
  2929. // 额外数据请求模块
  2930. // 临时的缓存写法
  2931. const fetchModule = (() => {
  2932. // 简单的统一请求
  2933. const request = (url, config = {}) =>
  2934. fetch(url, {
  2935. headers: {
  2936. "X-User-Agent": USER_AGENT,
  2937. },
  2938. ...config,
  2939. });
  2940.  
  2941. // 获取主题数量
  2942. // 缓存 1 小时
  2943. const getTopicNum = (() => {
  2944. const name = "TOPIC_NUM_CACHE";
  2945.  
  2946. const expireTime = 60 * 60 * 1000;
  2947.  
  2948. cacheModule.init(name, {
  2949. keyPath: "uid",
  2950. version: 1,
  2951. });
  2952.  
  2953. return async (uid) => {
  2954. const cache = await cacheModule.load(name, uid, expireTime);
  2955.  
  2956. if (cache) {
  2957. return cache.count;
  2958. }
  2959.  
  2960. const api = `/thread.php?lite=js&authorid=${uid}`;
  2961.  
  2962. const { __ROWS } = await new Promise((resolve) => {
  2963. request(api)
  2964. .then((res) => res.blob())
  2965. .then((blob) => {
  2966. const reader = new FileReader();
  2967.  
  2968. reader.onload = () => {
  2969. try {
  2970. const text = reader.result;
  2971. const result = JSON.parse(
  2972. text.replace("window.script_muti_get_var_store=", "")
  2973. );
  2974.  
  2975. resolve(result.data);
  2976. } catch {
  2977. resolve({});
  2978. }
  2979. };
  2980.  
  2981. reader.readAsText(blob, "GBK");
  2982. })
  2983. .catch(() => {
  2984. resolve({});
  2985. });
  2986. });
  2987.  
  2988. cacheModule.save(name, uid, {
  2989. uid,
  2990. count: __ROWS,
  2991. timestamp: new Date().getTime(),
  2992. });
  2993.  
  2994. return __ROWS;
  2995. };
  2996. })();
  2997.  
  2998. // 获取用户信息
  2999. // 缓存 1 小时
  3000. const getUserInfo = (() => {
  3001. const name = "USER_INFO_CACHE";
  3002.  
  3003. const expireTime = 60 * 60 * 1000;
  3004.  
  3005. cacheModule.init(name, {
  3006. keyPath: "uid",
  3007. version: 1,
  3008. });
  3009.  
  3010. return async (uid) => {
  3011. const cache = await cacheModule.load(name, uid, expireTime);
  3012.  
  3013. if (cache) {
  3014. return cache.data;
  3015. }
  3016.  
  3017. const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;
  3018.  
  3019. const data = await new Promise((resolve) => {
  3020. request(api)
  3021. .then((res) => res.blob())
  3022. .then((blob) => {
  3023. const reader = new FileReader();
  3024.  
  3025. reader.onload = () => {
  3026. try {
  3027. const text = reader.result;
  3028. const result = JSON.parse(
  3029. text.replace("window.script_muti_get_var_store=", "")
  3030. );
  3031.  
  3032. resolve(result.data[0] || null);
  3033. } catch {
  3034. resolve(null);
  3035. }
  3036. };
  3037.  
  3038. reader.readAsText(blob, "GBK");
  3039. })
  3040. .catch(() => {
  3041. resolve(null);
  3042. });
  3043. });
  3044.  
  3045. if (data) {
  3046. cacheModule.save(name, uid, {
  3047. uid,
  3048. data,
  3049. timestamp: new Date().getTime(),
  3050. });
  3051. }
  3052.  
  3053. return data;
  3054. };
  3055. })();
  3056.  
  3057. // 获取顶楼用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、声望
  3058. // 缓存 10 分钟
  3059. const getUserInfoAndReputation = (() => {
  3060. const name = "PAGE_CACHE";
  3061.  
  3062. const expireTime = 10 * 60 * 1000;
  3063.  
  3064. cacheModule.init(name, {
  3065. keyPath: "url",
  3066. version: 1,
  3067. });
  3068.  
  3069. return async (tid, pid) => {
  3070. if (tid === undefined && pid === undefined) {
  3071. return;
  3072. }
  3073.  
  3074. const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
  3075.  
  3076. const cache = await cacheModule.load(name, api, expireTime);
  3077.  
  3078. if (cache) {
  3079. return cache.data;
  3080. }
  3081.  
  3082. // 请求数据
  3083. const data = await new Promise((resolve) => {
  3084. request(api)
  3085. .then((res) => res.blob())
  3086. .then((blob) => {
  3087. const getLastIndex = (content, position) => {
  3088. if (position >= 0) {
  3089. let nextIndex = position + 1;
  3090.  
  3091. while (nextIndex < content.length) {
  3092. if (content[nextIndex] === "}") {
  3093. return nextIndex;
  3094. }
  3095.  
  3096. if (content[nextIndex] === "{") {
  3097. nextIndex = getLastIndex(content, nextIndex);
  3098.  
  3099. if (nextIndex < 0) {
  3100. break;
  3101. }
  3102. }
  3103.  
  3104. nextIndex = nextIndex + 1;
  3105. }
  3106. }
  3107.  
  3108. return -1;
  3109. };
  3110.  
  3111. const reader = new FileReader();
  3112.  
  3113. reader.onload = async () => {
  3114. const parser = new DOMParser();
  3115.  
  3116. const doc = parser.parseFromString(reader.result, "text/html");
  3117.  
  3118. const html = doc.body.innerHTML;
  3119.  
  3120. // 验证帖子正常
  3121. const verify = doc.querySelector("#m_posts");
  3122.  
  3123. if (verify) {
  3124. // 取得顶楼 UID
  3125. const uid = (() => {
  3126. const ele = doc.querySelector("#postauthor0");
  3127.  
  3128. if (ele) {
  3129. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  3130.  
  3131. if (res) {
  3132. return res[1];
  3133. }
  3134. }
  3135.  
  3136. return 0;
  3137. })();
  3138.  
  3139. // 取得顶楼标题
  3140. const subject = doc.querySelector("#postsubject0").innerHTML;
  3141.  
  3142. // 取得顶楼内容
  3143. const content = doc.querySelector("#postcontent0").innerHTML;
  3144.  
  3145. // 非匿名用户
  3146. if (uid && uid > 0) {
  3147. // 取得用户信息
  3148. const userInfo = (() => {
  3149. // 起始JSON
  3150. const str = `"${uid}":{`;
  3151.  
  3152. // 起始下标
  3153. const index = html.indexOf(str) + str.length;
  3154.  
  3155. // 结尾下标
  3156. const lastIndex = getLastIndex(html, index);
  3157.  
  3158. if (lastIndex >= 0) {
  3159. try {
  3160. return JSON.parse(
  3161. `{${html.substring(index, lastIndex)}}`
  3162. );
  3163. } catch {}
  3164. }
  3165.  
  3166. return null;
  3167. })();
  3168.  
  3169. // 取得用户声望
  3170. const reputation = (() => {
  3171. const reputations = (() => {
  3172. // 起始JSON
  3173. const str = `"__REPUTATIONS":{`;
  3174.  
  3175. // 起始下标
  3176. const index = html.indexOf(str) + str.length;
  3177.  
  3178. // 结尾下标
  3179. const lastIndex = getLastIndex(html, index);
  3180.  
  3181. if (lastIndex >= 0) {
  3182. return JSON.parse(
  3183. `{${html.substring(index, lastIndex)}}`
  3184. );
  3185. }
  3186.  
  3187. return null;
  3188. })();
  3189.  
  3190. if (reputations) {
  3191. for (let fid in reputations) {
  3192. return reputations[fid][uid] || 0;
  3193. }
  3194. }
  3195.  
  3196. return NaN;
  3197. })();
  3198.  
  3199. resolve({
  3200. uid,
  3201. subject,
  3202. content,
  3203. userInfo,
  3204. reputation,
  3205. });
  3206. return;
  3207. }
  3208.  
  3209. resolve({
  3210. uid,
  3211. subject,
  3212. content,
  3213. });
  3214. return;
  3215. }
  3216.  
  3217. resolve(null);
  3218. };
  3219.  
  3220. reader.readAsText(blob, "GBK");
  3221. })
  3222. .catch(() => {
  3223. resolve(null);
  3224. });
  3225. });
  3226.  
  3227. if (data) {
  3228. cacheModule.save(name, api, {
  3229. url: api,
  3230. data,
  3231. timestamp: new Date().getTime(),
  3232. });
  3233. }
  3234.  
  3235. return data;
  3236. };
  3237. })();
  3238.  
  3239. // 获取版面信息
  3240. // 不会频繁调用,无需缓存
  3241. const getForumInfo = async (fid) => {
  3242. if (Number.isNaN(fid)) {
  3243. return null;
  3244. }
  3245.  
  3246. const api = `/thread.php?lite=js&fid=${fid}`;
  3247.  
  3248. const data = await new Promise((resolve) => {
  3249. request(api)
  3250. .then((res) => res.blob())
  3251. .then((blob) => {
  3252. const reader = new FileReader();
  3253.  
  3254. reader.onload = () => {
  3255. try {
  3256. const text = reader.result;
  3257. const result = JSON.parse(
  3258. text.replace("window.script_muti_get_var_store=", "")
  3259. );
  3260.  
  3261. if (result.data) {
  3262. resolve(result.data.__F || null);
  3263. return;
  3264. }
  3265.  
  3266. resolve(null);
  3267. } catch {
  3268. resolve(null);
  3269. }
  3270. };
  3271.  
  3272. reader.readAsText(blob, "GBK");
  3273. })
  3274. .catch(() => {
  3275. resolve(null);
  3276. });
  3277. });
  3278.  
  3279. return data;
  3280. };
  3281.  
  3282. // 获取版面发言记录
  3283. // 缓存 1 天
  3284. const getForumPosted = (() => {
  3285. const name = "FORUM_POSTED_CACHE";
  3286.  
  3287. const expireTime = 24 * 60 * 60 * 1000;
  3288.  
  3289. cacheModule.init(name, {
  3290. keyPath: "url",
  3291. persistent: true,
  3292. version: 2,
  3293. });
  3294.  
  3295. return async (fid, uid) => {
  3296. if (uid <= 0) {
  3297. return;
  3298. }
  3299.  
  3300. const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`;
  3301.  
  3302. const cache = await cacheModule.load(name, api);
  3303.  
  3304. if (cache) {
  3305. // 发言是无法撤销的,只要有记录就永远不需要再获取
  3306. // 手动处理没有记录的缓存数据
  3307. if (
  3308. cache.data === false &&
  3309. cache.timestamp + expireTime < new Date().getTime()
  3310. ) {
  3311. await remove(name, api);
  3312. }
  3313.  
  3314. return cache.data;
  3315. }
  3316.  
  3317. let isComplete = false;
  3318. let isBusy = false;
  3319.  
  3320. const func = async (url) =>
  3321. await new Promise((resolve) => {
  3322. if (isComplete || isBusy) {
  3323. resolve();
  3324. return;
  3325. }
  3326.  
  3327. request(url)
  3328. .then((res) => res.blob())
  3329. .then((blob) => {
  3330. const reader = new FileReader();
  3331.  
  3332. reader.onload = () => {
  3333. const text = reader.result;
  3334.  
  3335. // 将所有匹配的 FID 写入缓存,即使并不在设置里
  3336. const matched = text.match(/"fid":(-?\d+),/g);
  3337.  
  3338. if (matched) {
  3339. [
  3340. ...new Set(
  3341. matched.map((item) =>
  3342. parseInt(item.match(/-?\d+/)[0], 10)
  3343. )
  3344. ),
  3345. ].forEach((item) => {
  3346. const key = api.replace(`&fid=${fid}`, `&fid=${item}`);
  3347.  
  3348. // 直接写入缓存
  3349. cacheModule.save(name, key, {
  3350. url: key,
  3351. data: true,
  3352. timestamp: new Date().getTime(),
  3353. });
  3354.  
  3355. // 已有结果,无需继续查询
  3356. if (fid === item) {
  3357. isComplete = true;
  3358. }
  3359. });
  3360.  
  3361. resolve();
  3362. return;
  3363. }
  3364.  
  3365. // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
  3366. if (text.indexOf("服务器忙") > 0) {
  3367. isBusy = true;
  3368. }
  3369.  
  3370. resolve();
  3371. };
  3372.  
  3373. reader.readAsText(blob, "GBK");
  3374. })
  3375. .catch(() => {
  3376. resolve();
  3377. });
  3378. });
  3379.  
  3380. // 先获取回复记录的第一页,顺便可以获取其他版面的记录
  3381. // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
  3382. await func(api.replace(`&fid=${fid}`, `&searchpost=1`));
  3383. await func(api + "&searchpost=1");
  3384. await func(api);
  3385.  
  3386. // 无论成功与否都写入缓存
  3387. if (isComplete === false) {
  3388. // 遇到服务器忙的情况,手动调整缓存时间至 1 小时
  3389. const timestamp = isBusy
  3390. ? new Date().getTime() - (expireTime - 60 * 60 * 1000)
  3391. : new Date().getTime();
  3392.  
  3393. // 写入失败缓存
  3394. cacheModule.save(name, api, {
  3395. url: api,
  3396. data: false,
  3397. timestamp,
  3398. });
  3399. }
  3400.  
  3401. return isComplete;
  3402. };
  3403. })();
  3404.  
  3405. // 每天清理缓存
  3406. (() => {
  3407. const today = new Date();
  3408.  
  3409. const lastTime = new Date(GM_getValue(CLEAR_TIME_KEY) || 0);
  3410.  
  3411. const isToday =
  3412. lastTime.getDate() === today.getDate() &&
  3413. lastTime.getMonth() === today.getMonth() &&
  3414. lastTime.getFullYear() === today.getFullYear();
  3415.  
  3416. if (isToday === false) {
  3417. cacheModule.clear();
  3418.  
  3419. GM_setValue(CLEAR_TIME_KEY, today.getTime());
  3420. }
  3421. })();
  3422.  
  3423. return {
  3424. getTopicNum,
  3425. getUserInfo,
  3426. getUserInfoAndReputation,
  3427. getForumInfo,
  3428. getForumPosted,
  3429. };
  3430. })();
  3431.  
  3432. // UI
  3433. const ui = (() => {
  3434. const modules = {};
  3435.  
  3436. // 主界面
  3437. const view = (() => {
  3438. const tabContainer = (() => {
  3439. const element = document.createElement("DIV");
  3440.  
  3441. element.className = "w100";
  3442. element.innerHTML = `
  3443. <div class="right_" style="margin-bottom: 5px;">
  3444. <table class="stdbtn" cellspacing="0">
  3445. <tbody>
  3446. <tr></tr>
  3447. </tbody>
  3448. </table>
  3449. </div>
  3450. <div class="clear"></div>
  3451. `;
  3452.  
  3453. return element;
  3454. })();
  3455.  
  3456. const tabPanelContainer = (() => {
  3457. const element = document.createElement("DIV");
  3458.  
  3459. element.style = "width: 80vw;";
  3460.  
  3461. return element;
  3462. })();
  3463.  
  3464. const content = (() => {
  3465. const element = document.createElement("DIV");
  3466.  
  3467. element.appendChild(tabContainer);
  3468. element.appendChild(tabPanelContainer);
  3469.  
  3470. return element;
  3471. })();
  3472.  
  3473. const addModule = (() => {
  3474. const tc = tabContainer.querySelector("TR");
  3475. const cc = tabPanelContainer;
  3476.  
  3477. return (name, module) => {
  3478. const tabBox = document.createElement("TD");
  3479.  
  3480. tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${name}</a>`;
  3481.  
  3482. const tab = tabBox.childNodes[0];
  3483.  
  3484. const toggle = () => {
  3485. Object.values(modules).forEach((item) => {
  3486. if (item.tab === tab) {
  3487. item.tab.className = "nobr";
  3488. item.content.style = "display: block";
  3489. item.refresh();
  3490. } else {
  3491. item.tab.className = "nobr silver";
  3492. item.content.style = "display: none";
  3493. }
  3494. });
  3495. };
  3496.  
  3497. tc.append(tabBox);
  3498. cc.append(module.content);
  3499.  
  3500. tab.onclick = toggle;
  3501.  
  3502. modules[name] = {
  3503. ...module,
  3504. tab,
  3505. toggle,
  3506. };
  3507.  
  3508. return modules[name];
  3509. };
  3510. })();
  3511.  
  3512. return {
  3513. content,
  3514. addModule,
  3515. };
  3516. })();
  3517.  
  3518. // 右上角菜单
  3519. const menu = (() => {
  3520. const container = document.createElement("DIV");
  3521.  
  3522. container.className = `td`;
  3523. container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;
  3524.  
  3525. const content = container.querySelector("A");
  3526.  
  3527. const create = (onclick) => {
  3528. const anchor = document.querySelector("#mainmenu .td:last-child");
  3529.  
  3530. if (anchor) {
  3531. anchor.before(container);
  3532.  
  3533. content.onclick = onclick;
  3534.  
  3535. return true;
  3536. }
  3537.  
  3538. return false;
  3539. };
  3540.  
  3541. const update = (list) => {
  3542. const count = list.length;
  3543.  
  3544. if (count) {
  3545. content.innerHTML = `屏蔽 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  3546. } else {
  3547. content.innerHTML = `屏蔽`;
  3548. }
  3549. };
  3550.  
  3551. return {
  3552. create,
  3553. update,
  3554. };
  3555. })();
  3556.  
  3557. return {
  3558. ...view,
  3559. ...menu,
  3560. };
  3561. })();
  3562.  
  3563. // 判断是否为当前用户 UID
  3564. const isCurrentUID = (uid) => {
  3565. return unsafeWindow.__CURRENT_UID === parseInt(uid, 10);
  3566. };
  3567.  
  3568. // 获取过滤方式
  3569. const getFilterMode = async (item) => {
  3570. // 声明结果
  3571. const result = {
  3572. mode: -1,
  3573. reason: ``,
  3574. };
  3575.  
  3576. // 获取 UID
  3577. const uid = parseInt(item.uid, 10);
  3578.  
  3579. // 获取链接参数
  3580. const params = new URLSearchParams(location.search);
  3581.  
  3582. // 跳过屏蔽(插件自定义)
  3583. if (params.has("nofilter")) {
  3584. return;
  3585. }
  3586.  
  3587. // 收藏
  3588. if (params.has("favor")) {
  3589. return;
  3590. }
  3591.  
  3592. // 只看某人
  3593. if (params.has("authorid")) {
  3594. return;
  3595. }
  3596.  
  3597. // 跳过自己
  3598. if (isCurrentUID(uid)) {
  3599. return "";
  3600. }
  3601.  
  3602. // 用户过滤
  3603. (() => {
  3604. // 获取屏蔽列表里匹配的用户
  3605. const user = userModule.get(uid);
  3606.  
  3607. // 没有则跳过
  3608. if (user === null) {
  3609. return;
  3610. }
  3611.  
  3612. const { filterMode } = user;
  3613.  
  3614. const mode = filterModule.getModeByName(filterMode);
  3615.  
  3616. // 低于当前的过滤模式则跳过
  3617. if (mode <= result.mode) {
  3618. return;
  3619. }
  3620.  
  3621. // 更新过滤模式和原因
  3622. result.mode = mode;
  3623. result.reason = `用户模式: ${filterMode}`;
  3624. })();
  3625.  
  3626. // 标记过滤
  3627. (() => {
  3628. // 获取屏蔽列表里匹配的用户
  3629. const user = userModule.get(uid);
  3630.  
  3631. // 获取用户对应的标记,并跳过低于当前的过滤模式
  3632. const tags = user
  3633. ? user.tags
  3634. .map((id) => tagModule.get({ id }))
  3635. .filter((i) => i !== null)
  3636. .filter(
  3637. (i) => filterModule.getModeByName(i.filterMode) > result.mode
  3638. )
  3639. : [];
  3640.  
  3641. // 没有则跳过
  3642. if (tags.length === 0) {
  3643. return;
  3644. }
  3645.  
  3646. // 取最高的过滤模式
  3647. const { filterMode, name } = tags.sort(
  3648. (a, b) =>
  3649. filterModule.getModeByName(b.filterMode) -
  3650. filterModule.getModeByName(a.filterMode)
  3651. )[0];
  3652.  
  3653. const mode = filterModule.getModeByName(filterMode);
  3654.  
  3655. // 更新过滤模式和原因
  3656. result.mode = mode;
  3657. result.reason = `标记: ${name}`;
  3658. })();
  3659.  
  3660. // 关键字过滤
  3661. await (async () => {
  3662. const { getContent } = item;
  3663.  
  3664. // 获取设置里的关键字列表,并跳过低于当前的过滤模式
  3665. const keywords = Object.values(keywordModule.list()).filter(
  3666. (i) => filterModule.getModeByName(i.filterMode) > result.mode
  3667. );
  3668.  
  3669. // 没有则跳过
  3670. if (keywords.length === 0) {
  3671. return;
  3672. }
  3673.  
  3674. // 根据过滤等级依次判断
  3675. const list = keywords.sort(
  3676. (a, b) =>
  3677. filterModule.getModeByName(b.filterMode) -
  3678. filterModule.getModeByName(a.filterMode)
  3679. );
  3680.  
  3681. for (let i = 0; i < list.length; i += 1) {
  3682. const { keyword, filterMode } = list[i];
  3683.  
  3684. // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
  3685. const filterLevel = list[i].filterLevel || 0;
  3686.  
  3687. // 过滤标题
  3688. if (filterLevel >= 0) {
  3689. const { subject } = item;
  3690.  
  3691. const match = subject.match(keyword);
  3692.  
  3693. if (match) {
  3694. const mode = filterModule.getModeByName(filterMode);
  3695.  
  3696. // 更新过滤模式和原因
  3697. result.mode = mode;
  3698. result.reason = `关键字: ${match[0]}`;
  3699. return;
  3700. }
  3701. }
  3702.  
  3703. // 过滤内容
  3704. if (filterLevel >= 1) {
  3705. // 如果没有内容,则请求
  3706. const content = await (async () => {
  3707. if (item.content === undefined) {
  3708. await getContent().catch(() => {});
  3709. }
  3710.  
  3711. return item.content || null;
  3712. })();
  3713.  
  3714. if (content) {
  3715. const match = content.match(keyword);
  3716.  
  3717. if (match) {
  3718. const mode = filterModule.getModeByName(filterMode);
  3719.  
  3720. // 更新过滤模式和原因
  3721. result.mode = mode;
  3722. result.reason = `关键字: ${match[0]}`;
  3723. return;
  3724. }
  3725. }
  3726. }
  3727. }
  3728. })();
  3729.  
  3730. // 杂项过滤
  3731. // 放在属地前是因为符合条件的过多,没必要再请求它们的属地
  3732. await (async () => {
  3733. const { getUserInfo, getReputation } = item;
  3734.  
  3735. // 如果当前模式是显示,则跳过
  3736. if (filterModule.getNameByMode(result.mode) === "显示") {
  3737. return;
  3738. }
  3739.  
  3740. // 获取隐藏模式下标
  3741. const mode = filterModule.getModeByName("隐藏");
  3742.  
  3743. // 匿名
  3744. if (uid <= 0) {
  3745. const filterAnony = dataModule.getFilterAnony();
  3746.  
  3747. if (filterAnony) {
  3748. // 更新过滤模式和原因
  3749. result.mode = mode;
  3750. result.reason = "匿名";
  3751. }
  3752.  
  3753. return;
  3754. }
  3755.  
  3756. // 注册时间过滤
  3757. await (async () => {
  3758. const filterRegdateLimit = dataModule.getFilterRegdateLimit();
  3759.  
  3760. // 如果没有用户信息,则请求
  3761. const userInfo = await (async () => {
  3762. if (item.userInfo === undefined) {
  3763. await getUserInfo().catch(() => {});
  3764. }
  3765.  
  3766. return item.userInfo || {};
  3767. })();
  3768.  
  3769. const { regdate } = userInfo;
  3770.  
  3771. if (regdate === undefined) {
  3772. return;
  3773. }
  3774.  
  3775. if (
  3776. filterRegdateLimit > 0 &&
  3777. regdate * 1000 > new Date() - filterRegdateLimit
  3778. ) {
  3779. // 更新过滤模式和原因
  3780. result.mode = mode;
  3781. result.reason = `注册时间: ${new Date(
  3782. regdate * 1000
  3783. ).toLocaleDateString()}`;
  3784. return;
  3785. }
  3786. })();
  3787.  
  3788. // 发帖数量过滤
  3789. await (async () => {
  3790. const filterPostnumLimit = dataModule.getFilterPostnumLimit();
  3791.  
  3792. // 如果没有用户信息,则请求
  3793. const userInfo = await (async () => {
  3794. if (item.userInfo === undefined) {
  3795. await getUserInfo().catch(() => {});
  3796. }
  3797.  
  3798. return item.userInfo || {};
  3799. })();
  3800.  
  3801. const { postnum } = userInfo;
  3802.  
  3803. if (postnum === undefined) {
  3804. return;
  3805. }
  3806.  
  3807. if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
  3808. // 更新过滤模式和原因
  3809. result.mode = mode;
  3810. result.reason = `发帖数量: ${postnum}`;
  3811. return;
  3812. }
  3813. })();
  3814.  
  3815. // 发帖比例过滤
  3816. await (async () => {
  3817. const filterTopicRateLimit = dataModule.getFilterTopicRateLimit();
  3818.  
  3819. // 如果没有用户信息,则请求
  3820. const userInfo = await (async () => {
  3821. if (item.userInfo === undefined) {
  3822. await getUserInfo().catch(() => {});
  3823. }
  3824.  
  3825. return item.userInfo || {};
  3826. })();
  3827.  
  3828. const { postnum } = userInfo;
  3829.  
  3830. if (postnum === undefined) {
  3831. return;
  3832. }
  3833.  
  3834. if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) {
  3835. // 获取主题数量
  3836. const topicNum = await fetchModule.getTopicNum(uid);
  3837.  
  3838. // 计算发帖比例
  3839. const topicRate = (topicNum / postnum) * 100;
  3840.  
  3841. if (topicRate > filterTopicRateLimit) {
  3842. // 更新过滤模式和原因
  3843. result.mode = mode;
  3844. result.reason = `发帖比例: ${topicRate.toFixed(
  3845. 0
  3846. )}% (${topicNum}/${postnum})`;
  3847. return;
  3848. }
  3849. }
  3850. })();
  3851.  
  3852. // 版面声望过滤
  3853. await (async () => {
  3854. const filterReputationLimit = dataModule.getFilterReputationLimit();
  3855.  
  3856. if (Number.isNaN(filterReputationLimit)) {
  3857. return;
  3858. }
  3859.  
  3860. // 如果没有版面声望,则请求
  3861. const reputation = await (async () => {
  3862. if (item.reputation === undefined) {
  3863. await getReputation().catch(() => {});
  3864. }
  3865.  
  3866. return item.reputation || NaN;
  3867. })();
  3868.  
  3869. if (reputation < filterReputationLimit) {
  3870. // 更新过滤模式和原因
  3871. result.mode = mode;
  3872. result.reason = `版面声望: ${reputation}`;
  3873. return;
  3874. }
  3875. })();
  3876. })();
  3877.  
  3878. // 属地过滤
  3879. await (async () => {
  3880. // 匿名用户则跳过
  3881. if (uid <= 0) {
  3882. return;
  3883. }
  3884.  
  3885. // 获取设置里的属地列表,并跳过低于当前的过滤模式
  3886. const locations = Object.values(locationModule.list()).filter(
  3887. (i) => filterModule.getModeByName(i.filterMode) > result.mode
  3888. );
  3889.  
  3890. // 没有则跳过
  3891. if (locations.length === 0) {
  3892. return;
  3893. }
  3894.  
  3895. // 请求属地
  3896. const { ipLoc } = await fetchModule.getUserInfo(uid);
  3897.  
  3898. // 请求失败则跳过
  3899. if (ipLoc === undefined) {
  3900. return;
  3901. }
  3902.  
  3903. // 根据过滤等级依次判断
  3904. const list = locations.sort(
  3905. (a, b) =>
  3906. filterModule.getModeByName(b.filterMode) -
  3907. filterModule.getModeByName(a.filterMode)
  3908. );
  3909.  
  3910. for (let i = 0; i < list.length; i += 1) {
  3911. const { keyword, filterMode } = list[i];
  3912.  
  3913. const match = ipLoc.match(keyword);
  3914.  
  3915. if (match) {
  3916. const mode = filterModule.getModeByName(filterMode);
  3917.  
  3918. // 更新过滤模式和原因
  3919. result.mode = mode;
  3920. result.reason = `属地: ${ipLoc}`;
  3921. return;
  3922. }
  3923. }
  3924. })();
  3925.  
  3926. // 猎巫过滤
  3927. (() => {
  3928. // 获取猎巫结果
  3929. const witchHunt = item.witchHunt;
  3930.  
  3931. // 没有则跳过
  3932. if (witchHunt === undefined) {
  3933. return;
  3934. }
  3935.  
  3936. // 获取设置
  3937. const list = Object.values(witchHuntModule.list()).filter(({ fid }) =>
  3938. witchHunt.includes(fid)
  3939. );
  3940.  
  3941. // 筛选出匹配的猎巫
  3942. const filtered = Object.values(list)
  3943. .filter(({ filterLevel }) => filterLevel > 0)
  3944. .filter(
  3945. ({ filterMode }) =>
  3946. filterModule.getModeByName(filterMode) > result.mode
  3947. );
  3948.  
  3949. // 没有则跳过
  3950. if (filtered.length === 0) {
  3951. return;
  3952. }
  3953.  
  3954. // 取最高的过滤模式
  3955. const { filterMode, label } = filtered.sort(
  3956. (a, b) =>
  3957. filterModule.getModeByName(b.filterMode) -
  3958. filterModule.getModeByName(a.filterMode)
  3959. )[0];
  3960.  
  3961. const mode = filterModule.getModeByName(filterMode);
  3962.  
  3963. // 更新过滤模式和原因
  3964. result.mode = mode;
  3965. result.reason = `猎巫: ${label}`;
  3966. })();
  3967.  
  3968. // 写入过滤模式和过滤原因
  3969. item.filterMode = filterModule.getNameByMode(result.mode);
  3970. item.reason = result.reason;
  3971.  
  3972. // 写入列表
  3973. listModule.add(item);
  3974.  
  3975. // 继承模式下返回默认过滤模式
  3976. if (item.filterMode === "继承") {
  3977. return dataModule.getDefaultFilterMode();
  3978. }
  3979.  
  3980. // 返回结果
  3981. return item.filterMode;
  3982. };
  3983.  
  3984. // 获取主题过滤方式
  3985. const getFilterModeByTopic = async (topic) => {
  3986. const { tid } = topic;
  3987.  
  3988. // 绑定额外的数据请求方式
  3989. if (topic.getContent === undefined) {
  3990. // 获取帖子内容,按需调用
  3991. const getTopic = () =>
  3992. new Promise((resolve, reject) => {
  3993. // 避免重复请求
  3994. if (topic.content || topic.userInfo || topic.reputation) {
  3995. resolve(topic);
  3996. return;
  3997. }
  3998.  
  3999. // 请求并写入数据
  4000. fetchModule
  4001. .getUserInfoAndReputation(tid, undefined)
  4002. .then(({ subject, content, userInfo, reputation }) => {
  4003. // 写入用户名
  4004. if (userInfo) {
  4005. topic.username = userInfo.username;
  4006. }
  4007.  
  4008. // 写入用户信息和声望
  4009. topic.userInfo = userInfo;
  4010. topic.reputation = reputation;
  4011.  
  4012. // 写入帖子标题和内容
  4013. topic.subject = subject;
  4014. topic.content = content;
  4015.  
  4016. // 返回结果
  4017. resolve(topic);
  4018. })
  4019. .catch(reject);
  4020. });
  4021.  
  4022. // 绑定请求方式
  4023. topic.getContent = getTopic;
  4024. topic.getUserInfo = getTopic;
  4025. topic.getReputation = getTopic;
  4026. }
  4027.  
  4028. // 获取过滤模式
  4029. const filterMode = await getFilterMode(topic);
  4030.  
  4031. // 返回结果
  4032. return filterMode;
  4033. };
  4034.  
  4035. // 获取回复过滤方式
  4036. const getFilterModeByReply = async (reply) => {
  4037. const { tid, pid, uid } = reply;
  4038.  
  4039. // 回复页面可以直接获取到用户信息和声望
  4040. if (uid > 0) {
  4041. // 取得用户信息
  4042. const userInfo = commonui.userInfo.users[uid];
  4043.  
  4044. // 取得用户声望
  4045. const reputation = (() => {
  4046. const reputations = commonui.userInfo.reputations;
  4047.  
  4048. if (reputations) {
  4049. for (let fid in reputations) {
  4050. return reputations[fid][uid] || 0;
  4051. }
  4052. }
  4053.  
  4054. return NaN;
  4055. })();
  4056.  
  4057. // 写入用户名
  4058. if (userInfo) {
  4059. reply.username = userInfo.username;
  4060. }
  4061.  
  4062. // 写入用户信息和声望
  4063. reply.userInfo = userInfo;
  4064. reply.reputation = reputation;
  4065. }
  4066.  
  4067. // 绑定额外的数据请求方式
  4068. if (reply.getContent === undefined) {
  4069. // 获取帖子内容,按需调用
  4070. const getReply = () =>
  4071. new Promise((resolve, reject) => {
  4072. // 避免重复请求
  4073. if (reply.userInfo || reply.reputation) {
  4074. resolve(reply);
  4075. return;
  4076. }
  4077.  
  4078. // 请求并写入数据
  4079. fetchModule
  4080. .getUserInfoAndReputation(tid, pid)
  4081. .then(({ subject, content, userInfo, reputation }) => {
  4082. // 写入用户名
  4083. if (userInfo) {
  4084. reply.username = userInfo.username;
  4085. }
  4086.  
  4087. // 写入用户信息和声望
  4088. reply.userInfo = userInfo;
  4089. reply.reputation = reputation;
  4090.  
  4091. // 写入帖子标题和内容
  4092. reply.subject = subject;
  4093. reply.content = content;
  4094.  
  4095. // 返回结果
  4096. resolve(reply);
  4097. })
  4098. .catch(reject);
  4099. });
  4100.  
  4101. // 绑定请求方式
  4102. reply.getContent = getReply;
  4103. reply.getUserInfo = getReply;
  4104. reply.getReputation = getReply;
  4105. }
  4106.  
  4107. // 获取过滤模式
  4108. const filterMode = await getFilterMode(reply);
  4109.  
  4110. // 返回结果
  4111. return filterMode;
  4112. };
  4113.  
  4114. // 处理引用
  4115. const handleQuote = async (item, content) => {
  4116. const quotes = content.querySelectorAll(".quote");
  4117.  
  4118. await Promise.all(
  4119. [...quotes].map(async (quote) => {
  4120. const uid = (() => {
  4121. const ele = quote.querySelector("a[href^='/nuke.php']");
  4122.  
  4123. if (ele) {
  4124. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  4125.  
  4126. if (res) {
  4127. return res[1];
  4128. }
  4129. }
  4130.  
  4131. return 0;
  4132. })();
  4133.  
  4134. const { tid, pid } = (() => {
  4135. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  4136.  
  4137. if (ele) {
  4138. const res = ele
  4139. .getAttribute("onclick")
  4140. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  4141.  
  4142. if (res) {
  4143. return {
  4144. tid: parseInt(res[2], 10),
  4145. pid: parseInt(res[3], 10) || 0,
  4146. };
  4147. }
  4148. }
  4149.  
  4150. return {};
  4151. })();
  4152.  
  4153. // 获取过滤方式
  4154. const filterMode = await getFilterModeByReply({
  4155. uid,
  4156. tid,
  4157. pid,
  4158. subject: "",
  4159. content: quote.innerText,
  4160. });
  4161.  
  4162. (() => {
  4163. if (filterMode === "标记") {
  4164. filterModule.collapse(uid, quote, quote.innerHTML);
  4165. return;
  4166. }
  4167.  
  4168. if (filterMode === "遮罩") {
  4169. const source = document.createElement("DIV");
  4170.  
  4171. source.innerHTML = quote.innerHTML;
  4172. source.style.display = "none";
  4173.  
  4174. const caption = document.createElement("CAPTION");
  4175.  
  4176. caption.className = "filter-mask filter-mask-block";
  4177.  
  4178. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  4179. caption.onclick = () => {
  4180. quote.removeChild(caption);
  4181.  
  4182. source.style.display = "";
  4183. };
  4184.  
  4185. quote.innerHTML = "";
  4186. quote.appendChild(source);
  4187. quote.appendChild(caption);
  4188. return;
  4189. }
  4190.  
  4191. if (filterMode === "隐藏") {
  4192. quote.innerHTML = "";
  4193. return;
  4194. }
  4195. })();
  4196.  
  4197. // 绑定 UID
  4198. item.quotes = item.quotes || {};
  4199. item.quotes[uid] = filterMode;
  4200. })
  4201. );
  4202. };
  4203.  
  4204. // 过滤主题
  4205. const filterTopic = async (item) => {
  4206. // 绑定事件
  4207. if (item.nFilter === undefined) {
  4208. // 主题 ID
  4209. const tid = item[8];
  4210.  
  4211. // 主题标题
  4212. const title = item[1];
  4213. const subject = title.innerText;
  4214.  
  4215. // 主题作者
  4216. const author = item[2];
  4217. const uid =
  4218. parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
  4219. const username = author.innerText;
  4220.  
  4221. // 主题容器
  4222. const container = title.closest("tr");
  4223.  
  4224. // 过滤函数
  4225. const execute = async () => {
  4226. // 获取过滤方式
  4227. const filterMode = await getFilterModeByTopic(item.nFilter);
  4228.  
  4229. // 样式处理
  4230. (() => {
  4231. // 还原样式
  4232. // TODO 应该整体采用 className 来实现
  4233. (() => {
  4234. // 标记模式
  4235. container.style.removeProperty("textDecoration");
  4236.  
  4237. // 遮罩模式
  4238. title.classList.remove("filter-mask");
  4239. author.classList.remove("filter-mask");
  4240. })();
  4241.  
  4242. // 样式处理
  4243. (() => {
  4244. // 标记模式下,主题标记会有删除线标识
  4245. if (filterMode === "标记") {
  4246. title.style.textDecoration = "line-through";
  4247. return;
  4248. }
  4249.  
  4250. // 遮罩模式下,主题和作者会有遮罩样式
  4251. if (filterMode === "遮罩") {
  4252. title.classList.add("filter-mask");
  4253. author.classList.add("filter-mask");
  4254. return;
  4255. }
  4256.  
  4257. // 隐藏模式下,容器会被隐藏
  4258. if (filterMode === "隐藏") {
  4259. container.style.display = "none";
  4260. return;
  4261. }
  4262. })();
  4263.  
  4264. // 非隐藏模式下,恢复显示
  4265. if (filterMode !== "隐藏") {
  4266. container.style.removeProperty("display");
  4267. }
  4268. })();
  4269.  
  4270. // 猎巫会影响效率,待猎巫结果出来后再次过滤
  4271. witchHuntModule.run(item.nFilter);
  4272. };
  4273.  
  4274. // 绑定事件
  4275. item.nFilter = {
  4276. tid,
  4277. uid,
  4278. username,
  4279. container,
  4280. title,
  4281. author,
  4282. subject,
  4283. execute,
  4284. };
  4285. }
  4286.  
  4287. // 等待过滤完成
  4288. await item.nFilter.execute();
  4289. };
  4290.  
  4291. // 过滤回复
  4292. const filterReply = async (item) => {
  4293. // 绑定事件
  4294. if (item.nFilter === undefined) {
  4295. // 回复 ID
  4296. const pid = item.pid;
  4297.  
  4298. // 判断是否是楼层
  4299. const isFloor = typeof item.i === "number";
  4300.  
  4301. // 回复容器
  4302. const container = isFloor
  4303. ? item.uInfoC.closest("tr")
  4304. : item.uInfoC.closest(".comment_c");
  4305.  
  4306. // 回复标题
  4307. const title = item.subjectC;
  4308. const subject = title.innerText;
  4309.  
  4310. // 回复内容
  4311. const content = item.contentC;
  4312. const contentBak = content.innerHTML;
  4313.  
  4314. // 回复作者
  4315. const author = container.querySelector(".posterInfoLine") || item.uInfoC;
  4316. const uid = parseInt(item.pAid, 10) || 0;
  4317. const username = author.querySelector(".author").innerText;
  4318. const avatar = author.querySelector(".avatar");
  4319.  
  4320. // 找到用户 ID,将其视为操作按钮
  4321. const action = container.querySelector('[name="uid"]');
  4322.  
  4323. // 创建一个元素,用于展示标记列表
  4324. // 贴条和高赞不显示
  4325. const tags = (() => {
  4326. if (isFloor === false) {
  4327. return null;
  4328. }
  4329.  
  4330. const element = document.createElement("div");
  4331.  
  4332. element.className = "filter-tags";
  4333.  
  4334. author.appendChild(element);
  4335.  
  4336. return element;
  4337. })();
  4338.  
  4339. // 过滤函数
  4340. const execute = async () => {
  4341. // 获取过滤方式
  4342. const filterMode = await getFilterModeByReply(item.nFilter);
  4343.  
  4344. // 样式处理
  4345. await (async () => {
  4346. // 还原样式
  4347. // TODO 应该整体采用 className 来实现
  4348. (() => {
  4349. // 标记模式
  4350. if (avatar) {
  4351. avatar.style.removeProperty("display");
  4352. }
  4353.  
  4354. content.innerHTML = contentBak;
  4355.  
  4356. // 遮罩模式
  4357. const caption = container.parentNode.querySelector("CAPTION");
  4358.  
  4359. if (caption) {
  4360. container.parentNode.removeChild(caption);
  4361. container.style.removeProperty("display");
  4362. }
  4363. })();
  4364.  
  4365. // 样式处理
  4366. (() => {
  4367. // 标记模式下,隐藏头像,采用泥潭的折叠样式
  4368. if (filterMode === "标记") {
  4369. if (avatar) {
  4370. avatar.style.display = "none";
  4371. }
  4372.  
  4373. filterModule.collapse(uid, content, contentBak);
  4374. return;
  4375. }
  4376.  
  4377. // 遮罩模式下,楼层会有遮罩样式
  4378. if (filterMode === "遮罩") {
  4379. const caption = document.createElement("CAPTION");
  4380.  
  4381. if (isFloor) {
  4382. caption.className = "filter-mask filter-mask-block";
  4383. } else {
  4384. caption.className = "filter-mask filter-mask-block left";
  4385. caption.style.width = "47%";
  4386. }
  4387.  
  4388. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  4389. caption.onclick = () => {
  4390. const caption = container.parentNode.querySelector("CAPTION");
  4391.  
  4392. if (caption) {
  4393. container.parentNode.removeChild(caption);
  4394. container.style.removeProperty("display");
  4395. }
  4396. };
  4397.  
  4398. container.parentNode.insertBefore(caption, container);
  4399. container.style.display = "none";
  4400. return;
  4401. }
  4402.  
  4403. // 隐藏模式下,容器会被隐藏
  4404. if (filterMode === "隐藏") {
  4405. container.style.display = "none";
  4406. return;
  4407. }
  4408. })();
  4409.  
  4410. // 处理引用
  4411. await handleQuote(item.nFilter, content);
  4412.  
  4413. // 非隐藏模式下,恢复显示
  4414. // 如果是隐藏模式,没必要再加载按钮和标记
  4415. if (filterMode !== "隐藏") {
  4416. // 获取当前用户
  4417. const user = userModule.get(uid);
  4418.  
  4419. // 修改操作按钮颜色
  4420. if (action) {
  4421. if (user) {
  4422. action.style.background = "#CB4042";
  4423. } else {
  4424. action.style.background = "#AAA";
  4425. }
  4426. }
  4427.  
  4428. // 加载标记和猎巫
  4429. if (tags) {
  4430. const witchHunt = item.nFilter.witchHunt || [];
  4431.  
  4432. const list = [
  4433. ...(user
  4434. ? user.tags
  4435. .map((id) => tagModule.get({ id }))
  4436. .filter((tag) => tag !== null)
  4437. .map((tag) => tagModule.format(tag.id)) || []
  4438. : []),
  4439. ...Object.values(witchHuntModule.list())
  4440. .filter(({ fid }) => witchHunt.includes(fid))
  4441. .map(({ label, color }) =>
  4442. tagModule.format(null, label, color)
  4443. ),
  4444. ];
  4445.  
  4446. tags.style.display = list.length ? "" : "none";
  4447. tags.innerHTML = list.join("");
  4448. }
  4449.  
  4450. // 恢复显示
  4451. // 楼层的遮罩模式下仍需隐藏
  4452. if (filterMode !== "遮罩") {
  4453. container.style.removeProperty("display");
  4454. }
  4455. }
  4456. })();
  4457.  
  4458. // 猎巫会影响效率,待猎巫结果出来后再次过滤
  4459. witchHuntModule.run(item.nFilter);
  4460. };
  4461.  
  4462. // 绑定操作按钮事件
  4463. (() => {
  4464. if (action) {
  4465. // 隐藏匿名操作按钮
  4466. if (uid <= 0) {
  4467. action.style.display = "none";
  4468. return;
  4469. }
  4470.  
  4471. action.innerHTML = `屏蔽`;
  4472. action.onclick = (e) => {
  4473. const user = userModule.get(uid);
  4474.  
  4475. if (e.ctrlKey === false) {
  4476. userModule.view.details(uid, username, execute);
  4477. return;
  4478. }
  4479.  
  4480. if (user) {
  4481. userModule.remove(uid);
  4482. } else {
  4483. userModule.add(uid, username, [], filterModule.defaultMode);
  4484. }
  4485.  
  4486. execute();
  4487. };
  4488. }
  4489. })();
  4490.  
  4491. // 绑定事件
  4492. item.nFilter = {
  4493. pid,
  4494. uid,
  4495. username,
  4496. container,
  4497. title,
  4498. author,
  4499. subject,
  4500. content: content.innerText,
  4501. execute,
  4502. };
  4503. }
  4504.  
  4505. // 等待过滤完成
  4506. await item.nFilter.execute();
  4507. };
  4508.  
  4509. // 加载 UI
  4510. const loadUI = () => {
  4511. // 右上角菜单
  4512. const result = (() => {
  4513. let window;
  4514.  
  4515. return ui.create(() => {
  4516. if (window === undefined) {
  4517. window = commonui.createCommmonWindow();
  4518. }
  4519.  
  4520. window._.addContent(null);
  4521. window._.addTitle(`屏蔽`);
  4522. window._.addContent(ui.content);
  4523. window._.show();
  4524. });
  4525. })();
  4526.  
  4527. // 加载失败
  4528. if (result === false) {
  4529. return;
  4530. }
  4531.  
  4532. // 模块
  4533. ui.addModule("列表", listModule.view).toggle();
  4534. ui.addModule("用户", userModule.view);
  4535. ui.addModule("标记", tagModule.view);
  4536. ui.addModule("关键字", keywordModule.view);
  4537. ui.addModule("属地", locationModule.view);
  4538. ui.addModule("猎巫", witchHuntModule.view);
  4539. ui.addModule("通用设置", commonModule.view);
  4540.  
  4541. // 绑定列表更新回调
  4542. listModule.bindCallback(ui.update);
  4543. };
  4544.  
  4545. // 处理 mainMenu 模块
  4546. const handleMenu = () => {
  4547. let init = menuModule.init;
  4548.  
  4549. // 劫持 init 函数,这个函数完成后才能添加 UI
  4550. Object.defineProperty(menuModule, "init", {
  4551. get: () => {
  4552. return (...args) => {
  4553. // 等待执行完毕
  4554. init.apply(menuModule, args);
  4555.  
  4556. // 加载 UI
  4557. loadUI();
  4558. };
  4559. },
  4560. set: (value) => {
  4561. init = value;
  4562. },
  4563. });
  4564.  
  4565. // 如果已经有模块,则直接加载 UI
  4566. if (init) {
  4567. loadUI();
  4568. }
  4569. };
  4570.  
  4571. // 处理 topicArg 模块
  4572. const handleTopicModule = async () => {
  4573. let add = topicModule.add;
  4574.  
  4575. // 劫持 add 函数,这是泥潭的主题添加事件
  4576. Object.defineProperty(topicModule, "add", {
  4577. get: () => {
  4578. return async (...args) => {
  4579. // 主题 ID
  4580. const tid = args[8];
  4581.  
  4582. // 先直接隐藏,等过滤完毕后再放出来
  4583. (() => {
  4584. // 主题标题
  4585. const title = document.getElementById(args[1]);
  4586.  
  4587. // 主题容器
  4588. const container = title.closest("tr");
  4589.  
  4590. // 隐藏元素
  4591. container.style.display = "none";
  4592. })();
  4593.  
  4594. // 加入列表
  4595. add.apply(topicModule, args);
  4596.  
  4597. // 找到对应数据
  4598. const topic = topicModule.data.find((item) => item[8] === tid);
  4599.  
  4600. // 开始过滤
  4601. await filterTopic(topic);
  4602. };
  4603. },
  4604. set: (value) => {
  4605. add = value;
  4606. },
  4607. });
  4608.  
  4609. // 如果已经有数据,则直接过滤
  4610. if (topicModule.data) {
  4611. await Promise.all(Object.values(topicModule.data).map(filterTopic));
  4612. }
  4613. };
  4614.  
  4615. // 处理 postArg 模块
  4616. const handleReplyModule = async () => {
  4617. let proc = replyModule.proc;
  4618.  
  4619. // 劫持 proc 函数,这是泥潭的回复添加事件
  4620. Object.defineProperty(replyModule, "proc", {
  4621. get: () => {
  4622. return async (...args) => {
  4623. // 楼层号
  4624. const index = args[0];
  4625.  
  4626. // 先直接隐藏,等过滤完毕后再放出来
  4627. (() => {
  4628. // 判断是否是楼层
  4629. const isFloor = typeof index === "number";
  4630.  
  4631. // 评论额外标签
  4632. const prefix = isFloor ? "" : "comment";
  4633.  
  4634. // 用户容器
  4635. const uInfoC = document.querySelector(
  4636. `#${prefix}posterinfo${index}`
  4637. );
  4638.  
  4639. // 回复容器
  4640. const container = isFloor
  4641. ? uInfoC.closest("tr")
  4642. : uInfoC.closest(".comment_c");
  4643.  
  4644. // 隐藏元素
  4645. container.style.display = "none";
  4646. })();
  4647.  
  4648. // 加入列表
  4649. proc.apply(replyModule, args);
  4650.  
  4651. // 找到对应数据
  4652. const reply = replyModule.data[index];
  4653.  
  4654. // 开始过滤
  4655. await filterReply(reply);
  4656. };
  4657. },
  4658. set: (value) => {
  4659. proc = value;
  4660. },
  4661. });
  4662.  
  4663. // 如果已经有数据,则直接过滤
  4664. if (replyModule.data) {
  4665. await Promise.all(Object.values(replyModule.data).map(filterReply));
  4666. }
  4667. };
  4668.  
  4669. // 处理 commonui 模块
  4670. const handleCommonui = () => {
  4671. // 监听 mainMenu 模块,UI 需要等待这个模块加载完成
  4672. (() => {
  4673. if (commonui.mainMenu) {
  4674. menuModule = commonui.mainMenu;
  4675.  
  4676. handleMenu();
  4677. return;
  4678. }
  4679.  
  4680. Object.defineProperty(commonui, "mainMenu", {
  4681. get: () => menuModule,
  4682. set: (value) => {
  4683. menuModule = value;
  4684.  
  4685. handleMenu();
  4686. },
  4687. });
  4688. })();
  4689.  
  4690. // 监听 topicArg 模块,这是泥潭的主题入口
  4691. (() => {
  4692. if (commonui.topicArg) {
  4693. topicModule = commonui.topicArg;
  4694.  
  4695. handleTopicModule();
  4696. return;
  4697. }
  4698.  
  4699. Object.defineProperty(commonui, "topicArg", {
  4700. get: () => topicModule,
  4701. set: (value) => {
  4702. topicModule = value;
  4703.  
  4704. handleTopicModule();
  4705. },
  4706. });
  4707. })();
  4708.  
  4709. // 监听 postArg 模块,这是泥潭的回复入口
  4710. (() => {
  4711. if (commonui.postArg) {
  4712. replyModule = commonui.postArg;
  4713.  
  4714. handleReplyModule();
  4715. return;
  4716. }
  4717.  
  4718. Object.defineProperty(commonui, "postArg", {
  4719. get: () => replyModule,
  4720. set: (value) => {
  4721. replyModule = value;
  4722.  
  4723. handleReplyModule();
  4724. },
  4725. });
  4726. })();
  4727. };
  4728.  
  4729. // 前置过滤
  4730. const handlePreFilter = () => {
  4731. // 监听 commonui 模块,这是泥潭的主入口
  4732. (() => {
  4733. if (unsafeWindow.commonui) {
  4734. commonui = unsafeWindow.commonui;
  4735.  
  4736. handleCommonui();
  4737. return;
  4738. }
  4739.  
  4740. Object.defineProperty(unsafeWindow, "commonui", {
  4741. get: () => commonui,
  4742. set: (value) => {
  4743. commonui = value;
  4744.  
  4745. handleCommonui();
  4746. },
  4747. });
  4748. })();
  4749. };
  4750.  
  4751. // 普通过滤
  4752. const handleFilter = () => {
  4753. const runFilter = async () => {
  4754. if (topicModule) {
  4755. await Promise.all(
  4756. Object.values(topicModule.data).map((item) => {
  4757. if (item.executed) {
  4758. return;
  4759. }
  4760.  
  4761. item.executed = true;
  4762.  
  4763. filterTopic(item);
  4764. })
  4765. );
  4766. }
  4767.  
  4768. if (replyModule) {
  4769. await Promise.all(
  4770. Object.values(replyModule.data).map((item) => {
  4771. if (item.executed) {
  4772. return;
  4773. }
  4774.  
  4775. item.executed = true;
  4776.  
  4777. filterReply(item);
  4778. })
  4779. );
  4780. }
  4781. };
  4782.  
  4783. const hookFunction = (object, functionName, callback) => {
  4784. ((originalFunction) => {
  4785. object[functionName] = function () {
  4786. const returnValue = originalFunction.apply(this, arguments);
  4787.  
  4788. callback.apply(this, [returnValue, originalFunction, arguments]);
  4789.  
  4790. return returnValue;
  4791. };
  4792. })(object[functionName]);
  4793. };
  4794.  
  4795. const hook = () => {
  4796. (() => {
  4797. if (topicModule) {
  4798. return;
  4799. }
  4800.  
  4801. if (commonui.topicArg) {
  4802. topicModule = commonui.topicArg;
  4803.  
  4804. hookFunction(topicModule, "add", runFilter);
  4805. }
  4806. })();
  4807.  
  4808. (() => {
  4809. if (replyModule) {
  4810. return;
  4811. }
  4812.  
  4813. if (commonui.postArg) {
  4814. replyModule = commonui.postArg;
  4815.  
  4816. hookFunction(replyModule, "add", runFilter);
  4817. }
  4818. })();
  4819. };
  4820.  
  4821. hook();
  4822. runFilter();
  4823.  
  4824. hookFunction(commonui, "eval", hook);
  4825. };
  4826.  
  4827. // 主函数
  4828. (() => {
  4829. // 前置过滤
  4830. if (preFilter) {
  4831. handlePreFilter();
  4832. return;
  4833. }
  4834.  
  4835. // 等待页面加载完毕后过滤
  4836. unsafeWindow.addEventListener("load", () => {
  4837. if (unsafeWindow.commonui === undefined) {
  4838. return;
  4839. }
  4840.  
  4841. commonui = unsafeWindow.commonui;
  4842.  
  4843. menuModule = commonui.mainMenu;
  4844.  
  4845. loadUI();
  4846. handleFilter();
  4847. });
  4848. })();
  4849. })();