NGA Filter

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        NGA Filter
// @namespace   https://greasyfork.org/users/263018
// @version     1.13.1
// @author      snyssss
// @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
// @license     MIT

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand

// @noframes
// ==/UserScript==

((n, self) => {
  if (n === undefined) return;

  // KEY
  const DATA_KEY = "NGAFilter";
  const USER_AGENT_KEY = "USER_AGENT_KEY";

  // User Agent
  const USER_AGENT = (() => {
    const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official";

    GM_registerMenuCommand(`修改UA:${data}`, () => {
      const value = prompt("修改UA", data);

      if (value) {
        GM_setValue(USER_AGENT_KEY, value);

        location.reload();
      }
    });

    return data;
  })();

  // 简单的统一请求
  const request = (url, config = {}) =>
    fetch(url, {
      headers: {
        "X-User-Agent": USER_AGENT,
      },
      ...config,
    });

  // 过滤提示
  const FILTER_TIPS =
    "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承 &gt; 显示<br/>相同类型按最高级别过滤";

  // 过滤方式
  const FILTER_MODE = ["继承", "标记", "遮罩", "隐藏", "显示"];

  // 切换过滤方式
  const switchFilterMode = (value) => {
    const next = FILTER_MODE.indexOf(value) + 1;

    if (next >= FILTER_MODE.length) {
      return FILTER_MODE[0];
    }

    return FILTER_MODE[next];
  };

  // 数据
  const data = (() => {
    const d = {
      tags: {},
      users: {},
      keywords: {},
      locations: {},
      options: {
        filterRegdateLimit: 0,
        filterPostnumLimit: 0,
        filterTopicRateLimit: 100,
        filterReputationLimit: NaN,
        filterAnony: false,
        filterMode: "隐藏",
      },
    };

    const v = GM_getValue(DATA_KEY);

    if (typeof v !== "object") {
      return d;
    }

    return Object.assign(d, v);
  })();

  // 保存数据
  const saveData = () => {
    GM_setValue(DATA_KEY, data);
  };

  // 增加标记
  const addTag = (name) => {
    const tag = Object.values(data.tags).find((item) => item.name === name);

    if (tag) return tag.id;

    const id =
      Math.max(...Object.values(data.tags).map((item) => item.id), 0) + 1;

    const hash = (() => {
      let h = 5381;
      for (var i = 0; i < name.length; i++) {
        h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
      }
      return h;
    })();

    const hex = Math.abs(hash).toString(16) + "000000";

    const hsv = [
      `0x${hex.substring(2, 4)}` / 255,
      `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
      `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
    ];

    const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);

    const color = ["#", ...rgb].reduce((a, b) => {
      return a + ("0" + b.toString(16)).slice(-2);
    });

    data.tags[id] = {
      id,
      name,
      color,
      filterMode: FILTER_MODE[0],
    };

    saveData();

    return id;
  };

  // 增加用户
  const addUser = (id, name = null, tags = [], filterMode = FILTER_MODE[0]) => {
    if (data.users[id]) return data.users[id];

    data.users[id] = {
      id,
      name,
      tags,
      filterMode,
    };

    saveData();

    return data.users[id];
  };

  // 增加关键字
  const addKeyword = (
    keyword,
    filterMode = FILTER_MODE[0],
    filterLevel = 0
  ) => {
    const id =
      Math.max(...Object.values(data.keywords).map((item) => item.id), 0) + 1;

    data.keywords[id] = {
      id,
      keyword,
      filterMode,
      filterLevel,
    };

    saveData();

    return id;
  };

  // 增加属地
  const addLocation = (keyword, filterMode = FILTER_MODE[0]) => {
    const id =
      Math.max(...Object.values(data.locations).map((item) => item.id), 0) + 1;

    data.locations[id] = {
      id,
      keyword,
      filterMode,
    };

    saveData();

    return id;
  };

  // 旧版本数据迁移
  {
    const dataKey = "troll_data";
    const modeKey = "troll_mode";
    const keywordKey = "troll_keyword";

    if (localStorage.getItem(dataKey)) {
      let trollMap = (function () {
        try {
          return JSON.parse(localStorage.getItem(dataKey)) || {};
        } catch (e) {}

        return {};
      })();

      let filterMode = ~~localStorage.getItem(modeKey);

      let filterKeyword = localStorage.getItem(keywordKey) || "";

      // 整理标签
      [...new Set(Object.values(trollMap).flat())].forEach((item) =>
        addTag(item)
      );

      // 整理用户
      Object.keys(trollMap).forEach((item) => {
        addUser(
          item,
          null,
          (typeof trollMap[item] === "object" ? trollMap[item] : []).map(
            (tag) => addTag(tag)
          )
        );
      });

      data.options.filterMode = filterMode ? "隐藏" : "标记";
      data.options.keyword = filterKeyword;

      localStorage.removeItem(dataKey);
      localStorage.removeItem(modeKey);
      localStorage.removeItem(keywordKey);

      saveData();
    }

    // v1.1.0 -> v1.1.1
    {
      Object.values(data.users).forEach(({ id, name, tags, enabled }) => {
        if (enabled !== undefined) {
          data.users[id] = {
            id,
            name,
            tags,
            filterMode: enabled ? "继承" : "显示",
          };
        }
      });

      Object.values(data.tags).forEach(({ id, name, color, enabled }) => {
        if (enabled !== undefined) {
          data.tags[id] = {
            id,
            name,
            color,
            filterMode: enabled ? "继承" : "显示",
          };
        }
      });

      if (data.options.filterMode === 0) {
        data.options.filterMode = "隐藏";
      } else if (data.options.filterMode === 1) {
        data.options.filterMode = "标记";
      }

      saveData();
    }

    // v1.2.x -> v1.3.0
    {
      if (data.options.keyword) {
        addKeyword(data.options.keyword);

        delete data.options.keyword;

        saveData();
      }
    }
  }

  // 编辑用户标记
  const editUser = (() => {
    let window;
    return (uid, name, callback) => {
      if (window === undefined) {
        window = n.createCommmonWindow();
      }

      const user = data.users[uid];

      const content = document.createElement("div");

      const size = Math.floor((screen.width * 0.8) / 200);

      const items = Object.values(data.tags).map(
        (tag, index) => `
          <td class="c1">
            <label for="s-tag-${index}" style="display: block; cursor: pointer;">
              <b class="block_txt nobr" style="background:${
                tag.color
              }; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>
            </label>
          </td>
          <td class="c2" width="1">
              <input id="s-tag-${index}" type="checkbox" value="${tag.id}" ${
          user && user.tags.find((item) => item === tag.id) && "checked"
        }/>
          </td>
        `
      );

      const rows = [...new Array(Math.ceil(items.length / size))].map(
        (item, index) =>
          `
          <tr class="row${(index % 2) + 1}">
            ${items.slice(size * index, size * (index + 1)).join("")}
          </tr>
          `
      );

      content.className = "w100";
      content.innerHTML = `
        <div class="filter-table-wrapper" style="width: 80vw;">
          <table class="filter-table forumbox">
            <tbody>
              ${rows.join("")}
            </tbody>
          </table>
        </div>
        <div style="margin: 10px 0;">
            <input placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
        </div>
        <div style="margin: 10px 0;">
            <span>过滤方式:</span>
            <button>${(user && user.filterMode) || FILTER_MODE[0]}</button>
            <div class="right_">
                <button>删除</button>
                <button>保存</button>
            </div>
        </div>
        <div class="silver" style="margin-top: 5px;">${FILTER_TIPS}</div>
    `;

      const actions = content.getElementsByTagName("button");

      actions[0].onclick = () => {
        actions[0].innerText = switchFilterMode(
          actions[0].innerText || FILTER_MODE[0]
        );
      };

      actions[1].onclick = () => {
        if (confirm("是否确认?")) {
          delete data.users[uid];

          saveData();
          runFilter();

          callback && callback();

          window._.hide();
        }
      };

      actions[2].onclick = () => {
        if (confirm("是否确认?")) {
          const values = [...content.getElementsByTagName("input")];
          const newTags = values[values.length - 1].value
            .split("|")
            .filter((item) => item.length)
            .map((item) => addTag(item));
          const tags = [
            ...new Set(
              values
                .filter((item) => item.type === "checkbox" && item.checked)
                .map((item) => ~~item.value)
                .concat(newTags)
            ),
          ].sort();

          if (user) {
            user.tags = tags;
            user.filterMode = actions[0].innerText;
          } else {
            addUser(uid, name, tags, actions[0].innerText);
          }

          saveData();
          runFilter();

          callback && callback();

          window._.hide();
        }
      };

      if (user === undefined) {
        actions[1].style = "display: none;";
      }

      window._.addContent(null);
      window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
      window._.addContent(content);
      window._.show();
    };
  })();

  // 猎巫
  const witchHunter = (() => {
    const key = "WITCH_HUNTER";

    const data = GM_getValue(key) || {};

    const add = async (fid, label) => {
      if (Object.values(data).find((item) => item.fid === fid)) {
        alert("已有相同版面ID");
        return;
      }

      const info = await new Promise((resolve) => {
        request(`/thread.php?lite=js&fid=${fid}`)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              resolve(result.data);
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve({});
          });
      });

      if (info.__F === undefined) {
        alert("版面ID有误");
        return;
      }

      const name = info.__F.name;

      const id = Math.max(...Object.values(data).map((item) => item.id), 0) + 1;

      const hash = (() => {
        let h = 5381;
        for (var i = 0; i < label.length; i++) {
          h = ((h << 5) + h + label.charCodeAt(i)) & 0xffffffff;
        }
        return h;
      })();

      const hex = Math.abs(hash).toString(16) + "000000";

      const hsv = [
        `0x${hex.substring(2, 4)}` / 255,
        `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
        `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
      ];

      const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);

      const color = ["#", ...rgb].reduce((a, b) => {
        return a + ("0" + b.toString(16)).slice(-2);
      });

      data[id] = {
        id,
        fid,
        name,
        label,
        color,
      };

      GM_setValue(key, data);
    };

    const remove = (id) => {
      delete data[id];

      GM_setValue(key, data);
    };

    const run = (uid, element) => {
      if (uid < 0) {
        return;
      }

      Promise.all(
        Object.values(data).map(async (item) => {
          const api = `/thread.php?lite=js&fid=${item.fid}&authorid=${uid}`;

          const verify =
            (await new Promise((resolve) => {
              request(api)
                .then((res) => res.blob())
                .then((blob) => {
                  const reader = new FileReader();

                  reader.onload = () => {
                    const text = reader.result;
                    const result = JSON.parse(
                      text.replace("window.script_muti_get_var_store=", "")
                    );

                    if (result.error) {
                      resolve(false);
                      return;
                    }

                    resolve(true);
                  };

                  reader.readAsText(blob, "GBK");
                })
                .catch(() => {
                  resolve(false);
                });
            })) ||
            (await new Promise((resolve) => {
              request(`${api}&searchpost=1`)
                .then((res) => res.blob())
                .then((blob) => {
                  const reader = new FileReader();

                  reader.onload = () => {
                    const text = reader.result;
                    const result = JSON.parse(
                      text.replace("window.script_muti_get_var_store=", "")
                    );

                    if (result.error) {
                      resolve(false);
                      return;
                    }

                    resolve(true);
                  };

                  reader.readAsText(blob, "GBK");
                })
                .catch(() => {
                  resolve(false);
                });
            }));

          if (verify) {
            return item;
          }
        })
      )
        .then((res) => res.filter((item) => item))
        .then((res) => {
          res
            .filter(
              (current, index) =>
                res.findIndex((item) => item.label === current.label) === index
            )
            .forEach((item) => {
              element.style.display = "block";
              element.innerHTML += `<b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>`;
            });
        });
    };

    return {
      add,
      remove,
      run,
      data,
    };
  })();

  // 获取主题数量
  const getTopicNum = (() => {
    const key = "TOPIC_NUM_CACHE";

    const cache = GM_getValue(key) || {};

    const cacheTime = 60 * 60 * 1000;

    const headKey = Object.keys(cache)[0];

    if (headKey) {
      const timestamp = cache[headKey].timestamp;

      if (timestamp + 24 * 60 * 60 * 1000 < new Date().getTime()) {
        const keys = Object.keys(cache);

        for (const key of keys) {
          delete cache[key];
        }

        GM_setValue(key, {});
      }
    }

    return async (uid) => {
      if (
        cache[uid] &&
        cache[uid].timestamp + cacheTime > new Date().getTime()
      ) {
        return cache[uid].count;
      }

      const api = `/thread.php?lite=js&authorid=${uid}`;

      const { __ROWS } = await new Promise((resolve) => {
        request(api)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              resolve(result.data);
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve({});
          });
      });

      if (__ROWS > 100) {
        cache[uid] = {
          count: __ROWS,
          timestamp: new Date().getTime(),
        };

        GM_setValue(key, cache);
      }

      return __ROWS;
    };
  })();

  // 获取顶楼用户信息、声望
  const getUserInfoAndReputation = (tid, pid) =>
    new Promise((resolve, reject) => {
      if (tid === undefined && pid === undefined) {
        reject();
        return;
      }

      const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;

      // 请求数据
      request(api)
        .then((res) => res.blob())
        .then((blob) => {
          const getLastIndex = (content, position) => {
            if (position >= 0) {
              let nextIndex = position + 1;

              while (nextIndex < content.length) {
                if (content[nextIndex] === "}") {
                  return nextIndex;
                }

                if (content[nextIndex] === "{") {
                  nextIndex = getLastIndex(content, nextIndex);

                  if (nextIndex < 0) {
                    break;
                  }
                }

                nextIndex = nextIndex + 1;
              }
            }

            return -1;
          };

          const reader = new FileReader();

          reader.onload = async () => {
            const parser = new DOMParser();

            const doc = parser.parseFromString(reader.result, "text/html");

            const html = doc.body.innerHTML;

            // 验证帖子正常
            const verify = doc.querySelector("#m_posts");

            if (verify) {
              // 取得顶楼 UID
              const uid = (() => {
                const ele = doc.querySelector("#postauthor0");

                if (ele) {
                  const res = ele.getAttribute("href").match(/uid=(\S+)/);

                  if (res) {
                    return res[1];
                  }
                }

                return 0;
              })();

              // 取得顶楼标题
              const subject = doc.querySelector("#postsubject0").innerHTML;

              // 取得顶楼内容
              const content = doc.querySelector("#postcontent0").innerHTML;

              // 非匿名用户
              if (uid && uid > 0) {
                // 取得用户信息
                const userInfo = (() => {
                  // 起始JSON
                  const str = `"${uid}":{`;

                  // 起始下标
                  const index = html.indexOf(str) + str.length;

                  // 结尾下标
                  const lastIndex = getLastIndex(html, index);

                  if (lastIndex >= 0) {
                    try {
                      return JSON.parse(
                        `{${html.substring(index, lastIndex)}}`
                      );
                    } catch {}
                  }

                  return null;
                })();

                // 取得用户声望
                const reputation = (() => {
                  const reputations = (() => {
                    // 起始JSON
                    const str = `"__REPUTATIONS":{`;

                    // 起始下标
                    const index = html.indexOf(str) + str.length;

                    // 结尾下标
                    const lastIndex = getLastIndex(html, index);

                    if (lastIndex >= 0) {
                      return JSON.parse(
                        `{${html.substring(index, lastIndex)}}`
                      );
                    }

                    return null;
                  })();

                  if (reputations) {
                    for (let fid in reputations) {
                      return reputations[fid][uid] || 0;
                    }
                  }

                  return NaN;
                })();

                resolve({
                  uid,
                  subject,
                  content,
                  userInfo,
                  reputation,
                });
                return;
              }

              resolve({
                uid,
                subject,
                content,
              });
            } else {
              reject();
            }
          };

          reader.readAsText(blob, "GBK");
        })
        .catch(() => {
          reject();
        });
    });

  // 获取过滤方式
  const getFilterMode = async (item) => {
    // 声明结果
    const result = {
      mode: -1,
      reason: ``,
    };

    // 获取 UID
    const { uid } = item;

    // 是自己则跳过
    if (uid === self) {
      return "";
    }

    // 用户过滤
    (() => {
      // 获取屏蔽列表里匹配的用户
      const user = data.users[uid];

      // 没有则跳过
      if (user === undefined) {
        return;
      }

      const { filterMode } = user;

      const mode = FILTER_MODE.indexOf(filterMode) || 0;

      // 低于当前的过滤模式则跳过
      if (mode <= result.mode) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `用户模式: ${filterMode}`;
    })();

    // 标记过滤
    (() => {
      // 获取屏蔽列表里匹配的用户
      const user = data.users[uid];

      // 获取用户对应的标记,并跳过低于当前的过滤模式
      const tags = user
        ? user.tags
            .map((i) => data.tags[i])
            .filter(
              (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
            )
        : [];

      // 没有则跳过
      if (tags.length === 0) {
        return;
      }

      // 取最高的过滤模式
      const { filterMode, name } = tags.sort(
        (a, b) =>
          (FILTER_MODE.indexOf(b.filterMode) || 0) -
          (FILTER_MODE.indexOf(a.filterMode) || 0)
      )[0];

      const mode = FILTER_MODE.indexOf(filterMode) || 0;

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `标记: ${name}`;
    })();

    // 关键词过滤
    await (async () => {
      const { getContent } = item;

      // 获取设置里的关键词列表,并跳过低于当前的过滤模式
      const keywords = Object.values(data.keywords).filter(
        (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
      );

      // 没有则跳过
      if (keywords.length === 0) {
        return;
      }

      // 根据过滤等级依次判断
      const list = keywords.sort(
        (a, b) =>
          (FILTER_MODE.indexOf(b.filterMode) || 0) -
          (FILTER_MODE.indexOf(a.filterMode) || 0)
      );

      for (let i = 0; i < list.length; i += 1) {
        const { keyword, filterMode } = list[i];

        // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
        const filterLevel = list[i].filterLevel || 0;

        // 过滤标题
        if (filterLevel >= 0) {
          const { subject } = item;

          const match = subject.match(keyword);

          if (match) {
            const mode = FILTER_MODE.indexOf(filterMode) || 0;

            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `关键词: ${match[0]}`;
            return;
          }
        }

        // 过滤内容
        if (filterLevel >= 1) {
          // 如果没有内容,则请求
          const content = await (async () => {
            if (item.content === undefined) {
              await getContent().catch(() => {});
            }

            return item.content || null;
          })();

          if (content) {
            const match = content.match(keyword);

            if (match) {
              const mode = FILTER_MODE.indexOf(filterMode) || 0;

              // 更新过滤模式和原因
              result.mode = mode;
              result.reason = `关键词: ${match[0]}`;
              return;
            }
          }
        }
      }
    })();

    // 杂项过滤
    // 放在属地前是因为符合条件的过多,没必要再请求它们的属地
    await (async () => {
      const { getUserInfo, getReputation } = item;

      // 如果当前模式是显示,则跳过
      if (FILTER_MODE[result.mode] === "显示") {
        return;
      }

      // 获取隐藏模式下标
      const mode = FILTER_MODE.indexOf("隐藏");

      // 匿名
      if (uid <= 0) {
        const filterAnony = data.options.filterAnony;

        if (filterAnony) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = "匿名";
        }

        return;
      }

      // 注册时间过滤
      await (async () => {
        const filterRegdateLimit = data.options.filterRegdateLimit || 0;

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { regdate } = userInfo;

        if (regdate === undefined) {
          return;
        }

        if (
          filterRegdateLimit > 0 &&
          regdate * 1000 > new Date() - filterRegdateLimit
        ) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `注册时间: ${new Date(
            regdate * 1000
          ).toLocaleDateString()}`;
          return;
        }
      })();

      // 发帖数量过滤
      await (async () => {
        const filterPostnumLimit = data.options.filterPostnumLimit || 0;

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { postnum } = userInfo;

        if (postnum === undefined) {
          return;
        }

        if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `发帖数量: ${postnum}`;
          return;
        }
      })();

      // 发帖比例过滤
      await (async () => {
        const filterTopicRateLimit = data.options.filterTopicRateLimit || 100;

        // 如果没有用户信息,则请求
        const userInfo = await (async () => {
          if (item.userInfo === undefined) {
            await getUserInfo().catch(() => {});
          }

          return item.userInfo || {};
        })();

        const { postnum } = userInfo;

        if (postnum === undefined) {
          return;
        }

        if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) {
          // 获取主题数量
          const topicNum = await getTopicNum(uid);

          // 计算发帖比例
          const topicRate = (topicNum / postnum) * 100;

          if (topicRate > filterTopicRateLimit) {
            // 更新过滤模式和原因
            result.mode = mode;
            result.reason = `发帖比例: ${topicRate.toFixed(
              0
            )}% (${topicNum}/${postnum})`;
            return;
          }
        }
      })();

      // 版面声望过滤
      await (async () => {
        const filterReputationLimit = data.options.filterReputationLimit || NaN;

        if (Number.isNaN(filterReputationLimit)) {
          return;
        }

        // 如果没有版面声望,则请求
        const reputation = await (async () => {
          if (item.reputation === undefined) {
            await getReputation().catch(() => {});
          }

          return item.reputation || NaN;
        })();

        if (reputation < filterReputationLimit) {
          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `声望: ${reputation}`;
          return;
        }
      })();
    })();

    // 属地过滤
    await (async () => {
      // 匿名用户则跳过
      if (uid <= 0) {
        return;
      }

      // 获取设置里的属地列表,并跳过低于当前的过滤模式
      const locations = Object.values(data.locations).filter(
        (i) => (FILTER_MODE.indexOf(i.filterMode) || 0) > result.mode
      );

      // 没有则跳过
      if (locations.length === 0) {
        return;
      }

      // 请求属地
      // TODO 应该类似 getContent 在另外的地方绑定请求方式
      const { ipLoc } = await new Promise((resolve) => {
        // 临时的缓存机制,避免单页多次重复请求
        n.ipLocCache = n.ipLocCache || {};

        if (n.ipLocCache[uid]) {
          resolve(n.ipLocCache[uid]);
          return;
        }

        // 发起请求
        const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;

        request(api)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              const data = result.data[0] || {};

              if (data.ipLoc) {
                n.ipLocCache[uid] = data.ipLoc;
              }

              resolve(data);
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve({});
          });
      });

      // 请求失败则跳过
      if (ipLoc === undefined) {
        return;
      }

      // 根据过滤等级依次判断
      const list = locations.sort(
        (a, b) =>
          (FILTER_MODE.indexOf(b.filterMode) || 0) -
          (FILTER_MODE.indexOf(a.filterMode) || 0)
      );

      for (let i = 0; i < list.length; i += 1) {
        const { keyword, filterMode } = list[i];

        const match = ipLoc.match(keyword);

        if (match) {
          const mode = FILTER_MODE.indexOf(filterMode) || 0;

          // 更新过滤模式和原因
          result.mode = mode;
          result.reason = `属地: ${ipLoc}`;
          return;
        }
      }
    })();

    if (result.mode === 0) {
      result.mode = FILTER_MODE.indexOf(data.options.filterMode) || -1;
    }

    if (result.mode > 0) {
      const { uid, username, tid, pid } = item;

      const mode = FILTER_MODE[result.mode];

      const reason = result.reason;

      // 用户
      const user = uid > 0
        ? `<a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[${
            username ? "@" + username : "#" + uid
          }]</a>`
        : ``;

      // 移除 BR 标签
      item.content = item.content.replace(/<br>/g, "");

      // 主题
      const subject = (() => {
        if (tid) {
          // 如果有 TID 但没有标题,是引用,采用内容逻辑
          if (item.subject.length === 0) {
            return `<a href="${`/read.php?tid=${tid}`}&nofilter">${
              item.content
            }</a>`;
          }

          return `<a href="${`/read.php?tid=${tid}`}&nofilter" title="${
            item.content
          }" class="b nobr">${item.subject}</a>`;
        }

        return item.subject;
      })();

      // 内容
      const content = (() => {
        if (pid) {
          return `<a href="${`/read.php?pid=${pid}`}&nofilter">${
            item.content
          }</a>`;
        }

        return item.content;
      })();

      m.add({
        user,
        mode,
        subject,
        content,
        reason,
      });

      return mode;
    }

    return "";
  };

  // 获取主题过滤方式
  const getFilterModeByTopic = async ({ nFilter: topic }) => {
    const { tid } = topic;

    // 绑定额外的数据请求方式
    if (topic.getContent === undefined) {
      // 获取帖子内容,按需调用
      const getTopic = () =>
        new Promise((resolve, reject) => {
          // 避免重复请求
          // TODO 严格来说需要加入缓存,避免频繁请求
          if (topic.content || topic.userInfo || topic.reputation) {
            resolve(topic);
            return;
          }

          // 请求并写入数据
          getUserInfoAndReputation(tid, undefined)
            .then(({ subject, content, userInfo, reputation }) => {
              // 写入用户名
              if (userInfo) {
                topic.username = userInfo.username;
              }

              // 写入用户信息和声望
              topic.userInfo = userInfo;
              topic.reputation = reputation;

              // 写入帖子标题和内容
              topic.subject = subject;
              topic.content = content;

              // 返回结果
              resolve(topic);
            })
            .catch(reject);
        });

      // 绑定请求方式
      topic.getContent = getTopic;
      topic.getUserInfo = getTopic;
      topic.getReputation = getTopic;
    }

    // 获取过滤模式
    const filterMode = await getFilterMode(topic);

    // 返回结果
    return filterMode;
  };

  // 获取回复过滤方式
  const getFilterModeByReply = async ({ nFilter: reply }) => {
    const { tid, pid, uid } = reply;

    // 回复页面可以直接获取到用户信息和声望
    if (uid > 0) {
      // 取得用户信息
      const userInfo = n.userInfo.users[uid];

      // 取得用户声望
      const reputation = (() => {
        const reputations = n.userInfo.reputations;

        if (reputations) {
          for (let fid in reputations) {
            return reputations[fid][uid] || 0;
          }
        }

        return NaN;
      })();
      
      // 写入用户名
      if (userInfo) {
        reply.username = userInfo.username;
      }

      // 写入用户信息和声望
      reply.userInfo = userInfo;
      reply.reputation = reputation;
    }

    // 绑定额外的数据请求方式
    if (reply.getContent === undefined) {
      // 获取帖子内容,按需调用
      const getReply = () =>
        new Promise((resolve, reject) => {
          // 避免重复请求
          // TODO 严格来说需要加入缓存,避免频繁请求
          if (reply.userInfo || reply.reputation) {
            resolve(reply);
            return;
          }

          // 请求并写入数据
          getUserInfoAndReputation(tid, pid)
            .then(({ subject, content, userInfo, reputation }) => {
              // 写入用户名
              if (userInfo) {
                reply.username = userInfo.username;
              }

              // 写入用户信息和声望
              reply.userInfo = userInfo;
              reply.reputation = reputation;

              // 写入帖子标题和内容
              reply.subject = subject;
              reply.content = content;

              // 返回结果
              resolve(reply);
            })
            .catch(reject);
        });

      // 绑定请求方式
      reply.getContent = getReply;
      reply.getUserInfo = getReply;
      reply.getReputation = getReply;
    }

    // 获取过滤模式
    const filterMode = await getFilterMode(reply);

    // 返回结果
    return filterMode;
  };

  // 处理引用
  const handleQuote = async (content) => {
    const quotes = content.querySelectorAll(".quote");

    await Promise.all(
      [...quotes].map(async (quote) => {
        const uid = (() => {
          const ele = quote.querySelector("a[href^='/nuke.php']");

          if (ele) {
            const res = ele.getAttribute("href").match(/uid=(\S+)/);

            if (res) {
              return res[1];
            }
          }

          return 0;
        })();

        const { tid, pid } = (() => {
          const ele = quote.querySelector("[title='快速浏览这个帖子']");

          if (ele) {
            const res = ele
              .getAttribute("onclick")
              .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);

            if (res) {
              return {
                tid: parseInt(res[2], 10),
                pid: parseInt(res[3], 10) || 0,
              };
            }
          }

          return {};
        })();

        // 获取过滤方式
        const filterMode = await getFilterModeByReply({
          nFilter: {
            uid,
            tid,
            pid,
            subject: "",
            content: quote.innerText,
          },
        });

        (() => {
          if (filterMode === "标记") {
            quote.innerHTML = `
              <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
                  <span class="crimson">Troll must die.</span>
                  <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
                  <div style="display: none;" name="troll_${uid}">
                      ${quote.innerHTML}
                  </div>
              </div>`;
            return;
          }

          if (filterMode === "遮罩") {
            const source = document.createElement("DIV");

            source.innerHTML = quote.innerHTML;
            source.style.display = "none";

            const caption = document.createElement("CAPTION");

            caption.className = "filter-mask filter-mask-block";

            caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
            caption.onclick = () => {
              quote.removeChild(caption);

              source.style.display = "";
            };

            quote.innerHTML = "";
            quote.appendChild(source);
            quote.appendChild(caption);
            return;
          }

          if (filterMode === "隐藏") {
            quote.innerHTML = "";
            return;
          }
        })();
      })
    );
  };

  // 过滤
  const runFilter = (() => {
    let hasNext = false;
    let isRunning = false;

    const func = async (reFilter = true) => {
      const params = new URLSearchParams(location.search);

      // 判断是否是主题页
      const isTopic = location.pathname === "/thread.php";

      // 判断是否是回复页
      const isReply = location.pathname === "/read.php";

      // 跳过屏蔽(插件自定义)
      if (params.has("nofilter")) {
        return;
      }

      // 收藏
      if (params.has("favor")) {
        return;
      }

      // 只看某人
      if (params.has("authorid")) {
        return;
      }

      // 重新过滤时,清除列表
      if (reFilter) {
        m.clear();
      }

      // 主题过滤
      if (isTopic) {
        const list = n.topicArg.data;

        // 绑定过滤事件
        for (let i = 0; i < list.length; i += 1) {
          const item = list[i];

          // 绑定事件
          if (item.nFilter === undefined) {
            // 主题 ID
            const tid = item[8];

            // 主题标题
            const title = item[1];
            const subject = title.innerText;

            // 主题作者
            const author = item[2];
            const uid =
              parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) ||
              0;
            const username = author.innerText;

            // 主题容器
            const container = title.closest("tr");

            // 过滤函数
            const execute = async (reFilter = false) => {
              // 已过滤则跳过
              if (item.nFilter.executed && reFilter === false) {
                return;
              }

              // 获取过滤方式
              const filterMode = await getFilterModeByTopic(item);

              (() => {
                // 还原样式
                // TODO 应该整体采用 className 来实现
                (() => {
                  // 标记模式
                  container.style.removeProperty("textDecoration");

                  // 遮罩模式
                  title.classList.remove("filter-mask");
                  author.classList.remove("filter-mask");

                  // 隐藏模式
                  container.style.removeProperty("display");
                })();

                // 标记模式下,主题标记会有删除线标识
                if (filterMode === "标记") {
                  title.style.textDecoration = "line-through";
                  return;
                }

                // 遮罩模式下,主题和作者会有遮罩样式
                if (filterMode === "遮罩") {
                  title.classList.add("filter-mask");
                  author.classList.add("filter-mask");
                  return;
                }

                // 隐藏模式下,容器会被隐藏
                if (filterMode === "隐藏") {
                  container.style.display = "none";
                  return;
                }
              })();

              // 标记为已过滤
              item.nFilter.executed = true;
            };

            // 绑定事件
            item.nFilter = {
              tid,
              uid,
              username,
              container,
              title,
              author,
              subject,
              execute,
              executed: false,
            };
          }
        }

        // 执行过滤
        await Promise.all(
          Object.values(list).map((item) => item.nFilter.execute(reFilter))
        );
      }

      // 回复过滤
      if (isReply) {
        const list = Object.values(n.postArg.data);

        // 绑定过滤事件
        for (let i = 0; i < list.length; i += 1) {
          const item = list[i];

          // 绑定事件
          if (item.nFilter === undefined) {
            // 回复 ID
            const pid = item.pid;

            // 判断是否是楼层
            const isFloor = typeof item.i === "number";

            // 回复容器
            const container = isFloor
              ? item.uInfoC.closest("tr")
              : item.uInfoC.closest(".comment_c");

            // 回复标题
            const title = item.subjectC;
            const subject = title.innerText;

            // 回复内容
            const content = item.contentC;
            const contentBak = content.innerHTML;

            // 回复作者
            const author =
              container.querySelector(".posterInfoLine") || item.uInfoC;
            const uid = parseInt(item.pAid, 10) || 0;
            const username = author.querySelector(".author").innerText;
            const avatar = author.querySelector(".avatar");

            // 找到用户 ID,将其视为操作按钮
            const action = container.querySelector('[name="uid"]');

            // 创建一个元素,用于展示标记列表
            // 贴条和高赞不显示
            const tags = (() => {
              if (isFloor === false) {
                return null;
              }

              const element = document.createElement("div");

              element.className = "filter-tags";

              author.appendChild(element);

              return element;
            })();

            // 过滤函数
            const execute = async (reFilter = false) => {
              // 已过滤则跳过
              if (item.nFilter.executed && reFilter === false) {
                return;
              }

              // 获取过滤方式
              const filterMode = await getFilterModeByReply(item);

              await (async () => {
                // 还原样式
                // TODO 应该整体采用 className 来实现
                (() => {
                  // 标记模式
                  if (avatar) {
                    avatar.style.removeProperty("display");
                  }

                  content.innerHTML = contentBak;

                  // 遮罩模式
                  const caption = container.parentNode.querySelector("CAPTION");

                  if (caption) {
                    container.parentNode.removeChild(caption);
                    container.style.removeProperty("display");
                  }

                  // 隐藏模式
                  container.style.removeProperty("display");
                })();

                // 标记模式下,隐藏头像,采用泥潭的折叠样式
                if (filterMode === "标记") {
                  if (avatar) {
                    avatar.style.display = "none";
                  }

                  content.innerHTML = `
                    <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7; ">
                        <span class="crimson">Troll must die.</span>
                        <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
                        <div style="display: none;" name="troll_${uid}">
                            ${contentBak}
                        </div>
                    </div>`;
                  return;
                }

                // 遮罩模式下,楼层会有遮罩样式
                if (filterMode === "遮罩") {
                  const caption = document.createElement("CAPTION");

                  if (isFloor) {
                    caption.className = "filter-mask filter-mask-block";
                  } else {
                    caption.className = "filter-mask filter-mask-block left";
                    caption.style.width = "47%";
                  }

                  caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
                  caption.onclick = () => {
                    const caption =
                      container.parentNode.querySelector("CAPTION");

                    if (caption) {
                      container.parentNode.removeChild(caption);
                      container.style.removeProperty("display");
                    }
                  };

                  container.parentNode.insertBefore(caption, container);
                  container.style.display = "none";
                  return;
                }

                // 隐藏模式下,容器会被隐藏
                if (filterMode === "隐藏") {
                  container.style.display = "none";
                  return;
                }

                // 处理引用
                await handleQuote(content);
              })();

              // 如果是隐藏模式,没必要再加载按钮和标记
              if (filterMode !== "隐藏") {
                // 修改操作按钮颜色
                if (action) {
                  const user = data.users[uid];

                  if (user) {
                    action.style.background = "#CB4042";
                  } else {
                    action.style.background = "#AAA";
                  }
                }

                // 加载标记
                if (tags) {
                  const list = data.users[uid]
                    ? data.users[uid].tags.map((i) => data.tags[i]) || []
                    : [];

                  tags.style.display = list.length ? "" : "none";
                  tags.innerHTML = list
                    .map(
                      (tag) =>
                        `<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
                    )
                    .join("");

                  witchHunter.run(uid, tags);
                }
              }

              // 标记为已过滤
              item.nFilter.executed = true;
            };

            // 绑定操作按钮事件
            (() => {
              if (action) {
                // 隐藏匿名操作按钮
                if (uid <= 0) {
                  action.style.display = "none";
                  return;
                }

                action.innerHTML = `屏蔽`;
                action.onclick = (e) => {
                  const user = data.users[uid];
                  if (e.ctrlKey === false) {
                    editUser(uid, username, () => {
                      execute(true);
                    });
                    return;
                  }

                  if (user) {
                    delete data.users[user.id];
                  } else {
                    addUser(uid, username);
                  }

                  execute(true);
                  saveData();
                };
              }
            })();

            // 绑定事件
            item.nFilter = {
              pid,
              uid,
              username,
              container,
              title,
              author,
              subject,
              content: content.innerText,
              execute,
              executed: false,
            };
          }
        }

        // 执行过滤
        await Promise.all(
          Object.values(list).map((item) => item.nFilter.execute(reFilter))
        );
      }
    };

    const execute = (reFilter = true) =>
      func(reFilter).finally(() => {
        if (hasNext) {
          hasNext = false;

          execute(reFilter);
        } else {
          isRunning = false;
        }
      });

    return async (reFilter = true) => {
      if (isRunning) {
        hasNext = true;
      } else {
        isRunning = true;

        await execute(reFilter);
      }
    };
  })();

  // STYLE
  GM_addStyle(`
    .filter-table-wrapper {
        max-height: 80vh;
        overflow-y: auto;
    }
    .filter-table {
        margin: 0;
    }
    .filter-table th,
    .filter-table td {
        position: relative;
        white-space: nowrap;
    }
    .filter-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
    }
    .filter-table input:not([type]), .filter-table input[type="text"] {
        margin: 0;
        box-sizing: border-box;
        height: 100%;
        width: 100%;
    }
    .filter-input-wrapper {
        position: absolute;
        top: 6px;
        right: 6px;
        bottom: 6px;
        left: 6px;
    }
    .filter-text-ellipsis {
        display: flex;
    }
    .filter-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .filter-button-group {
        margin: -.1em -.2em;
    }
    .filter-tags {
        margin: 2px -0.2em 0;
        text-align: left;
    }
    .filter-mask {
        margin: 1px;
        color: #81C7D4;
        background: #81C7D4;
    }
    .filter-mask-block {
        display: block;
        border: 1px solid #66BAB7;
        text-align: center !important;
    }
    .filter-input-wrapper {
      position: absolute;
      top: 6px;
      right: 6px;
      bottom: 6px;
      left: 6px;
    }
  `);

  // MENU
  const m = (() => {
    const list = [];

    const container = document.createElement("DIV");

    container.className = `td`;
    container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;

    const content = container.querySelector("A");

    const create = (onclick) => {
      const anchor = document.querySelector("#mainmenu .td:last-child");

      anchor.before(container);

      content.onclick = onclick;
    };

    const update = () => {
      const count = list.length;

      if (count) {
        content.innerHTML = `屏蔽 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
      } else {
        content.innerHTML = `屏蔽`;
      }
    };

    const clear = () => {
      list.splice(0, list.length);

      update();
    };

    const add = ({ user, mode, subject, content, reason }) => {
      list.unshift({ user, mode, subject, content, reason });

      listModule.refresh();

      update();
    };

    return {
      create,
      clear,
      list,
      add,
    };
  })();

  // UI
  const u = (() => {
    const modules = {};

    const tabContainer = (() => {
      const c = document.createElement("div");

      c.className = "w100";
      c.innerHTML = `
          <div class="right_" style="margin-bottom: 5px;">
              <table class="stdbtn" cellspacing="0">
                  <tbody>
                      <tr></tr>
                  </tbody>
              </table>
          </div>
          <div class="clear"></div>
          `;

      return c;
    })();

    const tabPanelContainer = (() => {
      const c = document.createElement("div");

      c.style = "width: 80vw;";

      return c;
    })();

    const content = (() => {
      const c = document.createElement("div");

      c.append(tabContainer);
      c.append(tabPanelContainer);

      return c;
    })();

    const addModule = (() => {
      const tc = tabContainer.getElementsByTagName("tr")[0];
      const cc = tabPanelContainer;

      return (module) => {
        const tabBox = document.createElement("td");

        tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;

        const tab = tabBox.childNodes[0];

        const toggle = () => {
          Object.values(modules).forEach((item) => {
            if (item.tab === tab) {
              item.tab.className = "nobr";
              item.content.style = "display: block";
              item.refresh();
            } else {
              item.tab.className = "nobr silver";
              item.content.style = "display: none";
            }
          });
        };

        tc.append(tabBox);
        cc.append(module.content);

        tab.onclick = toggle;

        modules[module.name] = {
          ...module,
          tab,
          toggle,
        };

        return modules[module.name];
      };
    })();

    return {
      content,
      modules,
      addModule,
    };
  })();

  // 屏蔽列表
  const listModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">用户</th>
                <th class="c2" width="1">过滤方式</th>
                <th class="c3">内容</th>
                <th class="c4" width="1">原因</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(m.list).forEach((item) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.refresh = () => {
            const { user, mode, subject, content, reason } = item;

            tc.innerHTML = `
                <td class="c1">${user}</td>
                <td class="c2">${mode}</td>
                <td class="c3">
                  <div class="filter-text-ellipsis">
                    ${subject || content}
                  </div>
                </td>
                <td class="c4">${reason}</td>
              `;
          };

          tc.refresh();

          container.appendChild(tc);
        });
      };

      return func;
    })();

    return {
      name: "列表",
      content,
      refresh,
    };
  })();

  // 用户
  const userModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">昵称</th>
                <th class="c2">标记</th>
                <th class="c3" width="1">过滤方式</th>
                <th class="c4" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.users).forEach((item) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.refresh = () => {
            if (data.users[item.id]) {
              tc.innerHTML = `
                <td class="c1">
                    <a href="/nuke.php?func=ucp&uid=${
                      item.id
                    }" class="b nobr">[${
                item.name ? "@" + item.name : "#" + item.id
              }]</a>
                </td>
                <td class="c2">
                    ${item.tags
                      .map((tag) => {
                        if (data.tags[tag]) {
                          return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
                        }
                      })
                      .join("")}
                </td>
                <td class="c3">
                    <div class="filter-table-button-group">
                      <button>${item.filterMode || FILTER_MODE[0]}</button>
                    </div>
                </td>
                <td class="c4">
                    <div class="filter-table-button-group">
                      <button>编辑</button>
                      <button>删除</button>
                    </div>
                </td>
              `;

              const actions = tc.getElementsByTagName("button");

              actions[0].onclick = () => {
                data.users[item.id].filterMode = switchFilterMode(
                  data.users[item.id].filterMode || FILTER_MODE[0]
                );

                actions[0].innerHTML = data.users[item.id].filterMode;

                saveData();
                runFilter();
              };

              actions[1].onclick = () => {
                editUser(item.id, item.name, tc.refresh);
              };

              actions[2].onclick = () => {
                if (confirm("是否确认?")) {
                  delete data.users[item.id];
                  container.removeChild(tc);

                  saveData();
                  runFilter();
                }
              };
            } else {
              tc.remove();
            }
          };

          tc.refresh();

          container.appendChild(tc);
        });
      };

      return func;
    })();

    return {
      name: "用户",
      content,
      refresh,
    };
  })();

  // 标记
  const tagModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">标记</th>
                <th class="c2">列表</th>
                <th class="c3" width="1">过滤方式</th>
                <th class="c4" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.tags).forEach((item) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <b class="block_txt nobr" style="background:${
                  item.color
                }; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
            </td>
            <td class="c2">
                <button>${
                  Object.values(data.users).filter((user) =>
                    user.tags.find((tag) => tag === item.id)
                  ).length
                }
                </button>
                <div style="white-space: normal; display: none;">
                    ${Object.values(data.users)
                      .filter((user) =>
                        user.tags.find((tag) => tag === item.id)
                      )
                      .map(
                        (user) =>
                          `<a href="/nuke.php?func=ucp&uid=${
                            user.id
                          }" class="b nobr">[${
                            user.name ? "@" + user.name : "#" + user.id
                          }]</a>`
                      )
                      .join("")}
                </div>
            </td>
            <td class="c3">
                <div class="filter-table-button-group">
                  <button>${item.filterMode || FILTER_MODE[0]}</button>
                </div>
            </td>
            <td class="c4">
                <div class="filter-table-button-group">
                  <button>删除</button>
                </div>
            </td>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = (() => {
            let hide = true;
            return () => {
              hide = !hide;
              actions[0].nextElementSibling.style.display = hide
                ? "none"
                : "block";
            };
          })();

          actions[1].onclick = () => {
            data.tags[item.id].filterMode = switchFilterMode(
              data.tags[item.id].filterMode || FILTER_MODE[0]
            );

            actions[1].innerHTML = data.tags[item.id].filterMode;

            saveData();
            runFilter();
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?")) {
              delete data.tags[item.id];

              Object.values(data.users).forEach((user) => {
                const index = user.tags.findIndex((tag) => tag === item.id);
                if (index >= 0) {
                  user.tags.splice(index, 1);
                }
              });

              container.removeChild(tc);

              saveData();
              runFilter();
            }
          };

          container.appendChild(tc);
        });
      };

      return func;
    })();

    return {
      name: "标记",
      content,
      refresh,
    };
  })();

  // 关键字
  const keywordModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1">列表</th>
                <th class="c2" width="1">过滤方式</th>
                <th class="c3" width="1">包括内容</th>
                <th class="c4" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。</div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.keywords).forEach((item) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <input value="${item.keyword || ""}" />
                </div>
            </td>
            <td class="c2">
                <div class="filter-table-button-group">
                  <button>${item.filterMode || FILTER_MODE[0]}</button>
                </div>
            </td>
            <td class="c3">
              <div style="text-align: center;">
                <input type="checkbox" ${
                  item.filterLevel ? `checked="checked"` : ""
                } />
              </div>
            </td>
            <td class="c4">
                <div class="filter-table-button-group">
                    <button>保存</button>
                    <button>删除</button>
                </div>
            </td>
          `;

          const inputElement = tc.querySelector("INPUT");
          const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
          };

          actions[1].onclick = () => {
            if (inputElement.value) {
              data.keywords[item.id] = {
                id: item.id,
                keyword: inputElement.value,
                filterMode: actions[0].innerHTML,
                filterLevel: levelElement.checked ? 1 : 0,
              };

              saveData();
              runFilter();
              refresh();
            }
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?")) {
              delete data.keywords[item.id];

              saveData();
              runFilter();
              refresh();
            }
          };

          container.appendChild(tc);
        });

        {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <input value="" />
                </div>
            </td>
            <td class="c2">
                <div class="filter-table-button-group">
                  <button>${FILTER_MODE[0]}</button>
                </div>
            </td>
            <td class="c3">
              <div style="text-align: center;">
                <input type="checkbox" />
              </div>
            </td>
            <td class="c4">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
            </td>
          `;

          const inputElement = tc.querySelector("INPUT");
          const levelElement = tc.querySelector(`INPUT[type="checkbox"]`);
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
          };

          actions[1].onclick = () => {
            if (inputElement.value) {
              addKeyword(
                inputElement.value,
                actions[0].innerHTML,
                levelElement.checked ? 1 : 0
              );

              saveData();
              runFilter();
              refresh();
            }
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    return {
      name: "关键字",
      content,
      refresh,
    };
  })();

  // 属地
  const locationModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1">列表</th>
                <th class="c2" width="1">过滤方式</th>
                <th class="c3" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用&quot;|&quot;隔开,&quot;ABC|DEF&quot;即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.locations).forEach((item) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <input value="${item.keyword || ""}" />
                </div>
            </td>
            <td class="c2">
                <div class="filter-table-button-group">
                  <button>${item.filterMode || FILTER_MODE[0]}</button>
                </div>
            </td>
            <td class="c3">
                <div class="filter-table-button-group">
                    <button>保存</button>
                    <button>删除</button>
                </div>
            </td>
          `;

          const inputElement = tc.querySelector("INPUT");
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
          };

          actions[1].onclick = () => {
            if (inputElement.value) {
              data.locations[item.id] = {
                id: item.id,
                keyword: inputElement.value,
                filterMode: actions[0].innerHTML,
              };

              saveData();
              runFilter();
              refresh();
            }
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?")) {
              delete data.locations[item.id];

              saveData();
              runFilter();
              refresh();
            }
          };

          container.appendChild(tc);
        });

        {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <input value="" />
                </div>
            </td>
            <td class="c2">
                <div class="filter-table-button-group">
                  <button>${FILTER_MODE[0]}</button>
                </div>
            </td>
            <td class="c3">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
            </td>
          `;

          const inputElement = tc.querySelector("INPUT");
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            actions[0].innerHTML = switchFilterMode(actions[0].innerHTML);
          };

          actions[1].onclick = () => {
            if (inputElement.value) {
              addLocation(inputElement.value, actions[0].innerHTML);

              saveData();
              runFilter();
              refresh();
            }
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    return {
      name: "属地",
      content,
      refresh,
    };
  })();

  // 猎巫
  const witchHuntModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="filter-table-wrapper">
          <table class="filter-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1">版面</th>
                <th class="c2">标签</th>
                <th class="c3" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启<br/>该功能为实验性功能,仅判断用户是否曾经在某个版面发言<br/>未来可能会加入发言的筛选或是屏蔽功能,也可能移除此功能</div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(witchHunter.data).forEach((item, index) => {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <a href="/thread.php?fid=${item.fid}" class="b nobr">[${item.name}]</a>
                </div>
            </td>
            <td class="c2">
                <b class="block_txt nobr" style="background:${item.color}; color:#fff; margin: 0.1em 0.2em;">${item.label}</b>
            </td>
            <td class="c3">
                <div class="filter-table-button-group">
                    <button>删除</button>
                </div>
            </td>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            if (confirm("是否确认?")) {
              witchHunter.remove(item.id);

              refresh();
            }
          };

          container.appendChild(tc);
        });

        {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <div class="filter-input-wrapper">
                  <input value="" placeholder="版面ID" />
                </div>
            </td>
            <td class="c2">
                <div class="filter-input-wrapper">
                  <input value="" />
                </div>
            </td>
            <td class="c3">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
            </td>
          `;

          const inputElement = tc.getElementsByTagName("INPUT");
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = async () => {
            const fid = parseInt(inputElement[0].value, 10);
            const tag = inputElement[1].value.trim();

            if (isNaN(fid) || tag.length === 0) {
              return;
            }

            await witchHunter.add(fid, tag);

            refresh();
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    return {
      name: "猎巫",
      content,
      refresh,
    };
  })();

  // 通用设置
  const commonModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";

      return c;
    })();

    const refresh = (() => {
      const container = content;

      const func = () => {
        container.innerHTML = "";

        // 默认过滤方式
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <div>默认过滤方式</div>
            <div></div>
            <div class="silver" style="margin-top: 10px;">${FILTER_TIPS}</div>
          `;

          ["标记", "遮罩", "隐藏"].forEach((item, index) => {
            const ele = document.createElement("SPAN");

            ele.innerHTML += `
            <input id="s-fm-${index}" type="radio" name="filterType" ${
              data.options.filterMode === item && "checked"
            }>
            <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
            `;

            const inp = ele.querySelector("input");

            inp.onchange = () => {
              if (inp.checked) {
                data.options.filterMode = item;
                saveData();
                runFilter();
              }
            };

            tc.querySelectorAll("div")[1].append(ele);
          });

          container.appendChild(tc);
        }

        // 小号过滤(时间)
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
              <br/>
              <div>
                隐藏注册时间小于<input value="${
                  (data.options.filterRegdateLimit || 0) / 86400000
                }" maxLength="4" style="width: 48px;" />天的用户
                <button>确认</button>
              </div>
            `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const v = actions[0].previousElementSibling.value;

            const n = Number(v) || 0;

            data.options.filterRegdateLimit = n < 0 ? 0 : n * 86400000;

            saveData();
            runFilter();
          };

          container.appendChild(tc);
        }

        // 小号过滤(发帖数)
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
              <br/>
              <div>
                隐藏发帖数量小于<input value="${
                  data.options.filterPostnumLimit || 0
                }" maxLength="5" style="width: 48px;" />贴的用户
                <button>确认</button>
              </div>
            `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const v = actions[0].previousElementSibling.value;

            const n = Number(v) || 0;

            data.options.filterPostnumLimit = n < 0 ? 0 : n;

            saveData();
            runFilter();
          };

          container.appendChild(tc);
        }

        // 流量号过滤(主题比例)
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
              <br/>
              <div>
                隐藏发帖比例大于<input value="${
                  data.options.filterTopicRateLimit || 100
                }" maxLength="3" style="width: 48px;" />%的用户
                <button>确认</button>
              </div>
            `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const v = actions[0].previousElementSibling.value;

            const n = Number(v) || 100;

            if (n <= 0 || n > 100) {
              return;
            }

            data.options.filterTopicRateLimit = n;

            saveData();
            runFilter();
          };

          container.appendChild(tc);
        }

        // 声望过滤
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
              <br/>
              <div>
                隐藏版面声望低于<input value="${
                  data.options.filterReputationLimit || ""
                }" maxLength="5" style="width: 48px;" />点的用户
                <button>确认</button>
              </div>
            `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const v = actions[0].previousElementSibling.value;

            const n = Number(v);

            data.options.filterReputationLimit = n;

            saveData();
            runFilter();
          };

          container.appendChild(tc);
        }

        // 匿名过滤
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
              <br/>
              <div>
                <label>
                  隐藏匿名的用户
                  <input type="checkbox" ${
                    data.options.filterAnony ? `checked="checked"` : ""
                  } />
                </label>
              </div>
            `;

          const checkbox = tc.querySelector("input");

          checkbox.onchange = () => {
            const v = checkbox.checked;

            data.options.filterAnony = v;

            saveData();
            runFilter();
          };

          container.appendChild(tc);
        }

        // 删除没有标记的用户
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>
                <button>删除没有标记的用户</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            if (confirm("是否确认?")) {
              Object.values(data.users).forEach((item) => {
                if (item.tags.length === 0) {
                  delete data.users[item.id];
                }
              });

              saveData();
              runFilter();
            }
          };

          container.appendChild(tc);
        }

        // 删除没有用户的标记
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>
                <button>删除没有用户的标记</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            if (confirm("是否确认?")) {
              Object.values(data.tags).forEach((item) => {
                if (
                  Object.values(data.users).filter((user) =>
                    user.tags.find((tag) => tag === item.id)
                  ).length === 0
                ) {
                  delete data.tags[item.id];
                }
              });

              saveData();
              runFilter();
            }
          };

          container.appendChild(tc);
        }

        // 删除非激活中的用户
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>
                <button>删除非激活中的用户</button>
                <div style="white-space: normal;"></div>
            </div>
          `;

          const action = tc.querySelector("button");
          const list = action.nextElementSibling;

          action.onclick = () => {
            if (confirm("是否确认?")) {
              const waitingQueue = Object.values(data.users).map(
                (item) => () =>
                  new Promise((resolve) => {
                    fetch(
                      `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${item.id}`
                    )
                      .then((res) => res.blob())
                      .then((blob) => {
                        const reader = new FileReader();

                        reader.onload = () => {
                          const text = reader.result;
                          const result = JSON.parse(
                            text.replace(
                              "window.script_muti_get_var_store=",
                              ""
                            )
                          );

                          if (!result.error) {
                            const { bit } = result.data[0];

                            const activeInfo = n.activeInfo(0, 0, bit);

                            const activeType = activeInfo[1];

                            if (!["ACTIVED", "LINKED"].includes(activeType)) {
                              list.innerHTML += `<a href="/nuke.php?func=ucp&uid=${
                                item.id
                              }" class="b nobr">[${
                                item.name ? "@" + item.name : "#" + item.id
                              }]</a>`;

                              delete data.users[item.id];
                            }
                          }

                          resolve();
                        };

                        reader.readAsText(blob, "GBK");
                      })
                      .catch(() => {
                        resolve();
                      });
                  })
              );

              const queueLength = waitingQueue.length;

              const execute = () => {
                if (waitingQueue.length) {
                  const next = waitingQueue.shift();

                  action.innerHTML = `删除非激活中的用户 (${
                    queueLength - waitingQueue.length
                  }/${queueLength})`;
                  action.disabled = true;

                  next().finally(execute);
                } else {
                  action.disabled = false;

                  saveData();
                  runFilter();
                }
              };

              execute();
            }
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    return {
      name: "通用设置",
      content,
      refresh,
    };
  })();

  u.addModule(listModule).toggle();
  u.addModule(userModule);
  u.addModule(tagModule);
  u.addModule(keywordModule);
  u.addModule(locationModule);
  u.addModule(witchHuntModule);
  u.addModule(commonModule);

  // 增加菜单项
  (() => {
    let window;

    m.create(() => {
      if (window === undefined) {
        window = n.createCommmonWindow();
      }

      window._.addContent(null);
      window._.addTitle(`屏蔽`);
      window._.addContent(u.content);
      window._.show();
    });
  })();

  // 执行过滤
  (() => {
    const hookFunction = (object, functionName, callback) => {
      ((originalFunction) => {
        object[functionName] = function () {
          const returnValue = originalFunction.apply(this, arguments);

          callback.apply(this, [returnValue, originalFunction, arguments]);

          return returnValue;
        };
      })(object[functionName]);
    };

    const initialized = {
      topicArg: false,
      postArg: false,
    };

    hookFunction(n, "eval", () => {
      if (Object.values(initialized).findIndex((item) => item === false) < 0) {
        return;
      }

      if (n.topicArg && initialized.topicArg === false) {
        hookFunction(n.topicArg, "add", () => {
          runFilter(false);
        });

        initialized.topicArg = true;
      }

      if (n.postArg && initialized.postArg === false) {
        hookFunction(n.postArg, "proc", () => {
          runFilter(false);
        });

        initialized.postArg = true;
      }
    });

    runFilter();
  })();
})(commonui, __CURRENT_UID);