NGA Filter

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

Version vom 04.01.2024. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        NGA Filter
// @namespace   https://greasyfork.org/users/263018
// @version     2.1.0
// @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
// @grant       unsafeWindow

// @run-at      document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块、菜单模块、主题模块、回复模块
  let commonui, menuModule, topicModule, replyModule;

  // KEY
  const DATA_KEY = "NGAFilter";
  const USER_AGENT_KEY = "USER_AGENT_KEY";
  const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  const CLEAR_TIME_KEY = "CLEAR_TIME_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 preFilter = (() => {
    const data = GM_getValue(PRE_FILTER_KEY);

    const value = data === undefined ? true : data;

    GM_registerMenuCommand(`前置过滤:${value ? "是" : "否"}`, () => {
      GM_setValue(PRE_FILTER_KEY, !value);

      location.reload();
    });

    return value;
  })();

  // 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;
    }
  `);

  // 重新过滤
  const reFilter = async (skip = () => false) => {
    // 清空列表
    listModule.clear();

    // 开始过滤
    [
      ...(topicModule ? Object.values(topicModule.data) : []),
      ...(replyModule ? Object.values(replyModule.data) : []),
    ].forEach((item) => {
      // 未绑定事件
      if (item.nFilter === undefined) {
        return;
      }

      // 如果跳过过滤,直接添加列表
      if (skip(item.nFilter)) {
        listModule.add(item.nFilter);
        return;
      }

      // 执行过滤
      item.nFilter.execute();
    });
  };

  // 缓存模块
  const cacheModule = (() => {
    // 声明模块集合
    const modules = {};

    // IndexedDB 操作
    const db = (() => {
      // 常量
      const VERSION = 2;
      const DB_NAME = "NGA_FILTER_CACHE";

      // 是否支持
      const support = unsafeWindow.indexedDB !== undefined;

      // 不支持,直接返回
      if (support === false) {
        return {
          support,
        };
      }

      // 创建或获取数据库实例
      const getInstance = (() => {
        let instance;

        return () =>
          new Promise((resolve) => {
            // 如果已存在实例,直接返回
            if (instance) {
              resolve(instance);
              return;
            }

            // 打开 IndexedDB 数据库
            const request = unsafeWindow.indexedDB.open(DB_NAME, VERSION);

            // 如果数据库不存在则创建
            request.onupgradeneeded = (event) => {
              // 获取旧版本号
              var oldVersion = event.oldVersion;

              // 根据版本号创建表
              Object.entries(modules).map(([name, { keyPath, version }]) => {
                if (version > oldVersion) {
                  // 创建表
                  const store = event.target.result.createObjectStore(name, {
                    keyPath,
                  });

                  // 创建索引,用于清除过期数据
                  store.createIndex("timestamp", "timestamp");
                }
              });
            };

            // 成功后写入实例并返回
            request.onsuccess = (event) => {
              instance = event.target.result;

              resolve(instance);
            };
          });
      })();

      return {
        support,
        getInstance,
      };
    })();

    // 删除缓存
    const remove = async (name, key) => {
      // 不支持 IndexedDB,使用 GM_setValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        delete cache[key];

        GM_setValue(name, cache);
        return;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 写入 IndexedDB
      await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 删除数据
        const r = store.delete(key);

        r.onsuccess = () => {
          resolve();
        };

        r.onerror = () => {
          resolve();
        };
      });
    };

    // 写入缓存
    const save = async (name, key, value) => {
      // 不支持 IndexedDB,使用 GM_setValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        cache[key] = value;

        GM_setValue(name, cache);
        return;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 写入 IndexedDB
      await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 插入数据
        const r = store.put({
          ...value,
          timestamp: Date.now(),
        });

        r.onsuccess = () => {
          resolve();
        };

        r.onerror = () => {
          resolve();
        };
      });
    };

    // 读取缓存
    const load = async (name, key, expireTime = 0) => {
      // 不支持 IndexedDB,使用 GM_getValue
      if (db.support === false) {
        const cache = GM_getValue(name) || {};

        if (cache[key]) {
          const result = cache[key];

          // 如果已超时则删除
          if (expireTime > 0) {
            if (result.timestamp + expireTime < new Date().getTime()) {
              await remove(name, key);

              return null;
            }
          }

          return result;
        }

        return null;
      }

      // 获取实例
      const instance = await db.getInstance();

      // 查找 IndexedDB
      const result = await new Promise((resolve) => {
        // 创建事务
        const transaction = instance.transaction([name], "readonly");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 获取数据
        const request = store.get(key);

        // 成功后处理数据
        request.onsuccess = (event) => {
          const data = event.target.result;

          if (data) {
            resolve(data);
            return;
          }

          resolve(null);
        };

        // 失败后处理
        request.onerror = () => {
          resolve(null);
        };
      });

      // 没有数据
      if (result === null) {
        return null;
      }

      // 如果已超时则删除
      if (expireTime > 0) {
        if (result.timestamp + expireTime < new Date().getTime()) {
          await remove(name, key);

          return null;
        }
      }

      // 返回结果
      return result;
    };

    // 定时清理
    const clear = async () => {
      // 获取实例
      const instance = await db.getInstance();

      // 清理 IndexedDB
      Object.entries(modules).map(([name, { persistent }]) => {
        // 持久化,不进行自动清理
        if (persistent) {
          return;
        }

        // 创建事务
        const transaction = instance.transaction([name], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(name);

        // 清理数据
        store.clear();
      });
    };

    // 初始化,用于写入表信息
    const init = (name, value) => {
      modules[name] = value;
    };

    return {
      init,
      save,
      load,
      remove,
      clear,
    };
  })();

  // 过滤模块
  const filterModule = (() => {
    // 过滤提示
    const tips =
      "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承";

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

    // 默认过滤方式
    const defaultMode = modes[0];

    // 切换过滤方式
    const switchModeByName = (value) =>
      modes[modes.indexOf(value) + 1] || defaultMode;

    // 获取当前过滤方式下标
    const getModeByName = (name, defaultValue = 0) => {
      const index = modes.indexOf(name);

      if (index < 0) {
        return defaultValue;
      }

      return index;
    };

    // 获取指定下标过滤方式
    const getNameByMode = (index) => modes[index] || "";

    // 折叠样式
    const collapse = (uid, element, content) => {
      element.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}">
                ${content}
            </div>
        </div>`;
    };

    return {
      tips,
      modes,
      defaultMode,
      collapse,
      getModeByName,
      getNameByMode,
      switchModeByName,
    };
  })();

  // 数据(及配置)模块
  const dataModule = (() => {
    // 合并数据
    const merge = (() => {
      const isObject = (value) => {
        return value !== null && typeof value === "object";
      };

      const deepClone = (value) => {
        if (isObject(value)) {
          const clone = Array.isArray(value) ? [] : {};

          for (const key in value) {
            if (Object.prototype.hasOwnProperty.call(value, key)) {
              clone[key] = deepClone(value[key]);
            }
          }

          return clone;
        }

        return value;
      };

      return (target, ...sources) => {
        for (const source of sources) {
          for (const key in source) {
            if (isObject(source[key])) {
              if (isObject(target[key])) {
                merge(target[key], source[key]);
              } else {
                target[key] = deepClone(source[key]);
              }
            } else {
              target[key] = source[key];
            }
          }
        }

        return target;
      };
    })();

    // 初始化数据
    const data = (() => {
      // 默认配置
      const defaultData = {
        tags: {},
        users: {},
        keywords: {},
        locations: {},
        options: {
          filterRegdateLimit: 0,
          filterPostnumLimit: 0,
          filterTopicRateLimit: 100,
          filterReputationLimit: NaN,
          filterAnony: false,
          filterMode: "隐藏",
        },
      };

      // 读取数据
      const storedData = GM_getValue(DATA_KEY);

      // 如果没有数据,则返回默认配置
      if (typeof storedData !== "object") {
        return defaultData;
      }

      // 返回数据
      return merge(defaultData, storedData);
    })();

    // 保存数据
    const save = (values) => {
      merge(data, values);

      GM_setValue(DATA_KEY, data);
    };

    // 返回标记列表
    const getTags = () => data.tags;

    // 返回用户列表
    const getUsers = () => data.users;

    // 返回关键字列表
    const getKeywords = () => data.keywords;

    // 返回属地列表
    const getLocations = () => data.locations;

    // 获取默认过滤模式
    const getDefaultFilterMode = () => data.options.filterMode;

    // 设置默认过滤模式
    const setDefaultFilterMode = (value) => {
      save({
        options: {
          filterMode: value,
        },
      });
    };

    // 获取注册时间限制
    const getFilterRegdateLimit = () => data.options.filterRegdateLimit || 0;

    // 设置注册时间限制
    const setFilterRegdateLimit = (value) => {
      save({
        options: {
          filterRegdateLimit: value,
        },
      });
    };

    // 获取发帖数量限制
    const getFilterPostnumLimit = () => data.options.filterPostnumLimit || 0;

    // 设置发帖数量限制
    const setFilterPostnumLimit = (value) => {
      save({
        options: {
          filterPostnumLimit: value,
        },
      });
    };

    // 获取发帖比例限制
    const getFilterTopicRateLimit = () =>
      data.options.filterTopicRateLimit || 100;

    // 设置发帖比例限制
    const setFilterTopicRateLimit = (value) => {
      save({
        options: {
          filterTopicRateLimit: value,
        },
      });
    };

    // 获取用户声望限制
    const getFilterReputationLimit = () =>
      data.options.filterReputationLimit || NaN;

    // 设置用户声望限制
    const setFilterReputationLimit = (value) => {
      save({
        options: {
          filterReputationLimit: value,
        },
      });
    };

    // 获取是否过滤匿名
    const getFilterAnony = () => data.options.filterAnony || false;

    // 设置是否过滤匿名
    const setFilterAnony = (value) => {
      save({
        options: {
          filterAnony: value,
        },
      });
    };

    return {
      save,
      getTags,
      getUsers,
      getKeywords,
      getLocations,
      getDefaultFilterMode,
      setDefaultFilterMode,
      getFilterRegdateLimit,
      setFilterRegdateLimit,
      getFilterPostnumLimit,
      setFilterPostnumLimit,
      getFilterTopicRateLimit,
      setFilterTopicRateLimit,
      getFilterReputationLimit,
      setFilterReputationLimit,
      getFilterAnony,
      setFilterAnony,
    };
  })();

  // 列表模块
  const listModule = (() => {
    const list = [];

    const callback = [];

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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 element;
      })();

      const tbody = content.querySelector("TBODY");

      const load = (item) => {
        const { uid, username, tid, pid, filterMode, reason } = item;

        // 用户
        const user = userModule.format(uid, username);

        // 移除 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;
        })();

        const row = document.createElement("TR");

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

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

        tbody.insertBefore(row, tbody.firstChild);
      };

      const refresh = () => {
        tbody.innerHTML = "";

        Object.values(list).forEach(load);
      };

      return {
        content,
        refresh,
        load,
      };
    })();

    const add = (value) => {
      if (
        list.find(
          (item) =>
            item.tid === value.tid &&
            item.pid === value.pid &&
            item.subject === value.subject
        )
      ) {
        return;
      }

      if ((value.filterMode || "显示") === "显示") {
        return;
      }

      list.push(value);

      view.load(value);

      callback.forEach((item) => item(list));
    };

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

      view.refresh();

      callback.forEach((item) => item(list));
    };

    const bindCallback = (func) => {
      func(list);

      callback.push(func);
    };

    return {
      add,
      clear,
      bindCallback,
      view,
    };
  })();

  // 用户模块
  const userModule = (() => {
    // 获取用户列表
    const list = () => dataModule.getUsers();

    // 获取用户
    const get = (uid) => {
      // 获取列表
      const users = list();

      // 如果已存在,则返回信息
      if (users[uid]) {
        return users[uid];
      }

      return null;
    };

    // 增加用户
    const add = (uid, username, tags, filterMode) => {
      // 获取对应的用户
      const user = get(uid);

      // 如果用户已存在,则返回用户信息,否则增加用户
      if (user) {
        return user;
      }

      // 保存用户
      // TODO id 和 name 属于历史遗留问题,应该改为 uid 和 username 以便更好的理解
      dataModule.save({
        users: {
          [uid]: {
            id: uid,
            name: username,
            tags,
            filterMode,
          },
        },
      });

      // 返回用户信息
      return get(uid);
    };

    // 编辑用户
    const edit = (uid, values) => {
      dataModule.save({
        users: {
          [uid]: values,
        },
      });
    };

    // 删除用户
    const remove = (uid) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[uid];

      // 保存数据
      dataModule.save({});
    };

    // 格式化用户
    const format = (uid, name) => {
      if (uid <= 0) {
        return "";
      }

      const user = get(uid);

      if (user) {
        name = name || user.name;
      }

      const username = name ? "@" + name : "#" + uid;

      return `<a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[${username}]</a>`;
    };

    // UI
    const view = (() => {
      const details = (() => {
        let window;

        return (uid, name, callback) => {
          if (window === undefined) {
            window = commonui.createCommmonWindow();
          }

          const user = get(uid);

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

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

          const items = Object.values(tagModule.list()).map((tag, index) => {
            const checked = user && user.tags.includes(tag.id) ? "checked" : "";

            return `
              <td class="c1">
                <label for="s-tag-${index}" style="display: block; cursor: pointer;">
                  ${tagModule.format(tag.id)}
                </label>
              </td>
              <td class="c2" width="1">
                <input id="s-tag-${index}" type="checkbox" value="${
              tag.id
            }" ${checked}/>
              </td>
            `;
          });

          const rows = [...new Array(Math.ceil(items.length / size))].map(
            (_, 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 type="text" placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
            </div>
            <div style="margin: 10px 0;">
              <span>过滤方式:</span>
              <button>${
                (user && user.filterMode) || filterModule.defaultMode
              }</button>
              <div class="right_">
                <button>删除</button>
                <button>保存</button>
              </div>
            </div>
            <div class="silver" style="margin-top: 5px;">${
              filterModule.tips
            }</div>
          `;

          const actions = content.querySelectorAll("BUTTON");

          actions[0].onclick = () => {
            actions[0].innerText = filterModule.switchModeByName(
              actions[0].innerText
            );
          };

          actions[1].onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            remove(uid);
            reFilter((item) => item.uid !== uid);

            if (callback) {
              callback({
                id: null,
              });
            }

            window._.hide();
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const filterMode = actions[0].innerText;

            const checked = [...content.querySelectorAll("INPUT:checked")].map(
              (input) => parseInt(input.value, 10)
            );

            const newTags = content
              .querySelector("INPUT[type='text']")
              .value.split("|")
              .filter((item) => item.length)
              .map((item) => tagModule.add(item));

            const tags = [...new Set([...checked, ...newTags])].sort();

            if (user) {
              user.tags = tags;

              edit(uid, {
                filterMode,
              });
            } else {
              add(uid, name, tags, filterMode);
            }

            reFilter((item) => item.uid !== uid);

            if (callback) {
              callback({
                uid,
                name,
                tags,
                filterMode,
              });
            }

            window._.hide();
          };

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

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

      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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 element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, name, tags, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

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

          tbody.appendChild(anchor);
        }

        anchor.innerHTML = `
          <td class="c1">
            ${format(id, name)}
          </td>
          <td class="c2">
            ${tags.map(tagModule.format).join("")}
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>编辑</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = () => {
          const filterMode = filterModule.switchModeByName(
            actions[0].innerHTML
          );

          actions[0].innerHTML = filterMode;

          edit(id, { filterMode });
          reFilter((item) => item.uid !== uid);
        };

        actions[1].onclick = () => {
          details(id, name, (item) => {
            load(item, anchor);
          });
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.uid !== uid);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        details,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      format,
      view,
    };
  })();

  // 标记模块
  const tagModule = (() => {
    // 获取标记列表
    const list = () => dataModule.getTags();

    // 计算标记颜色
    // 采用的是泥潭的颜色方案,参见 commonui.htmlName
    const generateColor = (name) => {
      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 = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]);

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

    // 获取标记
    const get = ({ id, name }) => {
      // 获取列表
      const tags = list();

      // 通过 ID 获取标记
      if (tags[id]) {
        return tags[id];
      }

      // 通过名称获取标记
      if (name) {
        const tag = Object.values(tags).find((item) => item.name === name);

        if (tag) {
          return tag;
        }
      }

      return null;
    };

    // 增加标记
    const add = (name) => {
      // 获取对应的标记
      const tag = get({ name });

      // 如果标记已存在,则返回标记信息,否则增加标记
      if (tag) {
        return tag;
      }

      // ID 为最大值 + 1
      const id = Math.max(Object.keys(list()), 0) + 1;

      // 标记的颜色
      const color = generateColor(name);

      // 保存标记
      dataModule.save({
        tags: {
          [id]: {
            id,
            name,
            color,
            filterMode: filterModule.defaultMode,
          },
        },
      });

      // 返回标记信息
      return get({ id });
    };

    // 编辑标记
    const edit = (id, values) => {
      dataModule.save({
        tags: {
          [id]: values,
        },
      });
    };

    // 删除标记
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 删除用户对应的标记
      Object.values(userModule.list()).forEach((user) => {
        const index = user.tags.findIndex((tag) => tag === id);

        if (index >= 0) {
          user.tags.splice(index, 1);
        }
      });

      // 保存数据
      dataModule.save({});
    };

    // 格式化标记
    const format = (id, name, color) => {
      if (id) {
        const tag = get({ id });

        if (tag) {
          name = tag.name;
          color = tag.color;
        }
      }

      if (name && color) {
        return `<b class="block_txt nobr" style="background: ${color}; color: #FFF; margin: 0.1em 0.2em;">${name}</b>`;
      }

      return "";
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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 element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

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

          tbody.appendChild(anchor);
        }

        const users = Object.values(userModule.list());

        const filteredUsers = users.filter((user) => user.tags.includes(id));

        anchor.innerHTML = `
          <td class="c1">
            ${format(id)}
          </td>
          <td class="c2">
            <button>${filteredUsers.length}</button>
            <div style="white-space: normal; display: none;">
              ${filteredUsers
                .map((user) => userModule.format(user.id))
                .join("")}
            </div>
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

        actions[0].onclick = (() => {
          let hide = true;

          return () => {
            hide = !hide;

            actions[0].nextElementSibling.style.display = hide
              ? "none"
              : "block";
          };
        })();

        actions[1].onclick = () => {
          const filterMode = filterModule.switchModeByName(
            actions[1].innerHTML
          );

          actions[1].innerHTML = filterMode;

          edit(id, { filterMode });
          reFilter((item) =>
            filteredUsers.find((user) => user.id === item.uid)
          );
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) =>
            filteredUsers.find((user) => user.id === item.uid)
          );
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      format,
      generateColor,
      view,
    };
  })();

  // 关键字模块
  const keywordModule = (() => {
    // 获取关键字列表
    const list = () => dataModule.getKeywords();

    // 获取关键字
    const get = (id) => {
      // 获取列表
      const keywords = list();

      // 如果已存在,则返回信息
      if (keywords[id]) {
        return keywords[id];
      }

      return null;
    };

    // 编辑关键字
    const edit = (id, values) => {
      dataModule.save({
        keywords: {
          [id]: values,
        },
      });
    };

    // 增加关键字
    // filterLevel: 0 - 仅过滤标题; 1 - 过滤标题和内容
    // 无需判重
    const add = (keyword, filterMode, filterLevel) => {
      // ID 为最大值 + 1
      const id = Math.max(Object.keys(list()), 0) + 1;

      // 保存关键字
      dataModule.save({
        keywords: {
          [id]: {
            id,
            keyword,
            filterMode,
            filterLevel,
          },
        },
      });

      // 返回关键字信息
      return get(id);
    };

    // 删除关键字
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 保存数据
      dataModule.save({});
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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 element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = (
        { id, keyword, filterMode, filterLevel },
        anchor = null
      ) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

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

          tbody.appendChild(anchor);
        }

        const checked = filterLevel ? "checked" : "";

        anchor.innerHTML = `
          <td class="c1">
            <div class="filter-input-wrapper">
              <input type="text" value="${keyword || ""}" />
            </div>
          </td>
          <td class="c2">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c3">
            <div style="text-align: center;">
              <input type="checkbox" ${checked} />
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

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

        actions[1].onclick = () => {
          const keyword = anchor.querySelector("INPUT[type='text']").value;

          const filterMode = actions[0].innerHTML;

          const filterLevel = anchor.querySelector(
            `INPUT[type="checkbox"]:checked`
          )
            ? 1
            : 0;

          if (keyword) {
            edit(id, {
              keyword,
              filterMode,
              filterLevel,
            });
            reFilter((item) => item.reason.indexOf("关键字") !== 0);
          }
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.reason.indexOf("关键字") !== 0);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

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

            row.innerHTML = `
              <td class="c1">
                <div class="filter-input-wrapper">
                  <input type="text" value="" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-table-button-group">
                  <button>${filterModule.defaultMode}</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 actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = () => {
              const keyword = row.querySelector("INPUT[type='text']").value;

              const filterMode = actions[0].innerHTML;

              const filterLevel = row.querySelector(
                `INPUT[type="checkbox"]:checked`
              )
                ? 1
                : 0;

              if (keyword) {
                const item = add(keyword, filterMode, filterLevel);

                load(item, row);
                loadNew();
                reFilter();
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      view,
    };
  })();

  // 属地模块
  const locationModule = (() => {
    // 获取属地列表
    const list = () => dataModule.getLocations();

    // 获取属地
    const get = (id) => {
      // 获取列表
      const locations = list();

      // 如果已存在,则返回信息
      if (locations[id]) {
        return locations[id];
      }

      return null;
    };

    // 增加属地
    // 无需判重
    const add = (keyword, filterMode) => {
      // ID 为最大值 + 1
      const id = Math.max(Object.keys(list()), 0) + 1;

      // 保存属地
      dataModule.save({
        locations: {
          [id]: {
            id,
            keyword,
            filterMode,
          },
        },
      });

      // 返回属地信息
      return get(id);
    };

    // 编辑属地
    const edit = (id, values) => {
      dataModule.save({
        locations: {
          [id]: values,
        },
      });
    };

    // 删除属地
    const remove = (id) => {
      // TODO 这里不可避免的直接操作了原始数据
      delete list()[id];

      // 保存数据
      dataModule.save({});
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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 element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = ({ id, keyword, filterMode }, anchor = null) => {
        if (id === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

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

          tbody.appendChild(anchor);
        }

        anchor.innerHTML = `
          <td class="c1">
            <div class="filter-input-wrapper">
              <input type="text" value="${keyword || ""}" />
            </div>
          </td>
          <td class="c2">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c3">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

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

        actions[1].onclick = () => {
          const keyword = anchor.querySelector("INPUT[type='text']").value;

          const filterMode = actions[0].innerHTML;

          if (keyword) {
            edit(id, {
              keyword,
              filterMode,
            });
            reFilter((item) => item.reason.indexOf("属地") !== 0);
          }
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(id);
          reFilter((item) => item.reason.indexOf("属地") !== 0);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

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

            row.innerHTML = `
              <td class="c1">
                <div class="filter-input-wrapper">
                  <input type="text" value="" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-table-button-group">
                  <button>${filterModule.defaultMode}</button>
                </div>
              </td>
              <td class="c3">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
              </td>
            `;

            const actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = () => {
              const keyword = row.querySelector("INPUT[type='text']").value;

              const filterMode = actions[0].innerHTML;

              if (keyword) {
                const item = add(keyword, filterMode);

                load(item, row);
                loadNew();
                reFilter();
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      view,
    };
  })();

  // 猎巫模块
  const witchHuntModule = (() => {
    const key = "WITCH_HUNT";

    const queue = [];

    const cache = {};

    // 获取设置列表
    const list = () => GM_getValue(key) || {};

    // 获取单条设置
    const get = (fid) => {
      // 获取列表
      const settings = list();

      // 如果已存在,则返回信息
      if (settings[fid]) {
        return settings[fid];
      }

      return null;
    };

    // 增加设置
    // filterLevel: 0 - 仅标记; 1 - 标记并过滤
    const add = async (fid, label, filterMode, filterLevel) => {
      // FID 只能是数字
      fid = parseInt(fid, 10);

      // 获取列表
      const settings = list();

      // 如果版面 ID 已存在,则提示错误
      if (Object.keys(settings).includes(fid)) {
        alert("已有相同版面ID");
        return;
      }

      // 请求版面信息
      const info = await fetchModule.getForumInfo(fid);

      // 如果版面不存在,则提示错误
      if (info === null) {
        alert("版面ID有误");
        return;
      }

      // 计算标记颜色
      const color = tagModule.generateColor(info.name);

      // 保存设置
      settings[fid] = {
        fid,
        name: info.name,
        label,
        color,
        filterMode,
        filterLevel,
      };

      GM_setValue(key, settings);

      // 返回设置信息
      return settings[fid];
    };

    // 编辑设置
    const edit = (fid, values) => {
      // 获取列表
      const settings = list();

      // 保存设置
      if (settings[fid]) {
        settings[fid] = {
          ...settings[fid],
          ...values,
        };

        GM_setValue(key, settings);
      }
    };

    // 删除设置
    const remove = (fid) => {
      // 获取列表
      const settings = list();

      // 保存设置
      if (settings[fid]) {
        delete settings[fid];

        GM_setValue(key, settings);
      }
    };

    // 格式化版面
    const format = (fid, name) => {
      return `<a href="/thread.php?fid=${fid}" class="b nobr">[${name}]</a>`;
    };

    // 猎巫
    const run = (item) => {
      // 猎巫任务
      const task = async () => {
        // 获取列表
        const settings = list();

        // 请求版面发言记录
        const result = cache[item.uid]
          ? cache[item.uid]
          : (
              await Promise.all(
                Object.keys(settings).map(async (fid) => {
                  // 当前版面发言数量
                  const result = await fetchModule.getForumPosted(
                    fid,
                    item.uid
                  );

                  // 写入当前设置
                  if (result) {
                    return parseInt(fid, 10);
                  }

                  return null;
                })
              )
            ).filter((i) => i !== null);

        // 写入缓存,同一个页面多次请求没意义
        cache[item.uid] = result;

        // 执行完毕,如果结果有变,重新过滤
        const isEqual =
          result.sort().join() === (item.witchHunt || []).sort().join();

        if (isEqual === false) {
          item.witchHunt = result;
          item.execute();
        }

        // 将当前任务移出队列
        queue.shift();

        // 如果还有任务,继续执行
        if (queue.length > 0) {
          queue[0]();
        }
      };

      // 队列里已经有任务
      const isRunning = queue.length > 0;

      // 加入队列
      queue.push(task);

      // 如果没有正在执行的任务,则立即执行
      if (isRunning === false) {
        task();
      }
    };

    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";
        element.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>
                  <th class="c5" width="1">操作</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
          <div class="silver" style="margin-top: 10px;">猎巫模块需要占用额外的资源,请谨慎开启</div>
        `;

        return element;
      })();

      let index = 0;
      let size = 50;
      let hasNext = false;

      const box = content.querySelector("DIV");

      const tbody = content.querySelector("TBODY");

      const wrapper = content.querySelector(".filter-table-wrapper");

      const load = (
        { fid, name, label, color, filterMode, filterLevel },
        anchor = null
      ) => {
        if (fid === null) {
          if (anchor) {
            tbody.removeChild(anchor);
          }
          return;
        }

        if (anchor === null) {
          anchor = document.createElement("TR");

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

          tbody.appendChild(anchor);
        }

        const checked = filterLevel ? "checked" : "";

        anchor.innerHTML = `
          <td class="c1">
            ${format(fid, name)}
          </td>
          <td class="c2">
            ${tagModule.format(null, label, color)}
          </td>
          <td class="c3">
            <div style="text-align: center;">
              <input type="checkbox" ${checked} />
            </div>
          </td>
          <td class="c4">
            <div class="filter-table-button-group">
              <button>${filterMode || filterModule.defaultMode}</button>
            </div>
          </td>
          <td class="c5">
            <div class="filter-table-button-group">
              <button>保存</button>
              <button>删除</button>
            </div>
          </td>
        `;

        const actions = anchor.querySelectorAll("BUTTON");

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

        actions[1].onclick = () => {
          const filterMode = actions[0].innerHTML;

          const filterLevel = anchor.querySelector(
            `INPUT[type="checkbox"]:checked`
          )
            ? 1
            : 0;

          edit(fid, {
            filterMode,
            filterLevel,
          });
          reFilter((item) => item.reason.indexOf("猎巫") !== 0);
        };

        actions[2].onclick = () => {
          if (confirm("是否确认?") === false) {
            return;
          }

          tbody.removeChild(anchor);

          remove(fid);
          reFilter((item) => item.reason.indexOf("猎巫") !== 0);
        };
      };

      const loadNext = () => {
        hasNext = index + size < Object.keys(list()).length;

        Object.values(list())
          .slice(index, index + size)
          .forEach((item) => load(item));

        if (hasNext === false) {
          const loadNew = () => {
            const row = document.createElement("TR");

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

            row.innerHTML = `
              <td class="c1" style="min-width: 200px;">
                <div class="filter-input-wrapper">
                  <input type="text" value="" placeholder="版面ID" />
                </div>
              </td>
              <td class="c2">
                <div class="filter-input-wrapper">
                  <input type="text" value="" placeholder="标签" />
                </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>${filterModule.defaultMode}</button>
                </div>
              </td>
              <td class="c5">
                <div class="filter-table-button-group">
                  <button>添加</button>
                </div>
              </td>
            `;

            const actions = row.querySelectorAll("BUTTON");

            actions[0].onclick = () => {
              const filterMode = filterModule.switchModeByName(
                actions[0].innerHTML
              );

              actions[0].innerHTML = filterMode;
            };

            actions[1].onclick = async () => {
              const inputs = row.querySelectorAll("INPUT[type='text']");

              const fid = inputs[0].value;
              const label = inputs[1].value;

              const filterMode = actions[0].innerHTML;

              const filterLevel = row.querySelector(
                `INPUT[type="checkbox"]:checked`
              )
                ? 1
                : 0;

              if (fid && label) {
                const item = await add(fid, label, filterMode, filterLevel);

                load(item, row);
                loadNew();
                reFilter();
              }
            };

            tbody.appendChild(row);
          };

          loadNew();
        }

        index += size;
      };

      box.onscroll = () => {
        if (hasNext === false) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          loadNext();
        }
      };

      const refresh = () => {
        index = 0;

        tbody.innerHTML = "";

        loadNext();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      list,
      get,
      add,
      edit,
      remove,
      run,
      view,
    };
  })();

  // 通用设置
  const commonModule = (() => {
    // UI
    const view = (() => {
      const content = (() => {
        const element = document.createElement("DIV");

        element.style = "display: none";

        return element;
      })();

      const refresh = () => {
        content.innerHTML = "";

        // 前置过滤
        (() => {
          const checked = preFilter ? "checked" : "";

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

          element.innerHTML += `
            <div>
              <label>
                前置过滤
                <input type="checkbox" ${checked} />
              </label>
            </div>
          `;

          const checkbox = element.querySelector("INPUT");

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

            GM_setValue(PRE_FILTER_KEY, newValue);

            location.reload();
          };

          content.appendChild(element);
        })();

        // 默认过滤方式
        (() => {
          const element = document.createElement("DIV");

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

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

            const checked =
              dataModule.getDefaultFilterMode() === item ? "checked" : "";

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

            const input = span.querySelector("INPUT");

            input.onchange = () => {
              if (input.checked) {
                dataModule.setDefaultFilterMode(item);

                reFilter((item) => item.filterMode === "继承");
              }
            };

            element.querySelectorAll("div")[1].append(span);
          });

          content.appendChild(element);
        })();

        // 小号过滤(时间)
        (() => {
          const value = dataModule.getFilterRegdateLimit() / 86400000;

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

          element.innerHTML += `
            <br/>
            <div>
              隐藏注册时间小于<input value="${value}" maxLength="4" style="width: 48px;" />天的用户
              <button>确认</button>
            </div>
          `;

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 0;

            dataModule.setFilterRegdateLimit(
              newValue < 0 ? 0 : newValue * 86400000
            );

            reFilter((item) => item.reason.indexOf("注册时间") !== 0);
          };

          content.appendChild(element);
        })();

        // 小号过滤(发帖数)
        (() => {
          const value = dataModule.getFilterPostnumLimit();

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

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

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 0;

            dataModule.setFilterPostnumLimit(newValue < 0 ? 0 : newValue);

            reFilter((item) => item.reason.indexOf("发帖数量") !== 0);
          };

          content.appendChild(element);
        })();

        // 流量号过滤(主题比例)
        (() => {
          const value = dataModule.getFilterTopicRateLimit();

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

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

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue =
              parseInt(element.querySelector("INPUT").value, 10) || 100;

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

            dataModule.setFilterTopicRateLimit(newValue);

            reFilter((item) => item.reason.indexOf("发帖比例") !== 0);
          };

          content.appendChild(element);
        })();

        // 声望过滤
        (() => {
          const value = dataModule.getFilterReputationLimit() || "";

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

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

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            const newValue = parseInt(element.querySelector("INPUT").value, 10);

            dataModule.setFilterReputationLimit(newValue);

            reFilter((item) => item.reason.indexOf("版面声望") !== 0);
          };

          content.appendChild(element);
        })();

        // 匿名过滤
        (() => {
          const checked = dataModule.getFilterAnony() ? "checked" : "";

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

          element.innerHTML += `
            <br/>
            <div>
              <label>
                隐藏匿名的用户
                <input type="checkbox" ${checked} />
              </label>
            </div>
          `;

          const checkbox = element.querySelector("INPUT");

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

            dataModule.setFilterAnony(newValue);

            reFilter((item) => item.reason.indexOf("匿名") !== 0);
          };

          content.appendChild(element);
        })();

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

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

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const filteredUsers = Object.values(userModule.list()).filter(
              ({ tags }) => tags.length === 0
            );

            filteredUsers.forEach(({ id }) => {
              userModule.remove(id);
            });

            reFilter((item) =>
              filteredUsers.find((user) => user.id === item.uid)
            );
          };

          content.appendChild(element);
        })();

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

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

          const action = element.querySelector("BUTTON");

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const users = Object.values(userModule.list());

            Object.values(tagModule.list()).forEach(({ id }) => {
              if (users.find(({ tags }) => tags.includes(id))) {
                return;
              }

              tagModule.remove(id);
            });
          };

          content.appendChild(element);
        })();

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

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

          const action = element.querySelector("BUTTON");
          const list = action.nextElementSibling;

          action.onclick = () => {
            if (confirm("是否确认?") === false) {
              return;
            }

            const users = Object.values(userModule.list());

            const filtered = [];

            const waitingQueue = users.map(
              ({ id }) =>
                () =>
                  fetchModule.getUserInfo(id).then(({ bit }) => {
                    const activeInfo = commonui.activeInfo(0, 0, bit);

                    const activeType = activeInfo[1];

                    if (["ACTIVED", "LINKED"].includes(activeType)) {
                      return;
                    }

                    list.innerHTML += userModule.format(id);

                    filtered.push(id);

                    userModule.remove(id);
                  })
            );

            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;

                reFilter((item) => filtered.includes(item.uid));
              }
            };

            execute();
          };

          content.appendChild(element);
        })();
      };

      return {
        content,
        refresh,
      };
    })();

    return {
      view,
    };
  })();

  // 额外数据请求模块
  // 临时的缓存写法
  const fetchModule = (() => {
    // 简单的统一请求
    const request = (url, config = {}) =>
      fetch(url, {
        headers: {
          "X-User-Agent": USER_AGENT,
        },
        ...config,
      });

    // 获取主题数量
    // 缓存 1 小时
    const getTopicNum = (() => {
      const name = "TOPIC_NUM_CACHE";

      const expireTime = 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "uid",
        version: 1,
      });

      return async (uid) => {
        const cache = await cacheModule.load(name, uid, expireTime);

        if (cache) {
          return cache.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 = () => {
                try {
                  const text = reader.result;
                  const result = JSON.parse(
                    text.replace("window.script_muti_get_var_store=", "")
                  );

                  resolve(result.data);
                } catch {
                  resolve({});
                }
              };

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

        cacheModule.save(name, uid, {
          uid,
          count: __ROWS,
          timestamp: new Date().getTime(),
        });

        return __ROWS;
      };
    })();

    // 获取用户信息
    // 缓存 1 小时
    const getUserInfo = (() => {
      const name = "USER_INFO_CACHE";

      const expireTime = 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "uid",
        version: 1,
      });

      return async (uid) => {
        const cache = await cacheModule.load(name, uid, expireTime);

        if (cache) {
          return cache.data;
        }

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

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

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

                  resolve(result.data[0] || null);
                } catch {
                  resolve(null);
                }
              };

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

        if (data) {
          cacheModule.save(name, uid, {
            uid,
            data,
            timestamp: new Date().getTime(),
          });
        }

        return data;
      };
    })();

    // 获取顶楼用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、声望
    // 缓存 10 分钟
    const getUserInfoAndReputation = (() => {
      const name = "PAGE_CACHE";

      const expireTime = 10 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "url",
        version: 1,
      });

      return async (tid, pid) => {
        if (tid === undefined && pid === undefined) {
          return;
        }

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

        const cache = await cacheModule.load(name, api, expireTime);

        if (cache) {
          return cache.data;
        }

        // 请求数据
        const data = await new Promise((resolve) => {
          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,
                  });
                  return;
                }

                resolve(null);
              };

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

        if (data) {
          cacheModule.save(name, api, {
            url: api,
            data,
            timestamp: new Date().getTime(),
          });
        }

        return data;
      };
    })();

    // 获取版面信息
    // 不会频繁调用,无需缓存
    const getForumInfo = async (fid) => {
      if (Number.isNaN(fid)) {
        return null;
      }

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

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

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

                if (result.data) {
                  resolve(result.data.__F || null);
                  return;
                }

                resolve(null);
              } catch {
                resolve(null);
              }
            };

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

      return data;
    };

    // 获取版面发言记录
    // 缓存 1 天
    const getForumPosted = (() => {
      const name = "FORUM_POSTED_CACHE";

      const expireTime = 24 * 60 * 60 * 1000;

      cacheModule.init(name, {
        keyPath: "url",
        persistent: true,
        version: 2,
      });

      return async (fid, uid) => {
        if (uid <= 0) {
          return;
        }

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

        const func = async (api) => {
          const cache = await cacheModule.load(name, url);

          if (cache) {
            // 发言是无法撤销的,只要有记录就永远不需要再获取
            if (cache.data) {
              return cache.data;
            }

            // 手动处理缓存数据
            if (cache.timestamp + expireTime < new Date().getTime()) {
              await remove(name, url);
            }

            return false;
          }

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

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

                    // __ROWS 有时获取不到具体数量,暂时通过 error 来笼统判断
                    const data = result.error === undefined;

                    // 写入缓存
                    cacheModule.save(name, url, {
                      url,
                      data,
                      timestamp: new Date().getTime(),
                    });

                    resolve(data);
                  } catch {
                    resolve(false);
                  }
                };

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

          return posted;
        };

        const data =
          (await func(url)) || (await func(url + "&searchpost=1")) || false;

        return data;
      };
    })();

    // 每天清理缓存
    (() => {
      const today = new Date();

      const lastTime = new Date(GM_getValue(CLEAR_TIME_KEY) || 0);

      const isToday =
        lastTime.getDate() === today.getDate() &&
        lastTime.getMonth() === today.getMonth() &&
        lastTime.getFullYear() === today.getFullYear();

      if (isToday === false) {
        cacheModule.clear();

        GM_setValue(CLEAR_TIME_KEY, today.getTime());
      }
    })();

    return {
      getTopicNum,
      getUserInfo,
      getUserInfoAndReputation,
      getForumInfo,
      getForumPosted,
    };
  })();

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

    // 主界面
    const view = (() => {
      const tabContainer = (() => {
        const element = document.createElement("DIV");

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

        return element;
      })();

      const tabPanelContainer = (() => {
        const element = document.createElement("DIV");

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

        return element;
      })();

      const content = (() => {
        const element = document.createElement("DIV");

        element.appendChild(tabContainer);
        element.appendChild(tabPanelContainer);

        return element;
      })();

      const addModule = (() => {
        const tc = tabContainer.querySelector("TR");
        const cc = tabPanelContainer;

        return (name, module) => {
          const tabBox = document.createElement("TD");

          tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${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[name] = {
            ...module,
            tab,
            toggle,
          };

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

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

    // 右上角菜单
    const menu = (() => {
      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");

        if (anchor) {
          anchor.before(container);

          content.onclick = onclick;

          return true;
        }

        return false;
      };

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

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

      return {
        create,
        update,
      };
    })();

    return {
      ...view,
      ...menu,
    };
  })();

  // 判断是否为当前用户 UID
  const isCurrentUID = (uid) => {
    return unsafeWindow.__CURRENT_UID === parseInt(uid, 10);
  };

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

    // 获取 UID
    const uid = parseInt(item.uid, 10);

    // 获取链接参数
    const params = new URLSearchParams(location.search);

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

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

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

    // 跳过自己
    if (isCurrentUID(uid)) {
      return "";
    }

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

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

      const { filterMode } = user;

      const mode = filterModule.getModeByName(filterMode);

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

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

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

      // 获取用户对应的标记,并跳过低于当前的过滤模式
      const tags = user
        ? user.tags
            .map((id) => tagModule.get({ id }))
            .filter((i) => i !== null)
            .filter(
              (i) => filterModule.getModeByName(i.filterMode) > result.mode
            )
        : [];

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

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

      const mode = filterModule.getModeByName(filterMode);

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

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

      // 获取设置里的关键字列表,并跳过低于当前的过滤模式
      const keywords = Object.values(keywordModule.list()).filter(
        (i) => filterModule.getModeByName(i.filterMode) > result.mode
      );

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

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

      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 = filterModule.getModeByName(filterMode);

            // 更新过滤模式和原因
            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 = filterModule.getModeByName(filterMode);

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

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

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

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

      // 匿名
      if (uid <= 0) {
        const filterAnony = dataModule.getFilterAnony();

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

        return;
      }

      // 注册时间过滤
      await (async () => {
        const filterRegdateLimit = dataModule.getFilterRegdateLimit();

        // 如果没有用户信息,则请求
        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 = dataModule.getFilterPostnumLimit();

        // 如果没有用户信息,则请求
        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 = dataModule.getFilterTopicRateLimit();

        // 如果没有用户信息,则请求
        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 fetchModule.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 = dataModule.getFilterReputationLimit();

        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(locationModule.list()).filter(
        (i) => filterModule.getModeByName(i.filterMode) > result.mode
      );

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

      // 请求属地
      const { ipLoc } = await fetchModule.getUserInfo(uid);

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

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

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

        const match = ipLoc.match(keyword);

        if (match) {
          const mode = filterModule.getModeByName(filterMode);

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

    // 猎巫过滤
    (() => {
      // 获取猎巫结果
      const witchHunt = item.witchHunt;

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

      // 获取设置
      const list = Object.values(witchHuntModule.list()).filter(({ fid }) =>
        witchHunt.includes(fid)
      );

      // 筛选出匹配的猎巫
      const filtered = Object.values(list)
        .filter(({ filterLevel }) => filterLevel > 0)
        .filter(
          ({ filterMode }) =>
            filterModule.getModeByName(filterMode) > result.mode
        );

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

      // 取最高的过滤模式
      const { filterMode, label } = filtered.sort(
        (a, b) =>
          filterModule.getModeByName(b.filterMode) -
          filterModule.getModeByName(a.filterMode)
      )[0];

      const mode = filterModule.getModeByName(filterMode);

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `猎巫: ${label}`;
    })();

    // 写入过滤模式和过滤原因
    item.filterMode = filterModule.getNameByMode(result.mode);
    item.reason = result.reason;

    // 写入列表
    listModule.add(item);

    // 继承模式下返回默认过滤模式
    if (item.filterMode === "继承") {
      return dataModule.getDefaultFilterMode();
    }

    // 返回结果
    return item.filterMode;
  };

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

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

          // 请求并写入数据
          fetchModule
            .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 (reply) => {
    const { tid, pid, uid } = reply;

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

      // 取得用户声望
      const reputation = (() => {
        const reputations = commonui.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) => {
          // 避免重复请求
          if (reply.userInfo || reply.reputation) {
            resolve(reply);
            return;
          }

          // 请求并写入数据
          fetchModule
            .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({
          uid,
          tid,
          pid,
          subject: "",
          content: quote.innerText,
        });

        (() => {
          if (filterMode === "标记") {
            filterModule.collapse(uid, quote, quote.innerHTML);
            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 filterTopic = async (item) => {
    // 绑定事件
    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 () => {
        // 获取过滤方式
        const filterMode = await getFilterModeByTopic(item.nFilter);

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

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

          // 样式处理
          (() => {
            // 标记模式下,主题标记会有删除线标识
            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;
            }
          })();

          // 非隐藏模式下,恢复显示
          if (filterMode !== "隐藏") {
            container.style.removeProperty("display");
          }
        })();

        // 猎巫会影响效率,待猎巫结果出来后再次过滤
        witchHuntModule.run(item.nFilter);
      };

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

    // 等待过滤完成
    await item.nFilter.execute();
  };

  // 过滤回复
  const filterReply = async (item) => {
    // 绑定事件
    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 () => {
        // 获取过滤方式
        const filterMode = await getFilterModeByReply(item.nFilter);

        // 样式处理
        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");
            }
          })();

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

              filterModule.collapse(uid, content, contentBak);
              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 !== "隐藏") {
            // 获取当前用户
            const user = userModule.get(uid);

            // 修改操作按钮颜色
            if (action) {
              if (user) {
                action.style.background = "#CB4042";
              } else {
                action.style.background = "#AAA";
              }
            }

            // 加载标记和猎巫
            if (tags) {
              const witchHunt = item.nFilter.witchHunt || [];

              const list = [
                ...(user
                  ? user.tags
                      .map((id) => tagModule.get({ id }))
                      .map((tag) => tagModule.format(tag.id)) || []
                  : []),
                ...Object.values(witchHuntModule.list())
                  .filter(({ fid }) => witchHunt.includes(fid))
                  .map(({ label, color }) =>
                    tagModule.format(null, label, color)
                  ),
              ];

              tags.style.display = list.length ? "" : "none";
              tags.innerHTML = list.join("");
            }

            // 恢复显示
            // 楼层的遮罩模式下仍需隐藏
            if (filterMode !== "遮罩") {
              container.style.removeProperty("display");
            }
          }
        })();

        // 猎巫会影响效率,待猎巫结果出来后再次过滤
        witchHuntModule.run(item.nFilter);
      };

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

          action.innerHTML = `屏蔽`;
          action.onclick = (e) => {
            const user = userModule.get(uid);

            if (e.ctrlKey === false) {
              userModule.view.details(uid, username, execute);
              return;
            }

            if (user) {
              userModule.remove(uid);
            } else {
              userModule.add(uid, username, [], filterModule.defaultMode);
            }

            execute();
          };
        }
      })();

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

    // 等待过滤完成
    await item.nFilter.execute();
  };

  // 加载 UI
  const loadUI = () => {
    // 右上角菜单
    const result = (() => {
      let window;

      return ui.create(() => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

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

    // 加载失败
    if (result === false) {
      return;
    }

    // 模块
    ui.addModule("列表", listModule.view).toggle();
    ui.addModule("用户", userModule.view);
    ui.addModule("标记", tagModule.view);
    ui.addModule("关键字", keywordModule.view);
    ui.addModule("属地", locationModule.view);
    ui.addModule("猎巫", witchHuntModule.view);
    ui.addModule("通用设置", commonModule.view);

    // 绑定列表更新回调
    listModule.bindCallback(ui.update);
  };

  // 处理 mainMenu 模块
  const handleMenu = () => {
    let init = menuModule.init;

    // 劫持 init 函数,这个函数完成后才能添加 UI
    Object.defineProperty(menuModule, "init", {
      get: () => {
        return (...arguments) => {
          // 等待执行完毕
          init.apply(menuModule, arguments);

          // 加载 UI
          loadUI();
        };
      },
      set: (value) => {
        init = value;
      },
    });

    // 如果已经有模块,则直接加载 UI
    if (init) {
      loadUI();
    }
  };

  // 处理 topicArg 模块
  const handleTopicModule = async () => {
    let add = topicModule.add;

    // 劫持 add 函数,这是泥潭的主题添加事件
    Object.defineProperty(topicModule, "add", {
      get: () => {
        return async (...arguments) => {
          // 主题 ID
          const tid = arguments[8];

          // 先直接隐藏,等过滤完毕后再放出来
          (() => {
            // 主题标题
            const title = document.getElementById(arguments[1]);

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

            // 隐藏元素
            container.style.display = "none";
          })();

          // 加入列表
          add.apply(topicModule, arguments);

          // 找到对应数据
          const topic = topicModule.data.find((item) => item[8] === tid);

          // 开始过滤
          await filterTopic(topic);
        };
      },
      set: (value) => {
        add = value;
      },
    });

    // 如果已经有数据,则直接过滤
    if (topicModule.data) {
      await Promise.all(Object.values(topicModule.data).map(filterTopic));
    }
  };

  // 处理 postArg 模块
  const handleReplyModule = async () => {
    let proc = replyModule.proc;

    // 劫持 proc 函数,这是泥潭的回复添加事件
    Object.defineProperty(replyModule, "proc", {
      get: () => {
        return async (...arguments) => {
          // 楼层号
          const index = arguments[0];

          // 先直接隐藏,等过滤完毕后再放出来
          (() => {
            // 判断是否是楼层
            const isFloor = typeof index === "number";

            // 评论额外标签
            const prefix = isFloor ? "" : "comment";

            // 用户容器
            const uInfoC = document.querySelector(
              `#${prefix}posterinfo${index}`
            );

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

            // 隐藏元素
            container.style.display = "none";
          })();

          // 加入列表
          proc.apply(replyModule, arguments);

          // 找到对应数据
          const reply = replyModule.data[index];

          // 开始过滤
          await filterReply(reply);
        };
      },
      set: (value) => {
        proc = value;
      },
    });

    // 如果已经有数据,则直接过滤
    if (replyModule.data) {
      await Promise.all(Object.values(replyModule.data).map(filterReply));
    }
  };

  // 处理 commonui 模块
  const handleCommonui = () => {
    // 监听 mainMenu 模块,UI 需要等待这个模块加载完成
    (() => {
      if (commonui.mainMenu) {
        menuModule = commonui.mainMenu;

        handleMenu();
        return;
      }

      Object.defineProperty(commonui, "mainMenu", {
        get: () => menuModule,
        set: (value) => {
          menuModule = value;

          handleMenu();
        },
      });
    })();

    // 监听 topicArg 模块,这是泥潭的主题入口
    (() => {
      if (commonui.topicArg) {
        topicModule = commonui.topicArg;

        handleTopicModule();
        return;
      }

      Object.defineProperty(commonui, "topicArg", {
        get: () => topicModule,
        set: (value) => {
          topicModule = value;

          handleTopicModule();
        },
      });
    })();

    // 监听 postArg 模块,这是泥潭的回复入口
    (() => {
      if (commonui.postArg) {
        replyModule = commonui.postArg;

        handleReplyModule();
        return;
      }

      Object.defineProperty(commonui, "postArg", {
        get: () => replyModule,
        set: (value) => {
          replyModule = value;

          handleReplyModule();
        },
      });
    })();
  };

  // 前置过滤
  const handlePreFilter = () => {
    // 监听 commonui 模块,这是泥潭的主入口
    (() => {
      if (unsafeWindow.commonui) {
        commonui = unsafeWindow.commonui;

        handleCommonui();
        return;
      }

      Object.defineProperty(unsafeWindow, "commonui", {
        get: () => commonui,
        set: (value) => {
          commonui = value;

          handleCommonui();
        },
      });
    })();
  };

  // 普通过滤
  const handleFilter = () => {
    const runFilter = async () => {
      if (topicModule) {
        await Promise.all(
          Object.values(topicModule.data).map((item) => {
            if (item.executed) {
              return;
            }

            item.executed = true;

            filterTopic(item);
          })
        );
      }

      if (replyModule) {
        await Promise.all(
          Object.values(replyModule.data).map((item) => {
            if (item.executed) {
              return;
            }

            item.executed = true;

            filterReply(item);
          })
        );
      }
    };

    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 hook = () => {
      (() => {
        if (topicModule) {
          return;
        }

        if (commonui.topicArg) {
          topicModule = commonui.topicArg;

          hookFunction(topicModule, "add", runFilter);
        }
      })();

      (() => {
        if (replyModule) {
          return;
        }

        if (commonui.postArg) {
          replyModule = commonui.postArg;

          hookFunction(replyModule, "add", runFilter);
        }
      })();
    };

    hook();
    runFilter();

    hookFunction(commonui, "eval", hook);
  };

  // 主函数
  (() => {
    // 前置过滤
    if (preFilter) {
      handlePreFilter();
      return;
    }

    // 等待页面加载完毕后过滤
    unsafeWindow.addEventListener("load", () => {
      if (unsafeWindow.commonui === undefined) {
        return;
      }

      commonui = unsafeWindow.commonui;

      menuModule = commonui.mainMenu;

      loadUI();
      handleFilter();
    });
  })();
})();