NGA Filter

troll must die

当前为 2022-11-04 提交的版本,查看 最新版本

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