NGA Filter

troll must die

当前为 2023-03-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NGA Filter
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 1.9.4
  5. // @author snyssss
  6. // @description 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. filterReputationLimit: NaN,
  83. filterMode: "隐藏",
  84. },
  85. };
  86.  
  87. const v = GM_getValue(DATA_KEY);
  88.  
  89. if (typeof v !== "object") {
  90. return d;
  91. }
  92.  
  93. return Object.assign(d, v);
  94. })();
  95.  
  96. // 保存数据
  97. const saveData = () => {
  98. GM_setValue(DATA_KEY, data);
  99. };
  100.  
  101. // 增加标记
  102. const addTag = (name) => {
  103. const tag = Object.values(data.tags).find((item) => item.name === name);
  104.  
  105. if (tag) return tag.id;
  106.  
  107. const id =
  108. Math.max(...Object.values(data.tags).map((item) => item.id), 0) + 1;
  109.  
  110. const hash = (() => {
  111. let h = 5381;
  112. for (var i = 0; i < name.length; i++) {
  113. h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
  114. }
  115. return h;
  116. })();
  117.  
  118. const hex = Math.abs(hash).toString(16) + "000000";
  119.  
  120. const hsv = [
  121. `0x${hex.substring(2, 4)}` / 255,
  122. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  123. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  124. ];
  125.  
  126. const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  127.  
  128. const color = ["#", ...rgb].reduce((a, b) => {
  129. return a + ("0" + b.toString(16)).slice(-2);
  130. });
  131.  
  132. data.tags[id] = {
  133. id,
  134. name,
  135. color,
  136. filterMode: FILTER_MODE[0],
  137. };
  138.  
  139. saveData();
  140.  
  141. return id;
  142. };
  143.  
  144. // 增加用户
  145. const addUser = (id, name = null, tags = [], filterMode = FILTER_MODE[0]) => {
  146. if (data.users[id]) return data.users[id];
  147.  
  148. data.users[id] = {
  149. id,
  150. name,
  151. tags,
  152. filterMode,
  153. };
  154.  
  155. saveData();
  156.  
  157. return data.users[id];
  158. };
  159.  
  160. // 增加关键字
  161. const addKeyword = (
  162. keyword,
  163. filterMode = FILTER_MODE[0],
  164. filterLevel = 0
  165. ) => {
  166. const id =
  167. Math.max(...Object.values(data.keywords).map((item) => item.id), 0) + 1;
  168.  
  169. data.keywords[id] = {
  170. id,
  171. keyword,
  172. filterMode,
  173. filterLevel,
  174. };
  175.  
  176. saveData();
  177.  
  178. return id;
  179. };
  180.  
  181. // 增加属地
  182. const addLocation = (keyword, filterMode = FILTER_MODE[0]) => {
  183. const id =
  184. Math.max(...Object.values(data.locations).map((item) => item.id), 0) + 1;
  185.  
  186. data.locations[id] = {
  187. id,
  188. keyword,
  189. filterMode,
  190. };
  191.  
  192. saveData();
  193.  
  194. return id;
  195. };
  196.  
  197. // 旧版本数据迁移
  198. {
  199. const dataKey = "troll_data";
  200. const modeKey = "troll_mode";
  201. const keywordKey = "troll_keyword";
  202.  
  203. if (localStorage.getItem(dataKey)) {
  204. let trollMap = (function () {
  205. try {
  206. return JSON.parse(localStorage.getItem(dataKey)) || {};
  207. } catch (e) {}
  208.  
  209. return {};
  210. })();
  211.  
  212. let filterMode = ~~localStorage.getItem(modeKey);
  213.  
  214. let filterKeyword = localStorage.getItem(keywordKey) || "";
  215.  
  216. // 整理标签
  217. [...new Set(Object.values(trollMap).flat())].forEach((item) =>
  218. addTag(item)
  219. );
  220.  
  221. // 整理用户
  222. Object.keys(trollMap).forEach((item) => {
  223. addUser(
  224. item,
  225. null,
  226. (typeof trollMap[item] === "object" ? trollMap[item] : []).map(
  227. (tag) => addTag(tag)
  228. )
  229. );
  230. });
  231.  
  232. data.options.filterMode = filterMode ? "隐藏" : "标记";
  233. data.options.keyword = filterKeyword;
  234.  
  235. localStorage.removeItem(dataKey);
  236. localStorage.removeItem(modeKey);
  237. localStorage.removeItem(keywordKey);
  238.  
  239. saveData();
  240. }
  241.  
  242. // v1.1.0 -> v1.1.1
  243. {
  244. Object.values(data.users).forEach(({ id, name, tags, enabled }) => {
  245. if (enabled !== undefined) {
  246. data.users[id] = {
  247. id,
  248. name,
  249. tags,
  250. filterMode: enabled ? "继承" : "显示",
  251. };
  252. }
  253. });
  254.  
  255. Object.values(data.tags).forEach(({ id, name, color, enabled }) => {
  256. if (enabled !== undefined) {
  257. data.tags[id] = {
  258. id,
  259. name,
  260. color,
  261. filterMode: enabled ? "继承" : "显示",
  262. };
  263. }
  264. });
  265.  
  266. if (data.options.filterMode === 0) {
  267. data.options.filterMode = "隐藏";
  268. } else if (data.options.filterMode === 1) {
  269. data.options.filterMode = "标记";
  270. }
  271.  
  272. saveData();
  273. }
  274.  
  275. // v1.2.x -> v1.3.0
  276. {
  277. if (data.options.keyword) {
  278. addKeyword(data.options.keyword);
  279.  
  280. delete data.options.keyword;
  281.  
  282. saveData();
  283. }
  284. }
  285. }
  286.  
  287. // 编辑用户标记
  288. const editUser = (() => {
  289. let window;
  290. return (uid, name, callback) => {
  291. if (window === undefined) {
  292. window = n.createCommmonWindow();
  293. }
  294.  
  295. const user = data.users[uid];
  296.  
  297. const content = document.createElement("div");
  298.  
  299. const size = Math.floor((screen.width * 0.8) / 200);
  300.  
  301. const items = Object.values(data.tags).map(
  302. (tag, index) => `
  303. <td class="c1">
  304. <label for="s-tag-${index}" style="display: block; cursor: pointer;">
  305. <b class="block_txt nobr" style="background:${
  306. tag.color
  307. }; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>
  308. </label>
  309. </td>
  310. <td class="c2" width="1">
  311. <input id="s-tag-${index}" type="checkbox" value="${tag.id}" ${
  312. user && user.tags.find((item) => item === tag.id) && "checked"
  313. }/>
  314. </td>
  315. `
  316. );
  317.  
  318. const rows = [...new Array(Math.ceil(items.length / size))].map(
  319. (item, index) =>
  320. `
  321. <tr class="row${(index % 2) + 1}">
  322. ${items.slice(size * index, size * (index + 1)).join("")}
  323. </tr>
  324. `
  325. );
  326.  
  327. content.className = "w100";
  328. content.innerHTML = `
  329. <div class="filter-table-wrapper" style="width: 80vw;">
  330. <table class="filter-table forumbox">
  331. <tbody>
  332. ${rows.join("")}
  333. </tbody>
  334. </table>
  335. </div>
  336. <div style="margin: 10px 0;">
  337. <input placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
  338. </div>
  339. <div style="margin: 10px 0;">
  340. <span>过滤方式:</span>
  341. <button>${(user && user.filterMode) || FILTER_MODE[0]}</button>
  342. <div class="right_">
  343. <button>删除</button>
  344. <button>保存</button>
  345. </div>
  346. </div>
  347. <div class="silver" style="margin-top: 5px;">${FILTER_TIPS}</div>
  348. `;
  349.  
  350. const actions = content.getElementsByTagName("button");
  351.  
  352. actions[0].onclick = () => {
  353. actions[0].innerText = switchFilterMode(
  354. actions[0].innerText || FILTER_MODE[0]
  355. );
  356. };
  357.  
  358. actions[1].onclick = () => {
  359. if (confirm("是否确认?")) {
  360. delete data.users[uid];
  361.  
  362. saveData();
  363.  
  364. callback && callback();
  365.  
  366. window._.hide();
  367. }
  368. };
  369.  
  370. actions[2].onclick = () => {
  371. if (confirm("是否确认?")) {
  372. const values = [...content.getElementsByTagName("input")];
  373. const newTags = values[values.length - 1].value
  374. .split("|")
  375. .filter((item) => item.length)
  376. .map((item) => addTag(item));
  377. const tags = [
  378. ...new Set(
  379. values
  380. .filter((item) => item.type === "checkbox" && item.checked)
  381. .map((item) => ~~item.value)
  382. .concat(newTags)
  383. ),
  384. ].sort();
  385.  
  386. if (user) {
  387. user.tags = tags;
  388. user.filterMode = actions[0].innerText;
  389. } else {
  390. addUser(uid, name, tags, actions[0].innerText);
  391. }
  392.  
  393. saveData();
  394.  
  395. callback && callback();
  396.  
  397. window._.hide();
  398. }
  399. };
  400.  
  401. if (user === undefined) {
  402. actions[1].style = "display: none;";
  403. }
  404.  
  405. window._.addContent(null);
  406. window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
  407. window._.addContent(content);
  408. window._.show();
  409. };
  410. })();
  411.  
  412. // 猎巫
  413. const witchHunter = (() => {
  414. const key = "WITCH_HUNTER";
  415.  
  416. const data = GM_getValue(key) || {};
  417.  
  418. const add = async (fid, label) => {
  419. if (Object.values(data).find((item) => item.fid === fid)) {
  420. alert("已有相同版面ID");
  421. return;
  422. }
  423.  
  424. const info = await new Promise((resolve) => {
  425. request(`/thread.php?lite=js&fid=${fid}`)
  426. .then((res) => res.blob())
  427. .then((blob) => {
  428. const reader = new FileReader();
  429.  
  430. reader.onload = () => {
  431. const text = reader.result;
  432. const result = JSON.parse(
  433. text.replace("window.script_muti_get_var_store=", "")
  434. );
  435.  
  436. resolve(result.data);
  437. };
  438.  
  439. reader.readAsText(blob, "GBK");
  440. })
  441. .catch(() => {
  442. resolve({});
  443. });
  444. });
  445.  
  446. if (info.__F === undefined) {
  447. alert("版面ID有误");
  448. return;
  449. }
  450.  
  451. const name = info.__F.name;
  452.  
  453. const id = Math.max(...Object.values(data).map((item) => item.id), 0) + 1;
  454.  
  455. const hash = (() => {
  456. let h = 5381;
  457. for (var i = 0; i < label.length; i++) {
  458. h = ((h << 5) + h + label.charCodeAt(i)) & 0xffffffff;
  459. }
  460. return h;
  461. })();
  462.  
  463. const hex = Math.abs(hash).toString(16) + "000000";
  464.  
  465. const hsv = [
  466. `0x${hex.substring(2, 4)}` / 255,
  467. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  468. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  469. ];
  470.  
  471. const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);
  472.  
  473. const color = ["#", ...rgb].reduce((a, b) => {
  474. return a + ("0" + b.toString(16)).slice(-2);
  475. });
  476.  
  477. data[id] = {
  478. id,
  479. fid,
  480. name,
  481. label,
  482. color,
  483. };
  484.  
  485. GM_setValue(key, data);
  486. };
  487.  
  488. const remove = (id) => {
  489. delete data[id];
  490.  
  491. GM_setValue(key, data);
  492. };
  493.  
  494. const run = (uid, element) => {
  495. if (uid < 0) {
  496. return;
  497. }
  498.  
  499. Promise.all(
  500. Object.values(data).map(async (item) => {
  501. const api = `/thread.php?lite=js&fid=${item.fid}&authorid=${uid}`;
  502.  
  503. const verify =
  504. (await new Promise((resolve) => {
  505. request(api)
  506. .then((res) => res.blob())
  507. .then((blob) => {
  508. const reader = new FileReader();
  509.  
  510. reader.onload = () => {
  511. const text = reader.result;
  512. const result = JSON.parse(
  513. text.replace("window.script_muti_get_var_store=", "")
  514. );
  515.  
  516. if (result.error) {
  517. resolve(false);
  518. return;
  519. }
  520.  
  521. resolve(true);
  522. };
  523.  
  524. reader.readAsText(blob, "GBK");
  525. })
  526. .catch(() => {
  527. resolve(false);
  528. });
  529. })) ||
  530. (await new Promise((resolve) => {
  531. request(`${api}&searchpost=1`)
  532. .then((res) => res.blob())
  533. .then((blob) => {
  534. const reader = new FileReader();
  535.  
  536. reader.onload = () => {
  537. const text = reader.result;
  538. const result = JSON.parse(
  539. text.replace("window.script_muti_get_var_store=", "")
  540. );
  541.  
  542. if (result.error) {
  543. resolve(false);
  544. return;
  545. }
  546.  
  547. resolve(true);
  548. };
  549.  
  550. reader.readAsText(blob, "GBK");
  551. })
  552. .catch(() => {
  553. resolve(false);
  554. });
  555. }));
  556.  
  557. if (verify) {
  558. return item;
  559. }
  560. })
  561. )
  562. .then((res) => res.filter((item) => item))
  563. .then((res) => {
  564. res
  565. .filter(
  566. (current, index) =>
  567. res.findIndex((item) => item.label === current.label) === index
  568. )
  569. .forEach((item) => {
  570. element.style.display = "block";
  571. element.innerHTML += `<b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>`;
  572. });
  573. });
  574. };
  575.  
  576. return {
  577. add,
  578. remove,
  579. run,
  580. data,
  581. };
  582. })();
  583.  
  584. // 小号过滤和声望过滤
  585. const getFilterModeByUserInfo = async (userInfo, reputation) => {
  586. const filterRegdateLimit = data.options.filterRegdateLimit || 0;
  587.  
  588. const filterPostnumLimit = data.options.filterPostnumLimit || 0;
  589.  
  590. const filterReputationLimit = data.options.filterReputationLimit || NaN;
  591.  
  592. if (userInfo) {
  593. const { regdate, postnum } = userInfo;
  594.  
  595. if (
  596. filterRegdateLimit > 0 &&
  597. regdate * 1000 > new Date() - filterRegdateLimit
  598. ) {
  599. return true;
  600. }
  601.  
  602. if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
  603. return true;
  604. }
  605. }
  606.  
  607. if (Number.isNaN(filterReputationLimit) === false) {
  608. if (reputation < filterReputationLimit) {
  609. return true;
  610. }
  611. }
  612.  
  613. return false;
  614. };
  615.  
  616. // 判断过滤方式
  617. const getFilterMode = async (uid, subject, content) => {
  618. let result = -1;
  619.  
  620. const user = data.users[uid];
  621.  
  622. const tags = user ? user.tags.map((tag) => data.tags[tag]) : [];
  623.  
  624. const keywords = Object.values(data.keywords);
  625.  
  626. const locations = Object.values(data.locations);
  627.  
  628. if (uid && uid > 0) {
  629. const userInfo = n.userInfo.users[uid];
  630.  
  631. const reputation = (() => {
  632. const reputations = n.userInfo.reputations;
  633.  
  634. if (reputations) {
  635. for (let fid in reputations) {
  636. return reputations[fid][uid] || 0;
  637. }
  638. }
  639.  
  640. return NaN;
  641. })();
  642.  
  643. if (await getFilterModeByUserInfo(userInfo, reputation)) {
  644. return FILTER_MODE.indexOf("隐藏");
  645. }
  646. }
  647.  
  648. if (user) {
  649. const filterMode = FILTER_MODE.indexOf(user.filterMode);
  650.  
  651. if (filterMode > 0) {
  652. return filterMode;
  653. }
  654.  
  655. result = filterMode;
  656. }
  657.  
  658. if (tags.length) {
  659. const filterMode = (() => {
  660. if (tags.some((tag) => tag.filterMode !== "显示")) {
  661. return tags
  662. .filter((tag) => tag.filterMode !== "显示")
  663. .map((tag) => FILTER_MODE.indexOf(tag.filterMode) || 0)
  664. .sort((a, b) => b - a)[0];
  665. }
  666.  
  667. return FILTER_MODE.indexOf("显示");
  668. })();
  669.  
  670. if (filterMode > 0) {
  671. return filterMode;
  672. }
  673.  
  674. result = filterMode;
  675. }
  676.  
  677. if (keywords.length) {
  678. const filterMode = (() => {
  679. const sR = (() => {
  680. if (subject) {
  681. const r = keywords
  682. .filter((item) => item.keyword && item.filterMode !== "显示")
  683. .filter((item) => (item.filterLevel || 0) >= 0)
  684. .sort(
  685. (a, b) =>
  686. FILTER_MODE.indexOf(b.filterMode) -
  687. FILTER_MODE.indexOf(a.filterMode)
  688. )
  689. .find((item) => subject.search(item.keyword) >= 0);
  690.  
  691. if (r) {
  692. return FILTER_MODE.indexOf(r.filterMode);
  693. }
  694. }
  695.  
  696. return -1;
  697. })();
  698.  
  699. const cR = (() => {
  700. if (content) {
  701. const r = keywords
  702. .filter((item) => item.keyword && item.filterMode !== "显示")
  703. .filter((item) => (item.filterLevel || 0) >= 1)
  704. .sort(
  705. (a, b) =>
  706. FILTER_MODE.indexOf(b.filterMode) -
  707. FILTER_MODE.indexOf(a.filterMode)
  708. )
  709. .find((item) => content.search(item.keyword) >= 0);
  710.  
  711. if (r) {
  712. return FILTER_MODE.indexOf(r.filterMode);
  713. }
  714. }
  715.  
  716. return -1;
  717. })();
  718.  
  719. return Math.max(sR, cR, result);
  720. })();
  721.  
  722. if (filterMode > 0) {
  723. return filterMode;
  724. }
  725.  
  726. result = filterMode;
  727. }
  728.  
  729. if (locations.length) {
  730. const { ipLoc } = await new Promise((resolve) => {
  731. request(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`)
  732. .then((res) => res.blob())
  733. .then((blob) => {
  734. const reader = new FileReader();
  735.  
  736. reader.onload = () => {
  737. const text = reader.result;
  738. const result = JSON.parse(
  739. text.replace("window.script_muti_get_var_store=", "")
  740. );
  741.  
  742. resolve(result.data[0]);
  743. };
  744.  
  745. reader.readAsText(blob, "GBK");
  746. })
  747. .catch(() => {
  748. resolve({});
  749. });
  750. });
  751.  
  752. if (ipLoc) {
  753. const filterMode = (() => {
  754. const r = locations
  755. .filter((item) => item.keyword && item.filterMode !== "显示")
  756. .sort(
  757. (a, b) =>
  758. FILTER_MODE.indexOf(b.filterMode) -
  759. FILTER_MODE.indexOf(a.filterMode)
  760. )
  761. .find((item) => ipLoc.search(item.keyword) >= 0);
  762.  
  763. if (r) {
  764. return FILTER_MODE.indexOf(r.filterMode);
  765. }
  766.  
  767. return Math.max(r, result);
  768. })();
  769.  
  770. if (filterMode > 0) {
  771. return filterMode;
  772. }
  773.  
  774. result = filterMode;
  775. }
  776. }
  777.  
  778. return result;
  779. };
  780.  
  781. // 根据 TID 获取过滤方式
  782. const getFilterModeByTopic = async (tid) => {
  783. return await new Promise((resolve, reject) => {
  784. const api = `/read.php?tid=${tid}`;
  785.  
  786. request(api)
  787. .then((res) => res.blob())
  788. .then((blob) => {
  789. const getLastIndex = (content, position) => {
  790. if (position >= 0) {
  791. let nextIndex = position + 1;
  792.  
  793. while (nextIndex < content.length) {
  794. if (content[nextIndex] === "}") {
  795. return nextIndex;
  796. }
  797.  
  798. if (content[nextIndex] === "{") {
  799. nextIndex = getLastIndex(content, nextIndex);
  800.  
  801. if (nextIndex < 0) {
  802. break;
  803. }
  804. }
  805.  
  806. nextIndex = nextIndex + 1;
  807. }
  808. }
  809.  
  810. return -1;
  811. };
  812.  
  813. const reader = new FileReader();
  814.  
  815. reader.onload = async () => {
  816. const parser = new DOMParser();
  817.  
  818. const doc = parser.parseFromString(reader.result, "text/html");
  819.  
  820. const html = doc.body.innerHTML;
  821.  
  822. // 验证帖子正常
  823. const verify = doc.querySelector("#m_posts");
  824.  
  825. if (verify) {
  826. // 取得顶楼 UID
  827. const uid = (() => {
  828. const ele = doc.querySelector("#postauthor0");
  829.  
  830. if (ele) {
  831. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  832.  
  833. if (res) {
  834. return res[1];
  835. }
  836. }
  837.  
  838. return 0;
  839. })();
  840.  
  841. // 取得顶楼标题
  842. const subject = doc.querySelector("#postsubject0").innerHTML;
  843.  
  844. // 取得顶楼内容
  845. const content = doc.querySelector("#postcontent0").innerHTML;
  846.  
  847. if (uid && uid > 0) {
  848. // 取得用户信息
  849. const userInfo = (() => {
  850. // 起始JSON
  851. const str = `"${uid}":{`;
  852.  
  853. // 起始下标
  854. const index = html.indexOf(str) + str.length;
  855.  
  856. // 结尾下标
  857. const lastIndex = getLastIndex(html, index);
  858.  
  859. if (lastIndex >= 0) {
  860. try {
  861. return JSON.parse(
  862. `{${html.substring(index, lastIndex)}}`
  863. );
  864. } catch {}
  865. }
  866.  
  867. return null;
  868. })();
  869.  
  870. // 取得用户声望
  871. const reputation = (() => {
  872. const reputations = (() => {
  873. // 起始JSON
  874. const str = `"__REPUTATIONS":{`;
  875.  
  876. // 起始下标
  877. const index = html.indexOf(str) + str.length;
  878.  
  879. // 结尾下标
  880. const lastIndex = getLastIndex(html, index);
  881.  
  882. if (lastIndex >= 0) {
  883. return JSON.parse(
  884. `{${html.substring(index, lastIndex)}}`
  885. );
  886. }
  887.  
  888. return null;
  889. })();
  890.  
  891. if (reputations) {
  892. for (let fid in reputations) {
  893. return reputations[fid][uid] || 0;
  894. }
  895. }
  896.  
  897. return NaN;
  898. })();
  899.  
  900. if (await getFilterModeByUserInfo(userInfo, reputation)) {
  901. resolve(FILTER_MODE.indexOf("隐藏"));
  902. }
  903. }
  904.  
  905. resolve(getFilterMode(uid, subject, content));
  906. } else {
  907. reject();
  908. }
  909. };
  910.  
  911. reader.readAsText(blob, "GBK");
  912. })
  913. .catch(() => {
  914. reject();
  915. });
  916. }).catch(() => {
  917. return FILTER_MODE.indexOf("隐藏");
  918. });
  919. };
  920.  
  921. // 处理引用
  922. const handleQuote = async (content) => {
  923. const quotes = content.querySelectorAll(".quote");
  924.  
  925. await Promise.all(
  926. [...quotes].map(async (quote) => {
  927. const uid = (() => {
  928. const ele = quote.querySelector("a[href^='/nuke.php']");
  929.  
  930. if (ele) {
  931. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  932.  
  933. if (res) {
  934. return res[1];
  935. }
  936. }
  937.  
  938. return 0;
  939. })();
  940.  
  941. const filterMode = await new Promise(async (resolve) => {
  942. const mode = await getFilterMode(uid, "", quote.innerText);
  943.  
  944. if (mode === 0) {
  945. resolve(data.options.filterMode);
  946. }
  947.  
  948. if (mode > 0) {
  949. resolve(FILTER_MODE[mode]);
  950. }
  951.  
  952. resolve("");
  953. });
  954.  
  955. if (filterMode === "标记") {
  956. quote.innerHTML = `
  957. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  958. <span class="crimson">Troll must die.</span>
  959. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  960. <div style="display: none;" name="troll_${uid}">
  961. ${quote.innerHTML}
  962. </div>
  963. </div>`;
  964. } else if (filterMode === "遮罩") {
  965. const source = document.createElement("DIV");
  966.  
  967. source.innerHTML = quote.innerHTML;
  968. source.style.display = "none";
  969.  
  970. const caption = document.createElement("CAPTION");
  971.  
  972. caption.className = "filter-mask filter-mask-block";
  973.  
  974. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  975. caption.onclick = () => {
  976. quote.removeChild(caption);
  977.  
  978. source.style.display = "";
  979. };
  980.  
  981. quote.innerHTML = "";
  982. quote.appendChild(source);
  983. quote.appendChild(caption);
  984. } else if (filterMode === "隐藏") {
  985. quote.innerHTML = "";
  986. }
  987. })
  988. );
  989. };
  990.  
  991. // 过滤
  992. const reFilter = (() => {
  993. let hasNext = false;
  994. let isRunning = false;
  995.  
  996. const func = async () => {
  997. const tPage = location.pathname === "/thread.php";
  998. const pPage = location.pathname === "/read.php";
  999.  
  1000. if (tPage) {
  1001. const params = new URLSearchParams(location.search);
  1002.  
  1003. if (params.has("favor")) {
  1004. return;
  1005. }
  1006.  
  1007. if (params.has("authorid")) {
  1008. return;
  1009. }
  1010. }
  1011.  
  1012. if (tPage) {
  1013. const tData = n.topicArg.data;
  1014.  
  1015. await Promise.all(
  1016. Object.values(tData).map(async (item) => {
  1017. if (item.containerC) return;
  1018.  
  1019. const tid = item[8];
  1020.  
  1021. const filterMode = await new Promise(async (resolve) => {
  1022. const mode = await getFilterModeByTopic(tid);
  1023.  
  1024. if (mode === 0) {
  1025. resolve(data.options.filterMode);
  1026. }
  1027.  
  1028. if (mode > 0) {
  1029. resolve(FILTER_MODE[mode]);
  1030. }
  1031.  
  1032. resolve("");
  1033. });
  1034.  
  1035. item.contentC = item[1];
  1036.  
  1037. item.contentB = item.contentB || item.contentC.innerHTML;
  1038.  
  1039. item.containerC =
  1040. item.containerC || item.contentC.parentNode.parentNode;
  1041.  
  1042. item.containerC.style = "";
  1043. item.contentC.style = "";
  1044. item[1].className = item[1].className.replace(" filter-mask", "");
  1045. item[2].className = item[2].className.replace(" filter-mask", "");
  1046.  
  1047. if (filterMode === "标记") {
  1048. item.contentC.style = "text-decoration: line-through;";
  1049. } else if (filterMode === "遮罩") {
  1050. item[1].className += " filter-mask";
  1051. item[2].className += " filter-mask";
  1052. } else if (filterMode === "隐藏") {
  1053. item.containerC.style = "display: none;";
  1054. }
  1055. })
  1056. );
  1057. } else if (pPage) {
  1058. const pData = n.postArg.data;
  1059.  
  1060. await Promise.all(
  1061. Object.values(pData).map(async (item) => {
  1062. if (~~item.pAid === self) return;
  1063. if (item.containerC) return;
  1064.  
  1065. if (typeof item.i === "number") {
  1066. item.actionC =
  1067. item.actionC ||
  1068. (() => {
  1069. const ele = item.uInfoC.querySelector('[name="uid"]');
  1070.  
  1071. ele.onclick = null;
  1072.  
  1073. return ele;
  1074. })();
  1075.  
  1076. item.tagC =
  1077. item.tagC ||
  1078. (() => {
  1079. const tc = document.createElement("div");
  1080.  
  1081. tc.className = "filter-tags";
  1082.  
  1083. item.uInfoC.appendChild(tc);
  1084.  
  1085. return tc;
  1086. })();
  1087. }
  1088.  
  1089. item.pName =
  1090. item.pName ||
  1091. item.uInfoC.getElementsByClassName("author")[0].innerText;
  1092.  
  1093. item.reFilter =
  1094. item.reFilter ||
  1095. (async () => {
  1096. const uid = item.pAid;
  1097.  
  1098. const filterMode = await new Promise(async (resolve) => {
  1099. const mode = await getFilterMode(
  1100. uid,
  1101. item.subjectC.innerText,
  1102. item.contentC.innerText
  1103. );
  1104.  
  1105. if (mode === 0) {
  1106. resolve(data.options.filterMode);
  1107. }
  1108.  
  1109. if (mode > 0) {
  1110. resolve(FILTER_MODE[mode]);
  1111. }
  1112.  
  1113. resolve("");
  1114. });
  1115.  
  1116. item.avatarC =
  1117. item.avatarC ||
  1118. (() => {
  1119. const tc = document.createElement("div");
  1120.  
  1121. const avatar = document.getElementById(
  1122. `posteravatar${item.i}`
  1123. );
  1124.  
  1125. if (avatar) {
  1126. avatar.parentNode.insertBefore(tc, avatar.nextSibling);
  1127.  
  1128. tc.appendChild(avatar);
  1129. }
  1130.  
  1131. return tc;
  1132. })();
  1133.  
  1134. item.contentB = item.contentB || item.contentC.innerHTML;
  1135.  
  1136. item.containerC =
  1137. item.containerC ||
  1138. (() => {
  1139. let temp = item.contentC;
  1140.  
  1141. if (item.i >= 0) {
  1142. while (temp.nodeName !== "TBODY") {
  1143. temp = temp.parentNode;
  1144. }
  1145. } else {
  1146. while (temp.nodeName !== "DIV") {
  1147. temp = temp.parentNode;
  1148. }
  1149. }
  1150.  
  1151. return temp;
  1152. })();
  1153.  
  1154. item.avatarC.style.display = "";
  1155. item.containerC.style.display = "";
  1156. item.contentC.innerHTML = item.contentB;
  1157.  
  1158. if (item.actionC) {
  1159. item.actionC.style = "background: #aaa;";
  1160. }
  1161.  
  1162. if (filterMode === "标记") {
  1163. item.avatarC.style.display = "none";
  1164. item.contentC.innerHTML = `
  1165. <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
  1166. <span class="crimson">Troll must die.</span>
  1167. <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
  1168. <div style="display: none;" name="troll_${uid}">
  1169. ${item.contentB}
  1170. </div>
  1171. </div>`;
  1172.  
  1173. if (item.actionC && data.users[uid]) {
  1174. item.actionC.style = "background: #cb4042;";
  1175. }
  1176. } else if (filterMode === "遮罩") {
  1177. const caption = document.createElement("CAPTION");
  1178.  
  1179. if (item.i >= 0) {
  1180. caption.className = "filter-mask filter-mask-block";
  1181. } else {
  1182. caption.className = "filter-mask filter-mask-block left";
  1183. caption.style = "width: 47%;";
  1184. }
  1185.  
  1186. caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
  1187. caption.onclick = () => {
  1188. item.containerC.parentNode.removeChild(caption);
  1189. item.containerC.style.display = "";
  1190. };
  1191.  
  1192. item.containerC.parentNode.insertBefore(
  1193. caption,
  1194. item.containerC
  1195. );
  1196. item.containerC.style.display = "none";
  1197.  
  1198. if (item.actionC && data.users[uid]) {
  1199. item.actionC.style = "background: #cb4042;";
  1200. }
  1201. } else if (filterMode === "隐藏") {
  1202. item.containerC.style.display = "none";
  1203. } else {
  1204. await handleQuote(item.contentC);
  1205. }
  1206.  
  1207. if (item.tagC) {
  1208. const tags = data.users[uid]
  1209. ? data.users[uid].tags.map((tag) => data.tags[tag]) || []
  1210. : [];
  1211.  
  1212. item.tagC.style.display = tags.length ? "" : "none";
  1213. item.tagC.innerHTML = tags
  1214. .map(
  1215. (tag) =>
  1216. `<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
  1217. )
  1218. .join("");
  1219.  
  1220. witchHunter.run(uid, item.tagC);
  1221. }
  1222. });
  1223.  
  1224. if (item.actionC) {
  1225. item.actionC.onclick =
  1226. item.actionC.onclick ||
  1227. ((e) => {
  1228. if (item.pAid < 0) return;
  1229.  
  1230. const user = data.users[item.pAid];
  1231.  
  1232. if (e.ctrlKey === false) {
  1233. editUser(item.pAid, item.pName, item.reFilter);
  1234. } else {
  1235. if (user) {
  1236. delete data.users[user.id];
  1237. } else {
  1238. addUser(item.pAid, item.pName);
  1239. }
  1240.  
  1241. saveData();
  1242. item.reFilter();
  1243. }
  1244. });
  1245. }
  1246.  
  1247. await item.reFilter();
  1248. })
  1249. );
  1250. }
  1251. };
  1252.  
  1253. const execute = () =>
  1254. func().finally(() => {
  1255. if (hasNext) {
  1256. hasNext = false;
  1257.  
  1258. execute();
  1259. } else {
  1260. isRunning = false;
  1261. }
  1262. });
  1263.  
  1264. return async () => {
  1265. if (isRunning) {
  1266. hasNext = true;
  1267. } else {
  1268. isRunning = true;
  1269.  
  1270. await execute();
  1271. }
  1272. };
  1273. })();
  1274.  
  1275. // STYLE
  1276. GM_addStyle(`
  1277. .filter-table-wrapper {
  1278. max-height: 80vh;
  1279. overflow-y: auto;
  1280. }
  1281. .filter-table {
  1282. margin: 0;
  1283. }
  1284. .filter-table th,
  1285. .filter-table td {
  1286. position: relative;
  1287. white-space: nowrap;
  1288. }
  1289. .filter-table th {
  1290. position: sticky;
  1291. top: 2px;
  1292. z-index: 1;
  1293. }
  1294. .filter-table input:not([type]), .filter-table input[type="text"] {
  1295. margin: 0;
  1296. box-sizing: border-box;
  1297. height: 100%;
  1298. width: 100%;
  1299. }
  1300. .filter-input-wrapper {
  1301. position: absolute;
  1302. top: 6px;
  1303. right: 6px;
  1304. bottom: 6px;
  1305. left: 6px;
  1306. }
  1307. .filter-text-ellipsis {
  1308. display: flex;
  1309. }
  1310. .filter-text-ellipsis > * {
  1311. flex: 1;
  1312. width: 1px;
  1313. overflow: hidden;
  1314. text-overflow: ellipsis;
  1315. }
  1316. .filter-button-group {
  1317. margin: -.1em -.2em;
  1318. }
  1319. .filter-tags {
  1320. margin: 2px -0.2em 0;
  1321. text-align: left;
  1322. }
  1323. .filter-mask {
  1324. margin: 1px;
  1325. color: #81C7D4;
  1326. background: #81C7D4;
  1327. }
  1328. .filter-mask-block {
  1329. display: block;
  1330. border: 1px solid #66BAB7;
  1331. text-align: center !important;
  1332. }
  1333. .filter-input-wrapper {
  1334. position: absolute;
  1335. top: 6px;
  1336. right: 6px;
  1337. bottom: 6px;
  1338. left: 6px;
  1339. }
  1340. `);
  1341.  
  1342. // UI
  1343. const u = (() => {
  1344. const modules = {};
  1345.  
  1346. const tabContainer = (() => {
  1347. const c = document.createElement("div");
  1348.  
  1349. c.className = "w100";
  1350. c.innerHTML = `
  1351. <div class="right_" style="margin-bottom: 5px;">
  1352. <table class="stdbtn" cellspacing="0">
  1353. <tbody>
  1354. <tr></tr>
  1355. </tbody>
  1356. </table>
  1357. </div>
  1358. <div class="clear"></div>
  1359. `;
  1360.  
  1361. return c;
  1362. })();
  1363.  
  1364. const tabPanelContainer = (() => {
  1365. const c = document.createElement("div");
  1366.  
  1367. c.style = "width: 80vw;";
  1368.  
  1369. return c;
  1370. })();
  1371.  
  1372. const content = (() => {
  1373. const c = document.createElement("div");
  1374.  
  1375. c.append(tabContainer);
  1376. c.append(tabPanelContainer);
  1377.  
  1378. return c;
  1379. })();
  1380.  
  1381. const addModule = (() => {
  1382. const tc = tabContainer.getElementsByTagName("tr")[0];
  1383. const cc = tabPanelContainer;
  1384.  
  1385. return (module) => {
  1386. const tabBox = document.createElement("td");
  1387.  
  1388. tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
  1389.  
  1390. const tab = tabBox.childNodes[0];
  1391.  
  1392. const toggle = () => {
  1393. Object.values(modules).forEach((item) => {
  1394. if (item.tab === tab) {
  1395. item.tab.className = "nobr";
  1396. item.content.style = "display: block";
  1397. item.refresh();
  1398. } else {
  1399. item.tab.className = "nobr silver";
  1400. item.content.style = "display: none";
  1401. }
  1402. });
  1403. };
  1404.  
  1405. tc.append(tabBox);
  1406. cc.append(module.content);
  1407.  
  1408. tab.onclick = toggle;
  1409.  
  1410. modules[module.name] = {
  1411. ...module,
  1412. tab,
  1413. toggle,
  1414. };
  1415.  
  1416. return modules[module.name];
  1417. };
  1418. })();
  1419.  
  1420. return {
  1421. content,
  1422. modules,
  1423. addModule,
  1424. };
  1425. })();
  1426.  
  1427. // 用户
  1428. const userModule = (() => {
  1429. const content = (() => {
  1430. const c = document.createElement("div");
  1431.  
  1432. c.style = "display: none";
  1433. c.innerHTML = `
  1434. <div class="filter-table-wrapper">
  1435. <table class="filter-table forumbox">
  1436. <thead>
  1437. <tr class="block_txt_c0">
  1438. <th class="c1" width="1">昵称</th>
  1439. <th class="c2">标记</th>
  1440. <th class="c3" width="1">过滤方式</th>
  1441. <th class="c4" width="1">操作</th>
  1442. </tr>
  1443. </thead>
  1444. <tbody></tbody>
  1445. </table>
  1446. </div>
  1447. `;
  1448.  
  1449. return c;
  1450. })();
  1451.  
  1452. const refresh = (() => {
  1453. const container = content.getElementsByTagName("tbody")[0];
  1454.  
  1455. const func = () => {
  1456. container.innerHTML = "";
  1457.  
  1458. Object.values(data.users).forEach((item) => {
  1459. const tc = document.createElement("tr");
  1460.  
  1461. tc.className = `row${
  1462. (container.querySelectorAll("TR").length % 2) + 1
  1463. }`;
  1464.  
  1465. tc.refresh = () => {
  1466. if (data.users[item.id]) {
  1467. tc.innerHTML = `
  1468. <td class="c1">
  1469. <a href="/nuke.php?func=ucp&uid=${
  1470. item.id
  1471. }" class="b nobr">[${
  1472. item.name ? "@" + item.name : "#" + item.id
  1473. }]</a>
  1474. </td>
  1475. <td class="c2">
  1476. ${item.tags
  1477. .map((tag) => {
  1478. if (data.tags[tag]) {
  1479. return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
  1480. }
  1481. })
  1482. .join("")}
  1483. </td>
  1484. <td class="c3">
  1485. <div class="filter-table-button-group">
  1486. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1487. </div>
  1488. </td>
  1489. <td class="c4">
  1490. <div class="filter-table-button-group">
  1491. <button>编辑</button>
  1492. <button>删除</button>
  1493. </div>
  1494. </td>
  1495. `;
  1496.  
  1497. const actions = tc.getElementsByTagName("button");
  1498.  
  1499. actions[0].onclick = () => {
  1500. data.users[item.id].filterMode = switchFilterMode(
  1501. data.users[item.id].filterMode || FILTER_MODE[0]
  1502. );
  1503.  
  1504. actions[0].innerHTML = data.users[item.id].filterMode;
  1505.  
  1506. saveData();
  1507. reFilter();
  1508. };
  1509.  
  1510. actions[1].onclick = () => {
  1511. editUser(item.id, item.name, tc.refresh);
  1512. };
  1513.  
  1514. actions[2].onclick = () => {
  1515. if (confirm("是否确认?")) {
  1516. delete data.users[item.id];
  1517. container.removeChild(tc);
  1518.  
  1519. saveData();
  1520. reFilter();
  1521. }
  1522. };
  1523. } else {
  1524. tc.remove();
  1525. }
  1526. };
  1527.  
  1528. tc.refresh();
  1529.  
  1530. container.appendChild(tc);
  1531. });
  1532. };
  1533.  
  1534. return func;
  1535. })();
  1536.  
  1537. return {
  1538. name: "用户",
  1539. content,
  1540. refresh,
  1541. };
  1542. })();
  1543.  
  1544. // 标记
  1545. const tagModule = (() => {
  1546. const content = (() => {
  1547. const c = document.createElement("div");
  1548.  
  1549. c.style = "display: none";
  1550. c.innerHTML = `
  1551. <div class="filter-table-wrapper">
  1552. <table class="filter-table forumbox">
  1553. <thead>
  1554. <tr class="block_txt_c0">
  1555. <th class="c1" width="1">标记</th>
  1556. <th class="c2">列表</th>
  1557. <th class="c3" width="1">过滤方式</th>
  1558. <th class="c4" width="1">操作</th>
  1559. </tr>
  1560. </thead>
  1561. <tbody></tbody>
  1562. </table>
  1563. </div>
  1564. `;
  1565.  
  1566. return c;
  1567. })();
  1568.  
  1569. const refresh = (() => {
  1570. const container = content.getElementsByTagName("tbody")[0];
  1571.  
  1572. const func = () => {
  1573. container.innerHTML = "";
  1574.  
  1575. Object.values(data.tags).forEach((item) => {
  1576. const tc = document.createElement("tr");
  1577.  
  1578. tc.className = `row${
  1579. (container.querySelectorAll("TR").length % 2) + 1
  1580. }`;
  1581.  
  1582. tc.innerHTML = `
  1583. <td class="c1">
  1584. <b class="block_txt nobr" style="background:${
  1585. item.color
  1586. }; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
  1587. </td>
  1588. <td class="c2">
  1589. <button>${
  1590. Object.values(data.users).filter((user) =>
  1591. user.tags.find((tag) => tag === item.id)
  1592. ).length
  1593. }
  1594. </button>
  1595. <div style="white-space: normal; display: none;">
  1596. ${Object.values(data.users)
  1597. .filter((user) =>
  1598. user.tags.find((tag) => tag === item.id)
  1599. )
  1600. .map(
  1601. (user) =>
  1602. `<a href="/nuke.php?func=ucp&uid=${
  1603. user.id
  1604. }" class="b nobr">[${
  1605. user.name ? "@" + user.name : "#" + user.id
  1606. }]</a>`
  1607. )
  1608. .join("")}
  1609. </div>
  1610. </td>
  1611. <td class="c3">
  1612. <div class="filter-table-button-group">
  1613. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1614. </div>
  1615. </td>
  1616. <td class="c4">
  1617. <div class="filter-table-button-group">
  1618. <button>删除</button>
  1619. </div>
  1620. </td>
  1621. `;
  1622.  
  1623. const actions = tc.getElementsByTagName("button");
  1624.  
  1625. actions[0].onclick = (() => {
  1626. let hide = true;
  1627. return () => {
  1628. hide = !hide;
  1629. actions[0].nextElementSibling.style.display = hide
  1630. ? "none"
  1631. : "block";
  1632. };
  1633. })();
  1634.  
  1635. actions[1].onclick = () => {
  1636. data.tags[item.id].filterMode = switchFilterMode(
  1637. data.tags[item.id].filterMode || FILTER_MODE[0]
  1638. );
  1639.  
  1640. actions[1].innerHTML = data.tags[item.id].filterMode;
  1641.  
  1642. saveData();
  1643. reFilter();
  1644. };
  1645.  
  1646. actions[2].onclick = () => {
  1647. if (confirm("是否确认?")) {
  1648. delete data.tags[item.id];
  1649.  
  1650. Object.values(data.users).forEach((user) => {
  1651. const index = user.tags.findIndex((tag) => tag === item.id);
  1652. if (index >= 0) {
  1653. user.tags.splice(index, 1);
  1654. }
  1655. });
  1656.  
  1657. container.removeChild(tc);
  1658.  
  1659. saveData();
  1660. reFilter();
  1661. }
  1662. };
  1663.  
  1664. container.appendChild(tc);
  1665. });
  1666. };
  1667.  
  1668. return func;
  1669. })();
  1670.  
  1671. return {
  1672. name: "标记",
  1673. content,
  1674. refresh,
  1675. };
  1676. })();
  1677.  
  1678. // 关键字
  1679. const keywordModule = (() => {
  1680. const content = (() => {
  1681. const c = document.createElement("div");
  1682.  
  1683. c.style = "display: none";
  1684. c.innerHTML = `
  1685. <div class="filter-table-wrapper">
  1686. <table class="filter-table forumbox">
  1687. <thead>
  1688. <tr class="block_txt_c0">
  1689. <th class="c1">列表</th>
  1690. <th class="c2" width="1">过滤方式</th>
  1691. <th class="c3" width="1">包括内容</th>
  1692. <th class="c4" width="1">操作</th>
  1693. </tr>
  1694. </thead>
  1695. <tbody></tbody>
  1696. </table>
  1697. </div>
  1698. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
  1699. `;
  1700.  
  1701. return c;
  1702. })();
  1703.  
  1704. const refresh = (() => {
  1705. const container = content.getElementsByTagName("tbody")[0];
  1706.  
  1707. const func = () => {
  1708. container.innerHTML = "";
  1709.  
  1710. Object.values(data.keywords).forEach((item) => {
  1711. const tc = document.createElement("tr");
  1712.  
  1713. tc.className = `row${
  1714. (container.querySelectorAll("TR").length % 2) + 1
  1715. }`;
  1716.  
  1717. tc.innerHTML = `
  1718. <td class="c1">
  1719. <div class="filter-input-wrapper">
  1720. <input value="${item.keyword || ""}" />
  1721. </div>
  1722. </td>
  1723. <td class="c2">
  1724. <div class="filter-table-button-group">
  1725. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1726. </div>
  1727. </td>
  1728. <td class="c3">
  1729. <div style="text-align: center;">
  1730. <input type="checkbox" ${
  1731. item.filterLevel ? `checked="checked"` : ""
  1732. } />
  1733. </div>
  1734. </td>
  1735. <td class="c4">
  1736. <div class="filter-table-button-group">
  1737. <button>保存</button>
  1738. <button>删除</button>
  1739. </div>
  1740. </td>
  1741. `;
  1742.  
  1743. const inputElement = tc.querySelector("INPUT");
  1744. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  1745. const actions = tc.getElementsByTagName("button");
  1746.  
  1747. actions[0].onclick = () => {
  1748. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1749. };
  1750.  
  1751. actions[1].onclick = () => {
  1752. if (inputElement.value) {
  1753. data.keywords[item.id] = {
  1754. id: item.id,
  1755. keyword: inputElement.value,
  1756. filterMode: actions[0].innerHTML,
  1757. filterLevel: levelElement.checked ? 1 : 0,
  1758. };
  1759.  
  1760. saveData();
  1761. refresh();
  1762. }
  1763. };
  1764.  
  1765. actions[2].onclick = () => {
  1766. if (confirm("是否确认?")) {
  1767. delete data.keywords[item.id];
  1768.  
  1769. saveData();
  1770. refresh();
  1771. }
  1772. };
  1773.  
  1774. container.appendChild(tc);
  1775. });
  1776.  
  1777. {
  1778. const tc = document.createElement("tr");
  1779.  
  1780. tc.className = `row${
  1781. (container.querySelectorAll("TR").length % 2) + 1
  1782. }`;
  1783.  
  1784. tc.innerHTML = `
  1785. <td class="c1">
  1786. <div class="filter-input-wrapper">
  1787. <input value="" />
  1788. </div>
  1789. </td>
  1790. <td class="c2">
  1791. <div class="filter-table-button-group">
  1792. <button>${FILTER_MODE[0]}</button>
  1793. </div>
  1794. </td>
  1795. <td class="c3">
  1796. <div style="text-align: center;">
  1797. <input type="checkbox" />
  1798. </div>
  1799. </td>
  1800. <td class="c4">
  1801. <div class="filter-table-button-group">
  1802. <button>添加</button>
  1803. </div>
  1804. </td>
  1805. `;
  1806.  
  1807. const inputElement = tc.querySelector("INPUT");
  1808. const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
  1809. const actions = tc.getElementsByTagName("button");
  1810.  
  1811. actions[0].onclick = () => {
  1812. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1813. };
  1814.  
  1815. actions[1].onclick = () => {
  1816. if (inputElement.value) {
  1817. addKeyword(
  1818. inputElement.value,
  1819. actions[0].innerHTML,
  1820. levelElement.checked ? 1 : 0
  1821. );
  1822.  
  1823. saveData();
  1824. refresh();
  1825. }
  1826. };
  1827.  
  1828. container.appendChild(tc);
  1829. }
  1830. };
  1831.  
  1832. return func;
  1833. })();
  1834.  
  1835. return {
  1836. name: "关键字",
  1837. content,
  1838. refresh,
  1839. };
  1840. })();
  1841.  
  1842. // 属地
  1843. const locationModule = (() => {
  1844. const content = (() => {
  1845. const c = document.createElement("div");
  1846.  
  1847. c.style = "display: none";
  1848. c.innerHTML = `
  1849. <div class="filter-table-wrapper">
  1850. <table class="filter-table forumbox">
  1851. <thead>
  1852. <tr class="block_txt_c0">
  1853. <th class="c1">列表</th>
  1854. <th class="c2" width="1">过滤方式</th>
  1855. <th class="c3" width="1">操作</th>
  1856. </tr>
  1857. </thead>
  1858. <tbody></tbody>
  1859. </table>
  1860. </div>
  1861. <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
  1862. `;
  1863.  
  1864. return c;
  1865. })();
  1866.  
  1867. const refresh = (() => {
  1868. const container = content.getElementsByTagName("tbody")[0];
  1869.  
  1870. const func = () => {
  1871. container.innerHTML = "";
  1872.  
  1873. Object.values(data.locations).forEach((item) => {
  1874. const tc = document.createElement("tr");
  1875.  
  1876. tc.className = `row${
  1877. (container.querySelectorAll("TR").length % 2) + 1
  1878. }`;
  1879.  
  1880. tc.innerHTML = `
  1881. <td class="c1">
  1882. <div class="filter-input-wrapper">
  1883. <input value="${item.keyword || ""}" />
  1884. </div>
  1885. </td>
  1886. <td class="c2">
  1887. <div class="filter-table-button-group">
  1888. <button>${item.filterMode || FILTER_MODE[0]}</button>
  1889. </div>
  1890. </td>
  1891. <td class="c3">
  1892. <div class="filter-table-button-group">
  1893. <button>保存</button>
  1894. <button>删除</button>
  1895. </div>
  1896. </td>
  1897. `;
  1898.  
  1899. const inputElement = tc.querySelector("INPUT");
  1900. const actions = tc.getElementsByTagName("button");
  1901.  
  1902. actions[0].onclick = () => {
  1903. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1904. };
  1905.  
  1906. actions[1].onclick = () => {
  1907. if (inputElement.value) {
  1908. data.locations[item.id] = {
  1909. id: item.id,
  1910. keyword: inputElement.value,
  1911. filterMode: actions[0].innerHTML,
  1912. };
  1913.  
  1914. saveData();
  1915. refresh();
  1916. }
  1917. };
  1918.  
  1919. actions[2].onclick = () => {
  1920. if (confirm("是否确认?")) {
  1921. delete data.locations[item.id];
  1922.  
  1923. saveData();
  1924. refresh();
  1925. }
  1926. };
  1927.  
  1928. container.appendChild(tc);
  1929. });
  1930.  
  1931. {
  1932. const tc = document.createElement("tr");
  1933.  
  1934. tc.className = `row${
  1935. (container.querySelectorAll("TR").length % 2) + 1
  1936. }`;
  1937.  
  1938. tc.innerHTML = `
  1939. <td class="c1">
  1940. <div class="filter-input-wrapper">
  1941. <input value="" />
  1942. </div>
  1943. </td>
  1944. <td class="c2">
  1945. <div class="filter-table-button-group">
  1946. <button>${FILTER_MODE[0]}</button>
  1947. </div>
  1948. </td>
  1949. <td class="c3">
  1950. <div class="filter-table-button-group">
  1951. <button>添加</button>
  1952. </div>
  1953. </td>
  1954. `;
  1955.  
  1956. const inputElement = tc.querySelector("INPUT");
  1957. const actions = tc.getElementsByTagName("button");
  1958.  
  1959. actions[0].onclick = () => {
  1960. actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
  1961. };
  1962.  
  1963. actions[1].onclick = () => {
  1964. if (inputElement.value) {
  1965. addLocation(inputElement.value, actions[0].innerHTML);
  1966.  
  1967. saveData();
  1968. refresh();
  1969. }
  1970. };
  1971.  
  1972. container.appendChild(tc);
  1973. }
  1974. };
  1975.  
  1976. return func;
  1977. })();
  1978.  
  1979. return {
  1980. name: "属地",
  1981. content,
  1982. refresh,
  1983. };
  1984. })();
  1985.  
  1986. // 猎巫
  1987. const witchHuntModule = (() => {
  1988. const content = (() => {
  1989. const c = document.createElement("div");
  1990.  
  1991. c.style = "display: none";
  1992. c.innerHTML = `
  1993. <div class="filter-table-wrapper">
  1994. <table class="filter-table forumbox">
  1995. <thead>
  1996. <tr class="block_txt_c0">
  1997. <th class="c1">版面</th>
  1998. <th class="c2">标签</th>
  1999. <th class="c3" width="1">操作</th>
  2000. </tr>
  2001. </thead>
  2002. <tbody></tbody>
  2003. </table>
  2004. </div>
  2005. <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启<br/>该功能为实验性功能,仅判断用户是否曾经在某个版面发言<br/>未来可能会加入发言的筛选或是屏蔽功能,也可能移除此功能</div>
  2006. `;
  2007.  
  2008. return c;
  2009. })();
  2010.  
  2011. const refresh = (() => {
  2012. const container = content.getElementsByTagName("tbody")[0];
  2013.  
  2014. const func = () => {
  2015. container.innerHTML = "";
  2016.  
  2017. Object.values(witchHunter.data).forEach((item, index) => {
  2018. const tc = document.createElement("tr");
  2019.  
  2020. tc.className = `row${
  2021. (container.querySelectorAll("TR").length % 2) + 1
  2022. }`;
  2023.  
  2024. tc.innerHTML = `
  2025. <td class="c1">
  2026. <div class="filter-input-wrapper">
  2027. <a href="/thread.php?fid=${item.fid}" class="b nobr">[${item.name}]</a>
  2028. </div>
  2029. </td>
  2030. <td class="c2">
  2031. <b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>
  2032. </td>
  2033. <td class="c3">
  2034. <div class="filter-table-button-group">
  2035. <button>删除</button>
  2036. </div>
  2037. </td>
  2038. `;
  2039.  
  2040. const actions = tc.getElementsByTagName("button");
  2041.  
  2042. actions[0].onclick = () => {
  2043. if (confirm("是否确认?")) {
  2044. witchHunter.remove(item.id);
  2045.  
  2046. refresh();
  2047. }
  2048. };
  2049.  
  2050. container.appendChild(tc);
  2051. });
  2052.  
  2053. {
  2054. const tc = document.createElement("tr");
  2055.  
  2056. tc.className = `row${
  2057. (container.querySelectorAll("TR").length % 2) + 1
  2058. }`;
  2059.  
  2060. tc.innerHTML = `
  2061. <td class="c1">
  2062. <div class="filter-input-wrapper">
  2063. <input value="" placeholder="版面ID" />
  2064. </div>
  2065. </td>
  2066. <td class="c2">
  2067. <div class="filter-input-wrapper">
  2068. <input value="" />
  2069. </div>
  2070. </td>
  2071. <td class="c3">
  2072. <div class="filter-table-button-group">
  2073. <button>添加</button>
  2074. </div>
  2075. </td>
  2076. `;
  2077.  
  2078. const inputElement = tc.getElementsByTagName("INPUT");
  2079. const actions = tc.getElementsByTagName("button");
  2080.  
  2081. actions[0].onclick = async () => {
  2082. const fid = parseInt(inputElement[0].value, 10);
  2083. const tag = inputElement[1].value.trim();
  2084.  
  2085. if (isNaN(fid) || tag.length === 0) {
  2086. return;
  2087. }
  2088.  
  2089. await witchHunter.add(fid, tag);
  2090.  
  2091. refresh();
  2092. };
  2093.  
  2094. container.appendChild(tc);
  2095. }
  2096. };
  2097.  
  2098. return func;
  2099. })();
  2100.  
  2101. return {
  2102. name: "猎巫",
  2103. content,
  2104. refresh,
  2105. };
  2106. })();
  2107.  
  2108. // 通用设置
  2109. const commonModule = (() => {
  2110. const content = (() => {
  2111. const c = document.createElement("div");
  2112.  
  2113. c.style = "display: none";
  2114.  
  2115. return c;
  2116. })();
  2117.  
  2118. const refresh = (() => {
  2119. const container = content;
  2120.  
  2121. const func = () => {
  2122. container.innerHTML = "";
  2123.  
  2124. // 默认过滤方式
  2125. {
  2126. const tc = document.createElement("div");
  2127.  
  2128. tc.innerHTML += `
  2129. <div>默认过滤方式</div>
  2130. <div></div>
  2131. <div class="silver" style="margin-top: 10px;">${FILTER_TIPS}</div>
  2132. `;
  2133.  
  2134. ["标记", "遮罩", "隐藏"].forEach((item, index) => {
  2135. const ele = document.createElement("SPAN");
  2136.  
  2137. ele.innerHTML += `
  2138. <input id="s-fm-${index}" type="radio" name="filterType" ${
  2139. data.options.filterMode === item && "checked"
  2140. }>
  2141. <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
  2142. `;
  2143.  
  2144. const inp = ele.querySelector("input");
  2145.  
  2146. inp.onchange = () => {
  2147. if (inp.checked) {
  2148. data.options.filterMode = item;
  2149. saveData();
  2150. reFilter();
  2151. }
  2152. };
  2153.  
  2154. tc.querySelectorAll("div")[1].append(ele);
  2155. });
  2156.  
  2157. container.appendChild(tc);
  2158. }
  2159.  
  2160. // 小号过滤(时间)
  2161. {
  2162. const tc = document.createElement("div");
  2163.  
  2164. tc.innerHTML += `
  2165. <br/>
  2166. <div>
  2167. 隐藏注册时间小于<input value="${
  2168. (data.options.filterRegdateLimit || 0) / 86400000
  2169. }" maxLength="4" style="width: 48px;" />天的用户
  2170. <button>确认</button>
  2171. </div>
  2172. `;
  2173.  
  2174. const actions = tc.getElementsByTagName("button");
  2175.  
  2176. actions[0].onclick = () => {
  2177. const v = actions[0].previousElementSibling.value;
  2178.  
  2179. const n = Number(v) || 0;
  2180.  
  2181. data.options.filterRegdateLimit = n < 0 ? 0 : n * 86400000;
  2182.  
  2183. saveData();
  2184. reFilter();
  2185. };
  2186.  
  2187. container.appendChild(tc);
  2188. }
  2189.  
  2190. // 小号过滤(发帖数)
  2191. {
  2192. const tc = document.createElement("div");
  2193.  
  2194. tc.innerHTML += `
  2195. <br/>
  2196. <div>
  2197. 隐藏发帖数量小于<input value="${
  2198. data.options.filterPostnumLimit || 0
  2199. }" maxLength="5" style="width: 48px;" />贴的用户
  2200. <button>确认</button>
  2201. </div>
  2202. `;
  2203.  
  2204. const actions = tc.getElementsByTagName("button");
  2205.  
  2206. actions[0].onclick = () => {
  2207. const v = actions[0].previousElementSibling.value;
  2208.  
  2209. const n = Number(v) || 0;
  2210.  
  2211. data.options.filterPostnumLimit = n < 0 ? 0 : n;
  2212.  
  2213. saveData();
  2214. reFilter();
  2215. };
  2216.  
  2217. container.appendChild(tc);
  2218. }
  2219.  
  2220. // 声望过滤
  2221. {
  2222. const tc = document.createElement("div");
  2223.  
  2224. tc.innerHTML += `
  2225. <br/>
  2226. <div>
  2227. 隐藏版面声望低于<input value="${
  2228. data.options.filterReputationLimit || ""
  2229. }" maxLength="5" style="width: 48px;" />点的用户
  2230. <button>确认</button>
  2231. </div>
  2232. `;
  2233.  
  2234. const actions = tc.getElementsByTagName("button");
  2235.  
  2236. actions[0].onclick = () => {
  2237. const v = actions[0].previousElementSibling.value;
  2238.  
  2239. const n = Number(v);
  2240.  
  2241. data.options.filterReputationLimit = n;
  2242.  
  2243. saveData();
  2244. reFilter();
  2245. };
  2246.  
  2247. container.appendChild(tc);
  2248. }
  2249.  
  2250. // 删除没有标记的用户
  2251. {
  2252. const tc = document.createElement("div");
  2253.  
  2254. tc.innerHTML += `
  2255. <br/>
  2256. <div>
  2257. <button>删除没有标记的用户</button>
  2258. </div>
  2259. `;
  2260.  
  2261. const actions = tc.getElementsByTagName("button");
  2262.  
  2263. actions[0].onclick = () => {
  2264. if (confirm("是否确认?")) {
  2265. Object.values(data.users).forEach((item) => {
  2266. if (item.tags.length === 0) {
  2267. delete data.users[item.id];
  2268. }
  2269. });
  2270.  
  2271. saveData();
  2272. reFilter();
  2273. }
  2274. };
  2275.  
  2276. container.appendChild(tc);
  2277. }
  2278.  
  2279. // 删除没有用户的标记
  2280. {
  2281. const tc = document.createElement("div");
  2282.  
  2283. tc.innerHTML += `
  2284. <br/>
  2285. <div>
  2286. <button>删除没有用户的标记</button>
  2287. </div>
  2288. `;
  2289.  
  2290. const actions = tc.getElementsByTagName("button");
  2291.  
  2292. actions[0].onclick = () => {
  2293. if (confirm("是否确认?")) {
  2294. Object.values(data.tags).forEach((item) => {
  2295. if (
  2296. Object.values(data.users).filter((user) =>
  2297. user.tags.find((tag) => tag === item.id)
  2298. ).length === 0
  2299. ) {
  2300. delete data.tags[item.id];
  2301. }
  2302. });
  2303.  
  2304. saveData();
  2305. reFilter();
  2306. }
  2307. };
  2308.  
  2309. container.appendChild(tc);
  2310. }
  2311.  
  2312. // 删除非激活中的用户
  2313. {
  2314. const tc = document.createElement("div");
  2315.  
  2316. tc.innerHTML += `
  2317. <br/>
  2318. <div>
  2319. <button>删除非激活中的用户</button>
  2320. <div style="white-space: normal;"></div>
  2321. </div>
  2322. `;
  2323.  
  2324. const action = tc.querySelector("button");
  2325. const list = action.nextElementSibling;
  2326.  
  2327. action.onclick = () => {
  2328. if (confirm("是否确认?")) {
  2329. const waitingQueue = Object.values(data.users).map(
  2330. (item) => () =>
  2331. new Promise((resolve) => {
  2332. fetch(
  2333. `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${item.id}`
  2334. )
  2335. .then((res) => res.blob())
  2336. .then((blob) => {
  2337. const reader = new FileReader();
  2338.  
  2339. reader.onload = () => {
  2340. const text = reader.result;
  2341. const result = JSON.parse(
  2342. text.replace(
  2343. "window.script_muti_get_var_store=",
  2344. ""
  2345. )
  2346. );
  2347.  
  2348. if (!result.error) {
  2349. const { bit } = result.data[0];
  2350.  
  2351. const activeInfo = n.activeInfo(0, 0, bit);
  2352.  
  2353. const activeType = activeInfo[1];
  2354.  
  2355. if (!["ACTIVED", "LINKED"].includes(activeType)) {
  2356. list.innerHTML += `<a href="/nuke.php?func=ucp&uid=${
  2357. item.id
  2358. }" class="b nobr">[${
  2359. item.name ? "@" + item.name : "#" + item.id
  2360. }]</a>`;
  2361.  
  2362. delete data.users[item.id];
  2363. }
  2364. }
  2365.  
  2366. resolve();
  2367. };
  2368.  
  2369. reader.readAsText(blob, "GBK");
  2370. })
  2371. .catch(() => {
  2372. resolve();
  2373. });
  2374. })
  2375. );
  2376.  
  2377. const queueLength = waitingQueue.length;
  2378.  
  2379. const execute = () => {
  2380. if (waitingQueue.length) {
  2381. const next = waitingQueue.shift();
  2382.  
  2383. action.innerHTML = `删除非激活中的用户 (${
  2384. queueLength - waitingQueue.length
  2385. }/${queueLength})`;
  2386. action.disabled = true;
  2387.  
  2388. next().finally(execute);
  2389. } else {
  2390. action.disabled = false;
  2391.  
  2392. saveData();
  2393. reFilter();
  2394. }
  2395. };
  2396.  
  2397. execute();
  2398. }
  2399. };
  2400.  
  2401. container.appendChild(tc);
  2402. }
  2403. };
  2404.  
  2405. return func;
  2406. })();
  2407.  
  2408. return {
  2409. name: "通用设置",
  2410. content,
  2411. refresh,
  2412. };
  2413. })();
  2414.  
  2415. u.addModule(userModule).toggle();
  2416. u.addModule(tagModule);
  2417. u.addModule(keywordModule);
  2418. u.addModule(locationModule);
  2419. u.addModule(witchHuntModule);
  2420. u.addModule(commonModule);
  2421.  
  2422. // 增加菜单项
  2423. (() => {
  2424. const title = "过滤设置";
  2425.  
  2426. let window;
  2427.  
  2428. const container = document.createElement("DIV");
  2429.  
  2430. container.className = `td`;
  2431. container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;
  2432.  
  2433. const content = container.querySelector("A");
  2434.  
  2435. const anchor = document.querySelector("#mainmenu .td:last-child");
  2436.  
  2437. anchor.before(container);
  2438.  
  2439. content.onclick = () => {
  2440. if (window === undefined) {
  2441. window = n.createCommmonWindow();
  2442. }
  2443.  
  2444. window._.addContent(null);
  2445. window._.addTitle(title);
  2446. window._.addContent(u.content);
  2447. window._.show();
  2448. };
  2449. })();
  2450.  
  2451. // 执行过滤
  2452. (() => {
  2453. const hookFunction = (object, functionName, callback) => {
  2454. ((originalFunction) => {
  2455. object[functionName] = function () {
  2456. const returnValue = originalFunction.apply(this, arguments);
  2457.  
  2458. callback.apply(this, [returnValue, originalFunction, arguments]);
  2459.  
  2460. return returnValue;
  2461. };
  2462. })(object[functionName]);
  2463. };
  2464.  
  2465. const initialized = {
  2466. topicArg: false,
  2467. postArg: false,
  2468. };
  2469.  
  2470. hookFunction(n, "eval", () => {
  2471. if (Object.values(initialized).findIndex((item) => item === false) < 0) {
  2472. return;
  2473. }
  2474.  
  2475. if (n.topicArg && initialized.topicArg === false) {
  2476. hookFunction(n.topicArg, "add", reFilter);
  2477.  
  2478. initialized.topicArg = true;
  2479. }
  2480.  
  2481. if (n.postArg && initialized.postArg === false) {
  2482. hookFunction(n.postArg, "proc", reFilter);
  2483.  
  2484. initialized.postArg = true;
  2485. }
  2486. });
  2487.  
  2488. reFilter();
  2489. })();
  2490. })(commonui, __CURRENT_UID);