NGA Filter

troll must die

当前为 2022-09-13 提交的版本,查看 最新版本

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