NGA Filter

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

当前为 2024-01-03 提交的版本,查看 最新版本

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