NGA Filter

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

当前为 2023-12-10 提交的版本,查看 最新版本

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