NGA Filter

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

От 30.03.2024. Виж последната версия.

// ==UserScript==
// @name        NGA Filter
// @namespace   https://greasyfork.org/users/263018
// @version     2.2.7
// @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, 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";

  // TIPS
  const TIPS = {
    filterMode:
      "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
    addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
    keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
    hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
  };

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

  /**
   * 工具类
   */
  class Tools {
    /**
     * 返回当前值的类型
     * @param   {*}      value  值
     * @returns {String}        值的类型
     */
    static getType = (value) => {
      return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
    };

    /**
     * 返回当前值是否为指定的类型
     * @param   {*}               value  值
     * @param   {Array<String>}   types  类型名称集合
     * @returns {Boolean}         值是否为指定的类型
     */
    static isType = (value, ...types) => {
      return types.includes(this.getType(value));
    };

    /**
     * 拦截属性
     * @param {Object}    target    目标对象
     * @param {String}    property  属性或函数名称
     * @param {Function}  beforeGet 获取属性前事件
     * @param {Function}  beforeSet 设置属性前事件
     * @param {Function}  afterGet  获取属性后事件
     * @param {Function}  afterSet  设置属性前事件
     */
    static interceptProperty = (
      target,
      property,
      { beforeGet, beforeSet, afterGet, afterSet }
    ) => {
      // 缓存数据
      let source = target[property];

      // 如果已经有结果,则直接处理写入后操作
      if (Object.hasOwn(target, property)) {
        if (afterSet) {
          afterSet.apply(target, [source]);
        }
      }

      // 拦截
      Object.defineProperty(target, property, {
        get: () => {
          // 如果是函数
          if (this.isType(source, "function")) {
            return (...args) => {
              try {
                // 执行前操作
                // 可以在这一步修改参数
                // 可以通过在这一步抛出来阻止执行
                if (beforeGet) {
                  args = beforeGet.apply(target, args);
                }

                // 执行函数
                const returnValue = source.apply(target, args);

                // 返回的可能是一个 Promise
                const result =
                  returnValue instanceof Promise
                    ? returnValue
                    : Promise.resolve(returnValue);

                // 执行后操作
                if (afterGet) {
                  result.then((value) => {
                    afterGet.apply(target, [value, args, source]);
                  });
                }
              } catch {}
            };
          }

          try {
            // 返回前操作
            // 可以在这一步修改返回结果
            // 可以通过在这一步抛出来返回 undefined
            const result = beforeGet
              ? beforeGet.apply(target, [source])
              : source;

            // 返回后操作
            // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 beforeGet 来操作处理后的数据
            if (afterGet) {
              afterGet.apply(target, [result, source]);
            }

            // 返回结果
            return result;
          } catch {
            return undefined;
          }
        },
        set: (value) => {
          try {
            // 写入前操作
            // 可以在这一步修改写入结果
            // 可以通过在这一步抛出来写入 undefined
            const result = beforeSet
              ? beforeSet.apply(target, [source, value])
              : value;

            // 写入结果
            source = result;

            // 写入后操作
            if (afterSet) {
              afterSet.apply(target, [result, value]);
            }
          } catch {
            source = undefined;
          }
        },
      });
    };

    /**
     * 合并数据
     * @param   {*}     target  目标对象
     * @param   {Array} sources 来源对象集合
     * @returns                 合并后的对象
     */
    static merge = (target, ...sources) => {
      for (const source of sources) {
        const targetType = this.getType(target);
        const sourceType = this.getType(source);

        // 如果来源对象的类型与目标对象不一致,替换为来源对象
        if (sourceType !== targetType) {
          target = source;
          continue;
        }

        // 如果来源对象是数组,直接合并
        if (targetType === "array") {
          target = [...target, ...source];
          continue;
        }

        // 如果来源对象是对象,合并对象
        if (sourceType === "object") {
          for (const key in source) {
            if (Object.hasOwn(target, key)) {
              target[key] = this.merge(target[key], source[key]);
            } else {
              target[key] = source[key];
            }
          }
          continue;
        }

        // 其他情况,更新值
        target = source;
      }

      return target;
    };

    /**
     * 数组排序
     * @param {Array}                    collection 数据集合
     * @param {Array<String | Function>} iterators  迭代器,要排序的属性名或排序函数
     */
    static sortBy = (collection, ...iterators) =>
      collection.slice().sort((a, b) => {
        for (let i = 0; i < iterators.length; i += 1) {
          const iteratee = iterators[i];

          const valueA = this.isType(iteratee, "function")
            ? iteratee(a)
            : a[iteratee];
          const valueB = this.isType(iteratee, "function")
            ? iteratee(b)
            : b[iteratee];

          if (valueA < valueB) {
            return -1;
          }

          if (valueA > valueB) {
            return 1;
          }
        }

        return 0;
      });

    /**
     * 读取论坛数据
     * @param {Response}  response  请求响应
     * @param {Boolean}   toJSON    是否转为 JSON 格式
     */
    static readForumData = async (response, toJSON = true) => {
      return new Promise(async (resolve) => {
        const blob = await response.blob();

        const reader = new FileReader();

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

          if (toJSON) {
            try {
              resolve(JSON.parse(text));
            } catch {
              resolve({});
            }
            return;
          }

          resolve(text);
        };

        reader.readAsText(blob, "GBK");
      });
    };

    /**
     * 获取成对括号的内容
     * @param   {String} content 内容
     * @param   {String} keyword 起始位置关键字
     * @param   {String} start   左括号
     * @param   {String} end     右括号
     * @returns {String}         包含括号的内容
     */
    static searchPair = (content, keyword, start = "{", end = "}") => {
      // 获取成对括号的位置
      const getLastIndex = (content, position, start = "{", end = "}") => {
        if (position >= 0) {
          let nextIndex = position + 1;

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

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

              if (nextIndex < 0) {
                break;
              }
            }

            nextIndex = nextIndex + 1;
          }
        }

        return -1;
      };

      // 起始位置
      const str = keyword + start;

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

      // 结尾下标
      const lastIndex = getLastIndex(content, index, start, end);

      if (lastIndex >= 0) {
        return start + content.substring(index, lastIndex) + end;
      }

      return null;
    };

    /**
     * 计算字符串的颜色
     *
     * 采用的是泥潭的颜色方案,参见 commonui.htmlName
     * @param   {String} value 字符串
     * @returns {String}       RGB代码
     */
    static generateColor(value) {
      const hash = (() => {
        let h = 5381;

        for (var i = 0; i < value.length; i++) {
          h = ((h << 5) + h + value.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);
      });
    }
  }

  /**
   * IndexedDB
   *
   * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑
   */

  class DBStorage {
    /**
     * 数据库名称
     */
    name = "NGA_FILTER_CACHE";

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 当前实例
     */
    instance = null;

    /**
     * 初始化
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      this.modules = modules;
    }

    /**
     * 是否支持
     */
    isSupport() {
      return unsafeWindow.indexedDB !== undefined;
    }

    /**
     * 打开数据库并创建表
     * @returns {Promise<IDBDatabase>} 实例
     */
    async open() {
      // 创建实例
      if (this.instance === null) {
        // 声明一个数组,用于等待全部表处理完毕
        const queue = [];

        // 创建实例
        await new Promise((resolve, reject) => {
          // 版本
          const version = Object.values(this.modules)
            .map(({ version }) => version)
            .reduce((a, b) => Math.max(a, b), 0);

          // 创建请求
          const request = unsafeWindow.indexedDB.open(this.name, version);

          // 创建或者升级表
          request.onupgradeneeded = (event) => {
            this.instance = event.target.result;

            const transaction = event.target.transaction;
            const oldVersion = event.oldVersion;

            Object.entries(this.modules).forEach(([key, values]) => {
              if (values.version > oldVersion) {
                queue.push(this.createOrUpdateStore(key, values, transaction));
              }
            });
          };

          // 成功后处理
          request.onsuccess = (event) => {
            this.instance = event.target.result;
            resolve();
          };

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

        // 等待全部表处理完毕
        await Promise.all(queue);
      }

      // 返回实例
      return this.instance;
    }

    /**
     * 获取表
     * @param   {String}          name        表名
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @param   {String}          mode        事务模式,默认为只读
     * @returns {Promise<IDBObjectStore>}     表
     */
    async getStore(name, transaction = null, mode = "readonly") {
      const db = await this.open();

      if (transaction === null) {
        transaction = db.transaction(name, mode);
      }

      return transaction.objectStore(name);
    }

    /**
     * 创建或升级表
     * @param   {String}          name        表名
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async createOrUpdateStore(name, { keyPath, indexes }, transaction) {
      const db = transaction.db;
      const data = [];

      // 检查是否存在表,如果存在,缓存数据并删除旧表
      if (db.objectStoreNames.contains(name)) {
        // 获取并缓存全部数据
        const result = await this.bulkGet(name, [], transaction);

        if (result) {
          data.push(...result);
        }

        // 删除旧表
        db.deleteObjectStore(name);
      }

      // 创建表
      const store = db.createObjectStore(name, {
        keyPath,
      });

      // 创建索引
      if (indexes) {
        indexes.forEach((index) => {
          store.createIndex(index, index);
        });
      }

      // 迁移数据
      if (data.length > 0) {
        await this.bulkAdd(name, data, transaction);
      }
    }

    /**
     * 插入指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async add(name, data, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 插入数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.add(data);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

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

      // 返回结果
      return result;
    }

    /**
     * 删除指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async delete(name, key, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 删除数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.delete(key);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

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

      // 返回结果
      return result;
    }

    /**
     * 插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}
     */
    async put(name, data, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction, "readwrite");

      // 插入或修改数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.put(data);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

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

      // 返回结果
      return result;
    }

    /**
     * 获取指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise}                     数据
     */
    async get(name, key, transaction = null) {
      // 获取表
      const store = await this.getStore(name, transaction);

      // 查询数据
      const result = await new Promise((resolve, reject) => {
        // 创建请求
        const request = store.get(key);

        // 成功后处理
        request.onsuccess = (event) => {
          resolve(event.target.result);
        };

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

      // 返回结果
      return result;
    }

    /**
     * 批量插入指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data, transaction = null) {
      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.add(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = [], transaction = null) {
      // 如果 keys 为空,删除全部数据
      if (keys.length === 0) {
        // 获取表
        const store = await this.getStore(name, transaction, "readwrite");

        // 清空数据
        await new Promise((resolve, reject) => {
          // 创建请求
          const request = store.clear();

          // 成功后处理
          request.onsuccess = (event) => {
            resolve(event.target.result);
          };

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

        return -1;
      }

      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.delete(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data, transaction = null) {
      // 等待操作结果
      const result = await Promise.all(
        data.map((item) =>
          this.put(name, item, transaction)
            .then(() => true)
            .catch(() => false)
        )
      );

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量获取指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @param   {IDBTransaction}  transaction 事务,空则根据表名创建新事务
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = [], transaction = null) {
      // 如果 keys 为空,查询全部数据
      if (keys.length === 0) {
        // 获取表
        const store = await this.getStore(name, transaction);

        // 查询数据
        const result = await new Promise((resolve, reject) => {
          // 创建请求
          const request = store.getAll();

          // 成功后处理
          request.onsuccess = (event) => {
            resolve(event.target.result || []);
          };

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

        // 返回结果
        return result;
      }

      // 返回符合的结果
      const result = [];

      await Promise.all(
        keys.map((key) =>
          this.get(name, key, transaction)
            .then((item) => {
              result.push(item);
            })
            .catch(() => {})
        )
      );

      return result;
    }
  }

  /**
   * 油猴存储
   *
   * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
   */
  class GMStorage extends DBStorage {
    /**
     * 初始化
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      super(modules);
    }

    /**
     * 插入指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.add(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;
      const key = data[keyPath];

      // 如果数据中不包含主键,抛出异常
      if (key === undefined) {
        throw new Error();
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键已存在,抛出异常
      if (Object.hasOwn(values, key)) {
        throw new Error();
      }

      // 插入数据
      values[key] = data;

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 删除指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}
     */
    async delete(name, key) {
      // 如果不在模块列表里,忽略 key,删除全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.delete(name, key);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键不存在,抛出异常
      if (Object.hasOwn(values, key) === false) {
        throw new Error();
      }

      // 删除数据
      delete values[key];

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async put(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.put(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;
      const key = data[keyPath];

      // 如果数据中不包含主键,抛出异常
      if (key === undefined) {
        throw new Error();
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 插入或修改数据
      values[key] = data;

      // 保存数据
      GM_setValue(name, values);
    }

    /**
     * 获取指定表的数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}                     数据
     */
    async get(name, key) {
      // 如果不在模块列表里,忽略 key,返回全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_getValue(name);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.get(name, key);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果对应主键不存在,抛出异常
      if (Object.hasOwn(values, key) === false) {
        throw new Error();
      }

      // 返回结果
      return values[key];
    }

    /**
     * 批量插入指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkAdd(name, data);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 添加数据
      const result = data.map((item) => {
        const key = item[keyPath];

        // 如果数据中不包含主键,抛出异常
        if (key === undefined) {
          return false;
        }

        // 如果对应主键已存在,抛出异常
        if (Object.hasOwn(values, key)) {
          return false;
        }

        // 插入数据
        values[key] = item;

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = []) {
      // 如果不在模块列表里,忽略 keys,删除全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, {});
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkDelete(name, keys);
      }

      // 如果 keys 为空,删除全部数据
      if (keys.length === 0) {
        GM_setValue(name, {});

        return -1;
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 删除数据
      const result = keys.map((key) => {
        // 如果对应主键不存在,抛出异常
        if (Object.hasOwn(values, key) === false) {
          return false;
        }

        // 删除数据
        delete values[key];

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量插入或修改指定表的数据
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data) {
      // 如果不在模块列表里,写入全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_setValue(name, data);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkPut(name, keys);
      }

      // 获取对应的主键
      const keyPath = this.modules[name].keyPath;

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 添加数据
      const result = data.map((item) => {
        const key = item[keyPath];

        // 如果数据中不包含主键,抛出异常
        if (key === undefined) {
          return false;
        }

        // 插入数据
        values[key] = item;

        return true;
      });

      // 保存数据
      GM_setValue(name, values);

      // 返回受影响的数量
      return result.filter((item) => item).length;
    }

    /**
     * 批量获取指定表的数据,如果不在模块列表里,返回全部数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = []) {
      // 如果不在模块列表里,忽略 keys,返回全部数据
      if (Object.hasOwn(this.modules, name) === false) {
        return GM_getValue(name);
      }

      // 如果支持 IndexedDB,使用 IndexedDB
      if (super.isSupport()) {
        return super.bulkGet(name, keys);
      }

      // 获取全部数据
      const values = GM_getValue(name, {});

      // 如果 keys 为空,返回全部数据
      if (keys.length === 0) {
        return Object.values(values);
      }

      // 返回符合的结果
      const result = [];

      keys.forEach((key) => {
        if (Object.hasOwn(values, key)) {
          result.push(values[key]);
        }
      });

      return result;
    }
  }

  /**
   * 缓存管理
   *
   * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
   */
  class Cache extends GMStorage {
    /**
     * 增加模块列表的 timestamp 索引
     * @param {*} modules 模块列表
     */
    constructor(modules) {
      Object.values(modules).forEach((item) => {
        item.indexes = item.indexes || [];

        if (item.indexes.includes("timestamp") === false) {
          item.indexes.push("timestamp");
        }
      });

      super(modules);

      this.autoClear();
    }

    /**
     * 插入指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async add(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.timestamp = data.timestamp || new Date().getTime();
      }

      return super.add(name, data);
    }

    /**
     * 插入或修改指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {*}               data        数据
     * @returns {Promise}
     */
    async put(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.timestamp = data.timestamp || new Date().getTime();
      }

      return super.put(name, data);
    }

    /**
     * 获取指定表的数据,并移除过期数据
     * @param   {String}          name        表名
     * @param   {String}          key         主键
     * @returns {Promise}                     数据
     */
    async get(name, key) {
      // 获取数据
      const value = await super.get(name, key).catch(() => null);

      // 如果不在模块里,直接返回结果
      if (Object.hasOwn(this.modules, name) === false) {
        return value;
      }

      // 如果有结果的话,移除超时数据
      if (value) {
        // 读取模块配置
        const { expireTime, persistent } = this.modules[name];

        // 持久化或未超时
        if (persistent || value.timestamp + expireTime > new Date().getTime()) {
          return value;
        }

        // 移除超时数据
        await super.delete(name, key);
      }

      return null;
    }

    /**
     * 批量插入指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkAdd(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.forEach((item) => {
          item.timestamp = item.timestamp || new Date().getTime();
        });
      }

      return super.bulkAdd(name, data);
    }

    /**
     * 批量删除指定表的数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则删除全部
     * @param   {boolean}         force       是否强制删除,否则只删除过期数据
     * @returns {Promise<number>}             成功数量,删除全部时返回 -1
     */
    async bulkDelete(name, keys = [], force = false) {
      // 如果不在模块里,强制删除
      if (Object.hasOwn(this.modules, name) === false) {
        force = true;
      }

      // 强制删除
      if (force) {
        return super.bulkDelete(name, keys);
      }

      // 批量获取指定表的数据,并移除过期数据
      const result = this.bulkGet(name, keys);

      // 返回成功数量
      if (keys.length === 0) {
        return -1;
      }

      return keys.length - result.length;
    }

    /**
     * 批量插入或修改指定表的数据,并增加 timestamp
     * @param   {String}          name        表名
     * @param   {Array}           data        数据集合
     * @returns {Promise<number>}             成功数量
     */
    async bulkPut(name, data) {
      // 如果在模块里,增加 timestamp
      if (Object.hasOwn(this.modules, name)) {
        data.forEach((item) => {
          item.timestamp = item.timestamp || new Date().getTime();
        });
      }

      return super.bulkPut(name, data);
    }

    /**
     * 批量获取指定表的数据,并移除过期数据
     * @param   {String}          name        表名
     * @param   {Array<String>}   keys        主键集合,空则获取全部
     * @returns {Promise<Array>}              数据集合
     */
    async bulkGet(name, keys = []) {
      // 获取数据
      const values = await super.bulkGet(name, keys).catch(() => []);

      // 如果不在模块里,直接返回结果
      if (Object.hasOwn(this.modules, name) === false) {
        return values;
      }

      // 读取模块配置
      const { keyPath, expireTime, persistent } = this.modules[name];

      // 筛选出超时数据
      const result = [];
      const expired = [];

      values.forEach((value) => {
        // 持久化或未超时
        if (persistent || value.timestamp + expireTime > new Date().getTime()) {
          result.push(value);
          return;
        }

        // 记录超时数据
        expired.push(value[keyPath]);
      });

      // 移除超时数据
      await super.bulkDelete(name, expired);

      // 返回结果
      return result;
    }

    /**
     * 自动清理缓存
     */
    async autoClear() {
      const data = await this.get(CLEAR_TIME_KEY);

      const now = new Date();
      const clearTime = new Date(data || 0);

      const isToday =
        now.getDate() === clearTime.getDate() &&
        now.getMonth() === clearTime.getMonth() &&
        now.getFullYear() === clearTime.getFullYear();

      if (isToday) {
        return;
      }

      await Promise.all(
        Object.keys(this.modules).map((name) => this.bulkDelete(name))
      );

      await this.put(CLEAR_TIME_KEY, now.getTime());
    }
  }

  /**
   * 设置
   *
   * 暂时整体处理模块设置,后续再拆分
   */
  class Settings {
    /**
     * 缓存管理
     */
    cache;

    /**
     * 当前设置
     */
    data = null;

    /**
     * 初始化并绑定缓存管理
     * @param {Cache} cache 缓存管理
     */
    constructor(cache) {
      this.cache = cache;
    }

    /**
     * 读取设置
     */
    async load() {
      // 读取设置
      if (this.data === null) {
        // 默认配置
        const defaultData = {
          tags: {},
          users: {},
          keywords: {},
          locations: {},
          options: {
            filterRegdateLimit: 0,
            filterPostnumLimit: 0,
            filterTopicRateLimit: 100,
            filterReputationLimit: NaN,
            filterAnony: false,
            filterMode: "隐藏",
          },
        };

        // 读取数据
        const storedData = await this.cache
          .get(DATA_KEY)
          .then((values) => values || {});

        // 写入缓存
        this.data = Tools.merge({}, defaultData, storedData);

        // 写入默认模块选项
        if (Object.hasOwn(this.data, "modules") === false) {
          this.data.modules = ["user", "tag", "misc"];

          if (Object.keys(this.data.keywords).length > 0) {
            this.data.modules.push("keyword");
          }

          if (Object.keys(this.data.locations).length > 0) {
            this.data.modules.push("location");
          }
        }
      }

      // 返回设置
      return this.data;
    }

    /**
     * 写入设置
     */
    async save() {
      return this.cache.put(DATA_KEY, this.data);
    }

    /**
     * 获取模块列表
     */
    get modules() {
      return this.data.modules;
    }

    /**
     * 设置模块列表
     */
    set modules(values) {
      this.data.modules = values;
      this.save();
    }

    /**
     * 获取标签列表
     */
    get tags() {
      return this.data.tags;
    }

    /**
     * 设置标签列表
     */
    set tags(values) {
      this.data.tags = values;
      this.save();
    }

    /**
     * 获取用户列表
     */
    get users() {
      return this.data.users;
    }

    /**
     * 设置用户列表
     */
    set users(values) {
      this.data.users = values;
      this.save();
    }

    /**
     * 获取关键字列表
     */
    get keywords() {
      return this.data.keywords;
    }

    /**
     * 设置关键字列表
     */
    set keywords(values) {
      this.data.keywords = values;
      this.save();
    }

    /**
     * 获取属地列表
     */
    get locations() {
      return this.data.locations;
    }

    /**
     * 设置属地列表
     */
    set locations(values) {
      this.data.locations = values;
      this.save();
    }

    /**
     * 获取默认过滤模式
     */
    get defaultFilterMode() {
      return this.data.options.filterMode;
    }

    /**
     * 设置默认过滤模式
     */
    set defaultFilterMode(value) {
      this.data.options.filterMode = value;
      this.save();
    }

    /**
     * 获取注册时间限制
     */
    get filterRegdateLimit() {
      return this.data.options.filterRegdateLimit || 0;
    }

    /**
     * 设置注册时间限制
     */
    set filterRegdateLimit(value) {
      this.data.options.filterRegdateLimit = value;
      this.save();
    }

    /**
     * 获取发帖数量限制
     */
    get filterPostnumLimit() {
      return this.data.options.filterPostnumLimit || 0;
    }

    /**
     * 设置发帖数量限制
     */
    set filterPostnumLimit(value) {
      this.data.options.filterPostnumLimit = value;
      this.save();
    }

    /**
     * 获取发帖比例限制
     */
    get filterTopicRateLimit() {
      return this.data.options.filterTopicRateLimit || 100;
    }

    /**
     * 设置发帖比例限制
     */
    set filterTopicRateLimit(value) {
      this.data.options.filterTopicRateLimit = value;
      this.save();
    }

    /**
     * 获取版面声望限制
     */
    get filterReputationLimit() {
      return this.data.options.filterReputationLimit || NaN;
    }

    /**
     * 设置版面声望限制
     */
    set filterReputationLimit(value) {
      this.data.options.filterReputationLimit = value;
      this.save();
    }

    /**
     * 获取是否过滤匿名
     */
    get filterAnonymous() {
      return this.data.options.filterAnony || false;
    }

    /**
     * 设置是否过滤匿名
     */
    set filterAnonymous(value) {
      this.data.options.filterAnony = value;
      this.save();
    }

    /**
     * 获取代理设置
     */
    get userAgent() {
      return this.cache.get(USER_AGENT_KEY).then((value) => {
        if (value === undefined) {
          return "Nga_Official";
        }

        return value;
      });
    }

    /**
     * 修改代理设置
     */
    set userAgent(value) {
      this.cache.put(USER_AGENT_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取是否启用前置过滤
     */
    get preFilterEnabled() {
      return this.cache.get(PRE_FILTER_KEY).then((value) => {
        if (value === undefined) {
          return true;
        }

        return value;
      });
    }

    /**
     * 设置是否启用前置过滤
     */
    set preFilterEnabled(value) {
      this.cache.put(PRE_FILTER_KEY, value).then(() => {
        location.reload();
      });
    }

    /**
     * 获取过滤模式列表
     *
     * 模拟成从配置中获取
     */
    get filterModes() {
      return ["继承", "标记", "遮罩", "隐藏", "显示"];
    }

    /**
     * 获取指定下标过滤模式
     * @param {Number} index 下标
     */
    getNameByMode(index) {
      const modes = this.filterModes;

      return modes[index] || "";
    }

    /**
     * 获取指定过滤模式下标
     * @param {String} name 过滤模式
     */
    getModeByName(name) {
      const modes = this.filterModes;

      return modes.indexOf(name);
    }

    /**
     * 切换过滤模式
     * @param   {String} value 过滤模式
     * @returns {String}       过滤模式
     */
    switchModeByName(value) {
      const index = this.getModeByName(value);

      const nextIndex = (index + 1) % this.filterModes.length;

      return this.filterModes[nextIndex];
    }
  }

  /**
   * API
   */
  class API {
    /**
     * 缓存模块
     */
    static modules = {
      TOPIC_NUM_CACHE: {
        keyPath: "uid",
        version: 1,
        expireTime: 1000 * 60 * 60,
        persistent: true,
      },
      USER_INFO_CACHE: {
        keyPath: "uid",
        version: 1,
        expireTime: 1000 * 60 * 60,
        persistent: false,
      },
      PAGE_CACHE: {
        keyPath: "url",
        version: 1,
        expireTime: 1000 * 60 * 10,
        persistent: false,
      },
      FORUM_POSTED_CACHE: {
        keyPath: "url",
        version: 2,
        expireTime: 1000 * 60 * 60 * 24,
        persistent: true,
      },
    };

    /**
     * 缓存管理
     */
    cache;

    /**
     * 设置
     */
    settings;

    /**
     * 初始化并绑定缓存管理、设置
     * @param {Cache}     cache     缓存管理
     * @param {Settings}  settings  设置
     */
    constructor(cache, settings) {
      this.cache = cache;
      this.settings = settings;
    }

    /**
     * 简单的统一请求
     * @param {String}  url    请求地址
     * @param {Object}  config 请求参数
     * @param {Boolean} toJSON 是否转为 JSON 格式
     */
    async request(url, config = {}, toJSON = true) {
      const userAgent = await this.settings.userAgent;

      const response = await fetch(url, {
        headers: {
          "X-User-Agent": userAgent,
        },
        ...config,
      });

      const result = await Tools.readForumData(response, toJSON);

      return result;
    }

    /**
     * 获取用户主题数量
     * @param {number} uid 用户 ID
     */
    async getTopicNum(uid) {
      const name = "TOPIC_NUM_CACHE";
      const expireTime = API.modules[name];

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

      const cache = await this.cache.get(name, uid);

      // 仍在缓存期间内,直接返回
      if (cache) {
        const expired = cache.timestamp + expireTime < new Date().getTime();

        if (expired === false) {
          return cache.count;
        }
      }

      // 请求数据
      const result = await this.request(api);

      // 服务器可能返回错误,遇到这种情况下,需要保留缓存
      const count = (() => {
        if (result.data) {
          return result.data.__ROWS || 0;
        }

        if (cache) {
          return cache.count;
        }

        return 0;
      })();

      // 更新缓存
      this.cache.put(name, {
        uid,
        count,
      });

      return count;
    }

    /**
     * 获取用户信息
     * @param {number} uid 用户 ID
     */
    async getUserInfo(uid) {
      const name = "USER_INFO_CACHE";

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

      const cache = await this.cache.get(name, uid);

      if (cache) {
        return cache.data;
      }

      const result = await this.request(api, {
        credentials: "omit",
      });

      const data = result.data ? result.data[0] : null;

      if (data) {
        this.cache.put(name, {
          uid,
          data,
        });
      }

      return data || {};
    }

    /**
     * 获取帖子内容、用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、版面声望
     * @param {number} tid 主题 ID
     * @param {number} pid 回复 ID
     */
    async getPostInfo(tid, pid) {
      const name = "PAGE_CACHE";

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

      const cache = await this.cache.get(name, api);

      if (cache) {
        return cache.data;
      }

      const result = await this.request(api, {}, false);

      const parser = new DOMParser();

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

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

      if (verify === null) {
        return {};
      }

      // 声明返回值
      const data = {};

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

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

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

        return 0;
      })();

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

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

      // 非匿名用户可以继续取得用户信息和版面声望
      if (data.uid > 0) {
        // 取得用户信息
        data.userInfo = (() => {
          const text = Tools.searchPair(result, `"${data.uid}":`);

          if (text) {
            try {
              return JSON.parse(text);
            } catch {
              return null;
            }
          }

          return null;
        })();

        // 取得用户声望
        data.reputation = (() => {
          const reputations = (() => {
            const text = Tools.searchPair(result, `"__REPUTATIONS":`);

            if (text) {
              try {
                return JSON.parse(text);
              } catch {
                return null;
              }
            }

            return null;
          })();

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

          return NaN;
        })();
      }

      // 写入缓存
      this.cache.put(name, {
        url: api,
        data,
      });

      // 返回结果
      return data;
    }

    /**
     * 获取版面信息
     * @param {number} fid 版面 ID
     */
    async getForumInfo(fid) {
      if (Number.isNaN(fid)) {
        return null;
      }

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

      const result = await this.request(api);

      const info = result.data ? result.data.__F : null;

      return info;
    }

    /**
     * 获取版面发言记录
     * @param {number} fid 版面 ID
     * @param {number} uid 用户 ID
     */
    async getForumPosted(fid, uid) {
      const name = "FORUM_POSTED_CACHE";
      const expireTime = API.modules[name];

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

      const cache = await this.cache.get(name, api);

      if (cache) {
        // 发言是无法撤销的,只要有记录就永远不需要再获取
        // 手动处理没有记录的缓存数据
        const expired = cache.timestamp + expireTime < new Date().getTime();
        if (expired && cache.data === false) {
          await this.cache.delete(name, api);
        }

        return cache.data;
      }

      let isComplete = false;
      let isBusy = false;

      const func = async (url) => {
        if (isComplete || isBusy) {
          return;
        }

        const result = await this.request(url, {}, false);

        // 将所有匹配的 FID 写入缓存,即使并不在设置里
        const matched = result.match(/"fid":(-?\d+),/g);

        if (matched) {
          const list = [
            ...new Set(
              matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10))
            ),
          ];

          list.forEach((item) => {
            const key = api.replace(`&fid=${fid}`, `&fid=${item}`);

            // 写入缓存
            this.cache.put(name, {
              url: key,
              data: true,
            });

            // 已有结果,无需继续查询
            if (fid === item) {
              isComplete = true;
            }
          });
        }

        // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
        if (result.indexOf("服务器忙") > 0) {
          isBusy = true;
        }
      };

      // 先获取回复记录的第一页,顺便可以获取其他版面的记录
      // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
      await func(api.replace(`&fid=${fid}`, `&searchpost=1`));
      await func(api + "&searchpost=1");
      await func(api);

      // 无论成功与否都写入缓存
      if (isComplete === false) {
        // 遇到服务器忙的情况,手动调整缓存时间至 1 小时
        const timestamp = isBusy
          ? new Date().getTime() - (expireTime - 1000 * 60 * 60)
          : new Date().getTime();

        // 写入失败缓存
        this.cache.put(name, {
          url: api,
          data: false,
          timestamp,
        });
      }

      return isComplete;
    }
  }

  /**
   * UI
   */
  class UI {
    /**
     * 标签
     */
    static label = "屏蔽";

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 菜单元素
     */
    menu = null;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API,注册脚本菜单
     * @param {Settings} settings 设置
     * @param {API}      api      API
     */
    constructor(settings, api) {
      this.settings = settings;
      this.api = api;

      this.init();
    }

    /**
     * 初始化,创建基础视图,初始化通用设置
     */
    init() {
      const tabs = this.createTabs({
        className: "right_",
      });

      const content = this.createElement("DIV", [], {
        style: "width: 80vw;",
      });

      const container = this.createElement("DIV", [tabs, content]);

      this.views = {
        tabs,
        content,
        container,
      };

      this.initSettings();
    }

    /**
     * 初始化设置
     */
    initSettings() {
      // 创建基础视图
      const settings = this.createElement("DIV", []);

      // 添加设置项
      const add = (order, ...elements) => {
        const items = [...settings.childNodes];

        if (items.find((item) => item.order === order)) {
          return;
        }

        const item = this.createElement(
          "DIV",
          [...elements, this.createElement("BR", [])],
          {
            order,
          }
        );

        const anchor = items.find((item) => item.order > order);

        settings.insertBefore(item, anchor || null);

        return item;
      };

      // 绑定事件
      Object.assign(settings, {
        add,
      });

      // 合并视图
      Object.assign(this.views, {
        settings,
      });

      // 创建标签页
      const { tabs, content } = this.views;

      this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
        onclick: () => {
          content.innerHTML = "";
          content.appendChild(settings);
        },
      });
    }

    /**
     * 弹窗确认
     * @param   {String}  message 提示信息
     * @returns {Promise}
     */
    confirm(message = "是否确认?") {
      return new Promise((resolve, reject) => {
        const result = confirm(message);

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

        reject();
      });
    }

    /**
     * 折叠
     * @param {String | Number} key     标识
     * @param {HTMLElement}     element 目标元素
     * @param {String}          content 内容
     */
    collapse(key, element, content) {
      key = "collapsed_" + key;

      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('${key}')].forEach(item => item.style.display = '')">点击查看</a>
            <div style="display: none;" name="${key}">
                ${content}
            </div>
        </div>`;
    }

    /**
     * 创建元素
     * @param   {String}                               tagName    标签
     * @param   {HTMLElement | HTMLElement[] | String} content    内容,元素或者 innerHTML
     * @param   {*}                                    properties 额外属性
     * @returns {HTMLElement}                                     元素
     */
    createElement(tagName, content, properties = {}) {
      const element = document.createElement(tagName);

      // 写入内容
      if (typeof content === "string") {
        element.innerHTML = content;
      } else {
        if (Array.isArray(content) === false) {
          content = [content];
        }

        content.forEach((item) => {
          if (item === null) {
            return;
          }

          if (typeof item === "string") {
            element.append(item);
            return;
          }

          element.appendChild(item);
        });
      }

      // 对 A 标签的额外处理
      if (tagName.toUpperCase() === "A") {
        if (Object.hasOwn(properties, "href") === false) {
          properties.href = "javascript: void(0);";
        }
      }

      // 附加属性
      Object.entries(properties).forEach(([key, value]) => {
        element[key] = value;
      });

      return element;
    }

    /**
     * 创建按钮
     * @param {String}   text       文字
     * @param {Function} onclick    点击事件
     * @param {*}        properties 额外属性
     */
    createButton(text, onclick, properties = {}) {
      return this.createElement("BUTTON", text, {
        ...properties,
        onclick,
      });
    }

    /**
     * 创建按钮组
     * @param {Array} buttons 按钮集合
     */
    createButtonGroup(...buttons) {
      return this.createElement("DIV", buttons, {
        className: "filter-button-group",
      });
    }

    /**
     * 创建表格
     * @param   {Array}       headers    表头集合
     * @param   {*}           properties 额外属性
     * @returns {HTMLElement}            元素和相关函数
     */
    createTable(headers, properties = {}) {
      const rows = [];

      const ths = headers.map((item, index) =>
        this.createElement("TH", item.label, {
          ...item,
          className: `c${index + 1}`,
        })
      );

      const tr =
        ths.length > 0
          ? this.createElement("TR", ths, {
              className: "block_txt_c0",
            })
          : null;

      const thead = tr !== null ? this.createElement("THEAD", tr) : null;

      const tbody = this.createElement("TBODY", []);

      const table = this.createElement("TABLE", [thead, tbody], {
        ...properties,
        className: "filter-table forumbox",
      });

      const wrapper = this.createElement("DIV", table, {
        className: "filter-table-wrapper",
      });

      const intersectionObserver = new IntersectionObserver((entries) => {
        if (entries[0].intersectionRatio <= 0) return;

        const list = rows.splice(0, 10);

        if (list.length === 0) {
          return;
        }

        intersectionObserver.disconnect();

        tbody.append(...list);

        intersectionObserver.observe(tbody.lastElementChild);
      });

      const add = (...columns) => {
        const tds = columns.map((column, index) => {
          if (ths[index]) {
            const { center, ellipsis } = ths[index];

            const properties = {};

            if (center) {
              properties.style = "text-align: center;";
            }

            if (ellipsis) {
              properties.className = "filter-text-ellipsis";
            }

            column = this.createElement("DIV", column, properties);
          }

          return this.createElement("TD", column, {
            className: `c${index + 1}`,
          });
        });

        const tr = this.createElement("TR", tds, {
          className: `row${(rows.length % 2) + 1}`,
        });

        intersectionObserver.disconnect();

        rows.push(tr);

        intersectionObserver.observe(tbody.lastElementChild || tbody);
      };

      const update = (e, ...columns) => {
        const row = e.target.closest("TR");

        if (row) {
          const tds = row.querySelectorAll("TD");

          columns.map((column, index) => {
            if (ths[index]) {
              const { center, ellipsis } = ths[index];

              const properties = {};

              if (center) {
                properties.style = "text-align: center;";
              }

              if (ellipsis) {
                properties.className = "filter-text-ellipsis";
              }

              column = this.createElement("DIV", column, properties);
            }

            if (tds[index]) {
              tds[index].innerHTML = "";
              tds[index].append(column);
            }
          });
        }
      };

      const remove = (e) => {
        const row = e.target.closest("TR");

        if (row) {
          tbody.removeChild(row);
        }
      };

      const clear = () => {
        rows.splice(0);
        intersectionObserver.disconnect();

        tbody.innerHTML = "";
      };

      Object.assign(wrapper, {
        add,
        update,
        remove,
        clear,
      });

      return wrapper;
    }

    /**
     * 创建标签组
     * @param {*} properties 额外属性
     */
    createTabs(properties = {}) {
      const tabs = this.createElement(
        "DIV",
        `<table class="stdbtn" cellspacing="0">
          <tbody>
            <tr></tr>
          </tbody>
        </table>`,
        properties
      );

      return this.createElement(
        "DIV",
        [
          tabs,
          this.createElement("DIV", [], {
            className: "clear",
          }),
        ],
        {
          style: "display: none; margin-bottom: 5px;",
        }
      );
    }

    /**
     * 创建标签
     * @param {Element} tabs       标签组
     * @param {String}  label      标签名称
     * @param {Number}  order      标签顺序,重复则跳过
     * @param {*}       properties 额外属性
     */
    createTab(tabs, label, order, properties = {}) {
      const group = tabs.querySelector("TR");

      const items = [...group.childNodes];

      if (items.find((item) => item.order === order)) {
        return;
      }

      if (items.length > 0) {
        tabs.style.removeProperty("display");
      }

      const tab = this.createElement("A", label, {
        ...properties,
        className: "nobr silver",
        onclick: () => {
          if (tab.className === "nobr") {
            return;
          }

          group.querySelectorAll("A").forEach((item) => {
            if (item === tab) {
              item.className = "nobr";
            } else {
              item.className = "nobr silver";
            }
          });

          if (properties.onclick) {
            properties.onclick();
          }
        },
      });

      const wrapper = this.createElement("TD", tab, {
        order,
      });

      const anchor = items.find((item) => item.order > order);

      group.insertBefore(wrapper, anchor || null);

      return wrapper;
    }

    /**
     * 创建对话框
     * @param {HTMLElement | null} anchor  要绑定的元素,如果为空,直接弹出
     * @param {String}             title   对话框的标题
     * @param {HTMLElement}        content 对话框的内容
     */
    createDialog(anchor, title, content) {
      let window;

      const show = () => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(title);
        window._.addContent(content);
        window._.show();
      };

      if (anchor) {
        anchor.onclick = show;
      } else {
        show();
      }

      return window;
    }

    /**
     * 渲染菜单
     */
    renderMenu() {
      // 如果泥潭的右上角菜单还没有加载完成,说明模块尚未加载完毕,跳过
      const anchor = document.querySelector("#mainmenu .td:last-child");

      if (anchor === null) {
        return;
      }

      const menu = this.createElement("A", this.constructor.label, {
        className: "mmdefault nobr",
      });

      const container = this.createElement("DIV", menu, {
        className: "td",
      });

      // 插入菜单
      anchor.before(container);

      // 绑定菜单元素
      this.menu = menu;
    }

    /**
     * 渲染视图
     */
    renderView() {
      // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
      if (this.menu === null) {
        return;
      }

      // 绑定菜单点击事件.
      this.createDialog(
        this.menu,
        this.constructor.label,
        this.views.container
      );

      // 启用第一个模块
      this.views.tabs.querySelector("A").click();
    }

    /**
     * 渲染
     */
    render() {
      this.renderMenu();
      this.renderView();
    }
  }

  /**
   * 基础模块
   */
  class Module {
    /**
     * 模块名称
     */
    static name;

    /**
     * 模块标签
     */
    static label;

    /**
     * 顺序
     */
    static order;

    /**
     * 依赖模块
     */
    static depends = [];

    /**
     * 附加模块
     */
    static addons = [];

    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 依赖模块
     */
    depends = {};

    /**
     * 附加模块
     */
    addons = {};

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定设置、API、UI、过滤列表,注册 UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui, data) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;

      this.data = data;

      this.init();
    }

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己或依赖的模块,则返回空
      const index = [this, ...this.depends].findIndex(
        (module) => modules.includes(module.name) === false
      );

      if (index >= 0) {
        return null;
      }

      // 创建实例
      const instance = new this(settings, api, ui, data);

      // 返回实例
      return instance;
    }

    /**
     * 判断指定附加模块是否启用
     * @param {typeof Module} module 模块
     */
    hasAddon(module) {
      return Object.hasOwn(this.addons, module.name);
    }

    /**
     * 初始化,创建基础视图和组件
     */
    init() {
      if (this.views.container) {
        this.destroy();
      }

      const { ui } = this;

      const container = ui.createElement("DIV", []);

      this.views = {
        container,
      };

      this.initComponents();
    }

    /**
     * 初始化组件
     */
    initComponents() {}

    /**
     * 销毁
     */
    destroy() {
      Object.values(this.views).forEach((view) => {
        if (view.parentNode) {
          view.parentNode.removeChild(view);
        }
      });

      this.views = {};
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      container.innerHTML = "";
      container.appendChild(this.views.container);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {}

    /**
     * 通知
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async notify(item, result) {}
  }

  /**
   * 过滤器
   */
  class Filter {
    /**
     * 设置
     */
    settings;

    /**
     * API
     */
    api;

    /**
     * UI
     */
    ui;

    /**
     * 过滤列表
     */
    data = [];

    /**
     * 模块列表
     */
    modules = {};

    /**
     * 初始化并绑定设置、API、UI
     * @param {Settings} settings 设置
     * @param {API}      api      API
     * @param {UI}       ui       UI
     */
    constructor(settings, api, ui) {
      this.settings = settings;
      this.api = api;
      this.ui = ui;
    }

    /**
     * 绑定两个模块的互相关系
     * @param {Module} moduleA 模块A
     * @param {Module} moduleB 模块B
     */
    bindModule(moduleA, moduleB) {
      const nameA = moduleA.constructor.name;
      const nameB = moduleB.constructor.name;

      // A 依赖 B
      if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.depends[nameB] = moduleB;
        moduleA.init();
      }

      // B 依赖 A
      if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.depends[nameA] = moduleA;
        moduleB.init();
      }

      // A 附加 B
      if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
        moduleA.addons[nameB] = moduleB;
        moduleA.init();
      }

      // B 附加 A
      if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
        moduleB.addons[nameA] = moduleA;
        moduleB.init();
      }
    }

    /**
     * 加载模块
     * @param {typeof Module} module 模块
     */
    initModule(module) {
      // 如果已经加载过则跳过
      if (Object.hasOwn(this.modules, module.name)) {
        return;
      }

      // 创建模块
      const instance = module.create(
        this.settings,
        this.api,
        this.ui,
        this.data
      );

      // 如果创建失败则跳过
      if (instance === null) {
        return;
      }

      // 绑定依赖模块和附加模块
      Object.values(this.modules).forEach((item) => {
        this.bindModule(item, instance);
      });

      // 合并模块
      this.modules[module.name] = instance;

      // 按照顺序重新整理模块
      this.modules = Tools.sortBy(
        Object.values(this.modules),
        (item) => item.constructor.order
      ).reduce(
        (result, item) => ({
          ...result,
          [item.constructor.name]: item,
        }),
        {}
      );
    }

    /**
     * 加载模块列表
     * @param {typeof Module[]} modules 模块列表
     */
    initModules(...modules) {
      // 根据依赖和附加模块决定初始化的顺序
      Tools.sortBy(
        modules,
        (item) => item.depends.length,
        (item) => item.addons.length
      ).forEach((module) => {
        this.initModule(module);
      });
    }

    /**
     * 添加到过滤列表
     * @param {*} item 绑定的 nFilter
     */
    pushData(item) {
      // 清除掉无效数据
      for (let i = 0; i < this.data.length; ) {
        if (document.body.contains(this.data[i].container) === false) {
          this.data.splice(i, 1);
          continue;
        }

        i += 1;
      }

      // 加入过滤列表
      if (this.data.includes(item) === false) {
        this.data.push(item);
      }
    }

    /**
     * 判断指定 UID 是否是自己
     * @param {Number} uid 用户 ID
     */
    isSelf(uid) {
      return unsafeWindow.__CURRENT_UID === uid;
    }

    /**
     * 获取过滤模式
     * @param {*} item 绑定的 nFilter
     */
    async getFilterMode(item) {
      // 获取链接参数
      const params = new URLSearchParams(location.search);

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

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

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

      // 跳过自己
      if (this.isSelf(item.uid)) {
        return;
      }

      // 声明结果
      const result = {
        mode: -1,
        reason: ``,
      };

      // 根据模块依次过滤
      for (const module of Object.values(this.modules)) {
        await module.filter(item, result);
      }

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

      // 通知各模块过滤结果
      for (const module of Object.values(this.modules)) {
        await module.notify(item, result);
      }

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

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

    /**
     * 过滤主题
     * @param {*} item 主题内容,见 commonui.topicArg.data
     */
    filterTopic(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 action = (() => {
          const anchor = item[2].parentNode;

          const element = this.ui.createElement("DIV", "", {
            style: Object.entries({
              position: "absolute",
              right: 0,
              bottom: 0,
              padding: "6px",
              "clip-path": "polygon(100% 0, 100% 100%, 0 100%)",
            })
              .map(([key, value]) => `${key}: ${value}`)
              .join(";"),
          });

          anchor.style.position = "relative";
          anchor.appendChild(element);

          return element;
        })();

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

        // 过滤函数
        const execute = async () => {
          // 获取过滤模式
          const filterMode = await this.getFilterMode(item.nFilter);

          // 样式处理
          (() => {
            // 还原样式
            // TODO 应该整体采用 className 来实现
            (() => {
              // 标记模式
              title.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");
            }
          })();
        };

        // 绑定事件
        item.nFilter = {
          tid,
          pid: 0,
          uid,
          username,
          container,
          title,
          author,
          subject,
          action,
          tags: null,
          execute,
        };

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤回复
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterReply(item) {
      // 绑定事件
      if (item.nFilter === undefined) {
        // 主题 ID
        const tid = item.tid;

        // 回复 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 this.getFilterMode(item.nFilter);

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

                this.ui.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;
              }
            })();

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

          // 过滤引用
          this.filterQuote(item);
        };

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

        // 添加至列表
        this.pushData(item.nFilter);
      }

      // 开始过滤
      item.nFilter.execute();
    }

    /**
     * 过滤引用
     * @param {*} item 回复内容,见 commonui.postArg.data
     */
    filterQuote(item) {
      // 未绑定事件,直接跳过
      if (item.nFilter === undefined) {
        return;
      }

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

      // 找到所有引用
      const quotes = content.querySelectorAll(".quote");

      // 处理引用
      [...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 parseInt(res[1], 10);
            }
          }

          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 {};
        })();

        // 临时的 nFilter
        const nFilter = {
          uid,
          tid,
          pid,
          subject: "",
          content: quote.innerText,
          action: null,
          tags: null,
        };

        // 获取过滤模式
        const filterMode = await this.getFilterMode(nFilter);

        (() => {
          if (filterMode === "标记") {
            this.ui.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;
          }
        })();

        // 绑定引用
        item.nFilter.quotes = item.nFilter.quotes || {};
        item.nFilter.quotes[uid] = nFilter.filterMode;
      });
    }
  }

  /**
   * 列表模块
   */
  class ListModule extends Module {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 模块标签
     */
    static label = "列表";

    /**
     * 顺序
     */
    static order = 10;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { tid, pid, filterMode, reason } = item;

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

      // 内容
      const content = (() => {
        if (pid) {
          return ui.createElement("A", item.content, {
            href: `/read.php?pid=${pid}&nofilter`,
          });
        }

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

        return ui.createElement("A", item.subject, {
          href: `/read.php?tid=${tid}&nofilter`,
          title: item.content,
          className: "b nobr",
        });
      })();

      return [content, filterMode, reason];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        const list = this.data.filter((item) => {
          return (item.filterMode || "显示") !== "显示";
        });

        Object.values(list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify() {
      // 获取过滤后的数量
      const count = this.data.filter((item) => {
        return (item.filterMode || "显示") !== "显示";
      }).length;

      // 更新菜单文字
      const { ui } = this;
      const { menu } = ui;

      if (menu === null) {
        return;
      }

      if (count) {
        menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
      } else {
        menu.innerHTML = `${ui.constructor.label}`;
      }

      // 重新渲染
      // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
      const { tab } = this.views;

      if (tab.querySelector("A").className === "nobr") {
        this.render(ui.views.content);
      }
    }
  }

  /**
   * 用户模块
   */
  class UserModule extends Module {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 模块标签
     */
    static label = "用户";

    /**
     * 顺序
     */
    static order = 20;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.users;
    }

    /**
     * 获取用户
     * @param {Number} uid 用户 ID
     */
    get(uid) {
      // 获取列表
      const list = this.list;

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

      return null;
    }

    /**
     * 添加用户
     * @param {Number} uid 用户 ID
     */
    add(uid, values) {
      // 获取列表
      const list = this.list;

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

      // 写入用户信息
      list[uid] = values;

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回添加的用户
      return values;
    }

    /**
     * 编辑用户
     * @param {Number} uid    用户 ID
     * @param {*}      values 用户信息
     */
    update(uid, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 更新用户
      Object.assign(entity, values);

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回编辑的用户
      return entity;
    }

    /**
     * 删除用户
     * @param   {Number}        uid 用户 ID
     * @returns {Object | null}     删除的用户
     */
    remove(uid) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, uid) === false) {
        return null;
      }

      // 获取用户
      const entity = list[uid];

      // 删除用户
      delete list[uid];

      // 保存数据
      this.settings.users = list;

      // 重新过滤
      this.reFilter(uid);

      // 返回删除的用户
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             uid  用户 ID
     * @param {String | undefined} name 用户名称
     */
    format(uid, name) {
      if (uid <= 0) {
        return null;
      }

      const { ui } = this;

      const user = this.get(uid);

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

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

      return ui.createElement("A", `[${username}]`, {
        className: "b nobr",
        href: `/nuke.php?func=ucp&uid=${uid}`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "昵称" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, name, filterMode } = item;

      // 昵称
      const user = this.format(id, name);

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [user, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);

      // 删除非激活中的用户
      {
        const list = ui.createElement("DIV", [], {
          style: "white-space: normal;",
        });

        const button = ui.createButton("删除非激活中的用户", () => {
          ui.confirm().then(() => {
            list.innerHTML = "";

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

            const waitingQueue = users.map(
              ({ id }) =>
                () =>
                  this.api.getUserInfo(id).then(({ bit }) => {
                    const activeInfo = commonui.activeInfo(0, 0, bit);
                    const activeType = activeInfo[1];

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

                    list.append(this.format(id));

                    this.remove(id);
                  })
            );

            const queueLength = waitingQueue.length;

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

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

                next().finally(execute);
                return;
              }

              button.disabled = false;
            };

            execute();
          });
        });

        const element = ui.createElement("DIV", [button, list]);

        add(this.constructor.order + 0, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

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

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags: [],
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement("DIV", [actions, tips], {
        style: "width: 80vw",
      });

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.get(item.uid);

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

      // 获取用户过滤模式
      const mode = this.settings.getModeByName(user.filterMode);

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

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

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, username, action } = item;

      // 如果没有 action 组件则跳过
      if (action === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        action.style.display = "none";
        return;
      }

      // 获取当前用户
      const user = this.get(uid);

      // 修改操作按钮文字
      if (action.tagName === "A") {
        action.innerText = "屏蔽";
      } else {
        action.title = "屏蔽";
      }

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

      // 绑定事件
      action.onclick = () => {
        this.renderDetails(uid, username);
      };
    }

    /**
     * 重新过滤
     * @param {Number} uid 用户 ID
     */
    reFilter(uid) {
      this.data.forEach((item) => {
        // 如果用户 ID 一致,则重新过滤
        if (item.uid === uid) {
          item.execute();
          return;
        }

        // 如果有引用,也重新过滤
        if (Object.hasOwn(item.quotes || {}, uid)) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 标记模块
   */
  class TagModule extends Module {
    /**
     * 模块名称
     */
    static name = "tag";

    /**
     * 模块标签
     */
    static label = "标记";

    /**
     * 顺序
     */
    static order = 30;

    /**
     * 依赖模块
     */
    static depends = [UserModule];

    /**
     * 依赖的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.depends[UserModule.name];
    }

    /**
     * 获取列表
     */
    get list() {
      return this.settings.tags;
    }

    /**
     * 获取标记
     * @param {Number} id   标记 ID
     * @param {String} name 标记名称
     */
    get({ id, name }) {
      // 获取列表
      const list = this.list;

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

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

        if (tag) {
          return tag;
        }
      }

      return null;
    }

    /**
     * 添加标记
     * @param {String} name 标记名称
     */
    add(name) {
      // 获取对应的标记
      const tag = this.get({ name });

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

      // 获取列表
      const list = this.list;

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

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

      // 写入标记信息
      list[id] = {
        id,
        name,
        color,
        filterMode: this.settings.filterModes[0],
      };

      // 保存数据
      this.settings.tags = list;

      // 返回添加的标记
      return list[id];
    }

    /**
     * 编辑标记
     * @param {Number} id     标记 ID
     * @param {*}      values 标记信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 更新标记
      Object.assign(entity, values);

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);
    }

    /**
     * 删除标记
     * @param {Number} id 标记 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取标记
      const entity = list[id];

      // 获取相关的用户
      const users = Object.values(this.userModule.list).filter((user) =>
        user.tags.includes(id)
      );

      // 删除标记
      delete list[id];

      // 删除相关的用户标记
      users.forEach((user) => {
        const index = user.tags.findIndex((item) => item === id);

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

      // 保存数据
      this.settings.tags = list;

      // 重新过滤
      this.reFilter(users);

      // 返回删除的标记
      return entity;
    }

    /**
     * 格式化
     * @param {Number}             id   标记 ID
     * @param {String | undefined} name 标记名称
     * @param {String | undefined} name 标记颜色
     */
    format(id, name, color) {
      const { ui } = this;

      if (id >= 0) {
        const tag = this.get({ id });

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

      if (name && color) {
        return ui.createElement("B", name, {
          className: "block_txt nobr",
          style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
        });
      }

      return "";
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "标记", width: 1 },
        { label: "列表" },
        { label: "过滤模式", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, filterMode } = item;

      // 标记
      const tag = this.format(id);

      // 用户列表
      const list = Object.values(this.userModule.list)
        .filter(({ tags }) => tags.includes(id))
        .map(({ id }) => this.userModule.format(id));

      const group = ui.createElement("DIV", list, {
        style: "white-space: normal; display: none;",
      });

      const switchButton = ui.createButton(list.length.toString(), () => {
        if (group.style.display === "none") {
          group.style.removeProperty("display");
        } else {
          group.style.display = "none";
        }
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          this.update(id, {
            filterMode: newMode,
          });

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(remove);
      })();

      return [tag, [switchButton, group], switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content, settings } = ui.views;
      const { add } = settings;

      const table = ui.createTable(this.columns());

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);

      // 删除没有标记的用户
      {
        const button = ui.createButton("删除没有标记的用户", () => {
          ui.confirm().then(() => {
            const users = Object.values(this.userModule.list);

            users.forEach(({ id, tags }) => {
              if (tags.length > 0) {
                return;
              }

              this.userModule.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 0, element);
      }

      // 删除没有用户的标记
      {
        const button = ui.createButton("删除没有用户的标记", () => {
          ui.confirm().then(() => {
            const items = Object.values(this.list);
            const users = Object.values(this.userModule.list);

            items.forEach(({ id }) => {
              if (users.find(({ tags }) => tags.includes(id))) {
                return;
              }

              this.remove(id);
            });
          });
        });

        const element = ui.createElement("DIV", button);

        add(this.constructor.order + 1, element);
      }
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取用户信息
      const user = this.userModule.get(item.uid);

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

      // 获取用户标记
      const tags = user.tags;

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let tag = null;

      for (const id of tags) {
        const entity = this.get({ id });

        if (entity === null) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(entity.filterMode);

        if (mode <= max) {
          continue;
        }

        max = mode;
        tag = entity;
      }

      // 没有匹配的则跳过
      if (tag === null) {
        return;
      }

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

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标记
      [...tags.querySelectorAll("[tid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 获取当前用户
      const user = this.userModule.get(uid);

      // 如果没有用户,则跳过
      if (user === null) {
        return;
      }

      // 格式化标记
      const items = user.tags.map((id) => {
        const item = this.format(id);

        if (item) {
          item.setAttribute("tid", id);
        }

        return item;
      });

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Array} users 用户集合
     */
    reFilter(users) {
      users.forEach((user) => {
        this.userModule.reFilter(user.id);
      });
    }
  }

  /**
   * 关键字模块
   */
  class KeywordModule extends Module {
    /**
     * 模块名称
     */
    static name = "keyword";

    /**
     * 模块标签
     */
    static label = "关键字";

    /**
     * 顺序
     */
    static order = 40;

    /**
     * 获取列表
     */
    get list() {
      return this.settings.keywords;
    }

    /**
     * 获取关键字
     * @param {Number} id 关键字 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

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

      return null;
    }

    /**
     * 添加关键字
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     * @param {Number} filterLevel 过滤等级: 0 - 仅过滤标题; 1 - 过滤标题和内容
     */
    add(keyword, filterMode, filterLevel) {
      // 获取列表
      const list = this.list;

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

      // 写入关键字信息
      list[id] = {
        id,
        keyword,
        filterMode,
        filterLevel,
      };

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的关键字
      return list[id];
    }

    /**
     * 编辑关键字
     * @param {Number} id     关键字 ID
     * @param {*}      values 关键字信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 更新关键字
      Object.assign(entity, values);

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除关键字
     * @param {Number} id 关键字 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取关键字
      const entity = list[id];

      // 删除关键字
      delete list[id];

      // 保存数据
      this.settings.keywords = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的关键字
      return entity;
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "包括内容", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterLevel, filterMode } = item;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 包括内容
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterLevel > 0,
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
            filterLevel: switchLevel.checked ? 1 : 0,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [inputWrapper, switchMode, switchLevel, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 包括内容
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(
            input.value,
            switchMode.innerText,
            switchLevel.checked ? 1 : 0
          );

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(inputWrapper, switchMode, switchLevel, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

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

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

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

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

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

          const match = subject.match(keyword);

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

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

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

          const { content } = item;

          const match = content.match(keyword);

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

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

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 属地模块
   */
  class LocationModule extends Module {
    /**
     * 模块名称
     */
    static name = "location";

    /**
     * 模块标签
     */
    static label = "属地";

    /**
     * 顺序
     */
    static order = 50;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 获取列表
     */
    get list() {
      return this.settings.locations;
    }

    /**
     * 获取属地
     * @param {Number} id 属地 ID
     */
    get(id) {
      // 获取列表
      const list = this.list;

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

      return null;
    }

    /**
     * 添加属地
     * @param {String} keyword     关键字
     * @param {String} filterMode  过滤模式
     */
    add(keyword, filterMode) {
      // 获取列表
      const list = this.list;

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

      // 写入属地信息
      list[id] = {
        id,
        keyword,
        filterMode,
      };

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回添加的属地
      return list[id];
    }

    /**
     * 编辑属地
     * @param {Number} id     属地 ID
     * @param {*}      values 属地信息
     */
    update(id, values) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 更新属地
      Object.assign(entity, values);

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();
    }

    /**
     * 删除属地
     * @param {Number} id 属地 ID
     */
    remove(id) {
      // 获取列表
      const list = this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, id) === false) {
        return null;
      }

      // 获取属地
      const entity = list[id];

      // 删除属地
      delete list[id];

      // 保存数据
      this.settings.locations = list;

      // 重新过滤
      this.reFilter();

      // 返回删除的属地
      return entity;
    }

    /**
     * 获取 IP 属地
     * @param {*} item 绑定的 nFilter
     */
    async getIpLocation(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache, uid)) {
        return this.cache[uid];
      }

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

      // 写入缓存
      if (ipLoc) {
        this.cache[uid] = ipLoc;
      }

      // 返回结果
      return ipLoc;
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "关键字" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { id, keyword, filterMode } = item;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
        value: keyword,
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(id, {
            keyword: input.value,
            filterMode: switchMode.innerText,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(() => {
            this.remove(id);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [inputWrapper, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.keyword, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.values(this.list).forEach((item) => {
          const column = this.column(item);

          add(...column);
        });

        this.renderNewLine();
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 关键字
      const input = ui.createElement("INPUT", [], {
        type: "text",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        className: "filter-input-wrapper",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", (e) => {
          const entity = this.add(input.value, switchMode.innerText);

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(inputWrapper, switchMode, buttons);
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取列表
      const list = this.list;

      // 跳过低于当前的过滤模式
      const filtered = Object.values(list).filter(
        (item) => this.settings.getModeByName(item.filterMode) > result.mode
      );

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

      // 获取当前属地
      const location = await this.getIpLocation(item);

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

      // 根据过滤模式依次判断
      const sorted = Tools.sortBy(filtered, (item) =>
        this.settings.getModeByName(item.filterMode)
      );

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

        const match = location.match(keyword);

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

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

    /**
     * 重新过滤
     */
    reFilter() {
      // 实际上应该根据过滤模式来筛选要过滤的部分
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 猎巫模块
   *
   * 其实是通过 Cache 模块读取配置,而非 Settings
   */
  class HunterModule extends Module {
    /**
     * 模块名称
     */
    static name = "hunter";

    /**
     * 模块标签
     */
    static label = "猎巫";

    /**
     * 顺序
     */
    static order = 60;

    /**
     * 请求缓存
     */
    cache = {};

    /**
     * 请求队列
     */
    queue = [];

    /**
     * 获取列表
     */
    get list() {
      return this.settings.cache
        .get("WITCH_HUNT")
        .then((values) => values || []);
    }

    /**
     * 获取猎巫
     * @param {Number} id 猎巫 ID
     */
    async get(id) {
      // 获取列表
      const list = await this.list;

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

      return null;
    }

    /**
     * 添加猎巫
     * @param {Number} fid         版面 ID
     * @param {String} label       标签
     * @param {String} filterMode  过滤模式
     * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
     */
    async add(fid, label, filterMode, filterLevel) {
      // FID 只能是数字
      fid = parseInt(fid, 10);

      // 获取列表
      const list = await this.list;

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

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

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

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

      // 写入猎巫信息
      list[fid] = {
        fid,
        name: info.name,
        label,
        color,
        filterMode,
        filterLevel,
      };

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回添加的猎巫
      return list[fid];
    }

    /**
     * 编辑猎巫
     * @param {Number} fid    版面 ID
     * @param {*}      values 猎巫信息
     */
    async update(fid, values) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 更新猎巫
      Object.assign(entity, values);

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤,更新样式即可
      this.reFilter(false);
    }

    /**
     * 删除猎巫
     * @param {Number} fid 版面 ID
     */
    async remove(fid) {
      // 获取列表
      const list = await this.list;

      // 如果不存在则跳过
      if (Object.hasOwn(list, fid) === false) {
        return null;
      }

      // 获取猎巫
      const entity = list[fid];

      // 删除猎巫
      delete list[fid];

      // 保存数据
      this.settings.cache.put("WITCH_HUNT", list);

      // 重新过滤
      this.reFilter(true);

      // 返回删除的属地
      return entity;
    }

    /**
     * 格式化版面
     * @param {Number} fid  版面 ID
     * @param {String} name 版面名称
     */
    formatForum(fid, name) {
      const { ui } = this;

      return ui.createElement("A", `[${name}]`, {
        className: "b nobr",
        href: `/thread.php?fid=${fid}`,
      });
    }

    /**
     * 格式化标签
     * @param {String} name 标签名称
     * @param {String} name 标签颜色
     */
    formatLabel(name, color) {
      const { ui } = this;

      return ui.createElement("B", name, {
        className: "block_txt nobr",
        style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
      });
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "版面", width: 200 },
        { label: "标签" },
        { label: "启用过滤", center: true, width: 1 },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 标记信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const { ui } = this;
      const { table } = this.views;
      const { fid, name, label, color, filterMode, filterLevel } = item;

      // 版面
      const forum = this.formatForum(fid, name);

      // 标签
      const labelElement = this.formatLabel(label, color);

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: filterLevel > 0,
      });

      // 切换过滤模式
      const switchMode = ui.createButton(
        filterMode || this.settings.filterModes[0],
        () => {
          const newMode = this.settings.switchModeByName(switchMode.innerText);

          switchMode.innerText = newMode;
        }
      );

      // 操作
      const buttons = (() => {
        const save = ui.createButton("保存", () => {
          this.update(fid, {
            filterMode: switchMode.innerText,
            filterLevel: switchLevel.checked ? 1 : 0,
          });
        });

        const remove = ui.createButton("删除", (e) => {
          ui.confirm().then(async () => {
            await this.remove(fid);

            table.remove(e);
          });
        });

        return ui.createButtonGroup(save, remove);
      })();

      return [forum, labelElement, switchLevel, switchMode, buttons];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { ui } = this;
      const { tabs, content } = ui.views;

      const table = ui.createTable(this.columns());

      const tips = ui.createElement("DIV", TIPS.hunter, {
        className: "silver",
      });

      const tab = ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
      this.views.container.appendChild(tips);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        this.list.then((values) => {
          Object.values(values).forEach((item) => {
            const column = this.column(item);

            add(...column);
          });

          this.renderNewLine();
        });
      }
    }

    /**
     * 渲染新行
     */
    renderNewLine() {
      const { ui } = this;
      const { table } = this.views;

      // 版面 ID
      const forumInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const forumInputWrapper = ui.createElement("DIV", forumInput, {
        className: "filter-input-wrapper",
      });

      // 标签
      const labelInput = ui.createElement("INPUT", [], {
        type: "text",
      });

      const labelInputWrapper = ui.createElement("DIV", labelInput, {
        className: "filter-input-wrapper",
      });

      // 启用过滤
      const switchLevel = ui.createElement("INPUT", [], {
        type: "checkbox",
      });

      // 切换过滤模式
      const switchMode = ui.createButton(this.settings.filterModes[0], () => {
        const newMode = this.settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      // 操作
      const buttons = (() => {
        const save = ui.createButton("添加", async (e) => {
          const entity = await this.add(
            forumInput.value,
            labelInput.value,
            switchMode.innerText,
            switchLevel.checked ? 1 : 0
          );

          table.update(e, ...this.column(entity));

          this.renderNewLine();
        });

        return ui.createButtonGroup(save);
      })();

      // 添加至列表
      table.add(
        forumInputWrapper,
        labelInputWrapper,
        switchLevel,
        switchMode,
        buttons
      );
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取当前猎巫结果
      const hunter = item.hunter || [];

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 获取列表
      const items = await this.list;

      // 筛选出匹配的猎巫
      const list = Object.values(items).filter(({ fid }) =>
        hunter.includes(fid)
      );

      // 取最高的过滤模式
      // 低于当前的过滤模式则跳过
      let max = result.mode;
      let res = null;

      for (const entity of list) {
        const { filterLevel, filterMode } = entity;

        // 仅标记
        if (filterLevel === 0) {
          continue;
        }

        // 获取过滤模式
        const mode = this.settings.getModeByName(filterMode);

        if (mode <= max) {
          continue;
        }

        max = mode;
        res = entity;
      }

      // 没有匹配的则跳过
      if (res === null) {
        return;
      }

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

    /**
     * 通知
     * @param {*} item 绑定的 nFilter
     */
    async notify(item) {
      const { uid, tags } = item;

      // 如果没有 tags 组件则跳过
      if (tags === null) {
        return;
      }

      // 如果是匿名,隐藏组件
      if (uid <= 0) {
        tags.style.display = "none";
        return;
      }

      // 删除旧标签
      [...tags.querySelectorAll("[fid]")].forEach((item) => {
        tags.removeChild(item);
      });

      // 如果没有请求,开始请求
      if (Object.hasOwn(item, "hunter") === false) {
        this.execute(item);
        return;
      }

      // 获取当前猎巫结果
      const hunter = item.hunter;

      // 如果没有猎巫结果,则跳过
      if (hunter.length === 0) {
        return;
      }

      // 格式化标签
      const items = await Promise.all(
        hunter.map(async (fid) => {
          const item = await this.get(fid);

          if (item) {
            const element = this.formatLabel(item.label, item.color);

            element.setAttribute("fid", fid);

            return element;
          }

          return null;
        })
      );

      // 加入组件
      items.forEach((item) => {
        if (item) {
          tags.appendChild(item);
        }
      });
    }

    /**
     * 重新过滤
     * @param {Boolean} clear 是否清除缓存
     */
    reFilter(clear) {
      // 清除缓存
      if (clear) {
        this.cache = {};
      }

      // 重新过滤
      this.data.forEach((item) => {
        // 不需要清除缓存的话,只要重新加载标记
        if (clear === false) {
          item.hunter = [];
        }

        // 重新猎巫
        this.execute(item);
      });
    }

    /**
     * 猎巫
     * @param {*} item 绑定的 nFilter
     */
    async execute(item) {
      const { uid } = item;
      const { api, cache, queue, list } = this;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

      // 初始化猎巫结果,用于标识正在猎巫
      item.hunter = item.hunter || [];

      // 获取列表
      const items = await list;

      // 没有设置且没有旧数据,直接跳过
      if (items.length === 0 && item.hunter.length === 0) {
        return;
      }

      // 重新过滤
      const reload = (newValue) => {
        const isEqual = newValue.sort().join() === item.hunter.sort().join();

        if (isEqual) {
          return;
        }

        item.hunter = newValue;
        item.execute();
      };

      // 创建任务
      const task = async () => {
        // 如果缓存里没有记录,请求数据并写入缓存
        if (Object.hasOwn(cache, uid) === false) {
          cache[uid] = [];

          await Promise.all(
            Object.keys(items).map(async (fid) => {
              // 转换为数字格式
              const id = parseInt(fid, 10);

              // 当前版面发言记录
              const result = await api.getForumPosted(id, uid);

              // 写入当前设置
              if (result) {
                cache[uid].push(id);
              }
            })
          );
        }

        // 重新过滤
        reload(cache[uid]);

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

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

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

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

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

  /**
   * 杂项模块
   */
  class MiscModule extends Module {
    /**
     * 模块名称
     */
    static name = "misc";

    /**
     * 模块标签
     */
    static label = "杂项";

    /**
     * 顺序
     */
    static order = 100;

    /**
     * 请求缓存
     */
    cache = {
      topicNums: {},
    };

    /**
     * 获取用户信息(从页面上)
     * @param {*} item 绑定的 nFilter
     */
    getUserInfo(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

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

        // 绑定用户信息和声望
        if (userInfo) {
          item.userInfo = userInfo;
          item.username = userInfo.username;

          item.reputation = (() => {
            const reputations = commonui.userInfo.reputations;

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

            return NaN;
          })();
        }
      }
    }

    /**
     * 获取帖子数据
     * @param {*} item 绑定的 nFilter
     */
    async getPostInfo(item) {
      const { tid, pid } = item;

      // 请求帖子数据
      const { subject, content, userInfo, reputation } =
        await this.api.getPostInfo(tid, pid);

      // 绑定用户信息和声望
      if (userInfo) {
        item.userInfo = userInfo;
        item.username = userInfo.username;
        item.reputation = reputation;
      }

      // 绑定标题和内容
      item.subject = subject;
      item.content = content;
    }

    /**
     * 获取主题数量
     * @param {*} item 绑定的 nFilter
     */
    async getTopicNum(item) {
      const { uid } = item;

      // 如果是匿名直接跳过
      if (uid <= 0) {
        return;
      }

      // 如果已有缓存,直接返回
      if (Object.hasOwn(this.cache.topicNums, uid)) {
        return this.cache.topicNums[uid];
      }

      // 请求数量
      const number = await this.api.getTopicNum(uid);

      // 写入缓存
      this.cache.topicNums[uid] = number;

      // 返回结果
      return number;
    }

    /**
     * 初始化,增加设置
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { add } = ui.views.settings;

      // 小号过滤(注册时间)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterRegdateLimit / 86400000,
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 0;

          if (newValue < 0) {
            return;
          }

          settings.filterRegdateLimit = newValue * 86400000;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏注册时间小于",
          input,
          "天的用户",
          button,
        ]);

        add(this.constructor.order + 0, element);
      }

      // 小号过滤(发帖数)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterPostnumLimit,
          maxLength: 5,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 0;

          if (newValue < 0) {
            return;
          }

          settings.filterPostnumLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖数量小于",
          input,
          "贴的用户",
          button,
        ]);

        add(this.constructor.order + 1, element);
      }

      // 流量号过滤(主题比例)
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterTopicRateLimit,
          maxLength: 3,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10) || 100;

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

          settings.filterTopicRateLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏发帖比例大于",
          input,
          "%的用户",
          button,
        ]);

        add(this.constructor.order + 2, element);
      }

      // 声望过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "text",
          value: settings.filterReputationLimit || "",
          maxLength: 4,
          style: "width: 48px;",
        });

        const button = ui.createButton("确认", () => {
          const newValue = parseInt(input.value, 10);

          settings.filterReputationLimit = newValue;

          this.reFilter();
        });

        const element = ui.createElement("DIV", [
          "隐藏版面声望低于",
          input,
          "点的用户",
          button,
        ]);

        add(this.constructor.order + 3, element);
      }

      // 匿名过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
          checked: settings.filterAnonymous,
        });

        const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
          style: "display: flex;",
        });

        const element = ui.createElement("DIV", label);

        input.onchange = () => {
          settings.filterAnonymous = input.checked;

          this.reFilter();
        };

        add(this.constructor.order + 4, element);
      }
    }

    /**
     * 过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filter(item, result) {
      // 获取隐藏模式下标
      const mode = this.settings.getModeByName("隐藏");

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 匿名过滤
      await this.filterByAnonymous(item, result);

      // 注册时间过滤
      await this.filterByRegdate(item, result);

      // 发帖数量过滤
      await this.filterByPostnum(item, result);

      // 发帖比例过滤
      await this.filterByTopicRate(item, result);

      // 版面声望过滤
      await this.filterByReputation(item, result);
    }

    /**
     * 根据匿名过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByAnonymous(item, result) {
      const { uid } = item;

      // 如果不是匿名,则跳过
      if (uid > 0) {
        return;
      }

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

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取过滤匿名设置
      const filterAnonymous = this.settings.filterAnonymous;

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

    /**
     * 根据注册时间过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByRegdate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

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

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取注册时间限制
      const filterRegdateLimit = this.settings.filterRegdateLimit;

      // 未启用则跳过
      if (filterRegdateLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取注册时间
      const { regdate } = item.userInfo || {};

      // 获取失败则跳过
      if (regdate === undefined) {
        return;
      }

      // 转换时间格式,泥潭接口只精确到秒
      const date = new Date(regdate * 1000);

      // 判断是否符合条件
      if (Date.now() - date > filterRegdateLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `注册时间: ${date.toLocaleDateString()}`;
    }

    /**
     * 根据发帖数量过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByPostnum(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

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

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖数量限制
      const filterPostnumLimit = this.settings.filterPostnumLimit;

      // 未启用则跳过
      if (filterPostnumLimit <= 0) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 判断是否符合条件
      if (postnum >= filterPostnumLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `发帖数量: ${postnum}`;
    }

    /**
     * 根据发帖比例过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByTopicRate(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

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

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取发帖比例限制
      const filterTopicRateLimit = this.settings.filterTopicRateLimit;

      // 未启用则跳过
      if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
        return;
      }

      // 没有用户信息,优先从页面上获取
      if (item.userInfo === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.userInfo === undefined) {
        await this.getPostInfo(item);
      }

      // 获取发帖数量
      const { postnum } = item.userInfo || {};

      // 获取失败则跳过
      if (postnum === undefined) {
        return;
      }

      // 获取主题数量
      const topicNum = await this.getTopicNum(item);

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

      // 判断是否符合条件
      if (topicRate < filterTopicRateLimit) {
        return;
      }

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

    /**
     * 根据版面声望过滤
     * @param {*} item    绑定的 nFilter
     * @param {*} result  过滤结果
     */
    async filterByReputation(item, result) {
      const { uid } = item;

      // 如果是匿名,则跳过
      if (uid <= 0) {
        return;
      }

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

      // 如果当前模式不低于隐藏模式,则跳过
      if (result.mode >= mode) {
        return;
      }

      // 获取版面声望限制
      const filterReputationLimit = this.settings.filterReputationLimit;

      // 未启用则跳过
      if (Number.isNaN(filterReputationLimit)) {
        return;
      }

      // 没有声望信息,优先从页面上获取
      if (item.reputation === undefined) {
        this.getUserInfo(item);
      }

      // 没有再从接口获取
      if (item.reputation === undefined) {
        await this.getPostInfo(item);
      }

      // 获取版面声望
      const reputation = item.reputation || 0;

      // 判断是否符合条件
      if (reputation >= filterReputationLimit) {
        return;
      }

      // 更新过滤模式和原因
      result.mode = mode;
      result.reason = `版面声望: ${reputation}`;
    }

    /**
     * 重新过滤
     */
    reFilter() {
      this.data.forEach((item) => {
        item.execute();
      });
    }
  }

  /**
   * 设置模块
   */
  class SettingsModule extends Module {
    /**
     * 模块名称
     */
    static name = "settings";

    /**
     * 顺序
     */
    static order = 0;

    /**
     * 创建实例
     * @param   {Settings}      settings 设置
     * @param   {API}           api      API
     * @param   {UI}            ui       UI
     * @param   {Array}         data     过滤列表
     * @returns {Module | null}          成功后返回模块实例
     */
    static create(settings, api, ui, data) {
      // 读取设置里的模块列表
      const modules = settings.modules;

      // 如果不包含自己,加入列表中,因为设置模块是必须的
      if (modules.includes(this.name) === false) {
        settings.modules = [...modules, this.name];
      }

      // 创建实例
      return super.create(settings, api, ui, data);
    }

    /**
     * 初始化,增加设置
     */
    initComponents() {
      super.initComponents();

      const { settings, ui } = this;
      const { add } = ui.views.settings;

      // 前置过滤
      {
        const input = ui.createElement("INPUT", [], {
          type: "checkbox",
        });

        const label = ui.createElement("LABEL", ["前置过滤", input], {
          style: "display: flex;",
        });

        settings.preFilterEnabled.then((checked) => {
          input.checked = checked;
          input.onchange = () => {
            settings.preFilterEnabled = !checked;
          };
        });

        add(this.constructor.order + 0, label);
      }

      // 模块选择
      {
        const modules = [
          ListModule,
          UserModule,
          TagModule,
          KeywordModule,
          LocationModule,
          HunterModule,
          MiscModule,
        ];

        const items = modules.map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "checkbox",
            value: item.name,
            checked: settings.modules.includes(item.name),
            onchange: () => {
              const checked = input.checked;

              modules.map((m, index) => {
                const isDepend = checked
                  ? item.depends.find((i) => i.name === m.name)
                  : m.depends.find((i) => i.name === item.name);

                if (isDepend) {
                  const element = items[index].querySelector("INPUT");

                  if (element) {
                    element.checked = checked;
                  }
                }
              });
            },
          });

          const label = ui.createElement("LABEL", [item.label, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const button = ui.createButton("确认", () => {
          const checked = group.querySelectorAll("INPUT:checked");
          const values = [...checked].map((item) => item.value);

          settings.modules = values;

          location.reload();
        });

        const group = ui.createElement("DIV", [...items, button], {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "启用模块");

        add(this.constructor.order + 1, label, group);
      }

      // 默认过滤模式
      {
        const modes = ["标记", "遮罩", "隐藏"].map((item) => {
          const input = ui.createElement("INPUT", [], {
            type: "radio",
            name: "defaultFilterMode",
            value: item,
            checked: settings.defaultFilterMode === item,
            onchange: () => {
              settings.defaultFilterMode = item;

              this.reFilter();
            },
          });

          const label = ui.createElement("LABEL", [item, input], {
            style: "display: flex; margin-right: 10px;",
          });

          return label;
        });

        const group = ui.createElement("DIV", modes, {
          style: "display: flex;",
        });

        const label = ui.createElement("LABEL", "默认过滤模式");

        const tips = ui.createElement("DIV", TIPS.filterMode, {
          className: "silver",
        });

        add(this.constructor.order + 2, label, group, tips);
      }
    }

    /**
     * 重新过滤
     */
    reFilter() {
      // 目前仅在修改默认过滤模式时重新过滤
      this.data.forEach((item) => {
        // 如果过滤模式是继承,则重新过滤
        if (item.filterMode === "继承") {
          item.execute();
        }

        // 如果有引用,也重新过滤
        if (Object.values(item.quotes || {}).includes("继承")) {
          item.execute();
          return;
        }
      });
    }
  }

  /**
   * 增强的列表模块,增加了用户作为附加模块
   */
  class ListEnhancedModule extends ListModule {
    /**
     * 模块名称
     */
    static name = "list";

    /**
     * 附加模块
     */
    static addons = [UserModule];

    /**
     * 附加的用户模块
     * @returns {UserModule} 用户模块
     */
    get userModule() {
      return this.addons[UserModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "用户", width: 1 },
        { label: "内容", ellipsis: true },
        { label: "过滤模式", center: true, width: 1 },
        { label: "原因", width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 绑定的 nFilter
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(UserModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { uid, username } = item;

      const user = this.userModule.format(uid, username);

      const buttons = (() => {
        if (uid <= 0) {
          return null;
        }

        const block = ui.createButton("屏蔽", (e) => {
          this.userModule.renderDetails(uid, username, (type) => {
            // 删除失效数据,等待重新过滤
            table.remove(e);

            // 如果是新增,不会因为用户重新过滤,需要主动触发
            if (type === "ADD") {
              this.userModule.reFilter(uid);
            }
          });
        });

        return ui.createButtonGroup(block);
      })();

      return [user, ...column, buttons];
    }
  }

  /**
   * 增强的用户模块,增加了标记作为附加模块
   */
  class UserEnhancedModule extends UserModule {
    /**
     * 模块名称
     */
    static name = "user";

    /**
     * 附加模块
     */
    static addons = [TagModule];

    /**
     * 附加的标记模块
     * @returns {TagModule} 标记模块
     */
    get tagModule() {
      return this.addons[TagModule.name];
    }

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.columns();
      }

      return [
        { label: "昵称", width: 1 },
        { label: "标记" },
        { label: "过滤模式", center: true, width: 1 },
        { label: "操作", width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {*}     item 用户信息
     * @returns {Array}      表格项集合
     */
    column(item) {
      const column = super.column(item);

      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return column;
      }

      const { ui } = this;
      const { table } = this.views;
      const { id, name } = item;

      const tags = ui.createElement(
        "DIV",
        item.tags.map((id) => this.tagModule.format(id))
      );

      const newColumn = [...column];

      newColumn.splice(1, 0, tags);

      const buttons = column[column.length - 1];

      const update = ui.createButton("编辑", (e) => {
        this.renderDetails(id, name, (type, newValue) => {
          if (type === "UPDATE") {
            table.update(e, ...this.column(newValue));
          }

          if (type === "REMOVE") {
            table.remove(e);
          }
        });
      });

      buttons.insertBefore(update, buttons.firstChild);

      return newColumn;
    }

    /**
     * 渲染详情
     * @param {Number}             uid      用户 ID
     * @param {String | undefined} name     用户名称
     * @param {Function}           callback 回调函数
     */
    renderDetails(uid, name, callback = () => {}) {
      const hasAddon = this.hasAddon(TagModule);

      if (hasAddon === false) {
        return super.renderDetails(uid, name, callback);
      }

      const { ui, settings } = this;

      // 只允许同时存在一个详情页
      if (this.views.details) {
        if (this.views.details.parentNode) {
          this.views.details.parentNode.removeChild(this.views.details);
        }
      }

      // 获取用户信息
      const user = this.get(uid);

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

      // TODO 需要优化

      const title =
        (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;

      const table = ui.createTable([]);

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

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

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

        table.querySelector("TBODY").innerHTML = rows.join("");
      }

      const input = ui.createElement("INPUT", [], {
        type: "text",
        placeholder: TIPS.addTags,
        style: "width: -webkit-fill-available;",
      });

      const inputWrapper = ui.createElement("DIV", input, {
        style: "margin-top: 10px;",
      });

      const filterMode = user ? user.filterMode : settings.filterModes[0];

      const switchMode = ui.createButton(filterMode, () => {
        const newMode = settings.switchModeByName(switchMode.innerText);

        switchMode.innerText = newMode;
      });

      const buttons = ui.createElement(
        "DIV",
        (() => {
          const remove = user
            ? ui.createButton("删除", () => {
                ui.confirm().then(() => {
                  this.remove(uid);

                  this.views.details._.hide();

                  callback("REMOVE");
                });
              })
            : null;

          const save = ui.createButton("保存", () => {
            const checked = [...table.querySelectorAll("INPUT:checked")].map(
              (input) => parseInt(input.value, 10)
            );

            const newTags = input.value
              .split("|")
              .filter((item) => item.length)
              .map((item) => this.tagModule.add(item))
              .filter((tag) => tag !== null)
              .map((tag) => tag.id);

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

            if (user === null) {
              const entity = this.add(uid, {
                id: uid,
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("ADD", entity);
            } else {
              const entity = this.update(uid, {
                name,
                tags,
                filterMode: switchMode.innerText,
              });

              this.views.details._.hide();

              callback("UPDATE", entity);
            }
          });

          return ui.createButtonGroup(remove, save);
        })(),
        {
          className: "right_",
        }
      );

      const actions = ui.createElement(
        "DIV",
        [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
        {
          style: "margin-top: 10px;",
        }
      );

      const tips = ui.createElement("DIV", TIPS.filterMode, {
        className: "silver",
        style: "margin-top: 10px;",
      });

      const content = ui.createElement(
        "DIV",
        [table, inputWrapper, actions, tips],
        {
          style: "width: 80vw",
        }
      );

      // 创建弹出框
      this.views.details = ui.createDialog(null, title, content);
    }
  }

  /**
   * 处理 topicArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.topicArg
   */
  const handleTopicModule = async (filter, value) => {
    // 绑定主题模块
    topicModule = value;

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

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

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

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

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 主题 ID
      const tid = args[8];

      // 回复 ID
      const pid = args[9];

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

      // 开始过滤
      if (data) {
        filter.filterTopic(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(topicModule.data).forEach(filter.filterTopic);

    // 拦截 add 函数,这是泥潭的主题添加事件
    Tools.interceptProperty(topicModule, "add", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 postArg 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui.postArg
   */
  const handleReplyModule = async (filter, value) => {
    // 绑定回复模块
    replyModule = value;

    // 是否启用前置过滤
    const preFilterEnabled = await filter.settings.preFilterEnabled;

    // 前置过滤
    // 先直接隐藏,等过滤完毕后再放出来
    const beforeGet = (...args) => {
      if (preFilterEnabled) {
        // 楼层号
        const index = args[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";
      }

      return args;
    };

    // 过滤
    const afterGet = (_, args) => {
      // 楼层号
      const index = args[0];

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

      // 开始过滤
      if (data) {
        filter.filterReply(data);
      }
    };

    // 如果已经有数据,则直接过滤
    Object.values(replyModule.data).forEach(filter.filterReply);

    // 拦截 proc 函数,这是泥潭的回复添加事件
    Tools.interceptProperty(replyModule, "proc", {
      beforeGet,
      afterGet,
    });
  };

  /**
   * 处理 commonui 模块
   * @param {Filter} filter 过滤器
   * @param {*}      value  commonui
   */
  const handleCommonui = (filter, value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 mainMenu 模块,UI 需要在 init 后加载
    Tools.interceptProperty(commonui, "mainMenu", {
      afterSet: (value) => {
        Tools.interceptProperty(value, "init", {
          afterGet: () => {
            filter.ui.render();
          },
          afterSet: () => {
            filter.ui.render();
          },
        });
      },
    });

    // 拦截 topicArg 模块,这是泥潭的主题入口
    Tools.interceptProperty(commonui, "topicArg", {
      afterSet: (value) => {
        handleTopicModule(filter, value);
      },
    });

    // 拦截 postArg 模块,这是泥潭的回复入口
    Tools.interceptProperty(commonui, "postArg", {
      afterSet: (value) => {
        handleReplyModule(filter, value);
      },
    });
  };

  /**
   * 注册脚本菜单
   * @param {Settings} settings 设置
   */
  const registerMenu = async (settings) => {
    // 修改 UA
    {
      const userAgent = await settings.userAgent;

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

        if (value) {
          settings.userAgent = value;
        }
      });
    }

    // 前置过滤
    {
      const enabled = await settings.preFilterEnabled;

      GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
        settings.preFilterEnabled = !enabled;
      });
    }
  };

  // 主函数
  (async () => {
    // 初始化缓存、设置
    const cache = new Cache(API.modules);
    const settings = new Settings(cache);

    // 读取设置
    await settings.load();

    // 初始化 API、UI
    const api = new API(cache, settings);
    const ui = new UI(settings, api);

    // 初始化过滤器
    const filter = new Filter(settings, api, ui);

    // 加载模块
    filter.initModules(
      SettingsModule,
      ListEnhancedModule,
      UserEnhancedModule,
      TagModule,
      KeywordModule,
      LocationModule,
      HunterModule,
      MiscModule
    );

    // 注册脚本菜单
    registerMenu(settings);

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(filter, unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(filter, value);
      },
    });
  })();
})();