NGA Filter

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

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

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 1.13.0
  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.  
  18. // @noframes
  19. // ==/UserScript==
  20.  
  21. ((n, self) => {
  22. if (n === undefined) return;
  23.  
  24. // KEY
  25. const DATA_KEY = "NGAFilter";
  26. const USER_AGENT_KEY = "USER_AGENT_KEY";
  27.  
  28. // User Agent
  29. const USER_AGENT = (() => {
  30. const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official";
  31.  
  32. GM_registerMenuCommand(`修改UA${data}`, () => {
  33. const value = prompt("修改UA", data);
  34.  
  35. if (value) {
  36. GM_setValue(USER_AGENT_KEY, value);
  37.  
  38. location.reload();
  39. }
  40. });
  41.  
  42. return data;
  43. })();
  44.  
  45. // 简单的统一请求
  46. const request = (url, config = {}) =>
  47. fetch(url, {
  48. headers: {
  49. "X-User-Agent": USER_AGENT,
  50. },
  51. ...config,
  52. });
  53.  
  54. // 过滤提示
  55. const FILTER_TIPS =
  56. "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承 &gt; 显示<br/>相同类型按最高级别过滤";
  57.  
  58. // 过滤方式
  59. const FILTER_MODE = ["继承", "标记", "遮罩", "隐藏", "显示"];
  60.  
  61. // 切换过滤方式
  62. const switchFilterMode = (value) => {
  63. const next = FILTER_MODE.indexOf(value) + 1;
  64.  
  65. if (next >= FILTER_MODE.length) {
  66. return FILTER_MODE[0];
  67. }
  68.  
  69. return FILTER_MODE[next];
  70. };
  71.  
  72. // 数据
  73. const data = (() => {
  74. const d = {
  75. tags: {},
  76. users: {},
  77. keywords: {},
  78. locations: {},
  79. options: {
  80. filterRegdateLimit: 0,
  81. filterPostnumLimit: 0,
  82. filterTopicRateLimit: 100,
  83. filterReputationLimit: NaN,
  84. filterAnony: false,
  85. filterMode: "隐藏",
  86. },
  87. };
  88.  
  89. const v = GM_getValue(DATA_KEY);
  90.  
  91. if (typeof v !== "object") {
  92. return d;
  93. }
  94.  
  95. return Object.assign(d, v);
  96. })();
  97.  
  98. // 保存数据
  99. const saveData = () => {
  100. GM_setValue(DATA_KEY, data);
  101. };
  102.  
  103. // 增加标记
  104. const addTag = (name) => {
  105. const tag = Object.values(data.tags).find((item) => item.name === name);
  106.  
  107. if (tag) return tag.id;
  108.  
  109. const id =
  110. Math.max(...Object.values(data.tags).map((item) => item.id), 0) + 1;
  111.  
  112. const hash = (() => {
  113. let h = 5381;
  114. for (var i = 0; i < name.length; i++) {
  115. h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
  116. }
  117. return h;
  118. })();
  119.  
  120. const hex = Math.abs(hash).toString(16) + "000000";
  121.  
  122. const hsv = [
  123. `0x${hex.substring(2, 4)}` / 255,
  124. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  125. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  126. ];
  127.  
  128. const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  129.  
  130. const color = ["#", ...rgb].reduce((a, b) => {
  131. return a + ("0" + b.toString(16)).slice(-2);
  132. });
  133.  
  134. data.tags[id] = {
  135. id,
  136. name,
  137. color,
  138. filterMode: FILTER_MODE[0],
  139. };
  140.  
  141. saveData();
  142.  
  143. return id;
  144. };
  145.  
  146. // 增加用户
  147. const addUser = (id, name = null, tags = [], filterMode = FILTER_MODE[0]) => {
  148. if (data.users[id]) return data.users[id];
  149.  
  150. data.users[id] = {
  151. id,
  152. name,
  153. tags,
  154. filterMode,
  155. };
  156.  
  157. saveData();
  158.  
  159. return data.users[id];
  160. };
  161.  
  162. // 增加关键字
  163. const addKeyword = (
  164. keyword,
  165. filterMode = FILTER_MODE[0],
  166. filterLevel = 0
  167. ) => {
  168. const id =
  169. Math.max(...Object.values(data.keywords).map((item) => item.id), 0) + 1;
  170.  
  171. data.keywords[id] = {
  172. id,
  173. keyword,
  174. filterMode,
  175. filterLevel,
  176. };
  177.  
  178. saveData();
  179.  
  180. return id;
  181. };
  182.  
  183. // 增加属地
  184. const addLocation = (keyword, filterMode = FILTER_MODE[0]) => {
  185. const id =
  186. Math.max(...Object.values(data.locations).map((item) => item.id), 0) + 1;
  187.  
  188. data.locations[id] = {
  189. id,
  190. keyword,
  191. filterMode,
  192. };
  193.  
  194. saveData();
  195.  
  196. return id;
  197. };
  198.  
  199. // 旧版本数据迁移
  200. {
  201. const dataKey = "troll_data";
  202. const modeKey = "troll_mode";
  203. const keywordKey = "troll_keyword";
  204.  
  205. if (localStorage.getItem(dataKey)) {
  206. let trollMap = (function () {
  207. try {
  208. return JSON.parse(localStorage.getItem(dataKey)) || {};
  209. } catch (e) {}
  210.  
  211. return {};
  212. })();
  213.  
  214. let filterMode = ~~localStorage.getItem(modeKey);
  215.  
  216. let filterKeyword = localStorage.getItem(keywordKey) || "";
  217.  
  218. // 整理标签
  219. [...new Set(Object.values(trollMap).flat())].forEach((item) =>
  220. addTag(item)
  221. );
  222.  
  223. // 整理用户
  224. Object.keys(trollMap).forEach((item) => {
  225. addUser(
  226. item,
  227. null,
  228. (typeof trollMap[item] === "object" ? trollMap[item] : []).map(
  229. (tag) => addTag(tag)
  230. )
  231. );
  232. });
  233.  
  234. data.options.filterMode = filterMode ? "隐藏" : "标记";
  235. data.options.keyword = filterKeyword;
  236.  
  237. localStorage.removeItem(dataKey);
  238. localStorage.removeItem(modeKey);
  239. localStorage.removeItem(keywordKey);
  240.  
  241. saveData();
  242. }
  243.  
  244. // v1.1.0 -> v1.1.1
  245. {
  246. Object.values(data.users).forEach(({ id, name, tags, enabled }) => {
  247. if (enabled !== undefined) {
  248. data.users[id] = {
  249. id,
  250. name,
  251. tags,
  252. filterMode: enabled ? "继承" : "显示",
  253. };
  254. }
  255. });
  256.  
  257. Object.values(data.tags).forEach(({ id, name, color, enabled }) => {
  258. if (enabled !== undefined) {
  259. data.tags[id] = {
  260. id,
  261. name,
  262. color,
  263. filterMode: enabled ? "继承" : "显示",
  264. };
  265. }
  266. });
  267.  
  268. if (data.options.filterMode === 0) {
  269. data.options.filterMode = "隐藏";
  270. } else if (data.options.filterMode === 1) {
  271. data.options.filterMode = "标记";
  272. }
  273.  
  274. saveData();
  275. }
  276.  
  277. // v1.2.x -> v1.3.0
  278. {
  279. if (data.options.keyword) {
  280. addKeyword(data.options.keyword);
  281.  
  282. delete data.options.keyword;
  283.  
  284. saveData();
  285. }
  286. }
  287. }
  288.  
  289. // 编辑用户标记
  290. const editUser = (() => {
  291. let window;
  292. return (uid, name, callback) => {
  293. if (window === undefined) {
  294. window = n.createCommmonWindow();
  295. }
  296.  
  297. const user = data.users[uid];
  298.  
  299. const content = document.createElement("div");
  300.  
  301. const size = Math.floor((screen.width * 0.8) / 200);
  302.  
  303. const items = Object.values(data.tags).map(
  304. (tag, index) => `
  305. <td class="c1">
  306. <label for="s-tag-${index}" style="display: block; cursor: pointer;">
  307. <b class="block_txt nobr" style="background:${
  308. tag.color
  309. }; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>
  310. </label>
  311. </td>
  312. <td class="c2" width="1">
  313. <input id="s-tag-${index}" type="checkbox" value="${tag.id}" ${
  314. user && user.tags.find((item) => item === tag.id) && "checked"
  315. }/>
  316. </td>
  317. `
  318. );
  319.  
  320. const rows = [...new Array(Math.ceil(items.length / size))].map(
  321. (item, index) =>
  322. `
  323. <tr class="row${(index % 2) + 1}">
  324. ${items.slice(size * index, size * (index + 1)).join("")}
  325. </tr>
  326. `
  327. );
  328.  
  329. content.className = "w100";
  330. content.innerHTML = `
  331. <div class="filter-table-wrapper" style="width: 80vw;">
  332. <table class="filter-table forumbox">
  333. <tbody>
  334. ${rows.join("")}
  335. </tbody>
  336. </table>
  337. </div>
  338. <div style="margin: 10px 0;">
  339. <input placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
  340. </div>
  341. <div style="margin: 10px 0;">
  342. <span>过滤方式:</span>
  343. <button>${(user && user.filterMode) || FILTER_MODE[0]}</button>
  344. <div class="right_">
  345. <button>删除</button>
  346. <button>保存</button>
  347. </div>
  348. </div>
  349. <div class="silver" style="margin-top: 5px;">${FILTER_TIPS}</div>
  350. `;
  351.  
  352. const actions = content.getElementsByTagName("button");
  353.  
  354. actions[0].onclick = () => {
  355. actions[0].innerText = switchFilterMode(
  356. actions[0].innerText || FILTER_MODE[0]
  357. );
  358. };
  359.  
  360. actions[1].onclick = () => {
  361. if (confirm("是否确认?")) {
  362. delete data.users[uid];
  363.  
  364. saveData();
  365. runFilter();
  366.  
  367. callback && callback();
  368.  
  369. window._.hide();
  370. }
  371. };
  372.  
  373. actions[2].onclick = () => {
  374. if (confirm("是否确认?")) {
  375. const values = [...content.getElementsByTagName("input")];
  376. const newTags = values[values.length - 1].value
  377. .split("|")
  378. .filter((item) => item.length)
  379. .map((item) => addTag(item));
  380. const tags = [
  381. ...new Set(
  382. values
  383. .filter((item) => item.type === "checkbox" && item.checked)
  384. .map((item) => ~~item.value)
  385. .concat(newTags)
  386. ),
  387. ].sort();
  388.  
  389. if (user) {
  390. user.tags = tags;
  391. user.filterMode = actions[0].innerText;
  392. } else {
  393. addUser(uid, name, tags, actions[0].innerText);
  394. }
  395.  
  396. saveData();
  397. runFilter();
  398.  
  399. callback && callback();
  400.  
  401. window._.hide();
  402. }
  403. };
  404.  
  405. if (user === undefined) {
  406. actions[1].style = "display: none;";
  407. }
  408.  
  409. window._.addContent(null);
  410. window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
  411. window._.addContent(content);
  412. window._.show();
  413. };
  414. })();
  415.  
  416. // 猎巫
  417. const witchHunter = (() => {
  418. const key = "WITCH_HUNTER";
  419.  
  420. const data = GM_getValue(key) || {};
  421.  
  422. const add = async (fid, label) => {
  423. if (Object.values(data).find((item) => item.fid === fid)) {
  424. alert("已有相同版面ID");
  425. return;
  426. }
  427.  
  428. const info = await new Promise((resolve) => {
  429. request(`/thread.php?lite=js&fid=${fid}`)
  430. .then((res) => res.blob())
  431. .then((blob) => {
  432. const reader = new FileReader();
  433.  
  434. reader.onload = () => {
  435. const text = reader.result;
  436. const result = JSON.parse(
  437. text.replace("window.script_muti_get_var_store=", "")
  438. );
  439.  
  440. resolve(result.data);
  441. };
  442.  
  443. reader.readAsText(blob, "GBK");
  444. })
  445. .catch(() => {
  446. resolve({});
  447. });
  448. });
  449.  
  450. if (info.__F === undefined) {
  451. alert("版面ID有误");
  452. return;
  453. }
  454.  
  455. const name = info.__F.name;
  456.  
  457. const id = Math.max(...Object.values(data).map((item) => item.id), 0) + 1;
  458.  
  459. const hash = (() => {
  460. let h = 5381;
  461. for (var i = 0; i < label.length; i++) {
  462. h = ((h << 5) + h + label.charCodeAt(i)) & 0xffffffff;
  463. }
  464. return h;
  465. })();
  466.  
  467. const hex = Math.abs(hash).toString(16) + "000000";
  468.  
  469. const hsv = [
  470. `0x${hex.substring(2, 4)}` / 255,
  471. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  472. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  473. ];
  474.  
  475. const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  476.  
  477. const color = ["#", ...rgb].reduce((a, b) => {
  478. return a + ("0" + b.toString(16)).slice(-2);
  479. });
  480.  
  481. data[id] = {
  482. id,
  483. fid,
  484. name,
  485. label,
  486. color,
  487. };
  488.  
  489. GM_setValue(key, data);
  490. };
  491.  
  492. const remove = (id) => {
  493. delete data[id];
  494.  
  495. GM_setValue(key, data);
  496. };
  497.  
  498. const run = (uid, element) => {
  499. if (uid < 0) {
  500. return;
  501. }
  502.  
  503. Promise.all(
  504. Object.values(data).map(async (item) => {
  505. const api = `/thread.php?lite=js&fid=${item.fid}&authorid=${uid}`;
  506.  
  507. const verify =
  508. (await new Promise((resolve) => {
  509. request(api)
  510. .then((res) => res.blob())
  511. .then((blob) => {
  512. const reader = new FileReader();
  513.  
  514. reader.onload = () => {
  515. const text = reader.result;
  516. const result = JSON.parse(
  517. text.replace("window.script_muti_get_var_store=", "")
  518. );
  519.  
  520. if (result.error) {
  521. resolve(false);
  522. return;
  523. }
  524.  
  525. resolve(true);
  526. };
  527.  
  528. reader.readAsText(blob, "GBK");
  529. })
  530. .catch(() => {
  531. resolve(false);
  532. });
  533. })) ||
  534. (await new Promise((resolve) => {
  535. request(`${api}&searchpost=1`)
  536. .then((res) => res.blob())
  537. .then((blob) => {
  538. const reader = new FileReader();
  539.  
  540. reader.onload = () => {
  541. const text = reader.result;
  542. const result = JSON.parse(
  543. text.replace("window.script_muti_get_var_store=", "")
  544. );
  545.  
  546. if (result.error) {
  547. resolve(false);
  548. return;
  549. }
  550.  
  551. resolve(true);
  552. };
  553.  
  554. reader.readAsText(blob, "GBK");
  555. })
  556. .catch(() => {
  557. resolve(false);
  558. });
  559. }));
  560.  
  561. if (verify) {
  562. return item;
  563. }
  564. })
  565. )
  566. .then((res) => res.filter((item) => item))
  567. .then((res) => {
  568. res
  569. .filter(
  570. (current, index) =>
  571. res.findIndex((item) => item.label === current.label) === index
  572. )
  573. .forEach((item) => {
  574. element.style.display = "block";
  575. element.innerHTML += `<b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>`;
  576. });
  577. });
  578. };
  579.  
  580. return {
  581. add,
  582. remove,
  583. run,
  584. data,
  585. };
  586. })();
  587.  
  588. // 获取主题数量
  589. const getTopicNum = (() => {
  590. const key = "TOPIC_NUM_CACHE";
  591.  
  592. const cache = GM_getValue(key) || {};
  593.  
  594. const cacheTime = 60 * 60 * 1000;
  595.  
  596. const headKey = Object.keys(cache)[0];
  597.  
  598. if (headKey) {
  599. const timestamp = cache[headKey].timestamp;
  600.  
  601. if (timestamp + 24 * 60 * 60 * 1000 < new Date().getTime()) {
  602. const keys = Object.keys(cache);
  603.  
  604. for (const key of keys) {
  605. delete cache[key];
  606. }
  607.  
  608. GM_setValue(key, {});
  609. }
  610. }
  611.  
  612. return async (uid) => {
  613. if (
  614. cache[uid] &&
  615. cache[uid].timestamp + cacheTime > new Date().getTime()
  616. ) {
  617. return cache[uid].count;
  618. }
  619.  
  620. const api = `/thread.php?lite=js&authorid=${uid}`;
  621.  
  622. const { __ROWS } = await new Promise((resolve) => {
  623. request(api)
  624. .then((res) => res.blob())
  625. .then((blob) => {
  626. const reader = new FileReader();
  627.  
  628. reader.onload = () => {
  629. const text = reader.result;
  630. const result = JSON.parse(
  631. text.replace("window.script_muti_get_var_store=", "")
  632. );
  633.  
  634. resolve(result.data);
  635. };
  636.  
  637. reader.readAsText(blob, "GBK");
  638. })
  639. .catch(() => {
  640. resolve({});
  641. });
  642. });
  643.  
  644. if (__ROWS > 100) {
  645. cache[uid] = {
  646. count: __ROWS,
  647. timestamp: new Date().getTime(),
  648. };
  649.  
  650. GM_setValue(key, cache);
  651. }
  652.  
  653. return __ROWS;
  654. };
  655. })();
  656.  
  657. // 获取顶楼用户信息、声望
  658. const getUserInfoAndReputation = (tid, pid) =>
  659. new Promise((resolve, reject) => {
  660. if (tid === undefined && pid === undefined) {
  661. reject();
  662. return;
  663. }
  664.  
  665. const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
  666.  
  667. // 请求数据
  668. request(api)
  669. .then((res) => res.blob())
  670. .then((blob) => {
  671. const getLastIndex = (content, position) => {
  672. if (position >= 0) {
  673. let nextIndex = position + 1;
  674.  
  675. while (nextIndex < content.length) {
  676. if (content[nextIndex] === "}") {
  677. return nextIndex;
  678. }
  679.  
  680. if (content[nextIndex] === "{") {
  681. nextIndex = getLastIndex(content, nextIndex);
  682.  
  683. if (nextIndex < 0) {
  684. break;
  685. }
  686. }
  687.  
  688. nextIndex = nextIndex + 1;
  689. }
  690. }
  691.  
  692. return -1;
  693. };
  694.  
  695. const reader = new FileReader();
  696.  
  697. reader.onload = async () => {
  698. const parser = new DOMParser();
  699.  
  700. const doc = parser.parseFromString(reader.result, "text/html");
  701.  
  702. const html = doc.body.innerHTML;
  703.  
  704. // 验证帖子正常
  705. const verify = doc.querySelector("#m_posts");
  706.  
  707. if (verify) {
  708. // 取得顶楼 UID
  709. const uid = (() => {
  710. const ele = doc.querySelector("#postauthor0");
  711.  
  712. if (ele) {
  713. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  714.  
  715. if (res) {
  716. return res[1];
  717. }
  718. }
  719.  
  720. return 0;
  721. })();
  722.  
  723. // 取得顶楼标题
  724. const subject = doc.querySelector("#postsubject0").innerHTML;
  725.  
  726. // 取得顶楼内容
  727. const content = doc.querySelector("#postcontent0").innerHTML;
  728.  
  729. // 非匿名用户
  730. if (uid && uid > 0) {
  731. // 取得用户信息
  732. const userInfo = (() => {
  733. // 起始JSON
  734. const str = `"${uid}":{`;
  735.  
  736. // 起始下标
  737. const index = html.indexOf(str) + str.length;
  738.  
  739. // 结尾下标
  740. const lastIndex = getLastIndex(html, index);
  741.  
  742. if (lastIndex >= 0) {
  743. try {
  744. return JSON.parse(
  745. `{${html.substring(index, lastIndex)}}`
  746. );
  747. } catch {}
  748. }
  749.  
  750. return null;
  751. })();
  752.  
  753. // 取得用户声望
  754. const reputation = (() => {
  755. const reputations = (() => {
  756. // 起始JSON
  757. const str = `"__REPUTATIONS":{`;
  758.  
  759. // 起始下标
  760. const index = html.indexOf(str) + str.length;
  761.  
  762. // 结尾下标
  763. const lastIndex = getLastIndex(html, index);
  764.  
  765. if (lastIndex >= 0) {
  766. return JSON.parse(
  767. `{${html.substring(index, lastIndex)}}`
  768. );
  769. }
  770.  
  771. return null;
  772. })();
  773.  
  774. if (reputations) {
  775. for (let fid in reputations) {
  776. return reputations[fid][uid] || 0;
  777. }
  778. }
  779.  
  780. return NaN;
  781. })();
  782.  
  783. resolve({
  784. uid,
  785. subject,
  786. content,
  787. userInfo,
  788. reputation,
  789. });
  790. return;
  791. }
  792.  
  793. resolve({
  794. uid,
  795. subject,
  796. content,
  797. });
  798. } else {
  799. reject();
  800. }
  801. };
  802.  
  803. reader.readAsText(blob, "GBK");
  804. })
  805. .catch(() => {
  806. reject();
  807. });
  808. });
  809.  
  810. // 获取过滤方式
  811. const getFilterMode = async (item) => {
  812. // 声明结果
  813. const result = {
  814. mode: -1,
  815. reason: ``,
  816. };
  817.  
  818. // 获取 UID
  819. const { uid } = item;
  820.  
  821. // 是自己则跳过
  822. if (uid === self) {
  823. return "";
  824. }
  825.  
  826. // 用户过滤
  827. (() => {
  828. // 获取屏蔽列表里匹配的用户
  829. const user = data.users[uid];
  830.  
  831. // 没有则跳过
  832. if (user === undefined) {
  833. return;
  834. }
  835.  
  836. const { filterMode } = user;
  837.  
  838. const mode = FILTER_MODE.indexOf(filterMode) || 0;
  839.  
  840. // 低于当前的过滤模式则跳过
  841. if (mode <= result.mode) {
  842. return;
  843. }
  844.  
  845. // 更新过滤模式和原因
  846. result.mode = mode;
  847. result.reason = `用户模式: ${filterMode}`;
  848. })();
  849.  
  850. // 标记过滤
  851. (() => {
  852. // 获取屏蔽列表里匹配的用户
  853. const user = data.users[uid];
  854.  
  855. // 获取用户对应的标记,并跳过低于当前的过滤模式
  856. const tags = user
  857. ? user.tags
  858. .map((i) => data.tags[i])
  859. .filter(
  860. (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
  861. )
  862. : [];
  863.  
  864. // 没有则跳过
  865. if (tags.length === 0) {
  866. return;
  867. }
  868.  
  869. // 取最高的过滤模式
  870. const { filterMode, name } = tags.sort(
  871. (a, b) =>
  872. (FILTER_MODE.indexOf(b.filterMode) || 0) -
  873. (FILTER_MODE.indexOf(a.filterMode) || 0)
  874. )[0];
  875.  
  876. const mode = FILTER_MODE.indexOf(filterMode) || 0;
  877.  
  878. // 更新过滤模式和原因
  879. result.mode = mode;
  880. result.reason = `标记: ${name}`;
  881. })();
  882.  
  883. // 关键词过滤
  884. await (async () => {
  885. const { getContent } = item;
  886.  
  887. // 获取设置里的关键词列表,并跳过低于当前的过滤模式
  888. const keywords = Object.values(data.keywords).filter(
  889. (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
  890. );
  891.  
  892. // 没有则跳过
  893. if (keywords.length === 0) {
  894. return;
  895. }
  896.  
  897. // 根据过滤等级依次判断
  898. const list = keywords.sort(
  899. (a, b) =>
  900. (FILTER_MODE.indexOf(b.filterMode) || 0) -
  901. (FILTER_MODE.indexOf(a.filterMode) || 0)
  902. );
  903.  
  904. for (let i = 0; i < list.length; i += 1) {
  905. const { keyword, filterMode } = list[i];
  906.  
  907. // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
  908. const filterLevel = list[i].filterLevel || 0;
  909.  
  910. // 过滤标题
  911. if (filterLevel >= 0) {
  912. const { subject } = item;
  913.  
  914. const match = subject.match(keyword);
  915.  
  916. if (match) {
  917. const mode = FILTER_MODE.indexOf(filterMode) || 0;
  918.  
  919. // 更新过滤模式和原因
  920. result.mode = mode;
  921. result.reason = `关键词: ${match[0]}`;
  922. return;
  923. }
  924. }
  925.  
  926. // 过滤内容
  927. if (filterLevel >= 1) {
  928. // 如果没有内容,则请求
  929. const content = await (async () => {
  930. if (item.content === undefined) {
  931. await getContent().catch(() => {});
  932. }
  933.  
  934. return item.content || null;
  935. })();
  936.  
  937. if (content) {
  938. const match = content.match(keyword);
  939.  
  940. if (match) {
  941. const mode = FILTER_MODE.indexOf(filterMode) || 0;
  942.  
  943. // 更新过滤模式和原因
  944. result.mode = mode;
  945. result.reason = `关键词: ${match[0]}`;
  946. return;
  947. }
  948. }
  949. }
  950. }
  951. })();
  952.  
  953. // 杂项过滤
  954. // 放在属地前是因为符合条件的过多,没必要再请求它们的属地
  955. await (async () => {
  956. const { getUserInfo, getReputation } = item;
  957.  
  958. // 如果当前模式是显示,则跳过
  959. if (FILTER_MODE[result.mode] === "显示") {
  960. return;
  961. }
  962.  
  963. // 获取隐藏模式下标
  964. const mode = FILTER_MODE.indexOf("隐藏");
  965.  
  966. // 匿名
  967. if (uid <= 0) {
  968. const filterAnony = data.options.filterAnony;
  969.  
  970. if (filterAnony) {
  971. // 更新过滤模式和原因
  972. result.mode = mode;
  973. result.reason = "匿名";
  974. }
  975.  
  976. return;
  977. }
  978.  
  979. // 注册时间过滤
  980. await (async () => {
  981. const filterRegdateLimit = data.options.filterRegdateLimit || 0;
  982.  
  983. // 如果没有用户信息,则请求
  984. const userInfo = await (async () => {
  985. if (item.userInfo === undefined) {
  986. await getUserInfo().catch(() => {});
  987. }
  988.  
  989. return item.userInfo || {};
  990. })();
  991.  
  992. const { regdate } = userInfo;
  993.  
  994. if (regdate === undefined) {
  995. return;
  996. }
  997.  
  998. if (
  999. filterRegdateLimit > 0 &&
  1000. regdate * 1000 > new Date() - filterRegdateLimit
  1001. ) {
  1002. // 更新过滤模式和原因
  1003. result.mode = mode;
  1004. result.reason = `注册时间: ${new Date(
  1005. regdate * 1000
  1006. ).toLocaleDateString()}`;
  1007. return;
  1008. }
  1009. })();
  1010.  
  1011. // 发帖数量过滤
  1012. await (async () => {
  1013. const filterPostnumLimit = data.options.filterPostnumLimit || 0;
  1014.  
  1015. // 如果没有用户信息,则请求
  1016. const userInfo = await (async () => {
  1017. if (item.userInfo === undefined) {
  1018. await getUserInfo().catch(() => {});
  1019. }
  1020.  
  1021. return item.userInfo || {};
  1022. })();
  1023.  
  1024. const { postnum } = userInfo;
  1025.  
  1026. if (postnum === undefined) {
  1027. return;
  1028. }
  1029.  
  1030. if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
  1031. // 更新过滤模式和原因
  1032. result.mode = mode;
  1033. result.reason = `发帖数量: ${postnum}`;
  1034. return;
  1035. }
  1036. })();
  1037.  
  1038. // 发帖比例过滤
  1039. await (async () => {
  1040. const filterTopicRateLimit = data.options.filterTopicRateLimit || 100;
  1041.  
  1042. // 如果没有用户信息,则请求
  1043. const userInfo = await (async () => {
  1044. if (item.userInfo === undefined) {
  1045. await getUserInfo().catch(() => {});
  1046. }
  1047.  
  1048. return item.userInfo || {};
  1049. })();
  1050.  
  1051. const { postnum } = userInfo;
  1052.  
  1053. if (postnum === undefined) {
  1054. return;
  1055. }
  1056.  
  1057. if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) {
  1058. // 获取主题数量
  1059. const topicNum = await getTopicNum(uid);
  1060.  
  1061. // 计算发帖比例
  1062. const topicRate = (topicNum / postnum) * 100;
  1063.  
  1064. if (topicRate > filterTopicRateLimit) {
  1065. // 更新过滤模式和原因
  1066. result.mode = mode;
  1067. result.reason = `发帖比例: ${topicRate.toFixed(
  1068. 0
  1069. )}% (${topicNum}/${postnum})`;
  1070. return;
  1071. }
  1072. }
  1073. })();
  1074.  
  1075. // 版面声望过滤
  1076. await (async () => {
  1077. const filterReputationLimit = data.options.filterReputationLimit || NaN;
  1078.  
  1079. if (Number.isNaN(filterReputationLimit)) {
  1080. return;
  1081. }
  1082.  
  1083. // 如果没有版面声望,则请求
  1084. const reputation = await (async () => {
  1085. if (item.reputation === undefined) {
  1086. await getReputation().catch(() => {});
  1087. }
  1088.  
  1089. return item.reputation || NaN;
  1090. })();
  1091.  
  1092. if (reputation < filterReputationLimit) {
  1093. // 更新过滤模式和原因
  1094. result.mode = mode;
  1095. result.reason = `声望: ${reputation}`;
  1096. return;
  1097. }
  1098. })();
  1099. })();
  1100.  
  1101. // 属地过滤
  1102. await (async () => {
  1103. // 匿名用户则跳过
  1104. if (uid <= 0) {
  1105. return;
  1106. }
  1107.  
  1108. // 获取设置里的属地列表,并跳过低于当前的过滤模式
  1109. const locations = Object.values(data.locations).filter(
  1110. (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
  1111. );
  1112.  
  1113. // 没有则跳过
  1114. if (locations.length === 0) {
  1115. return;
  1116. }
  1117.  
  1118. // 请求属地
  1119. // TODO 应该类似 getContent 在另外的地方绑定请求方式
  1120. const { ipLoc } = await new Promise((resolve) => {
  1121. // 临时的缓存机制,避免单页多次重复请求
  1122. n.ipLocCache = n.ipLocCache || {};
  1123.  
  1124. if (n.ipLocCache[uid]) {
  1125. resolve(n.ipLocCache[uid]);
  1126. return;
  1127. }
  1128.  
  1129. // 发起请求
  1130. const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;
  1131.  
  1132. request(api)
  1133. .then((res) => res.blob())
  1134. .then((blob) => {
  1135. const reader = new FileReader();
  1136.  
  1137. reader.onload = () => {
  1138. const text = reader.result;
  1139. const result = JSON.parse(
  1140. text.replace("window.script_muti_get_var_store=", "")
  1141. );
  1142.  
  1143. const data = result.data[0] || {};
  1144.  
  1145. if (data.ipLoc) {
  1146. n.ipLocCache[uid] = data.ipLoc;
  1147. }
  1148.  
  1149. resolve(data);
  1150. };
  1151.  
  1152. reader.readAsText(blob, "GBK");
  1153. })
  1154. .catch(() => {
  1155. resolve({});
  1156. });
  1157. });
  1158.  
  1159. // 请求失败则跳过
  1160. if (ipLoc === undefined) {
  1161. return;
  1162. }
  1163.  
  1164. // 根据过滤等级依次判断
  1165. const list = locations.sort(
  1166. (a, b) =>
  1167. (FILTER_MODE.indexOf(b.filterMode) || 0) -
  1168. (FILTER_MODE.indexOf(a.filterMode) || 0)
  1169. );
  1170.  
  1171. for (let i = 0; i < list.length; i += 1) {
  1172. const { keyword, filterMode } = list[i];
  1173.  
  1174. const match = ipLoc.match(keyword);
  1175.  
  1176. if (match) {
  1177. const mode = FILTER_MODE.indexOf(filterMode) || 0;
  1178.  
  1179. // 更新过滤模式和原因
  1180. result.mode = mode;
  1181. result.reason = `属地: ${ipLoc}`;
  1182. return;
  1183. }
  1184. }
  1185. })();
  1186.  
  1187. if (result.mode === 0) {
  1188. result.mode = FILTER_MODE.indexOf(data.options.filterMode) || -1;
  1189. }
  1190.  
  1191. if (result.mode > 0) {
  1192. const { uid, username, tid, pid } = item;
  1193.  
  1194. const mode = FILTER_MODE[result.mode];
  1195.  
  1196. const reason = result.reason;
  1197.  
  1198. // 用户
  1199. const user = `<a href="${
  1200. uid > 0 ? `/nuke.php?func=ucp&uid=${uid}` : `javascript:void(0)`
  1201. }" class="b nobr">[${username ? "@" + username : "#" + uid}]</a>`;
  1202.  
  1203. // 移除 BR 标签
  1204. item.content = item.content.replace(/<br>/g, "");
  1205.  
  1206. // 主题
  1207. const subject = (() => {
  1208. if (tid) {
  1209. // 如果有 TID 但没有标题,是引用,采用内容逻辑
  1210. if (item.subject.length === 0) {
  1211. return `<a href="${`/read.php?tid=${tid}`}&nofilter">${
  1212. item.content
  1213. }</a>`;
  1214. }
  1215.  
  1216. return `<a href="${`/read.php?tid=${tid}`}&nofilter" title="${
  1217. item.content
  1218. }" class="b nobr">${item.subject}</a>`;
  1219. }
  1220.  
  1221. return item.subject;
  1222. })();
  1223.  
  1224. // 内容
  1225. const content = (() => {
  1226. if (pid) {
  1227. return `<a href="${`/read.php?pid=${pid}`}&nofilter">${
  1228. item.content
  1229. }</a>`;
  1230. }
  1231.  
  1232. return item.content;
  1233. })();
  1234.  
  1235. m.add({
  1236. user,
  1237. mode,
  1238. subject,
  1239. content,
  1240. reason,
  1241. });
  1242.  
  1243. return mode;
  1244. }
  1245.  
  1246. return "";
  1247. };
  1248.  
  1249. // 获取主题过滤方式
  1250. const getFilterModeByTopic = async ({ nFilter: topic }) => {
  1251. const { tid } = topic;
  1252.  
  1253. // 绑定额外的数据请求方式
  1254. if (topic.getContent === undefined) {
  1255. // 获取帖子内容,按需调用
  1256. const getTopic = () =>
  1257. new Promise((resolve, reject) => {
  1258. // 避免重复请求
  1259. // TODO 严格来说需要加入缓存,避免频繁请求
  1260. if (topic.content || topic.userInfo || topic.reputation) {
  1261. resolve(topic);
  1262. return;
  1263. }
  1264.  
  1265. // 请求并写入数据
  1266. getUserInfoAndReputation(tid, undefined)
  1267. .then(({ subject, content, userInfo, reputation }) => {
  1268. // 写入用户名
  1269. if (userInfo) {
  1270. topic.username = userInfo.username;
  1271. }
  1272.  
  1273. // 写入用户信息和声望
  1274. topic.userInfo = userInfo;
  1275. topic.reputation = reputation;
  1276.  
  1277. // 写入帖子标题和内容
  1278. topic.subject = subject;
  1279. topic.content = content;
  1280.  
  1281. // 返回结果
  1282. resolve(topic);
  1283. })
  1284. .catch(reject);
  1285. });
  1286.  
  1287. // 绑定请求方式
  1288. topic.getContent = getTopic;
  1289. topic.getUserInfo = getTopic;
  1290. topic.getReputation = getTopic;
  1291. }
  1292.  
  1293. // 获取过滤模式
  1294. const filterMode = await getFilterMode(topic);
  1295.  
  1296. // 返回结果
  1297. return filterMode;
  1298. };
  1299.  
  1300. // 获取回复过滤方式
  1301. const getFilterModeByReply = async ({ nFilter: reply }) => {
  1302. const { tid, pid, uid } = reply;
  1303.  
  1304. // 回复页面可以直接获取到用户信息和声望
  1305. if (uid > 0) {
  1306. // 取得用户信息
  1307. const userInfo = n.userInfo.users[uid];
  1308.  
  1309. // 取得用户声望
  1310. const reputation = (() => {
  1311. const reputations = n.userInfo.reputations;
  1312.  
  1313. if (reputations) {
  1314. for (let fid in reputations) {
  1315. return reputations[fid][uid] || 0;
  1316. }
  1317. }
  1318.  
  1319. return NaN;
  1320. })();
  1321.  
  1322. // 写入用户信息和声望
  1323. reply.userInfo = userInfo;
  1324. reply.reputation = reputation;
  1325. }
  1326.  
  1327. // 绑定额外的数据请求方式
  1328. if (reply.getContent === undefined) {
  1329. // 获取帖子内容,按需调用
  1330. const getReply = () =>
  1331. new Promise((resolve, reject) => {
  1332. // 避免重复请求
  1333. // TODO 严格来说需要加入缓存,避免频繁请求
  1334. if (reply.userInfo || reply.reputation) {
  1335. resolve(reply);
  1336. return;
  1337. }
  1338.  
  1339. // 请求并写入数据
  1340. getUserInfoAndReputation(tid, pid)
  1341. .then(({ subject, content, userInfo, reputation }) => {
  1342. // 写入用户名
  1343. if (userInfo) {
  1344. reply.username = userInfo.username;
  1345. }
  1346.  
  1347. // 写入用户信息和声望
  1348. reply.userInfo = userInfo;
  1349. reply.reputation = reputation;
  1350.  
  1351. // 写入帖子标题和内容
  1352. reply.subject = subject;
  1353. reply.content = content;
  1354.  
  1355. // 返回结果
  1356. resolve(reply);
  1357. })
  1358. .catch(reject);
  1359. });
  1360.  
  1361. // 绑定请求方式
  1362. reply.getContent = getReply;
  1363. reply.getUserInfo = getReply;
  1364. reply.getReputation = getReply;
  1365. }
  1366.  
  1367. // 获取过滤模式
  1368. const filterMode = await getFilterMode(reply);
  1369.  
  1370. // 返回结果
  1371. return filterMode;
  1372. };
  1373.  
  1374. // 处理引用
  1375. const handleQuote = async (content) => {
  1376. const quotes = content.querySelectorAll(".quote");
  1377.  
  1378. await Promise.all(
  1379. [...quotes].map(async (quote) => {
  1380. const uid = (() => {
  1381. const ele = quote.querySelector("a[href^='/nuke.php']");
  1382.  
  1383. if (ele) {
  1384. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  1385.  
  1386. if (res) {
  1387. return res[1];
  1388. }
  1389. }
  1390.  
  1391. return 0;
  1392. })();
  1393.  
  1394. const { tid, pid } = (() => {
  1395. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  1396.  
  1397. if (ele) {
  1398. const res = ele
  1399. .getAttribute("onclick")
  1400. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  1401.  
  1402. if (res) {
  1403. return {
  1404. tid: parseInt(res[2], 10),
  1405. pid: parseInt(res[3], 10) || 0,
  1406. };
  1407. }
  1408. }
  1409.  
  1410. return {};
  1411. })();
  1412.  
  1413. // 获取过滤方式
  1414. const filterMode = await getFilterModeByReply({
  1415. nFilter: {
  1416. uid,
  1417. tid,
  1418. pid,
  1419. subject: "",
  1420. content: quote.innerText,
  1421. },
  1422. });
  1423.  
  1424. (() => {
  1425. if (filterMode === "标记") {
  1426. quote.innerHTML = `
  1427. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  1428. <span class="crimson">Troll must die.</span>
  1429. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  1430. <div style="display: none;" name="troll_${uid}">
  1431. ${quote.innerHTML}
  1432. </div>
  1433. </div>`;
  1434. return;
  1435. }
  1436.  
  1437. if (filterMode === "遮罩") {
  1438. const source = document.createElement("DIV");
  1439.  
  1440. source.innerHTML = quote.innerHTML;
  1441. source.style.display = "none";
  1442.  
  1443. const caption = document.createElement("CAPTION");
  1444.  
  1445. caption.className = "filter-mask filter-mask-block";
  1446.  
  1447. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  1448. caption.onclick = () => {
  1449. quote.removeChild(caption);
  1450.  
  1451. source.style.display = "";
  1452. };
  1453.  
  1454. quote.innerHTML = "";
  1455. quote.appendChild(source);
  1456. quote.appendChild(caption);
  1457. return;
  1458. }
  1459.  
  1460. if (filterMode === "隐藏") {
  1461. quote.innerHTML = "";
  1462. return;
  1463. }
  1464. })();
  1465. })
  1466. );
  1467. };
  1468.  
  1469. // 过滤
  1470. const runFilter = (() => {
  1471. let hasNext = false;
  1472. let isRunning = false;
  1473.  
  1474. const func = async (reFilter = true) => {
  1475. const params = new URLSearchParams(location.search);
  1476.  
  1477. // 判断是否是主题页
  1478. const isTopic = location.pathname === "/thread.php";
  1479.  
  1480. // 判断是否是回复页
  1481. const isReply = location.pathname === "/read.php";
  1482.  
  1483. // 跳过屏蔽(插件自定义)
  1484. if (params.has("nofilter")) {
  1485. return;
  1486. }
  1487.  
  1488. // 收藏
  1489. if (params.has("favor")) {
  1490. return;
  1491. }
  1492.  
  1493. // 只看某人
  1494. if (params.has("authorid")) {
  1495. return;
  1496. }
  1497.  
  1498. // 重新过滤时,清除列表
  1499. if (reFilter) {
  1500. m.clear();
  1501. }
  1502.  
  1503. // 主题过滤
  1504. if (isTopic) {
  1505. const list = n.topicArg.data;
  1506.  
  1507. // 绑定过滤事件
  1508. for (let i = 0; i < list.length; i += 1) {
  1509. const item = list[i];
  1510.  
  1511. // 绑定事件
  1512. if (item.nFilter === undefined) {
  1513. // 主题 ID
  1514. const tid = item[8];
  1515.  
  1516. // 主题标题
  1517. const title = item[1];
  1518. const subject = title.innerText;
  1519.  
  1520. // 主题作者
  1521. const author = item[2];
  1522. const uid =
  1523. parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) ||
  1524. 0;
  1525. const username = author.innerText;
  1526.  
  1527. // 主题容器
  1528. const container = title.closest("tr");
  1529.  
  1530. // 过滤函数
  1531. const execute = async (reFilter = false) => {
  1532. // 已过滤则跳过
  1533. if (item.nFilter.executed && reFilter === false) {
  1534. return;
  1535. }
  1536.  
  1537. // 获取过滤方式
  1538. const filterMode = await getFilterModeByTopic(item);
  1539.  
  1540. (() => {
  1541. // 还原样式
  1542. // TODO 应该整体采用 className 来实现
  1543. (() => {
  1544. // 标记模式
  1545. container.style.removeProperty("textDecoration");
  1546.  
  1547. // 遮罩模式
  1548. title.classList.remove("filter-mask");
  1549. author.classList.remove("filter-mask");
  1550.  
  1551. // 隐藏模式
  1552. container.style.removeProperty("display");
  1553. })();
  1554.  
  1555. // 标记模式下,主题标记会有删除线标识
  1556. if (filterMode === "标记") {
  1557. title.style.textDecoration = "line-through";
  1558. return;
  1559. }
  1560.  
  1561. // 遮罩模式下,主题和作者会有遮罩样式
  1562. if (filterMode === "遮罩") {
  1563. title.classList.add("filter-mask");
  1564. author.classList.add("filter-mask");
  1565. return;
  1566. }
  1567.  
  1568. // 隐藏模式下,容器会被隐藏
  1569. if (filterMode === "隐藏") {
  1570. container.style.display = "none";
  1571. return;
  1572. }
  1573. })();
  1574.  
  1575. // 标记为已过滤
  1576. item.nFilter.executed = true;
  1577. };
  1578.  
  1579. // 绑定事件
  1580. item.nFilter = {
  1581. tid,
  1582. uid,
  1583. username,
  1584. container,
  1585. title,
  1586. author,
  1587. subject,
  1588. execute,
  1589. executed: false,
  1590. };
  1591. }
  1592. }
  1593.  
  1594. // 执行过滤
  1595. await Promise.all(
  1596. Object.values(list).map((item) => item.nFilter.execute(reFilter))
  1597. );
  1598. }
  1599.  
  1600. // 回复过滤
  1601. if (isReply) {
  1602. const list = Object.values(n.postArg.data);
  1603.  
  1604. // 绑定过滤事件
  1605. for (let i = 0; i < list.length; i += 1) {
  1606. const item = list[i];
  1607.  
  1608. // 绑定事件
  1609. if (item.nFilter === undefined) {
  1610. // 回复 ID
  1611. const pid = item.pid;
  1612.  
  1613. // 判断是否是楼层
  1614. const isFloor = typeof item.i === "number";
  1615.  
  1616. // 回复容器
  1617. const container = isFloor
  1618. ? item.uInfoC.closest("tr")
  1619. : item.uInfoC.closest(".comment_c");
  1620.  
  1621. // 回复标题
  1622. const title = item.subjectC;
  1623. const subject = title.innerText;
  1624.  
  1625. // 回复内容
  1626. const content = item.contentC;
  1627. const contentBak = content.innerHTML;
  1628.  
  1629. // 回复作者
  1630. const author =
  1631. container.querySelector(".posterInfoLine") || item.uInfoC;
  1632. const uid = parseInt(item.pAid, 10) || 0;
  1633. const username = author.querySelector(".author").innerText;
  1634. const avatar = author.querySelector(".avatar");
  1635.  
  1636. // 找到用户 ID,将其视为操作按钮
  1637. const action = container.querySelector('[name="uid"]');
  1638.  
  1639. // 创建一个元素,用于展示标记列表
  1640. // 贴条和高赞不显示
  1641. const tags = (() => {
  1642. if (isFloor === false) {
  1643. return null;
  1644. }
  1645.  
  1646. const element = document.createElement("div");
  1647.  
  1648. element.className = "filter-tags";
  1649.  
  1650. author.appendChild(element);
  1651.  
  1652. return element;
  1653. })();
  1654.  
  1655. // 过滤函数
  1656. const execute = async (reFilter = false) => {
  1657. // 已过滤则跳过
  1658. if (item.nFilter.executed && reFilter === false) {
  1659. return;
  1660. }
  1661.  
  1662. // 获取过滤方式
  1663. const filterMode = await getFilterModeByReply(item);
  1664.  
  1665. await (async () => {
  1666. // 还原样式
  1667. // TODO 应该整体采用 className 来实现
  1668. (() => {
  1669. // 标记模式
  1670. if (avatar) {
  1671. avatar.style.removeProperty("display");
  1672. }
  1673.  
  1674. content.innerHTML = contentBak;
  1675.  
  1676. // 遮罩模式
  1677. const caption = container.parentNode.querySelector("CAPTION");
  1678.  
  1679. if (caption) {
  1680. container.parentNode.removeChild(caption);
  1681. container.style.removeProperty("display");
  1682. }
  1683.  
  1684. // 隐藏模式
  1685. container.style.removeProperty("display");
  1686. })();
  1687.  
  1688. // 标记模式下,隐藏头像,采用泥潭的折叠样式
  1689. if (filterMode === "标记") {
  1690. if (avatar) {
  1691. avatar.style.display = "none";
  1692. }
  1693.  
  1694. content.innerHTML = `
  1695. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  1696. <span class="crimson">Troll must die.</span>
  1697. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  1698. <div style="display: none;" name="troll_${uid}">
  1699. ${contentBak}
  1700. </div>
  1701. </div>`;
  1702. return;
  1703. }
  1704.  
  1705. // 遮罩模式下,楼层会有遮罩样式
  1706. if (filterMode === "遮罩") {
  1707. const caption = document.createElement("CAPTION");
  1708.  
  1709. if (isFloor) {
  1710. caption.className = "filter-mask filter-mask-block";
  1711. } else {
  1712. caption.className = "filter-mask filter-mask-block left";
  1713. caption.style.width = "47%";
  1714. }
  1715.  
  1716. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  1717. caption.onclick = () => {
  1718. const caption =
  1719. container.parentNode.querySelector("CAPTION");
  1720.  
  1721. if (caption) {
  1722. container.parentNode.removeChild(caption);
  1723. container.style.removeProperty("display");
  1724. }
  1725. };
  1726.  
  1727. container.parentNode.insertBefore(caption, container);
  1728. container.style.display = "none";
  1729. return;
  1730. }
  1731.  
  1732. // 隐藏模式下,容器会被隐藏
  1733. if (filterMode === "隐藏") {
  1734. container.style.display = "none";
  1735. return;
  1736. }
  1737.  
  1738. // 处理引用
  1739. await handleQuote(content);
  1740. })();
  1741.  
  1742. // 如果是隐藏模式,没必要再加载按钮和标记
  1743. if (filterMode !== "隐藏") {
  1744. // 修改操作按钮颜色
  1745. if (action) {
  1746. const user = data.users[uid];
  1747.  
  1748. if (user) {
  1749. action.style.background = "#CB4042";
  1750. } else {
  1751. action.style.background = "#AAA";
  1752. }
  1753. }
  1754.  
  1755. // 加载标记
  1756. if (tags) {
  1757. const list = data.users[uid]
  1758. ? data.users[uid].tags.map((i) => data.tags[i]) || []
  1759. : [];
  1760.  
  1761. tags.style.display = list.length ? "" : "none";
  1762. tags.innerHTML = list
  1763. .map(
  1764. (tag) =>
  1765. `<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
  1766. )
  1767. .join("");
  1768.  
  1769. witchHunter.run(uid, tags);
  1770. }
  1771. }
  1772.  
  1773. // 标记为已过滤
  1774. item.nFilter.executed = true;
  1775. };
  1776.  
  1777. // 绑定操作按钮事件
  1778. if (action) {
  1779. // 隐藏匿名操作按钮
  1780. if (uid <= 0) {
  1781. action.style.display = "none";
  1782. return;
  1783. }
  1784.  
  1785. action.innerHTML = `屏蔽`;
  1786. action.onclick = (e) => {
  1787. const user = data.users[uid];
  1788. if (e.ctrlKey === false) {
  1789. editUser(uid, username, () => {
  1790. execute(true);
  1791. });
  1792. return;
  1793. }
  1794.  
  1795. if (user) {
  1796. delete data.users[user.id];
  1797. } else {
  1798. addUser(uid, username);
  1799. }
  1800.  
  1801. execute(true);
  1802. saveData();
  1803. };
  1804. }
  1805.  
  1806. // 绑定事件
  1807. item.nFilter = {
  1808. pid,
  1809. uid,
  1810. username,
  1811. container,
  1812. title,
  1813. author,
  1814. subject,
  1815. content: content.innerText,
  1816. execute,
  1817. executed: false,
  1818. };
  1819. }
  1820. }
  1821.  
  1822. // 执行过滤
  1823. await Promise.all(
  1824. Object.values(list).map((item) => item.nFilter.execute(reFilter))
  1825. );
  1826. }
  1827. };
  1828.  
  1829. const execute = (reFilter = true) =>
  1830. func(reFilter).finally(() => {
  1831. if (hasNext) {
  1832. hasNext = false;
  1833.  
  1834. execute(reFilter);
  1835. } else {
  1836. isRunning = false;
  1837. }
  1838. });
  1839.  
  1840. return async (reFilter = true) => {
  1841. if (isRunning) {
  1842. hasNext = true;
  1843. } else {
  1844. isRunning = true;
  1845.  
  1846. await execute(reFilter);
  1847. }
  1848. };
  1849. })();
  1850.  
  1851. // STYLE
  1852. GM_addStyle(`
  1853. .filter-table-wrapper {
  1854. max-height: 80vh;
  1855. overflow-y: auto;
  1856. }
  1857. .filter-table {
  1858. margin: 0;
  1859. }
  1860. .filter-table th,
  1861. .filter-table td {
  1862. position: relative;
  1863. white-space: nowrap;
  1864. }
  1865. .filter-table th {
  1866. position: sticky;
  1867. top: 2px;
  1868. z-index: 1;
  1869. }
  1870. .filter-table input:not([type]), .filter-table input[type="text"] {
  1871. margin: 0;
  1872. box-sizing: border-box;
  1873. height: 100%;
  1874. width: 100%;
  1875. }
  1876. .filter-input-wrapper {
  1877. position: absolute;
  1878. top: 6px;
  1879. right: 6px;
  1880. bottom: 6px;
  1881. left: 6px;
  1882. }
  1883. .filter-text-ellipsis {
  1884. display: flex;
  1885. }
  1886. .filter-text-ellipsis > * {
  1887. flex: 1;
  1888. width: 1px;
  1889. overflow: hidden;
  1890. text-overflow: ellipsis;
  1891. }
  1892. .filter-button-group {
  1893. margin: -.1em -.2em;
  1894. }
  1895. .filter-tags {
  1896. margin: 2px -0.2em 0;
  1897. text-align: left;
  1898. }
  1899. .filter-mask {
  1900. margin: 1px;
  1901. color: #81C7D4;
  1902. background: #81C7D4;
  1903. }
  1904. .filter-mask-block {
  1905. display: block;
  1906. border: 1px solid #66BAB7;
  1907. text-align: center !important;
  1908. }
  1909. .filter-input-wrapper {
  1910. position: absolute;
  1911. top: 6px;
  1912. right: 6px;
  1913. bottom: 6px;
  1914. left: 6px;
  1915. }
  1916. `);
  1917.  
  1918. // MENU
  1919. const m = (() => {
  1920. const list = [];
  1921.  
  1922. const container = document.createElement("DIV");
  1923.  
  1924. container.className = `td`;
  1925. container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;
  1926.  
  1927. const content = container.querySelector("A");
  1928.  
  1929. const create = (onclick) => {
  1930. const anchor = document.querySelector("#mainmenu .td:last-child");
  1931.  
  1932. anchor.before(container);
  1933.  
  1934. content.onclick = onclick;
  1935. };
  1936.  
  1937. const update = () => {
  1938. const count = list.length;
  1939.  
  1940. if (count) {
  1941. content.innerHTML = `屏蔽 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  1942. } else {
  1943. content.innerHTML = `屏蔽`;
  1944. }
  1945. };
  1946.  
  1947. const clear = () => {
  1948. list.splice(0, list.length);
  1949.  
  1950. update();
  1951. };
  1952.  
  1953. const add = ({ user, mode, subject, content, reason }) => {
  1954. list.unshift({ user, mode, subject, content, reason });
  1955.  
  1956. listModule.refresh();
  1957.  
  1958. update();
  1959. };
  1960.  
  1961. return {
  1962. create,
  1963. clear,
  1964. list,
  1965. add,
  1966. };
  1967. })();
  1968.  
  1969. // UI
  1970. const u = (() => {
  1971. const modules = {};
  1972.  
  1973. const tabContainer = (() => {
  1974. const c = document.createElement("div");
  1975.  
  1976. c.className = "w100";
  1977. c.innerHTML = `
  1978. <div class="right_" style="margin-bottom: 5px;">
  1979. <table class="stdbtn" cellspacing="0">
  1980. <tbody>
  1981. <tr></tr>
  1982. </tbody>
  1983. </table>
  1984. </div>
  1985. <div class="clear"></div>
  1986. `;
  1987.  
  1988. return c;
  1989. })();
  1990.  
  1991. const tabPanelContainer = (() => {
  1992. const c = document.createElement("div");
  1993.  
  1994. c.style = "width: 80vw;";
  1995.  
  1996. return c;
  1997. })();
  1998.  
  1999. const content = (() => {
  2000. const c = document.createElement("div");
  2001.  
  2002. c.append(tabContainer);
  2003. c.append(tabPanelContainer);
  2004.  
  2005. return c;
  2006. })();
  2007.  
  2008. const addModule = (() => {
  2009. const tc = tabContainer.getElementsByTagName("tr")[0];
  2010. const cc = tabPanelContainer;
  2011.  
  2012. return (module) => {
  2013. const tabBox = document.createElement("td");
  2014.  
  2015. tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
  2016.  
  2017. const tab = tabBox.childNodes[0];
  2018.  
  2019. const toggle = () => {
  2020. Object.values(modules).forEach((item) => {
  2021. if (item.tab === tab) {
  2022. item.tab.className = "nobr";
  2023. item.content.style = "display: block";
  2024. item.refresh();
  2025. } else {
  2026. item.tab.className = "nobr silver";
  2027. item.content.style = "display: none";
  2028. }
  2029. });
  2030. };
  2031.  
  2032. tc.append(tabBox);
  2033. cc.append(module.content);
  2034.  
  2035. tab.onclick = toggle;
  2036.  
  2037. modules[module.name] = {
  2038. ...module,
  2039. tab,
  2040. toggle,
  2041. };
  2042.  
  2043. return modules[module.name];
  2044. };
  2045. })();
  2046.  
  2047. return {
  2048. content,
  2049. modules,
  2050. addModule,
  2051. };
  2052. })();
  2053.  
  2054. // 屏蔽列表
  2055. const listModule = (() => {
  2056. const content = (() => {
  2057. const c = document.createElement("div");
  2058.  
  2059. c.style = "display: none";
  2060. c.innerHTML = `
  2061. <div class="filter-table-wrapper">
  2062. <table class="filter-table forumbox">
  2063. <thead>
  2064. <tr class="block_txt_c0">
  2065. <th class="c1" width="1">用户</th>
  2066. <th class="c2" width="1">过滤方式</th>
  2067. <th class="c3">内容</th>
  2068. <th class="c4" width="1">原因</th>
  2069. </tr>
  2070. </thead>
  2071. <tbody></tbody>
  2072. </table>
  2073. </div>
  2074. `;
  2075.  
  2076. return c;
  2077. })();
  2078.  
  2079. const refresh = (() => {
  2080. const container = content.getElementsByTagName("tbody")[0];
  2081.  
  2082. const func = () => {
  2083. container.innerHTML = "";
  2084.  
  2085. Object.values(m.list).forEach((item) => {
  2086. const tc = document.createElement("tr");
  2087.  
  2088. tc.className = `row${
  2089. (container.querySelectorAll("TR").length % 2) + 1
  2090. }`;
  2091.  
  2092. tc.refresh = () => {
  2093. const { user, mode, subject, content, reason } = item;
  2094.  
  2095. tc.innerHTML = `
  2096. <td class="c1">${user}</td>
  2097. <td class="c2">${mode}</td>
  2098. <td class="c3">
  2099. <div class="filter-text-ellipsis">
  2100. ${subject || content}
  2101. </div>
  2102. </td>
  2103. <td class="c4">${reason}</td>
  2104. `;
  2105. };
  2106.  
  2107. tc.refresh();
  2108.  
  2109. container.appendChild(tc);
  2110. });
  2111. };
  2112.  
  2113. return func;
  2114. })();
  2115.  
  2116. return {
  2117. name: "列表",
  2118. content,
  2119. refresh,
  2120. };
  2121. })();
  2122.  
  2123. // 用户
  2124. const userModule = (() => {
  2125. const content = (() => {
  2126. const c = document.createElement("div");
  2127.  
  2128. c.style = "display: none";
  2129. c.innerHTML = `
  2130. <div class="filter-table-wrapper">
  2131. <table class="filter-table forumbox">
  2132. <thead>
  2133. <tr class="block_txt_c0">
  2134. <th class="c1" width="1">昵称</th>
  2135. <th class="c2">标记</th>
  2136. <th class="c3" width="1">过滤方式</th>
  2137. <th class="c4" width="1">操作</th>
  2138. </tr>
  2139. </thead>
  2140. <tbody></tbody>
  2141. </table>
  2142. </div>
  2143. `;
  2144.  
  2145. return c;
  2146. })();
  2147.  
  2148. const refresh = (() => {
  2149. const container = content.getElementsByTagName("tbody")[0];
  2150.  
  2151. const func = () => {
  2152. container.innerHTML = "";
  2153.  
  2154. Object.values(data.users).forEach((item) => {
  2155. const tc = document.createElement("tr");
  2156.  
  2157. tc.className = `row${
  2158. (container.querySelectorAll("TR").length % 2) + 1
  2159. }`;
  2160.  
  2161. tc.refresh = () => {
  2162. if (data.users[item.id]) {
  2163. tc.innerHTML = `
  2164. <td class="c1">
  2165. <a href="/nuke.php?func=ucp&uid=${
  2166. item.id
  2167. }" class="b nobr">[${
  2168. item.name ? "@" + item.name : "#" + item.id
  2169. }]</a>
  2170. </td>
  2171. <td class="c2">
  2172. ${item.tags
  2173. .map((tag) => {
  2174. if (data.tags[tag]) {
  2175. return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
  2176. }
  2177. })
  2178. .join("")}
  2179. </td>
  2180. <td class="c3">
  2181. <div class="filter-table-button-group">
  2182. <button>${item.filterMode || FILTER_MODE[0]}</button>
  2183. </div>
  2184. </td>
  2185. <td class="c4">
  2186. <div class="filter-table-button-group">
  2187. <button>编辑</button>
  2188. <button>删除</button>
  2189. </div>
  2190. </td>
  2191. `;
  2192.  
  2193. const actions = tc.getElementsByTagName("button");
  2194.  
  2195. actions[0].onclick = () => {
  2196. data.users[item.id].filterMode = switchFilterMode(
  2197. data.users[item.id].filterMode || FILTER_MODE[0]
  2198. );
  2199.  
  2200. actions[0].innerHTML = data.users[item.id].filterMode;
  2201.  
  2202. saveData();
  2203. runFilter();
  2204. };
  2205.  
  2206. actions[1].onclick = () => {
  2207. editUser(item.id, item.name, tc.refresh);
  2208. };
  2209.  
  2210. actions[2].onclick = () => {
  2211. if (confirm("是否确认?")) {
  2212. delete data.users[item.id];
  2213. container.removeChild(tc);
  2214.  
  2215. saveData();
  2216. runFilter();
  2217. }
  2218. };
  2219. } else {
  2220. tc.remove();
  2221. }
  2222. };
  2223.  
  2224. tc.refresh();
  2225.  
  2226. container.appendChild(tc);
  2227. });
  2228. };
  2229.  
  2230. return func;
  2231. })();
  2232.  
  2233. return {
  2234. name: "用户",
  2235. content,
  2236. refresh,
  2237. };
  2238. })();
  2239.  
  2240. // 标记
  2241. const tagModule = (() => {
  2242. const content = (() => {
  2243. const c = document.createElement("div");
  2244.  
  2245. c.style = "display: none";
  2246. c.innerHTML = `
  2247. <div class="filter-table-wrapper">
  2248. <table class="filter-table forumbox">
  2249. <thead>
  2250. <tr class="block_txt_c0">
  2251. <th class="c1" width="1">标记</th>
  2252. <th class="c2">列表</th>
  2253. <th class="c3" width="1">过滤方式</th>
  2254. <th class="c4" width="1">操作</th>
  2255. </tr>
  2256. </thead>
  2257. <tbody></tbody>
  2258. </table>
  2259. </div>
  2260. `;
  2261.  
  2262. return c;
  2263. })();
  2264.  
  2265. const refresh = (() => {
  2266. const container = content.getElementsByTagName("tbody")[0];
  2267.  
  2268. const func = () => {
  2269. container.innerHTML = "";
  2270.  
  2271. Object.values(data.tags).forEach((item) => {
  2272. const tc = document.createElement("tr");
  2273.  
  2274. tc.className = `row${
  2275. (container.querySelectorAll("TR").length % 2) + 1
  2276. }`;
  2277.  
  2278. tc.innerHTML = `
  2279. <td class="c1">
  2280. <b class="block_txt nobr" style="background:${
  2281. item.color
  2282. }; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
  2283. </td>
  2284. <td class="c2">
  2285. <button>${
  2286. Object.values(data.users).filter((user) =>
  2287. user.tags.find((tag) => tag === item.id)
  2288. ).length
  2289. }
  2290. </button>
  2291. <div style="white-space: normal; display: none;">
  2292. ${Object.values(data.users)
  2293. .filter((user) =>
  2294. user.tags.find((tag) => tag === item.id)
  2295. )
  2296. .map(
  2297. (user) =>
  2298. `<a href="/nuke.php?func=ucp&uid=${
  2299. user.id
  2300. }" class="b nobr">[${
  2301. user.name ? "@" + user.name : "#" + user.id
  2302. }]</a>`
  2303. )
  2304. .join("")}
  2305. </div>
  2306. </td>
  2307. <td class="c3">
  2308. <div class="filter-table-button-group">
  2309. <button>${item.filterMode || FILTER_MODE[0]}</button>
  2310. </div>
  2311. </td>
  2312. <td class="c4">
  2313. <div class="filter-table-button-group">
  2314. <button>删除</button>
  2315. </div>
  2316. </td>
  2317. `;
  2318.  
  2319. const actions = tc.getElementsByTagName("button");
  2320.  
  2321. actions[0].onclick = (() => {
  2322. let hide = true;
  2323. return () => {
  2324. hide = !hide;
  2325. actions[0].nextElementSibling.style.display = hide
  2326. ? "none"
  2327. : "block";
  2328. };
  2329. })();
  2330.  
  2331. actions[1].onclick = () => {
  2332. data.tags[item.id].filterMode = switchFilterMode(
  2333. data.tags[item.id].filterMode || FILTER_MODE[0]
  2334. );
  2335.  
  2336. actions[1].innerHTML = data.tags[item.id].filterMode;
  2337.  
  2338. saveData();
  2339. runFilter();
  2340. };
  2341.  
  2342. actions[2].onclick = () => {
  2343. if (confirm("是否确认?")) {
  2344. delete data.tags[item.id];
  2345.  
  2346. Object.values(data.users).forEach((user) => {
  2347. const index = user.tags.findIndex((tag) => tag === item.id);
  2348. if (index >= 0) {
  2349. user.tags.splice(index, 1);
  2350. }
  2351. });
  2352.  
  2353. container.removeChild(tc);
  2354.  
  2355. saveData();
  2356. runFilter();
  2357. }
  2358. };
  2359.  
  2360. container.appendChild(tc);
  2361. });
  2362. };
  2363.  
  2364. return func;
  2365. })();
  2366.  
  2367. return {
  2368. name: "标记",
  2369. content,
  2370. refresh,
  2371. };
  2372. })();
  2373.  
  2374. // 关键字
  2375. const keywordModule = (() => {
  2376. const content = (() => {
  2377. const c = document.createElement("div");
  2378.  
  2379. c.style = "display: none";
  2380. c.innerHTML = `
  2381. <div class="filter-table-wrapper">
  2382. <table class="filter-table forumbox">
  2383. <thead>
  2384. <tr class="block_txt_c0">
  2385. <th class="c1">列表</th>
  2386. <th class="c2" width="1">过滤方式</th>
  2387. <th class="c3" width="1">包括内容</th>
  2388. <th class="c4" width="1">操作</th>
  2389. </tr>
  2390. </thead>
  2391. <tbody></tbody>
  2392. </table>
  2393. </div>
  2394. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
  2395. `;
  2396.  
  2397. return c;
  2398. })();
  2399.  
  2400. const refresh = (() => {
  2401. const container = content.getElementsByTagName("tbody")[0];
  2402.  
  2403. const func = () => {
  2404. container.innerHTML = "";
  2405.  
  2406. Object.values(data.keywords).forEach((item) => {
  2407. const tc = document.createElement("tr");
  2408.  
  2409. tc.className = `row${
  2410. (container.querySelectorAll("TR").length % 2) + 1
  2411. }`;
  2412.  
  2413. tc.innerHTML = `
  2414. <td class="c1">
  2415. <div class="filter-input-wrapper">
  2416. <input value="${item.keyword || ""}" />
  2417. </div>
  2418. </td>
  2419. <td class="c2">
  2420. <div class="filter-table-button-group">
  2421. <button>${item.filterMode || FILTER_MODE[0]}</button>
  2422. </div>
  2423. </td>
  2424. <td class="c3">
  2425. <div style="text-align: center;">
  2426. <input type="checkbox" ${
  2427. item.filterLevel ? `checked="checked"` : ""
  2428. } />
  2429. </div>
  2430. </td>
  2431. <td class="c4">
  2432. <div class="filter-table-button-group">
  2433. <button>保存</button>
  2434. <button>删除</button>
  2435. </div>
  2436. </td>
  2437. `;
  2438.  
  2439. const inputElement = tc.querySelector("INPUT");
  2440. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  2441. const actions = tc.getElementsByTagName("button");
  2442.  
  2443. actions[0].onclick = () => {
  2444. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  2445. };
  2446.  
  2447. actions[1].onclick = () => {
  2448. if (inputElement.value) {
  2449. data.keywords[item.id] = {
  2450. id: item.id,
  2451. keyword: inputElement.value,
  2452. filterMode: actions[0].innerHTML,
  2453. filterLevel: levelElement.checked ? 1 : 0,
  2454. };
  2455.  
  2456. saveData();
  2457. runFilter();
  2458. refresh();
  2459. }
  2460. };
  2461.  
  2462. actions[2].onclick = () => {
  2463. if (confirm("是否确认?")) {
  2464. delete data.keywords[item.id];
  2465.  
  2466. saveData();
  2467. runFilter();
  2468. refresh();
  2469. }
  2470. };
  2471.  
  2472. container.appendChild(tc);
  2473. });
  2474.  
  2475. {
  2476. const tc = document.createElement("tr");
  2477.  
  2478. tc.className = `row${
  2479. (container.querySelectorAll("TR").length % 2) + 1
  2480. }`;
  2481.  
  2482. tc.innerHTML = `
  2483. <td class="c1">
  2484. <div class="filter-input-wrapper">
  2485. <input value="" />
  2486. </div>
  2487. </td>
  2488. <td class="c2">
  2489. <div class="filter-table-button-group">
  2490. <button>${FILTER_MODE[0]}</button>
  2491. </div>
  2492. </td>
  2493. <td class="c3">
  2494. <div style="text-align: center;">
  2495. <input type="checkbox" />
  2496. </div>
  2497. </td>
  2498. <td class="c4">
  2499. <div class="filter-table-button-group">
  2500. <button>添加</button>
  2501. </div>
  2502. </td>
  2503. `;
  2504.  
  2505. const inputElement = tc.querySelector("INPUT");
  2506. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  2507. const actions = tc.getElementsByTagName("button");
  2508.  
  2509. actions[0].onclick = () => {
  2510. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  2511. };
  2512.  
  2513. actions[1].onclick = () => {
  2514. if (inputElement.value) {
  2515. addKeyword(
  2516. inputElement.value,
  2517. actions[0].innerHTML,
  2518. levelElement.checked ? 1 : 0
  2519. );
  2520.  
  2521. saveData();
  2522. runFilter();
  2523. refresh();
  2524. }
  2525. };
  2526.  
  2527. container.appendChild(tc);
  2528. }
  2529. };
  2530.  
  2531. return func;
  2532. })();
  2533.  
  2534. return {
  2535. name: "关键字",
  2536. content,
  2537. refresh,
  2538. };
  2539. })();
  2540.  
  2541. // 属地
  2542. const locationModule = (() => {
  2543. const content = (() => {
  2544. const c = document.createElement("div");
  2545.  
  2546. c.style = "display: none";
  2547. c.innerHTML = `
  2548. <div class="filter-table-wrapper">
  2549. <table class="filter-table forumbox">
  2550. <thead>
  2551. <tr class="block_txt_c0">
  2552. <th class="c1">列表</th>
  2553. <th class="c2" width="1">过滤方式</th>
  2554. <th class="c3" width="1">操作</th>
  2555. </tr>
  2556. </thead>
  2557. <tbody></tbody>
  2558. </table>
  2559. </div>
  2560. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
  2561. `;
  2562.  
  2563. return c;
  2564. })();
  2565.  
  2566. const refresh = (() => {
  2567. const container = content.getElementsByTagName("tbody")[0];
  2568.  
  2569. const func = () => {
  2570. container.innerHTML = "";
  2571.  
  2572. Object.values(data.locations).forEach((item) => {
  2573. const tc = document.createElement("tr");
  2574.  
  2575. tc.className = `row${
  2576. (container.querySelectorAll("TR").length % 2) + 1
  2577. }`;
  2578.  
  2579. tc.innerHTML = `
  2580. <td class="c1">
  2581. <div class="filter-input-wrapper">
  2582. <input value="${item.keyword || ""}" />
  2583. </div>
  2584. </td>
  2585. <td class="c2">
  2586. <div class="filter-table-button-group">
  2587. <button>${item.filterMode || FILTER_MODE[0]}</button>
  2588. </div>
  2589. </td>
  2590. <td class="c3">
  2591. <div class="filter-table-button-group">
  2592. <button>保存</button>
  2593. <button>删除</button>
  2594. </div>
  2595. </td>
  2596. `;
  2597.  
  2598. const inputElement = tc.querySelector("INPUT");
  2599. const actions = tc.getElementsByTagName("button");
  2600.  
  2601. actions[0].onclick = () => {
  2602. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  2603. };
  2604.  
  2605. actions[1].onclick = () => {
  2606. if (inputElement.value) {
  2607. data.locations[item.id] = {
  2608. id: item.id,
  2609. keyword: inputElement.value,
  2610. filterMode: actions[0].innerHTML,
  2611. };
  2612.  
  2613. saveData();
  2614. runFilter();
  2615. refresh();
  2616. }
  2617. };
  2618.  
  2619. actions[2].onclick = () => {
  2620. if (confirm("是否确认?")) {
  2621. delete data.locations[item.id];
  2622.  
  2623. saveData();
  2624. runFilter();
  2625. refresh();
  2626. }
  2627. };
  2628.  
  2629. container.appendChild(tc);
  2630. });
  2631.  
  2632. {
  2633. const tc = document.createElement("tr");
  2634.  
  2635. tc.className = `row${
  2636. (container.querySelectorAll("TR").length % 2) + 1
  2637. }`;
  2638.  
  2639. tc.innerHTML = `
  2640. <td class="c1">
  2641. <div class="filter-input-wrapper">
  2642. <input value="" />
  2643. </div>
  2644. </td>
  2645. <td class="c2">
  2646. <div class="filter-table-button-group">
  2647. <button>${FILTER_MODE[0]}</button>
  2648. </div>
  2649. </td>
  2650. <td class="c3">
  2651. <div class="filter-table-button-group">
  2652. <button>添加</button>
  2653. </div>
  2654. </td>
  2655. `;
  2656.  
  2657. const inputElement = tc.querySelector("INPUT");
  2658. const actions = tc.getElementsByTagName("button");
  2659.  
  2660. actions[0].onclick = () => {
  2661. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  2662. };
  2663.  
  2664. actions[1].onclick = () => {
  2665. if (inputElement.value) {
  2666. addLocation(inputElement.value, actions[0].innerHTML);
  2667.  
  2668. saveData();
  2669. runFilter();
  2670. refresh();
  2671. }
  2672. };
  2673.  
  2674. container.appendChild(tc);
  2675. }
  2676. };
  2677.  
  2678. return func;
  2679. })();
  2680.  
  2681. return {
  2682. name: "属地",
  2683. content,
  2684. refresh,
  2685. };
  2686. })();
  2687.  
  2688. // 猎巫
  2689. const witchHuntModule = (() => {
  2690. const content = (() => {
  2691. const c = document.createElement("div");
  2692.  
  2693. c.style = "display: none";
  2694. c.innerHTML = `
  2695. <div class="filter-table-wrapper">
  2696. <table class="filter-table forumbox">
  2697. <thead>
  2698. <tr class="block_txt_c0">
  2699. <th class="c1">版面</th>
  2700. <th class="c2">标签</th>
  2701. <th class="c3" width="1">操作</th>
  2702. </tr>
  2703. </thead>
  2704. <tbody></tbody>
  2705. </table>
  2706. </div>
  2707. <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启<br/>该功能为实验性功能,仅判断用户是否曾经在某个版面发言<br/>未来可能会加入发言的筛选或是屏蔽功能,也可能移除此功能</div>
  2708. `;
  2709.  
  2710. return c;
  2711. })();
  2712.  
  2713. const refresh = (() => {
  2714. const container = content.getElementsByTagName("tbody")[0];
  2715.  
  2716. const func = () => {
  2717. container.innerHTML = "";
  2718.  
  2719. Object.values(witchHunter.data).forEach((item, index) => {
  2720. const tc = document.createElement("tr");
  2721.  
  2722. tc.className = `row${
  2723. (container.querySelectorAll("TR").length % 2) + 1
  2724. }`;
  2725.  
  2726. tc.innerHTML = `
  2727. <td class="c1">
  2728. <div class="filter-input-wrapper">
  2729. <a href="/thread.php?fid=${item.fid}" class="b nobr">[${item.name}]</a>
  2730. </div>
  2731. </td>
  2732. <td class="c2">
  2733. <b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>
  2734. </td>
  2735. <td class="c3">
  2736. <div class="filter-table-button-group">
  2737. <button>删除</button>
  2738. </div>
  2739. </td>
  2740. `;
  2741.  
  2742. const actions = tc.getElementsByTagName("button");
  2743.  
  2744. actions[0].onclick = () => {
  2745. if (confirm("是否确认?")) {
  2746. witchHunter.remove(item.id);
  2747.  
  2748. refresh();
  2749. }
  2750. };
  2751.  
  2752. container.appendChild(tc);
  2753. });
  2754.  
  2755. {
  2756. const tc = document.createElement("tr");
  2757.  
  2758. tc.className = `row${
  2759. (container.querySelectorAll("TR").length % 2) + 1
  2760. }`;
  2761.  
  2762. tc.innerHTML = `
  2763. <td class="c1">
  2764. <div class="filter-input-wrapper">
  2765. <input value="" placeholder="版面ID" />
  2766. </div>
  2767. </td>
  2768. <td class="c2">
  2769. <div class="filter-input-wrapper">
  2770. <input value="" />
  2771. </div>
  2772. </td>
  2773. <td class="c3">
  2774. <div class="filter-table-button-group">
  2775. <button>添加</button>
  2776. </div>
  2777. </td>
  2778. `;
  2779.  
  2780. const inputElement = tc.getElementsByTagName("INPUT");
  2781. const actions = tc.getElementsByTagName("button");
  2782.  
  2783. actions[0].onclick = async () => {
  2784. const fid = parseInt(inputElement[0].value, 10);
  2785. const tag = inputElement[1].value.trim();
  2786.  
  2787. if (isNaN(fid) || tag.length === 0) {
  2788. return;
  2789. }
  2790.  
  2791. await witchHunter.add(fid, tag);
  2792.  
  2793. refresh();
  2794. };
  2795.  
  2796. container.appendChild(tc);
  2797. }
  2798. };
  2799.  
  2800. return func;
  2801. })();
  2802.  
  2803. return {
  2804. name: "猎巫",
  2805. content,
  2806. refresh,
  2807. };
  2808. })();
  2809.  
  2810. // 通用设置
  2811. const commonModule = (() => {
  2812. const content = (() => {
  2813. const c = document.createElement("div");
  2814.  
  2815. c.style = "display: none";
  2816.  
  2817. return c;
  2818. })();
  2819.  
  2820. const refresh = (() => {
  2821. const container = content;
  2822.  
  2823. const func = () => {
  2824. container.innerHTML = "";
  2825.  
  2826. // 默认过滤方式
  2827. {
  2828. const tc = document.createElement("div");
  2829.  
  2830. tc.innerHTML += `
  2831. <div>默认过滤方式</div>
  2832. <div></div>
  2833. <div class="silver" style="margin-top: 10px;">${FILTER_TIPS}</div>
  2834. `;
  2835.  
  2836. ["标记", "遮罩", "隐藏"].forEach((item, index) => {
  2837. const ele = document.createElement("SPAN");
  2838.  
  2839. ele.innerHTML += `
  2840. <input id="s-fm-${index}" type="radio" name="filterType" ${
  2841. data.options.filterMode === item && "checked"
  2842. }>
  2843. <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
  2844. `;
  2845.  
  2846. const inp = ele.querySelector("input");
  2847.  
  2848. inp.onchange = () => {
  2849. if (inp.checked) {
  2850. data.options.filterMode = item;
  2851. saveData();
  2852. runFilter();
  2853. }
  2854. };
  2855.  
  2856. tc.querySelectorAll("div")[1].append(ele);
  2857. });
  2858.  
  2859. container.appendChild(tc);
  2860. }
  2861.  
  2862. // 小号过滤(时间)
  2863. {
  2864. const tc = document.createElement("div");
  2865.  
  2866. tc.innerHTML += `
  2867. <br/>
  2868. <div>
  2869. 隐藏注册时间小于<input value="${
  2870. (data.options.filterRegdateLimit || 0) / 86400000
  2871. }" maxLength="4" style="width: 48px;" />天的用户
  2872. <button>确认</button>
  2873. </div>
  2874. `;
  2875.  
  2876. const actions = tc.getElementsByTagName("button");
  2877.  
  2878. actions[0].onclick = () => {
  2879. const v = actions[0].previousElementSibling.value;
  2880.  
  2881. const n = Number(v) || 0;
  2882.  
  2883. data.options.filterRegdateLimit = n < 0 ? 0 : n * 86400000;
  2884.  
  2885. saveData();
  2886. runFilter();
  2887. };
  2888.  
  2889. container.appendChild(tc);
  2890. }
  2891.  
  2892. // 小号过滤(发帖数)
  2893. {
  2894. const tc = document.createElement("div");
  2895.  
  2896. tc.innerHTML += `
  2897. <br/>
  2898. <div>
  2899. 隐藏发帖数量小于<input value="${
  2900. data.options.filterPostnumLimit || 0
  2901. }" maxLength="5" style="width: 48px;" />贴的用户
  2902. <button>确认</button>
  2903. </div>
  2904. `;
  2905.  
  2906. const actions = tc.getElementsByTagName("button");
  2907.  
  2908. actions[0].onclick = () => {
  2909. const v = actions[0].previousElementSibling.value;
  2910.  
  2911. const n = Number(v) || 0;
  2912.  
  2913. data.options.filterPostnumLimit = n < 0 ? 0 : n;
  2914.  
  2915. saveData();
  2916. runFilter();
  2917. };
  2918.  
  2919. container.appendChild(tc);
  2920. }
  2921.  
  2922. // 流量号过滤(主题比例)
  2923. {
  2924. const tc = document.createElement("div");
  2925.  
  2926. tc.innerHTML += `
  2927. <br/>
  2928. <div>
  2929. 隐藏发帖比例大于<input value="${
  2930. data.options.filterTopicRateLimit || 100
  2931. }" maxLength="3" style="width: 48px;" />%的用户
  2932. <button>确认</button>
  2933. </div>
  2934. `;
  2935.  
  2936. const actions = tc.getElementsByTagName("button");
  2937.  
  2938. actions[0].onclick = () => {
  2939. const v = actions[0].previousElementSibling.value;
  2940.  
  2941. const n = Number(v) || 100;
  2942.  
  2943. if (n <= 0 || n > 100) {
  2944. return;
  2945. }
  2946.  
  2947. data.options.filterTopicRateLimit = n;
  2948.  
  2949. saveData();
  2950. runFilter();
  2951. };
  2952.  
  2953. container.appendChild(tc);
  2954. }
  2955.  
  2956. // 声望过滤
  2957. {
  2958. const tc = document.createElement("div");
  2959.  
  2960. tc.innerHTML += `
  2961. <br/>
  2962. <div>
  2963. 隐藏版面声望低于<input value="${
  2964. data.options.filterReputationLimit || ""
  2965. }" maxLength="5" style="width: 48px;" />点的用户
  2966. <button>确认</button>
  2967. </div>
  2968. `;
  2969.  
  2970. const actions = tc.getElementsByTagName("button");
  2971.  
  2972. actions[0].onclick = () => {
  2973. const v = actions[0].previousElementSibling.value;
  2974.  
  2975. const n = Number(v);
  2976.  
  2977. data.options.filterReputationLimit = n;
  2978.  
  2979. saveData();
  2980. runFilter();
  2981. };
  2982.  
  2983. container.appendChild(tc);
  2984. }
  2985.  
  2986. // 匿名过滤
  2987. {
  2988. const tc = document.createElement("div");
  2989.  
  2990. tc.innerHTML += `
  2991. <br/>
  2992. <div>
  2993. <label>
  2994. 隐藏匿名的用户
  2995. <input type="checkbox" ${
  2996. data.options.filterAnony ? `checked="checked"` : ""
  2997. } />
  2998. </label>
  2999. </div>
  3000. `;
  3001.  
  3002. const checkbox = tc.querySelector("input");
  3003.  
  3004. checkbox.onchange = () => {
  3005. const v = checkbox.checked;
  3006.  
  3007. data.options.filterAnony = v;
  3008.  
  3009. saveData();
  3010. runFilter();
  3011. };
  3012.  
  3013. container.appendChild(tc);
  3014. }
  3015.  
  3016. // 删除没有标记的用户
  3017. {
  3018. const tc = document.createElement("div");
  3019.  
  3020. tc.innerHTML += `
  3021. <br/>
  3022. <div>
  3023. <button>删除没有标记的用户</button>
  3024. </div>
  3025. `;
  3026.  
  3027. const actions = tc.getElementsByTagName("button");
  3028.  
  3029. actions[0].onclick = () => {
  3030. if (confirm("是否确认?")) {
  3031. Object.values(data.users).forEach((item) => {
  3032. if (item.tags.length === 0) {
  3033. delete data.users[item.id];
  3034. }
  3035. });
  3036.  
  3037. saveData();
  3038. runFilter();
  3039. }
  3040. };
  3041.  
  3042. container.appendChild(tc);
  3043. }
  3044.  
  3045. // 删除没有用户的标记
  3046. {
  3047. const tc = document.createElement("div");
  3048.  
  3049. tc.innerHTML += `
  3050. <br/>
  3051. <div>
  3052. <button>删除没有用户的标记</button>
  3053. </div>
  3054. `;
  3055.  
  3056. const actions = tc.getElementsByTagName("button");
  3057.  
  3058. actions[0].onclick = () => {
  3059. if (confirm("是否确认?")) {
  3060. Object.values(data.tags).forEach((item) => {
  3061. if (
  3062. Object.values(data.users).filter((user) =>
  3063. user.tags.find((tag) => tag === item.id)
  3064. ).length === 0
  3065. ) {
  3066. delete data.tags[item.id];
  3067. }
  3068. });
  3069.  
  3070. saveData();
  3071. runFilter();
  3072. }
  3073. };
  3074.  
  3075. container.appendChild(tc);
  3076. }
  3077.  
  3078. // 删除非激活中的用户
  3079. {
  3080. const tc = document.createElement("div");
  3081.  
  3082. tc.innerHTML += `
  3083. <br/>
  3084. <div>
  3085. <button>删除非激活中的用户</button>
  3086. <div style="white-space: normal;"></div>
  3087. </div>
  3088. `;
  3089.  
  3090. const action = tc.querySelector("button");
  3091. const list = action.nextElementSibling;
  3092.  
  3093. action.onclick = () => {
  3094. if (confirm("是否确认?")) {
  3095. const waitingQueue = Object.values(data.users).map(
  3096. (item) => () =>
  3097. new Promise((resolve) => {
  3098. fetch(
  3099. `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${item.id}`
  3100. )
  3101. .then((res) => res.blob())
  3102. .then((blob) => {
  3103. const reader = new FileReader();
  3104.  
  3105. reader.onload = () => {
  3106. const text = reader.result;
  3107. const result = JSON.parse(
  3108. text.replace(
  3109. "window.script_muti_get_var_store=",
  3110. ""
  3111. )
  3112. );
  3113.  
  3114. if (!result.error) {
  3115. const { bit } = result.data[0];
  3116.  
  3117. const activeInfo = n.activeInfo(0, 0, bit);
  3118.  
  3119. const activeType = activeInfo[1];
  3120.  
  3121. if (!["ACTIVED", "LINKED"].includes(activeType)) {
  3122. list.innerHTML += `<a href="/nuke.php?func=ucp&uid=${
  3123. item.id
  3124. }" class="b nobr">[${
  3125. item.name ? "@" + item.name : "#" + item.id
  3126. }]</a>`;
  3127.  
  3128. delete data.users[item.id];
  3129. }
  3130. }
  3131.  
  3132. resolve();
  3133. };
  3134.  
  3135. reader.readAsText(blob, "GBK");
  3136. })
  3137. .catch(() => {
  3138. resolve();
  3139. });
  3140. })
  3141. );
  3142.  
  3143. const queueLength = waitingQueue.length;
  3144.  
  3145. const execute = () => {
  3146. if (waitingQueue.length) {
  3147. const next = waitingQueue.shift();
  3148.  
  3149. action.innerHTML = `删除非激活中的用户 (${
  3150. queueLength - waitingQueue.length
  3151. }/${queueLength})`;
  3152. action.disabled = true;
  3153.  
  3154. next().finally(execute);
  3155. } else {
  3156. action.disabled = false;
  3157.  
  3158. saveData();
  3159. runFilter();
  3160. }
  3161. };
  3162.  
  3163. execute();
  3164. }
  3165. };
  3166.  
  3167. container.appendChild(tc);
  3168. }
  3169. };
  3170.  
  3171. return func;
  3172. })();
  3173.  
  3174. return {
  3175. name: "通用设置",
  3176. content,
  3177. refresh,
  3178. };
  3179. })();
  3180.  
  3181. u.addModule(listModule).toggle();
  3182. u.addModule(userModule);
  3183. u.addModule(tagModule);
  3184. u.addModule(keywordModule);
  3185. u.addModule(locationModule);
  3186. u.addModule(witchHuntModule);
  3187. u.addModule(commonModule);
  3188.  
  3189. // 增加菜单项
  3190. (() => {
  3191. let window;
  3192.  
  3193. m.create(() => {
  3194. if (window === undefined) {
  3195. window = n.createCommmonWindow();
  3196. }
  3197.  
  3198. window._.addContent(null);
  3199. window._.addTitle(`屏蔽`);
  3200. window._.addContent(u.content);
  3201. window._.show();
  3202. });
  3203. })();
  3204.  
  3205. // 执行过滤
  3206. (() => {
  3207. const hookFunction = (object, functionName, callback) => {
  3208. ((originalFunction) => {
  3209. object[functionName] = function () {
  3210. const returnValue = originalFunction.apply(this, arguments);
  3211.  
  3212. callback.apply(this, [returnValue, originalFunction, arguments]);
  3213.  
  3214. return returnValue;
  3215. };
  3216. })(object[functionName]);
  3217. };
  3218.  
  3219. const initialized = {
  3220. topicArg: false,
  3221. postArg: false,
  3222. };
  3223.  
  3224. hookFunction(n, "eval", () => {
  3225. if (Object.values(initialized).findIndex((item) => item === false) < 0) {
  3226. return;
  3227. }
  3228.  
  3229. if (n.topicArg && initialized.topicArg === false) {
  3230. hookFunction(n.topicArg, "add", () => {
  3231. runFilter(false);
  3232. });
  3233.  
  3234. initialized.topicArg = true;
  3235. }
  3236.  
  3237. if (n.postArg && initialized.postArg === false) {
  3238. hookFunction(n.postArg, "proc", () => {
  3239. runFilter(false);
  3240. });
  3241.  
  3242. initialized.postArg = true;
  3243. }
  3244. });
  3245.  
  3246. runFilter();
  3247. })();
  3248. })(commonui, __CURRENT_UID);