- // ==UserScript==
- // @name NGA Filter
- // @namespace https://greasyfork.org/users/263018
- // @version 2.0.1
- // @author snyssss
- // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名过滤。troll must die。
- // @license MIT
-
- // @match *://bbs.nga.cn/*
- // @match *://ngabbs.com/*
- // @match *://nga.178.com/*
-
- // @grant GM_addStyle
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @grant unsafeWindow
-
- // @run-at document-start
- // @noframes
- // ==/UserScript==
-
- (() => {
- // 声明泥潭主模块、菜单模块、主题模块、回复模块
- let commonui, menuModule, topicModule, replyModule;
-
- // KEY
- const DATA_KEY = "NGAFilter";
- const USER_AGENT_KEY = "USER_AGENT_KEY";
- const PRE_FILTER_KEY = "PRE_FILTER_KEY";
- const CLEAR_TIME_KEY = "CLEAR_TIME_KEY";
-
- // User Agent
- const USER_AGENT = (() => {
- const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official";
-
- GM_registerMenuCommand(`修改UA:${data}`, () => {
- const value = prompt("修改UA", data);
-
- if (value) {
- GM_setValue(USER_AGENT_KEY, value);
-
- location.reload();
- }
- });
-
- return data;
- })();
-
- // 前置过滤
- const preFilter = (() => {
- const data = GM_getValue(PRE_FILTER_KEY);
-
- const value = data === undefined ? true : data;
-
- GM_registerMenuCommand(`前置过滤:${value ? "是" : "否"}`, () => {
- GM_setValue(PRE_FILTER_KEY, !value);
-
- location.reload();
- });
-
- return value;
- })();
-
- // STYLE
- GM_addStyle(`
- .filter-table-wrapper {
- max-height: 80vh;
- overflow-y: auto;
- }
- .filter-table {
- margin: 0;
- }
- .filter-table th,
- .filter-table td {
- position: relative;
- white-space: nowrap;
- }
- .filter-table th {
- position: sticky;
- top: 2px;
- z-index: 1;
- }
- .filter-table input:not([type]), .filter-table input[type="text"] {
- margin: 0;
- box-sizing: border-box;
- height: 100%;
- width: 100%;
- }
- .filter-input-wrapper {
- position: absolute;
- top: 6px;
- right: 6px;
- bottom: 6px;
- left: 6px;
- }
- .filter-text-ellipsis {
- display: flex;
- }
- .filter-text-ellipsis > * {
- flex: 1;
- width: 1px;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .filter-button-group {
- margin: -.1em -.2em;
- }
- .filter-tags {
- margin: 2px -0.2em 0;
- text-align: left;
- }
- .filter-mask {
- margin: 1px;
- color: #81C7D4;
- background: #81C7D4;
- }
- .filter-mask-block {
- display: block;
- border: 1px solid #66BAB7;
- text-align: center !important;
- }
- .filter-input-wrapper {
- position: absolute;
- top: 6px;
- right: 6px;
- bottom: 6px;
- left: 6px;
- }
- `);
-
- // 重新过滤
- // TODO 暂时全部重新过滤,应该根据修改的设置来过滤相应数据
- const reFilter = async () => {
- listModule.clear();
-
- if (topicModule) {
- await Promise.all(
- Object.values(topicModule.data).map(
- (item) => item.nFilter && item.nFilter.execute()
- )
- );
- }
-
- if (replyModule) {
- await Promise.all(
- Object.values(replyModule.data).map(
- (item) => item.nFilter && item.nFilter.execute()
- )
- );
- }
- };
-
- // 缓存模块
- const cacheModule = (() => {
- // 声明模块集合
- const modules = {};
-
- // IndexedDB 操作
- const db = (() => {
- // 常量
- const VERSION = 1;
- const DB_NAME = "NGA_FILTER_CACHE";
-
- // 是否支持
- const support = unsafeWindow.indexedDB !== undefined;
-
- // 不支持,直接返回
- if (support === false) {
- return {
- support,
- };
- }
-
- // 创建或获取数据库实例
- const getInstance = (() => {
- let instance;
-
- return () =>
- new Promise((resolve) => {
- // 如果已存在实例,直接返回
- if (instance) {
- resolve(instance);
- return;
- }
-
- // 打开 IndexedDB 数据库
- const request = unsafeWindow.indexedDB.open(DB_NAME, VERSION);
-
- // 如果数据库不存在则创建
- request.onupgradeneeded = (event) => {
- Object.entries(modules).map(([name, keyPath]) => {
- // 创建表
- const store = event.target.result.createObjectStore(name, {
- keyPath,
- });
-
- // 创建索引,用于清除过期数据
- store.createIndex("timestamp", "timestamp");
- });
- };
-
- // 成功后写入实例并返回
- request.onsuccess = (event) => {
- instance = event.target.result;
-
- resolve(instance);
- };
- });
- })();
-
- return {
- support,
- getInstance,
- };
- })();
-
- // 删除缓存
- const remove = async (name, key) => {
- // 不支持 IndexedDB,使用 GM_setValue
- if (db.support === false) {
- const cache = GM_getValue(name) || {};
-
- delete cache[key];
-
- GM_setValue(name, cache);
- return;
- }
-
- // 获取实例
- const instance = await db.getInstance();
-
- // 写入 IndexedDB
- await new Promise((resolve) => {
- // 创建事务
- const transaction = instance.transaction([name], "readwrite");
-
- // 获取对象仓库
- const store = transaction.objectStore(name);
-
- // 删除数据
- const r = store.delete(key);
-
- r.onsuccess = () => {
- resolve();
- };
-
- r.onerror = () => {
- resolve();
- };
-
- // 失败后处理
- request.onerror = () => {
- resolve();
- };
- });
- };
-
- // 写入缓存
- const save = async (name, key, value) => {
- // 不支持 IndexedDB,使用 GM_setValue
- if (db.support === false) {
- const cache = GM_getValue(name) || {};
-
- cache[key] = value;
-
- GM_setValue(name, cache);
- return;
- }
-
- // 获取实例
- const instance = await db.getInstance();
-
- // 写入 IndexedDB
- await new Promise((resolve) => {
- // 创建事务
- const transaction = instance.transaction([name], "readwrite");
-
- // 获取对象仓库
- const store = transaction.objectStore(name);
-
- // 插入数据
- const r = store.put({
- ...value,
- timestamp: Date.now(),
- });
-
- r.onsuccess = () => {
- resolve();
- };
-
- r.onerror = () => {
- resolve();
- };
-
- // 失败后处理
- request.onerror = () => {
- resolve();
- };
- });
- };
-
- // 读取缓存
- const load = async (name, key, expireTime) => {
- // 不支持 IndexedDB,使用 GM_getValue
- if (db.support === false) {
- const cache = GM_getValue(name) || {};
-
- if (cache[key]) {
- return cache[key];
- }
-
- return null;
- }
-
- // 获取实例
- const instance = await db.getInstance();
-
- // 查找 IndexedDB
- const result = await new Promise((resolve) => {
- // 创建事务
- const transaction = instance.transaction([name], "readonly");
-
- // 获取对象仓库
- const store = transaction.objectStore(name);
-
- // 获取数据
- const request = store.get(key);
-
- // 成功后处理数据
- request.onsuccess = (event) => {
- const data = event.target.result;
-
- if (data) {
- resolve(data);
- return;
- }
-
- resolve(null);
- };
-
- // 失败后处理
- request.onerror = () => {
- resolve(null);
- };
- });
-
- // 没有数据
- if (result === null) {
- return null;
- }
-
- // 如果已超时则删除
- if (result.timestamp + expireTime < new Date().getTime()) {
- await remove(name, key);
-
- return null;
- }
-
- // 返回结果
- return result;
- };
-
- // 定时清理
- const clear = async (list = Object.keys(modules)) => {
- // 获取实例
- const instance = await db.getInstance();
-
- // 清理 IndexedDB
- list.map((name) => {
- // 创建事务
- const transaction = instance.transaction([name], "readwrite");
-
- // 获取对象仓库
- const store = transaction.objectStore(name);
-
- // 清理数据
- store.clear();
- });
- };
-
- // 初始化,用于写入表信息
- const init = (name, keyPath) => {
- modules[name] = keyPath;
- };
-
- return {
- init,
- save,
- load,
- remove,
- clear,
- };
- })();
-
- // 过滤模块
- const filterModule = (() => {
- // 过滤提示
- const tips =
- "过滤顺序:用户 > 标记 > 关键字 > 属地<br/>过滤级别:显示 > 隐藏 > 遮罩 > 标记 > 继承";
-
- // 过滤方式
- const modes = ["继承", "标记", "遮罩", "隐藏", "显示"];
-
- // 默认过滤方式
- const defaultMode = modes[0];
-
- // 切换过滤方式
- const switchModeByName = (value) =>
- modes[modes.indexOf(value) + 1] || defaultMode;
-
- // 获取当前过滤方式下标
- const getModeByName = (name, defaultValue = 0) => {
- const index = modes.indexOf(name);
-
- if (index < 0) {
- return defaultValue;
- }
-
- return index;
- };
-
- // 获取指定下标过滤方式
- const getNameByMode = (index) => modes[index] || "";
-
- // 折叠样式
- const collapse = (uid, element, content) => {
- element.innerHTML = `
- <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
- <span class="crimson">Troll must die.</span>
- <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${uid}')].forEach(item => item.style.display = '')">点击查看</a>
- <div style="display: none;" name="troll_${uid}">
- ${content}
- </div>
- </div>`;
- };
-
- return {
- tips,
- modes,
- defaultMode,
- collapse,
- getModeByName,
- getNameByMode,
- switchModeByName,
- };
- })();
-
- // 数据(及配置)模块
- const dataModule = (() => {
- // 合并数据
- const merge = (() => {
- const isObject = (value) => {
- return value !== null && typeof value === "object";
- };
-
- const deepClone = (value) => {
- if (isObject(value)) {
- const clone = Array.isArray(value) ? [] : {};
-
- for (const key in value) {
- if (Object.prototype.hasOwnProperty.call(value, key)) {
- clone[key] = deepClone(value[key]);
- }
- }
-
- return clone;
- }
-
- return value;
- };
-
- return (target, ...sources) => {
- for (const source of sources) {
- for (const key in source) {
- if (isObject(source[key])) {
- if (isObject(target[key])) {
- merge(target[key], source[key]);
- } else {
- target[key] = deepClone(source[key]);
- }
- } else {
- target[key] = source[key];
- }
- }
- }
-
- return target;
- };
- })();
-
- // 初始化数据
- const data = (() => {
- // 默认配置
- const defaultData = {
- tags: {},
- users: {},
- keywords: {},
- locations: {},
- options: {
- filterRegdateLimit: 0,
- filterPostnumLimit: 0,
- filterTopicRateLimit: 100,
- filterReputationLimit: NaN,
- filterAnony: false,
- filterMode: "隐藏",
- },
- };
-
- // 读取数据
- const storedData = GM_getValue(DATA_KEY);
-
- // 如果没有数据,则返回默认配置
- if (typeof storedData !== "object") {
- return defaultData;
- }
-
- // 返回数据
- return merge(defaultData, storedData);
- })();
-
- // 保存数据
- const save = (values) => {
- merge(data, values);
-
- GM_setValue(DATA_KEY, data);
- };
-
- // 返回标记列表
- const getTags = () => data.tags;
-
- // 返回用户列表
- const getUsers = () => data.users;
-
- // 返回关键字列表
- const getKeywords = () => data.keywords;
-
- // 返回属地列表
- const getLocations = () => data.locations;
-
- // 获取默认过滤模式
- const getDefaultFilterMode = () => data.options.filterMode;
-
- // 设置默认过滤模式
- const setDefaultFilterMode = (value) => {
- save({
- options: {
- filterMode: value,
- },
- });
- };
-
- // 获取注册时间限制
- const getFilterRegdateLimit = () => data.options.filterRegdateLimit || 0;
-
- // 设置注册时间限制
- const setFilterRegdateLimit = (value) => {
- save({
- options: {
- filterRegdateLimit: value,
- },
- });
- };
-
- // 获取发帖数量限制
- const getFilterPostnumLimit = () => data.options.filterPostnumLimit || 0;
-
- // 设置发帖数量限制
- const setFilterPostnumLimit = (value) => {
- save({
- options: {
- filterPostnumLimit: value,
- },
- });
- };
-
- // 获取发帖比例限制
- const getFilterTopicRateLimit = () =>
- data.options.filterTopicRateLimit || 100;
-
- // 设置发帖比例限制
- const setFilterTopicRateLimit = (value) => {
- save({
- options: {
- filterTopicRateLimit: value,
- },
- });
- };
-
- // 获取用户声望限制
- const getFilterReputationLimit = () =>
- data.options.filterReputationLimit || NaN;
-
- // 设置用户声望限制
- const setFilterReputationLimit = (value) => {
- save({
- options: {
- filterReputationLimit: value,
- },
- });
- };
-
- // 获取是否过滤匿名
- const getFilterAnony = () => data.options.filterAnony || false;
-
- // 设置是否过滤匿名
- const setFilterAnony = (value) => {
- save({
- options: {
- filterAnony: value,
- },
- });
- };
-
- return {
- save,
- getTags,
- getUsers,
- getKeywords,
- getLocations,
- getDefaultFilterMode,
- setDefaultFilterMode,
- getFilterRegdateLimit,
- setFilterRegdateLimit,
- getFilterPostnumLimit,
- setFilterPostnumLimit,
- getFilterTopicRateLimit,
- setFilterTopicRateLimit,
- getFilterReputationLimit,
- setFilterReputationLimit,
- getFilterAnony,
- setFilterAnony,
- };
- })();
-
- // 列表模块
- const listModule = (() => {
- const list = [];
-
- const callback = [];
-
- // UI
- const view = (() => {
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
- element.innerHTML = `
- <div class="filter-table-wrapper">
- <table class="filter-table forumbox">
- <thead>
- <tr class="block_txt_c0">
- <th class="c1" width="1">用户</th>
- <th class="c2" width="1">过滤方式</th>
- <th class="c3">内容</th>
- <th class="c4" width="1">原因</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- `;
-
- return element;
- })();
-
- const tbody = content.querySelector("TBODY");
-
- const load = ({ user, mode, subject, content, reason }) => {
- const row = document.createElement("TR");
-
- row.className = `row${(tbody.querySelectorAll("TR").length % 2) + 1}`;
-
- row.innerHTML = `
- <td class="c1">${user}</td>
- <td class="c2">${mode}</td>
- <td class="c3">
- <div class="filter-text-ellipsis">
- ${subject || content}
- </div>
- </td>
- <td class="c4">${reason}</td>
- `;
-
- tbody.insertBefore(row, tbody.firstChild);
- };
-
- const refresh = () => {
- tbody.innerHTML = "";
-
- Object.values(list).forEach(load);
- };
-
- return {
- content,
- refresh,
- load,
- };
- })();
-
- const add = (value) => {
- if (
- list.find(
- (item) =>
- item.user === value.user &&
- item.mode === value.mode &&
- item.subject === value.subject &&
- item.content === value.content &&
- item.reason === value.reason
- )
- ) {
- return;
- }
-
- list.push(value);
-
- view.load(value);
-
- callback.forEach((item) => item(list));
- };
-
- const clear = () => {
- list.splice(0, list.length);
-
- view.refresh();
-
- callback.forEach((item) => item(list));
- };
-
- const bindCallback = (func) => {
- func(list);
-
- callback.push(func);
- };
-
- return {
- add,
- clear,
- bindCallback,
- view,
- };
- })();
-
- // 用户模块
- const userModule = (() => {
- // 获取用户列表
- const list = () => dataModule.getUsers();
-
- // 获取用户
- const get = (uid) => {
- // 获取列表
- const users = list();
-
- // 如果已存在,则返回信息
- if (users[uid]) {
- return users[uid];
- }
-
- return null;
- };
-
- // 增加用户
- const add = (uid, username, tags, filterMode) => {
- // 获取对应的用户
- const user = get(uid);
-
- // 如果用户已存在,则返回用户信息,否则增加用户
- if (user) {
- return user;
- }
-
- // 保存用户
- // TODO id 和 name 属于历史遗留问题,应该改为 uid 和 username 以便更好的理解
- dataModule.save({
- users: {
- [uid]: {
- id: uid,
- name: username,
- tags,
- filterMode,
- },
- },
- });
-
- // 返回用户信息
- return get(uid);
- };
-
- // 编辑用户
- const edit = (uid, values) => {
- dataModule.save({
- users: {
- [uid]: values,
- },
- });
- };
-
- // 删除用户
- const remove = (uid) => {
- // TODO 这里不可避免的直接操作了原始数据
- delete list()[uid];
-
- // 保存数据
- dataModule.save({});
- };
-
- // 格式化用户
- const format = (uid, name) => {
- if (uid <= 0) {
- return "";
- }
-
- const user = get(uid);
-
- if (user) {
- name = name || user.name;
- }
-
- const username = name ? "@" + name : "#" + uid;
-
- return `<a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[${username}]</a>`;
- };
-
- // UI
- const view = (() => {
- const details = (() => {
- let window;
-
- return (uid, name, callback) => {
- if (window === undefined) {
- window = commonui.createCommmonWindow();
- }
-
- const user = get(uid);
-
- const content = document.createElement("DIV");
-
- const size = Math.floor((screen.width * 0.8) / 200);
-
- const items = Object.values(tagModule.list()).map((tag, index) => {
- const checked = user && user.tags.includes(tag.id) ? "checked" : "";
-
- return `
- <td class="c1">
- <label for="s-tag-${index}" style="display: block; cursor: pointer;">
- ${tagModule.format(tag.id)}
- </label>
- </td>
- <td class="c2" width="1">
- <input id="s-tag-${index}" type="checkbox" value="${
- tag.id
- }" ${checked}/>
- </td>
- `;
- });
-
- const rows = [...new Array(Math.ceil(items.length / size))].map(
- (_, index) => `
- <tr class="row${(index % 2) + 1}">
- ${items.slice(size * index, size * (index + 1)).join("")}
- </tr>
- `
- );
-
- content.className = "w100";
- content.innerHTML = `
- <div class="filter-table-wrapper" style="width: 80vw;">
- <table class="filter-table forumbox">
- <tbody>
- ${rows.join("")}
- </tbody>
- </table>
- </div>
- <div style="margin: 10px 0;">
- <input type="text" placeholder="一次性添加多个标记用"|"隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
- </div>
- <div style="margin: 10px 0;">
- <span>过滤方式:</span>
- <button>${
- (user && user.filterMode) || filterModule.defaultMode
- }</button>
- <div class="right_">
- <button>删除</button>
- <button>保存</button>
- </div>
- </div>
- <div class="silver" style="margin-top: 5px;">${
- filterModule.tips
- }</div>
- `;
-
- const actions = content.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- actions[0].innerText = filterModule.switchModeByName(
- actions[0].innerText
- );
- };
-
- actions[1].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- remove(uid);
- reFilter();
-
- if (callback) {
- callback({
- id: null,
- });
- }
-
- window._.hide();
- };
-
- actions[2].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- const filterMode = actions[0].innerText;
-
- const checked = [...content.querySelectorAll("INPUT:checked")].map(
- (input) => parseInt(input.value, 10)
- );
-
- const newTags = content
- .querySelector("INPUT[type='text']")
- .value.split("|")
- .filter((item) => item.length)
- .map((item) => tagModule.add(item));
-
- const tags = [...new Set([...checked, ...newTags])].sort();
-
- if (user) {
- user.tags = tags;
-
- edit(uid, {
- filterMode,
- });
- } else {
- add(uid, name, tags, filterMode);
- }
-
- reFilter();
-
- if (callback) {
- callback({
- uid,
- name,
- tags,
- filterMode,
- });
- }
-
- window._.hide();
- };
-
- if (user === null) {
- actions[1].style.display = "none";
- }
-
- window._.addContent(null);
- window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
- window._.addContent(content);
- window._.show();
- };
- })();
-
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
- element.innerHTML = `
- <div class="filter-table-wrapper">
- <table class="filter-table forumbox">
- <thead>
- <tr class="block_txt_c0">
- <th class="c1" width="1">昵称</th>
- <th class="c2">标记</th>
- <th class="c3" width="1">过滤方式</th>
- <th class="c4" width="1">操作</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- `;
-
- return element;
- })();
-
- let index = 0;
- let size = 50;
- let hasNext = false;
-
- const box = content.querySelector("DIV");
-
- const tbody = content.querySelector("TBODY");
-
- const wrapper = content.querySelector(".filter-table-wrapper");
-
- const load = ({ id, name, tags, filterMode }, anchor = null) => {
- if (id === null) {
- if (anchor) {
- tbody.removeChild(anchor);
- }
- return;
- }
-
- if (anchor === null) {
- anchor = document.createElement("TR");
-
- anchor.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- tbody.appendChild(anchor);
- }
-
- anchor.innerHTML = `
- <td class="c1">
- ${format(id, name)}
- </td>
- <td class="c2">
- ${tags.map(tagModule.format).join("")}
- </td>
- <td class="c3">
- <div class="filter-table-button-group">
- <button>${filterMode || filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c4">
- <div class="filter-table-button-group">
- <button>编辑</button>
- <button>删除</button>
- </div>
- </td>
- `;
-
- const actions = anchor.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- const filterMode = filterModule.switchModeByName(
- actions[0].innerHTML
- );
-
- actions[0].innerHTML = filterMode;
-
- edit(id, { filterMode });
- reFilter();
- };
-
- actions[1].onclick = () => {
- details(id, name, (item) => {
- load(item, anchor);
- });
- };
-
- actions[2].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- tbody.removeChild(anchor);
-
- remove(id);
- reFilter();
- };
- };
-
- const loadNext = () => {
- hasNext = index + size < Object.keys(list()).length;
-
- Object.values(list())
- .slice(index, index + size)
- .forEach((item) => load(item));
-
- index += size;
- };
-
- box.onscroll = () => {
- if (hasNext === false) {
- return;
- }
-
- if (
- box.scrollHeight - box.scrollTop - box.clientHeight <=
- wrapper.clientHeight
- ) {
- loadNext();
- }
- };
-
- const refresh = () => {
- index = 0;
-
- tbody.innerHTML = "";
-
- loadNext();
- };
-
- return {
- content,
- details,
- refresh,
- };
- })();
-
- return {
- list,
- get,
- add,
- edit,
- remove,
- format,
- view,
- };
- })();
-
- // 标记模块
- const tagModule = (() => {
- // 获取标记列表
- const list = () => dataModule.getTags();
-
- // 获取标记
- const get = ({ id, name }) => {
- // 获取列表
- const tags = list();
-
- // 通过 ID 获取标记
- if (tags[id]) {
- return tags[id];
- }
-
- // 通过名称获取标记
- if (name) {
- const tag = Object.values(tags).find((item) => item.name === name);
-
- if (tag) {
- return tag;
- }
- }
-
- return null;
- };
-
- // 增加标记
- const add = (name) => {
- // 获取对应的标记
- const tag = get({ name });
-
- // 如果标记已存在,则返回标记信息,否则增加标记
- if (tag) {
- return tag;
- }
-
- // ID 为最大值 + 1
- const id = Math.max(Object.keys(list()), 0) + 1;
-
- // 标记的颜色
- // 采用的是泥潭的颜色方案,参见 commonui.htmlName
- const color = (() => {
- const hash = (() => {
- let h = 5381;
-
- for (var i = 0; i < name.length; i++) {
- h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
- }
-
- return h;
- })();
-
- const hex = Math.abs(hash).toString(16) + "000000";
-
- const hsv = [
- `0x${hex.substring(2, 4)}` / 255,
- `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
- `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
- ];
-
- const rgb = commonui.hsvToRgb(hsv[0], hsv[1], hsv[2]);
-
- return ["#", ...rgb].reduce((a, b) => {
- return a + ("0" + b.toString(16)).slice(-2);
- });
- })();
-
- // 保存标记
- dataModule.save({
- tags: {
- [id]: {
- id,
- name,
- color,
- filterMode: filterModule.defaultMode,
- },
- },
- });
-
- // 返回标记信息
- return get({ id });
- };
-
- // 编辑标记
- const edit = (id, values) => {
- dataModule.save({
- tags: {
- [id]: values,
- },
- });
- };
-
- // 删除标记
- const remove = (id) => {
- // TODO 这里不可避免的直接操作了原始数据
- delete list()[id];
-
- // 删除用户对应的标记
- Object.values(userModule.list()).forEach((user) => {
- const index = user.tags.findIndex((tag) => tag === id);
-
- if (index >= 0) {
- user.tags.splice(index, 1);
- }
- });
-
- // 保存数据
- dataModule.save({});
- };
-
- // 格式化标记
- const format = (id) => {
- const tag = get({ id });
-
- if (tag) {
- return `<b class="block_txt nobr" style="background: ${tag.color}; color: #FFF; margin: 0.1em 0.2em;">${tag.name}</b>`;
- }
-
- return "";
- };
-
- // UI
- const view = (() => {
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
- element.innerHTML = `
- <div class="filter-table-wrapper">
- <table class="filter-table forumbox">
- <thead>
- <tr class="block_txt_c0">
- <th class="c1" width="1">标记</th>
- <th class="c2">列表</th>
- <th class="c3" width="1">过滤方式</th>
- <th class="c4" width="1">操作</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- `;
-
- return element;
- })();
-
- let index = 0;
- let size = 50;
- let hasNext = false;
-
- const box = content.querySelector("DIV");
-
- const tbody = content.querySelector("TBODY");
-
- const wrapper = content.querySelector(".filter-table-wrapper");
-
- const load = ({ id, filterMode }, anchor = null) => {
- if (id === null) {
- if (anchor) {
- tbody.removeChild(anchor);
- }
- return;
- }
-
- if (anchor === null) {
- anchor = document.createElement("TR");
-
- anchor.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- tbody.appendChild(anchor);
- }
-
- const users = Object.values(userModule.list());
-
- const filteredUsers = users.filter((user) => user.tags.includes(id));
-
- anchor.innerHTML = `
- <td class="c1">
- ${format(id)}
- </td>
- <td class="c2">
- <button>${filteredUsers.length}</button>
- <div style="white-space: normal; display: none;">
- ${filteredUsers
- .map((user) => userModule.format(user.id))
- .join("")}
- </div>
- </td>
- <td class="c3">
- <div class="filter-table-button-group">
- <button>${filterMode || filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c4">
- <div class="filter-table-button-group">
- <button>删除</button>
- </div>
- </td>
- `;
-
- const actions = anchor.querySelectorAll("BUTTON");
-
- actions[0].onclick = (() => {
- let hide = true;
-
- return () => {
- hide = !hide;
-
- actions[0].nextElementSibling.style.display = hide
- ? "none"
- : "block";
- };
- })();
-
- actions[1].onclick = () => {
- const filterMode = filterModule.switchModeByName(
- actions[1].innerHTML
- );
-
- actions[1].innerHTML = filterMode;
-
- edit(id, { filterMode });
- reFilter();
- };
-
- actions[2].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- tbody.removeChild(anchor);
-
- remove(id);
- reFilter();
- };
- };
-
- const loadNext = () => {
- hasNext = index + size < Object.keys(list()).length;
-
- Object.values(list())
- .slice(index, index + size)
- .forEach((item) => load(item));
-
- index += size;
- };
-
- box.onscroll = () => {
- if (hasNext === false) {
- return;
- }
-
- if (
- box.scrollHeight - box.scrollTop - box.clientHeight <=
- wrapper.clientHeight
- ) {
- loadNext();
- }
- };
-
- const refresh = () => {
- index = 0;
-
- tbody.innerHTML = "";
-
- loadNext();
- };
-
- return {
- content,
- refresh,
- };
- })();
-
- return {
- list,
- get,
- add,
- edit,
- remove,
- format,
- view,
- };
- })();
-
- // 关键字模块
- const keywordModule = (() => {
- // 获取关键字列表
- const list = () => dataModule.getKeywords();
-
- // 获取关键字
- const get = (id) => {
- // 获取列表
- const keywords = list();
-
- // 如果已存在,则返回信息
- if (keywords[id]) {
- return keywords[id];
- }
-
- return null;
- };
-
- // 编辑关键字
- const edit = (id, values) => {
- dataModule.save({
- keywords: {
- [id]: values,
- },
- });
- };
-
- // 增加关键字
- // filterLevel: 0 - 仅过滤标题; 1 - 过滤标题和内容
- // 无需判重
- const add = (keyword, filterMode, filterLevel) => {
- // ID 为最大值 + 1
- const id = Math.max(Object.keys(list()), 0) + 1;
-
- // 保存关键字
- dataModule.save({
- keywords: {
- [id]: {
- id,
- keyword,
- filterMode,
- filterLevel,
- },
- },
- });
-
- // 返回关键字信息
- return get(id);
- };
-
- // 删除关键字
- const remove = (id) => {
- // TODO 这里不可避免的直接操作了原始数据
- delete list()[id];
-
- // 保存数据
- dataModule.save({});
- };
-
- // UI
- const view = (() => {
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
- element.innerHTML = `
- <div class="filter-table-wrapper">
- <table class="filter-table forumbox">
- <thead>
- <tr class="block_txt_c0">
- <th class="c1">列表</th>
- <th class="c2" width="1">过滤方式</th>
- <th class="c3" width="1">包括内容</th>
- <th class="c4" width="1">操作</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。</div>
- `;
-
- return element;
- })();
-
- let index = 0;
- let size = 50;
- let hasNext = false;
-
- const box = content.querySelector("DIV");
-
- const tbody = content.querySelector("TBODY");
-
- const wrapper = content.querySelector(".filter-table-wrapper");
-
- const load = (
- { id, keyword, filterMode, filterLevel },
- anchor = null
- ) => {
- if (keyword === null) {
- if (anchor) {
- tbody.removeChild(anchor);
- }
- return;
- }
-
- if (anchor === null) {
- anchor = document.createElement("TR");
-
- anchor.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- tbody.appendChild(anchor);
- }
-
- const checked = filterLevel ? "checked" : "";
-
- anchor.innerHTML = `
- <td class="c1">
- <div class="filter-input-wrapper">
- <input type="text" value="${keyword || ""}" />
- </div>
- </td>
- <td class="c2">
- <div class="filter-table-button-group">
- <button>${filterMode || filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c3">
- <div style="text-align: center;">
- <input type="checkbox" ${checked} />
- </div>
- </td>
- <td class="c4">
- <div class="filter-table-button-group">
- <button>保存</button>
- <button>删除</button>
- </div>
- </td>
- `;
-
- const actions = anchor.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- actions[0].innerHTML = filterModule.switchModeByName(
- actions[0].innerHTML
- );
- };
-
- actions[1].onclick = () => {
- const keyword = anchor.querySelector("INPUT[type='text']").value;
-
- const filterMode = actions[0].innerHTML;
-
- const filterLevel = anchor.querySelector(
- `INPUT[type="checkbox"]:checked`
- )
- ? 1
- : 0;
-
- if (keyword) {
- edit(id, {
- keyword,
- filterMode,
- filterLevel,
- });
- reFilter();
- }
- };
-
- actions[2].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- tbody.removeChild(anchor);
-
- remove(id);
- reFilter();
- };
- };
-
- const loadNext = () => {
- hasNext = index + size < Object.keys(list()).length;
-
- Object.values(list())
- .slice(index, index + size)
- .forEach((item) => load(item));
-
- if (hasNext === false) {
- const loadNew = () => {
- const row = document.createElement("TR");
-
- row.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- row.innerHTML = `
- <td class="c1">
- <div class="filter-input-wrapper">
- <input type="text" value="" />
- </div>
- </td>
- <td class="c2">
- <div class="filter-table-button-group">
- <button>${filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c3">
- <div style="text-align: center;">
- <input type="checkbox" />
- </div>
- </td>
- <td class="c4">
- <div class="filter-table-button-group">
- <button>添加</button>
- </div>
- </td>
- `;
-
- const actions = row.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- const filterMode = filterModule.switchModeByName(
- actions[0].innerHTML
- );
-
- actions[0].innerHTML = filterMode;
- };
-
- actions[1].onclick = () => {
- const keyword = row.querySelector("INPUT[type='text']").value;
-
- const filterMode = actions[0].innerHTML;
-
- const filterLevel = row.querySelector(
- `INPUT[type="checkbox"]:checked`
- )
- ? 1
- : 0;
-
- if (keyword) {
- const item = add(keyword, filterMode, filterLevel);
-
- load(item, row);
- loadNew();
- reFilter();
- }
- };
-
- tbody.appendChild(row);
- };
-
- loadNew();
- }
-
- index += size;
- };
-
- box.onscroll = () => {
- if (hasNext === false) {
- return;
- }
-
- if (
- box.scrollHeight - box.scrollTop - box.clientHeight <=
- wrapper.clientHeight
- ) {
- loadNext();
- }
- };
-
- const refresh = () => {
- index = 0;
-
- tbody.innerHTML = "";
-
- loadNext();
- };
-
- return {
- content,
- refresh,
- };
- })();
-
- return {
- list,
- get,
- add,
- edit,
- remove,
- view,
- };
- })();
-
- // 属地模块
- const locationModule = (() => {
- // 获取属地列表
- const list = () => dataModule.getLocations();
-
- // 获取属地
- const get = (id) => {
- // 获取列表
- const locations = list();
-
- // 如果已存在,则返回信息
- if (locations[id]) {
- return locations[id];
- }
-
- return null;
- };
-
- // 增加属地
- // 无需判重
- const add = (keyword, filterMode) => {
- // ID 为最大值 + 1
- const id = Math.max(Object.keys(list()), 0) + 1;
-
- // 保存属地
- dataModule.save({
- locations: {
- [id]: {
- id,
- keyword,
- filterMode,
- },
- },
- });
-
- // 返回属地信息
- return get(id);
- };
-
- // 编辑属地
- const edit = (id, values) => {
- dataModule.save({
- locations: {
- [id]: values,
- },
- });
- };
-
- // 删除属地
- const remove = (id) => {
- // TODO 这里不可避免的直接操作了原始数据
- delete list()[id];
-
- // 保存数据
- dataModule.save({});
- };
-
- // UI
- const view = (() => {
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
- element.innerHTML = `
- <div class="filter-table-wrapper">
- <table class="filter-table forumbox">
- <thead>
- <tr class="block_txt_c0">
- <th class="c1">列表</th>
- <th class="c2" width="1">过滤方式</th>
- <th class="c3" width="1">操作</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
- <div class="silver" style="margin-top: 10px;">支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。<br/>属地过滤功能需要占用额外的资源,请谨慎开启</div>
- `;
-
- return element;
- })();
-
- let index = 0;
- let size = 50;
- let hasNext = false;
-
- const box = content.querySelector("DIV");
-
- const tbody = content.querySelector("TBODY");
-
- const wrapper = content.querySelector(".filter-table-wrapper");
-
- const load = ({ id, keyword, filterMode }, anchor = null) => {
- if (keyword === null) {
- if (anchor) {
- tbody.removeChild(anchor);
- }
- return;
- }
-
- if (anchor === null) {
- anchor = document.createElement("TR");
-
- anchor.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- tbody.appendChild(anchor);
- }
-
- anchor.innerHTML = `
- <td class="c1">
- <div class="filter-input-wrapper">
- <input type="text" value="${keyword || ""}" />
- </div>
- </td>
- <td class="c2">
- <div class="filter-table-button-group">
- <button>${filterMode || filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c3">
- <div class="filter-table-button-group">
- <button>保存</button>
- <button>删除</button>
- </div>
- </td>
- `;
-
- const actions = anchor.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- actions[0].innerHTML = filterModule.switchModeByName(
- actions[0].innerHTML
- );
- };
-
- actions[1].onclick = () => {
- const keyword = anchor.querySelector("INPUT[type='text']").value;
-
- const filterMode = actions[0].innerHTML;
-
- if (keyword) {
- edit(id, {
- keyword,
- filterMode,
- });
- reFilter();
- }
- };
-
- actions[2].onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- tbody.removeChild(anchor);
-
- remove(id);
- reFilter();
- };
- };
-
- const loadNext = () => {
- hasNext = index + size < Object.keys(list()).length;
-
- Object.values(list())
- .slice(index, index + size)
- .forEach((item) => load(item));
-
- if (hasNext === false) {
- const loadNew = () => {
- const row = document.createElement("TR");
-
- row.className = `row${
- (tbody.querySelectorAll("TR").length % 2) + 1
- }`;
-
- row.innerHTML = `
- <td class="c1">
- <div class="filter-input-wrapper">
- <input type="text" value="" />
- </div>
- </td>
- <td class="c2">
- <div class="filter-table-button-group">
- <button>${filterModule.defaultMode}</button>
- </div>
- </td>
- <td class="c3">
- <div class="filter-table-button-group">
- <button>添加</button>
- </div>
- </td>
- `;
-
- const actions = row.querySelectorAll("BUTTON");
-
- actions[0].onclick = () => {
- const filterMode = filterModule.switchModeByName(
- actions[0].innerHTML
- );
-
- actions[0].innerHTML = filterMode;
- };
-
- actions[1].onclick = () => {
- const keyword = row.querySelector("INPUT[type='text']").value;
-
- const filterMode = actions[0].innerHTML;
-
- if (keyword) {
- const item = add(keyword, filterMode);
-
- load(item, row);
- loadNew();
- reFilter();
- }
- };
-
- tbody.appendChild(row);
- };
-
- loadNew();
- }
-
- index += size;
- };
-
- box.onscroll = () => {
- if (hasNext === false) {
- return;
- }
-
- if (
- box.scrollHeight - box.scrollTop - box.clientHeight <=
- wrapper.clientHeight
- ) {
- loadNext();
- }
- };
-
- const refresh = () => {
- index = 0;
-
- tbody.innerHTML = "";
-
- loadNext();
- };
-
- return {
- content,
- refresh,
- };
- })();
-
- return {
- list,
- get,
- add,
- edit,
- remove,
- view,
- };
- })();
-
- // 通用设置
- const commonModule = (() => {
- // UI
- const view = (() => {
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.style = "display: none";
-
- return element;
- })();
-
- const refresh = () => {
- content.innerHTML = "";
-
- // 前置过滤
- (() => {
- const checked = preFilter ? "checked" : "";
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <div>
- <label>
- 前置过滤
- <input type="checkbox" ${checked} />
- </label>
- </div>
- `;
-
- const checkbox = element.querySelector("INPUT");
-
- checkbox.onchange = () => {
- const newValue = checkbox.checked;
-
- GM_setValue(PRE_FILTER_KEY, newValue);
-
- location.reload();
- };
-
- content.appendChild(element);
- })();
-
- // 默认过滤方式
- (() => {
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>默认过滤方式</div>
- <div></div>
- <div class="silver" style="margin-top: 10px;">${filterModule.tips}</div>
- `;
-
- ["标记", "遮罩", "隐藏"].forEach((item, index) => {
- const span = document.createElement("SPAN");
-
- const checked =
- dataModule.getDefaultFilterMode() === item ? "checked" : "";
-
- span.innerHTML += `
- <input id="s-fm-${index}" type="radio" name="filterType" ${checked}>
- <label for="s-fm-${index}" style="cursor: pointer;">${item}</label>
- `;
-
- const input = span.querySelector("INPUT");
-
- input.onchange = () => {
- if (input.checked) {
- dataModule.setDefaultFilterMode(item);
-
- reFilter();
- }
- };
-
- element.querySelectorAll("div")[1].append(span);
- });
-
- content.appendChild(element);
- })();
-
- // 小号过滤(时间)
- (() => {
- const value = dataModule.getFilterRegdateLimit() / 86400000;
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- 隐藏注册时间小于<input value="${value}" maxLength="4" style="width: 48px;" />天的用户
- <button>确认</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- const newValue =
- parseInt(element.querySelector("INPUT").value, 10) || 0;
-
- dataModule.setFilterRegdateLimit(
- newValue < 0 ? 0 : newValue * 86400000
- );
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 小号过滤(发帖数)
- (() => {
- const value = dataModule.getFilterPostnumLimit();
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- 隐藏发帖数量小于<input value="${value}" maxLength="5" style="width: 48px;" />贴的用户
- <button>确认</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- const newValue =
- parseInt(element.querySelector("INPUT").value, 10) || 0;
-
- dataModule.setFilterPostnumLimit(newValue < 0 ? 0 : newValue);
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 流量号过滤(主题比例)
- (() => {
- const value = dataModule.getFilterTopicRateLimit();
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- 隐藏发帖比例大于<input value="${value}" maxLength="3" style="width: 48px;" />%的用户
- <button>确认</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- const newValue =
- parseInt(element.querySelector("INPUT").value, 10) || 100;
-
- if (newValue <= 0 || newValue > 100) {
- return;
- }
-
- dataModule.setFilterTopicRateLimit(newValue);
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 声望过滤
- (() => {
- const value = dataModule.getFilterReputationLimit() || "";
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- 隐藏版面声望低于<input value="${value}" maxLength="5" style="width: 48px;" />点的用户
- <button>确认</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- const newValue = parseInt(element.querySelector("INPUT").value, 10);
-
- dataModule.setFilterReputationLimit(newValue);
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 匿名过滤
- (() => {
- const checked = dataModule.getFilterAnony() ? "checked" : "";
-
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- <label>
- 隐藏匿名的用户
- <input type="checkbox" ${checked} />
- </label>
- </div>
- `;
-
- const checkbox = element.querySelector("INPUT");
-
- checkbox.onchange = () => {
- const newValue = checkbox.checked;
-
- dataModule.setFilterAnony(newValue);
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 删除没有标记的用户
- (() => {
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- <button>删除没有标记的用户</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- Object.values(userModule.list()).forEach(({ id, tags }) => {
- if (tags.length === 0) {
- userModule.remove(id);
- }
- });
-
- dataModule.save({});
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 删除没有用户的标记
- (() => {
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- <button>删除没有用户的标记</button>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
-
- action.onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- const users = Object.values(userModule.list());
-
- Object.values(tagModule.list()).forEach(({ id }) => {
- if (users.find(({ tags }) => tags.includes(id))) {
- return;
- }
-
- tagModule.remove(id);
- });
-
- dataModule.save({});
-
- reFilter();
- };
-
- content.appendChild(element);
- })();
-
- // 删除非激活中的用户
- (() => {
- const element = document.createElement("DIV");
-
- element.innerHTML += `
- <br/>
- <div>
- <button>删除非激活中的用户</button>
- <div style="white-space: normal;"></div>
- </div>
- `;
-
- const action = element.querySelector("BUTTON");
- const list = action.nextElementSibling;
-
- action.onclick = () => {
- if (confirm("是否确认?") === false) {
- return;
- }
-
- const users = Object.values(userModule.list());
-
- const waitingQueue = users.map(({ id }) => () => {
- return fetchModule.getUserInfo(id).then(({ bit }) => {
- const activeInfo = commonui.activeInfo(0, 0, bit);
-
- const activeType = activeInfo[1];
-
- if (["ACTIVED", "LINKED"].includes(activeType)) {
- return;
- }
-
- list.innerHTML += userModule.format(id);
-
- userModule.remove(id);
- });
- });
-
- const queueLength = waitingQueue.length;
-
- const execute = () => {
- if (waitingQueue.length) {
- const next = waitingQueue.shift();
-
- action.innerHTML = `删除非激活中的用户 (${
- queueLength - waitingQueue.length
- }/${queueLength})`;
- action.disabled = true;
-
- next().finally(execute);
- } else {
- action.disabled = false;
-
- dataModule.save({});
-
- reFilter();
- }
- };
-
- execute();
- };
-
- content.appendChild(element);
- })();
- };
-
- return {
- content,
- refresh,
- };
- })();
-
- return {
- view,
- };
- })();
-
- // 额外数据请求模块
- // 临时的缓存写法
- const fetchModule = (() => {
- // 简单的统一请求
- const request = (url, config = {}) =>
- fetch(url, {
- headers: {
- "X-User-Agent": USER_AGENT,
- },
- ...config,
- });
-
- // 获取主题数量
- // 缓存 1 小时
- const getTopicNum = (() => {
- const name = "TOPIC_NUM_CACHE";
-
- const expireTime = 60 * 60 * 1000;
-
- cacheModule.init(name, "uid");
-
- return async (uid) => {
- const cache = await cacheModule.load(name, uid, expireTime);
-
- if (cache) {
- return cache.count;
- }
-
- const api = `/thread.php?lite=js&authorid=${uid}`;
-
- const { __ROWS } = await new Promise((resolve) => {
- request(api)
- .then((res) => res.blob())
- .then((blob) => {
- const reader = new FileReader();
-
- reader.onload = () => {
- const text = reader.result;
- const result = JSON.parse(
- text.replace("window.script_muti_get_var_store=", "")
- );
-
- resolve(result.data);
- };
-
- reader.readAsText(blob, "GBK");
- })
- .catch(() => {
- resolve({});
- });
- });
-
- if (__ROWS > 100) {
- cacheModule.save(name, uid, {
- uid,
- count: __ROWS,
- timestamp: new Date().getTime(),
- });
- }
-
- return __ROWS;
- };
- })();
-
- // 获取用户信息
- // 缓存 1 小时
- const getUserInfo = (() => {
- const name = "USER_INFO_CACHE";
-
- const expireTime = 60 * 60 * 1000;
-
- cacheModule.init(name, "uid");
-
- return async (uid) => {
- const cache = await cacheModule.load(name, uid, expireTime);
-
- if (cache) {
- return cache.data;
- }
-
- const api = `/nuke.php?lite=js&__lib=ucp&__act=get&uid=${uid}`;
-
- const data = await new Promise((resolve) => {
- request(api)
- .then((res) => res.blob())
- .then((blob) => {
- const reader = new FileReader();
-
- reader.onload = () => {
- const text = reader.result;
- const result = JSON.parse(
- text.replace("window.script_muti_get_var_store=", "")
- );
-
- resolve(result.data[0] || null);
- };
-
- reader.readAsText(blob, "GBK");
- })
- .catch(() => {
- resolve(null);
- });
- });
-
- if (data) {
- cacheModule.save(name, uid, {
- uid,
- data,
- timestamp: new Date().getTime(),
- });
- }
-
- return data;
- };
- })();
-
- // 获取顶楼用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、声望
- // 缓存 10 分钟
- const getUserInfoAndReputation = (() => {
- const name = "PAGE_CACHE";
-
- const expireTime = 10 * 60 * 1000;
-
- cacheModule.init(name, "url");
-
- return async (tid, pid) => {
- if (tid === undefined && pid === undefined) {
- return;
- }
-
- const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
-
- const cache = await cacheModule.load(name, api, expireTime);
-
- if (cache) {
- return cache.data;
- }
-
- // 请求数据
- const data = await new Promise((resolve) => {
- request(api)
- .then((res) => res.blob())
- .then((blob) => {
- const getLastIndex = (content, position) => {
- if (position >= 0) {
- let nextIndex = position + 1;
-
- while (nextIndex < content.length) {
- if (content[nextIndex] === "}") {
- return nextIndex;
- }
-
- if (content[nextIndex] === "{") {
- nextIndex = getLastIndex(content, nextIndex);
-
- if (nextIndex < 0) {
- break;
- }
- }
-
- nextIndex = nextIndex + 1;
- }
- }
-
- return -1;
- };
-
- const reader = new FileReader();
-
- reader.onload = async () => {
- const parser = new DOMParser();
-
- const doc = parser.parseFromString(reader.result, "text/html");
-
- const html = doc.body.innerHTML;
-
- // 验证帖子正常
- const verify = doc.querySelector("#m_posts");
-
- if (verify) {
- // 取得顶楼 UID
- const uid = (() => {
- const ele = doc.querySelector("#postauthor0");
-
- if (ele) {
- const res = ele.getAttribute("href").match(/uid=(\S+)/);
-
- if (res) {
- return res[1];
- }
- }
-
- return 0;
- })();
-
- // 取得顶楼标题
- const subject = doc.querySelector("#postsubject0").innerHTML;
-
- // 取得顶楼内容
- const content = doc.querySelector("#postcontent0").innerHTML;
-
- // 非匿名用户
- if (uid && uid > 0) {
- // 取得用户信息
- const userInfo = (() => {
- // 起始JSON
- const str = `"${uid}":{`;
-
- // 起始下标
- const index = html.indexOf(str) + str.length;
-
- // 结尾下标
- const lastIndex = getLastIndex(html, index);
-
- if (lastIndex >= 0) {
- try {
- return JSON.parse(
- `{${html.substring(index, lastIndex)}}`
- );
- } catch {}
- }
-
- return null;
- })();
-
- // 取得用户声望
- const reputation = (() => {
- const reputations = (() => {
- // 起始JSON
- const str = `"__REPUTATIONS":{`;
-
- // 起始下标
- const index = html.indexOf(str) + str.length;
-
- // 结尾下标
- const lastIndex = getLastIndex(html, index);
-
- if (lastIndex >= 0) {
- return JSON.parse(
- `{${html.substring(index, lastIndex)}}`
- );
- }
-
- return null;
- })();
-
- if (reputations) {
- for (let fid in reputations) {
- return reputations[fid][uid] || 0;
- }
- }
-
- return NaN;
- })();
-
- resolve({
- uid,
- subject,
- content,
- userInfo,
- reputation,
- });
- return;
- }
-
- resolve({
- uid,
- subject,
- content,
- });
- return;
- }
-
- resolve(null);
- };
-
- reader.readAsText(blob, "GBK");
- })
- .catch(() => {
- resolve(null);
- });
- });
-
- if (data) {
- cacheModule.save(name, api, {
- url: api,
- data,
- timestamp: new Date().getTime(),
- });
- }
-
- return data;
- };
- })();
-
- // 每天清理缓存
- (() => {
- const today = new Date();
-
- const lastTime = new Date(GM_getValue(CLEAR_TIME_KEY) || 0);
-
- const isToday =
- lastTime.getDate() === today.getDate() &&
- lastTime.getMonth() === today.getMonth() &&
- lastTime.getFullYear() === today.getFullYear();
-
- if (isToday === false) {
- cacheModule.clear();
-
- GM_setValue(CLEAR_TIME_KEY, today.getTime());
- }
- })();
-
- return {
- getTopicNum,
- getUserInfo,
- getUserInfoAndReputation,
- };
- })();
-
- // UI
- const ui = (() => {
- const modules = {};
-
- // 主界面
- const view = (() => {
- const tabContainer = (() => {
- const element = document.createElement("DIV");
-
- element.className = "w100";
- element.innerHTML = `
- <div class="right_" style="margin-bottom: 5px;">
- <table class="stdbtn" cellspacing="0">
- <tbody>
- <tr></tr>
- </tbody>
- </table>
- </div>
- <div class="clear"></div>
- `;
-
- return element;
- })();
-
- const tabPanelContainer = (() => {
- const element = document.createElement("DIV");
-
- element.style = "width: 80vw;";
-
- return element;
- })();
-
- const content = (() => {
- const element = document.createElement("DIV");
-
- element.appendChild(tabContainer);
- element.appendChild(tabPanelContainer);
-
- return element;
- })();
-
- const addModule = (() => {
- const tc = tabContainer.querySelector("TR");
- const cc = tabPanelContainer;
-
- return (name, module) => {
- const tabBox = document.createElement("TD");
-
- tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${name}</a>`;
-
- const tab = tabBox.childNodes[0];
-
- const toggle = () => {
- Object.values(modules).forEach((item) => {
- if (item.tab === tab) {
- item.tab.className = "nobr";
- item.content.style = "display: block";
- item.refresh();
- } else {
- item.tab.className = "nobr silver";
- item.content.style = "display: none";
- }
- });
- };
-
- tc.append(tabBox);
- cc.append(module.content);
-
- tab.onclick = toggle;
-
- modules[name] = {
- ...module,
- tab,
- toggle,
- };
-
- return modules[name];
- };
- })();
-
- return {
- content,
- addModule,
- };
- })();
-
- // 右上角菜单
- const menu = (() => {
- const container = document.createElement("DIV");
-
- container.className = `td`;
- container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">屏蔽</a>`;
-
- const content = container.querySelector("A");
-
- const create = (onclick) => {
- const anchor = document.querySelector("#mainmenu .td:last-child");
-
- if (anchor) {
- anchor.before(container);
-
- content.onclick = onclick;
-
- return true;
- }
-
- return false;
- };
-
- const update = (list) => {
- const count = list.length;
-
- if (count) {
- content.innerHTML = `屏蔽 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
- } else {
- content.innerHTML = `屏蔽`;
- }
- };
-
- return {
- create,
- update,
- };
- })();
-
- return {
- ...view,
- ...menu,
- };
- })();
-
- // 判断是否为当前用户 UID
- const isCurrentUID = (uid) => {
- return unsafeWindow.__CURRENT_UID === parseInt(uid, 10);
- };
-
- // 获取过滤方式
- const getFilterMode = async (item) => {
- // 声明结果
- const result = {
- mode: -1,
- reason: ``,
- };
-
- // 获取 UID
- const uid = parseInt(item.uid, 10);
-
- // 获取链接参数
- const params = new URLSearchParams(location.search);
-
- // 跳过屏蔽(插件自定义)
- if (params.has("nofilter")) {
- return;
- }
-
- // 收藏
- if (params.has("favor")) {
- return;
- }
-
- // 只看某人
- if (params.has("authorid")) {
- return;
- }
-
- // 跳过自己
- if (isCurrentUID(uid)) {
- return "";
- }
-
- // 用户过滤
- (() => {
- // 获取屏蔽列表里匹配的用户
- const user = userModule.get(uid);
-
- // 没有则跳过
- if (user === null) {
- return;
- }
-
- const { filterMode } = user;
-
- const mode = filterModule.getModeByName(filterMode);
-
- // 低于当前的过滤模式则跳过
- if (mode <= result.mode) {
- return;
- }
-
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `用户模式: ${filterMode}`;
- })();
-
- // 标记过滤
- (() => {
- // 获取屏蔽列表里匹配的用户
- const user = userModule.get(uid);
-
- // 获取用户对应的标记,并跳过低于当前的过滤模式
- const tags = user
- ? user.tags
- .map((id) => tagModule.get({ id }))
- .filter((i) => i !== null)
- .filter(
- (i) => filterModule.getModeByName(i.filterMode) > result.mode
- )
- : [];
-
- // 没有则跳过
- if (tags.length === 0) {
- return;
- }
-
- // 取最高的过滤模式
- const { filterMode, name } = tags.sort(
- (a, b) =>
- filterModule.getModeByName(b.filterMode) -
- filterModule.getModeByName(a.filterMode)
- )[0];
-
- const mode = filterModule.getModeByName(filterMode);
-
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `标记: ${name}`;
- })();
-
- // 关键词过滤
- await (async () => {
- const { getContent } = item;
-
- // 获取设置里的关键词列表,并跳过低于当前的过滤模式
- const keywords = Object.values(keywordModule.list()).filter(
- (i) => filterModule.getModeByName(i.filterMode) > result.mode
- );
-
- // 没有则跳过
- if (keywords.length === 0) {
- return;
- }
-
- // 根据过滤等级依次判断
- const list = keywords.sort(
- (a, b) =>
- filterModule.getModeByName(b.filterMode) -
- filterModule.getModeByName(a.filterMode)
- );
-
- for (let i = 0; i < list.length; i += 1) {
- const { keyword, filterMode } = list[i];
-
- // 过滤等级,0 为只过滤标题,1 为过滤标题和内容
- const filterLevel = list[i].filterLevel || 0;
-
- // 过滤标题
- if (filterLevel >= 0) {
- const { subject } = item;
-
- const match = subject.match(keyword);
-
- if (match) {
- const mode = filterModule.getModeByName(filterMode);
-
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `关键词: ${match[0]}`;
- return;
- }
- }
-
- // 过滤内容
- if (filterLevel >= 1) {
- // 如果没有内容,则请求
- const content = await (async () => {
- if (item.content === undefined) {
- await getContent().catch(() => {});
- }
-
- return item.content || null;
- })();
-
- if (content) {
- const match = content.match(keyword);
-
- if (match) {
- const mode = filterModule.getModeByName(filterMode);
-
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `关键词: ${match[0]}`;
- return;
- }
- }
- }
- }
- })();
-
- // 杂项过滤
- // 放在属地前是因为符合条件的过多,没必要再请求它们的属地
- await (async () => {
- const { getUserInfo, getReputation } = item;
-
- // 如果当前模式是显示,则跳过
- if (filterModule.getNameByMode(result.mode) === "显示") {
- return;
- }
-
- // 获取隐藏模式下标
- const mode = filterModule.getModeByName("隐藏");
-
- // 匿名
- if (uid <= 0) {
- const filterAnony = dataModule.getFilterAnony();
-
- if (filterAnony) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = "匿名";
- }
-
- return;
- }
-
- // 注册时间过滤
- await (async () => {
- const filterRegdateLimit = dataModule.getFilterRegdateLimit();
-
- // 如果没有用户信息,则请求
- const userInfo = await (async () => {
- if (item.userInfo === undefined) {
- await getUserInfo().catch(() => {});
- }
-
- return item.userInfo || {};
- })();
-
- const { regdate } = userInfo;
-
- if (regdate === undefined) {
- return;
- }
-
- if (
- filterRegdateLimit > 0 &&
- regdate * 1000 > new Date() - filterRegdateLimit
- ) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `注册时间: ${new Date(
- regdate * 1000
- ).toLocaleDateString()}`;
- return;
- }
- })();
-
- // 发帖数量过滤
- await (async () => {
- const filterPostnumLimit = dataModule.getFilterPostnumLimit();
-
- // 如果没有用户信息,则请求
- const userInfo = await (async () => {
- if (item.userInfo === undefined) {
- await getUserInfo().catch(() => {});
- }
-
- return item.userInfo || {};
- })();
-
- const { postnum } = userInfo;
-
- if (postnum === undefined) {
- return;
- }
-
- if (filterPostnumLimit > 0 && postnum < filterPostnumLimit) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `发帖数量: ${postnum}`;
- return;
- }
- })();
-
- // 发帖比例过滤
- await (async () => {
- const filterTopicRateLimit = dataModule.getFilterTopicRateLimit();
-
- // 如果没有用户信息,则请求
- const userInfo = await (async () => {
- if (item.userInfo === undefined) {
- await getUserInfo().catch(() => {});
- }
-
- return item.userInfo || {};
- })();
-
- const { postnum } = userInfo;
-
- if (postnum === undefined) {
- return;
- }
-
- if (filterTopicRateLimit > 0 && filterTopicRateLimit < 100) {
- // 获取主题数量
- const topicNum = await fetchModule.getTopicNum(uid);
-
- // 计算发帖比例
- const topicRate = (topicNum / postnum) * 100;
-
- if (topicRate > filterTopicRateLimit) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `发帖比例: ${topicRate.toFixed(
- 0
- )}% (${topicNum}/${postnum})`;
- return;
- }
- }
- })();
-
- // 版面声望过滤
- await (async () => {
- const filterReputationLimit = dataModule.getFilterReputationLimit();
-
- if (Number.isNaN(filterReputationLimit)) {
- return;
- }
-
- // 如果没有版面声望,则请求
- const reputation = await (async () => {
- if (item.reputation === undefined) {
- await getReputation().catch(() => {});
- }
-
- return item.reputation || NaN;
- })();
-
- if (reputation < filterReputationLimit) {
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `声望: ${reputation}`;
- return;
- }
- })();
- })();
-
- // 属地过滤
- await (async () => {
- // 匿名用户则跳过
- if (uid <= 0) {
- return;
- }
-
- // 获取设置里的属地列表,并跳过低于当前的过滤模式
- const locations = Object.values(locationModule.list()).filter(
- (i) => filterModule.getModeByName(i.filterMode) > result.mode
- );
-
- // 没有则跳过
- if (locations.length === 0) {
- return;
- }
-
- // 请求属地
- const { ipLoc } = await fetchModule.getUserInfo(uid);
-
- // 请求失败则跳过
- if (ipLoc === undefined) {
- return;
- }
-
- // 根据过滤等级依次判断
- const list = locations.sort(
- (a, b) =>
- filterModule.getModeByName(b.filterMode) -
- filterModule.getModeByName(a.filterMode)
- );
-
- for (let i = 0; i < list.length; i += 1) {
- const { keyword, filterMode } = list[i];
-
- const match = ipLoc.match(keyword);
-
- if (match) {
- const mode = filterModule.getModeByName(filterMode);
-
- // 更新过滤模式和原因
- result.mode = mode;
- result.reason = `属地: ${ipLoc}`;
- return;
- }
- }
- })();
-
- // 继承模式下使用默认过滤模式
- if (result.mode === 0) {
- result.mode = filterModule.getModeByName(
- dataModule.getDefaultFilterMode(),
- -1
- );
- }
-
- // 返回过滤结果
- if (result.mode > 0) {
- const { uid, username, tid, pid } = item;
-
- const mode = filterModule.getNameByMode(result.mode);
-
- // 非显示模式下,写入列表
- if (mode !== "显示") {
- const reason = result.reason;
-
- // 用户
- const user = userModule.format(uid, username);
-
- // 移除 BR 标签
- item.content = (item.content || "").replace(/<br>/g, "");
-
- // 主题
- const subject = (() => {
- if (tid) {
- // 如果有 TID 但没有标题,是引用,采用内容逻辑
- if (item.subject.length === 0) {
- return `<a href="${`/read.php?tid=${tid}`}&nofilter">${
- item.content
- }</a>`;
- }
-
- return `<a href="${`/read.php?tid=${tid}`}&nofilter" title="${
- item.content
- }" class="b nobr">${item.subject}</a>`;
- }
-
- return item.subject;
- })();
-
- // 内容
- const content = (() => {
- if (pid) {
- return `<a href="${`/read.php?pid=${pid}`}&nofilter">${
- item.content
- }</a>`;
- }
-
- return item.content;
- })();
-
- listModule.add({
- user,
- mode,
- subject,
- content,
- reason,
- });
- }
-
- return mode;
- }
-
- return "";
- };
-
- // 获取主题过滤方式
- const getFilterModeByTopic = async (topic) => {
- const { tid } = topic;
-
- // 绑定额外的数据请求方式
- if (topic.getContent === undefined) {
- // 获取帖子内容,按需调用
- const getTopic = () =>
- new Promise((resolve, reject) => {
- // 避免重复请求
- if (topic.content || topic.userInfo || topic.reputation) {
- resolve(topic);
- return;
- }
-
- // 请求并写入数据
- fetchModule
- .getUserInfoAndReputation(tid, undefined)
- .then(({ subject, content, userInfo, reputation }) => {
- // 写入用户名
- if (userInfo) {
- topic.username = userInfo.username;
- }
-
- // 写入用户信息和声望
- topic.userInfo = userInfo;
- topic.reputation = reputation;
-
- // 写入帖子标题和内容
- topic.subject = subject;
- topic.content = content;
-
- // 返回结果
- resolve(topic);
- })
- .catch(reject);
- });
-
- // 绑定请求方式
- topic.getContent = getTopic;
- topic.getUserInfo = getTopic;
- topic.getReputation = getTopic;
- }
-
- // 获取过滤模式
- const filterMode = await getFilterMode(topic);
-
- // 返回结果
- return filterMode;
- };
-
- // 获取回复过滤方式
- const getFilterModeByReply = async (reply) => {
- const { tid, pid, uid } = reply;
-
- // 回复页面可以直接获取到用户信息和声望
- if (uid > 0) {
- // 取得用户信息
- const userInfo = commonui.userInfo.users[uid];
-
- // 取得用户声望
- const reputation = (() => {
- const reputations = commonui.userInfo.reputations;
-
- if (reputations) {
- for (let fid in reputations) {
- return reputations[fid][uid] || 0;
- }
- }
-
- return NaN;
- })();
-
- // 写入用户名
- if (userInfo) {
- reply.username = userInfo.username;
- }
-
- // 写入用户信息和声望
- reply.userInfo = userInfo;
- reply.reputation = reputation;
- }
-
- // 绑定额外的数据请求方式
- if (reply.getContent === undefined) {
- // 获取帖子内容,按需调用
- const getReply = () =>
- new Promise((resolve, reject) => {
- // 避免重复请求
- if (reply.userInfo || reply.reputation) {
- resolve(reply);
- return;
- }
-
- // 请求并写入数据
- fetchModule
- .getUserInfoAndReputation(tid, pid)
- .then(({ subject, content, userInfo, reputation }) => {
- // 写入用户名
- if (userInfo) {
- reply.username = userInfo.username;
- }
-
- // 写入用户信息和声望
- reply.userInfo = userInfo;
- reply.reputation = reputation;
-
- // 写入帖子标题和内容
- reply.subject = subject;
- reply.content = content;
-
- // 返回结果
- resolve(reply);
- })
- .catch(reject);
- });
-
- // 绑定请求方式
- reply.getContent = getReply;
- reply.getUserInfo = getReply;
- reply.getReputation = getReply;
- }
-
- // 获取过滤模式
- const filterMode = await getFilterMode(reply);
-
- // 返回结果
- return filterMode;
- };
-
- // 处理引用
- const handleQuote = async (content) => {
- const quotes = content.querySelectorAll(".quote");
-
- await Promise.all(
- [...quotes].map(async (quote) => {
- const uid = (() => {
- const ele = quote.querySelector("a[href^='/nuke.php']");
-
- if (ele) {
- const res = ele.getAttribute("href").match(/uid=(\S+)/);
-
- if (res) {
- return res[1];
- }
- }
-
- return 0;
- })();
-
- const { tid, pid } = (() => {
- const ele = quote.querySelector("[title='快速浏览这个帖子']");
-
- if (ele) {
- const res = ele
- .getAttribute("onclick")
- .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
-
- if (res) {
- return {
- tid: parseInt(res[2], 10),
- pid: parseInt(res[3], 10) || 0,
- };
- }
- }
-
- return {};
- })();
-
- // 获取过滤方式
- const filterMode = await getFilterModeByReply({
- uid,
- tid,
- pid,
- subject: "",
- content: quote.innerText,
- });
-
- (() => {
- if (filterMode === "标记") {
- filterModule.collapse(uid, quote, quote.innerHTML);
- return;
- }
-
- if (filterMode === "遮罩") {
- const source = document.createElement("DIV");
-
- source.innerHTML = quote.innerHTML;
- source.style.display = "none";
-
- const caption = document.createElement("CAPTION");
-
- caption.className = "filter-mask filter-mask-block";
-
- caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
- caption.onclick = () => {
- quote.removeChild(caption);
-
- source.style.display = "";
- };
-
- quote.innerHTML = "";
- quote.appendChild(source);
- quote.appendChild(caption);
- return;
- }
-
- if (filterMode === "隐藏") {
- quote.innerHTML = "";
- return;
- }
- })();
- })
- );
- };
-
- // 过滤主题
- const filterTopic = async (item) => {
- // 绑定事件
- if (item.nFilter === undefined) {
- // 主题 ID
- const tid = item[8];
-
- // 主题标题
- const title =
- typeof item[1] === "string"
- ? document.getElementById(item[1])
- : item[1];
- const subject = title.innerText;
-
- // 主题作者
- const author =
- typeof item[2] === "string"
- ? document.getElementById(item[2])
- : item[2];
- const uid =
- parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
- const username = author.innerText;
-
- // 主题容器
- const container = title.closest("tr");
-
- // 过滤函数
- const execute = async () => {
- // 获取过滤方式
- const filterMode = await getFilterModeByTopic(item.nFilter);
-
- // 样式处理
- (() => {
- // 还原样式
- // TODO 应该整体采用 className 来实现
- (() => {
- // 标记模式
- container.style.removeProperty("textDecoration");
-
- // 遮罩模式
- title.classList.remove("filter-mask");
- author.classList.remove("filter-mask");
- })();
-
- // 样式处理
- (() => {
- // 标记模式下,主题标记会有删除线标识
- if (filterMode === "标记") {
- title.style.textDecoration = "line-through";
- return;
- }
-
- // 遮罩模式下,主题和作者会有遮罩样式
- if (filterMode === "遮罩") {
- title.classList.add("filter-mask");
- author.classList.add("filter-mask");
- return;
- }
-
- // 隐藏模式下,容器会被隐藏
- if (filterMode === "隐藏") {
- container.style.display = "none";
- return;
- }
- })();
-
- // 非隐藏模式下,恢复显示
- if (filterMode !== "隐藏") {
- container.style.removeProperty("display");
- }
- })();
- };
-
- // 绑定事件
- item.nFilter = {
- tid,
- uid,
- username,
- container,
- title,
- author,
- subject,
- execute,
- };
- }
-
- // 等待过滤完成
- await item.nFilter.execute();
- };
-
- // 过滤回复
- const filterReply = async (item) => {
- // 绑定事件
- if (item.nFilter === undefined) {
- // 回复 ID
- const pid = item.pid;
-
- // 判断是否是楼层
- const isFloor = typeof item.i === "number";
-
- // 回复容器
- const container = isFloor
- ? item.uInfoC.closest("tr")
- : item.uInfoC.closest(".comment_c");
-
- // 回复标题
- const title = item.subjectC;
- const subject = title.innerText;
-
- // 回复内容
- const content = item.contentC;
- const contentBak = content.innerHTML;
-
- // 回复作者
- const author = container.querySelector(".posterInfoLine") || item.uInfoC;
- const uid = parseInt(item.pAid, 10) || 0;
- const username = author.querySelector(".author").innerText;
- const avatar = author.querySelector(".avatar");
-
- // 找到用户 ID,将其视为操作按钮
- const action = container.querySelector('[name="uid"]');
-
- // 创建一个元素,用于展示标记列表
- // 贴条和高赞不显示
- const tags = (() => {
- if (isFloor === false) {
- return null;
- }
-
- const element = document.createElement("div");
-
- element.className = "filter-tags";
-
- author.appendChild(element);
-
- return element;
- })();
-
- // 过滤函数
- const execute = async () => {
- // 获取过滤方式
- const filterMode = await getFilterModeByReply(item.nFilter);
-
- // 样式处理
- await (async () => {
- // 还原样式
- // TODO 应该整体采用 className 来实现
- (() => {
- // 标记模式
- if (avatar) {
- avatar.style.removeProperty("display");
- }
-
- content.innerHTML = contentBak;
-
- // 遮罩模式
- const caption = container.parentNode.querySelector("CAPTION");
-
- if (caption) {
- container.parentNode.removeChild(caption);
- container.style.removeProperty("display");
- }
- })();
-
- // 样式处理
- (() => {
- // 标记模式下,隐藏头像,采用泥潭的折叠样式
- if (filterMode === "标记") {
- if (avatar) {
- avatar.style.display = "none";
- }
-
- filterModule.collapse(uid, content, contentBak);
- return;
- }
-
- // 遮罩模式下,楼层会有遮罩样式
- if (filterMode === "遮罩") {
- const caption = document.createElement("CAPTION");
-
- if (isFloor) {
- caption.className = "filter-mask filter-mask-block";
- } else {
- caption.className = "filter-mask filter-mask-block left";
- caption.style.width = "47%";
- }
-
- caption.innerHTML = `<span class="crimson">Troll must die.</span>`;
- caption.onclick = () => {
- const caption = container.parentNode.querySelector("CAPTION");
-
- if (caption) {
- container.parentNode.removeChild(caption);
- container.style.removeProperty("display");
- }
- };
-
- container.parentNode.insertBefore(caption, container);
- container.style.display = "none";
- return;
- }
-
- // 隐藏模式下,容器会被隐藏
- if (filterMode === "隐藏") {
- container.style.display = "none";
- return;
- }
- })();
-
- // 处理引用
- await handleQuote(content);
-
- // 非隐藏模式下,恢复显示
- // 如果是隐藏模式,没必要再加载按钮和标记
- if (filterMode !== "隐藏") {
- // 获取当前用户
- const user = userModule.get(uid);
-
- // 修改操作按钮颜色
- if (action) {
- if (user) {
- action.style.background = "#CB4042";
- } else {
- action.style.background = "#AAA";
- }
- }
-
- // 加载标记
- if (tags) {
- const list = user
- ? user.tags.map((id) => tagModule.get({ id })) || []
- : [];
-
- tags.style.display = list.length ? "" : "none";
- tags.innerHTML = list
- .map((tag) => tagModule.format(tag.id))
- .join("");
- }
-
- // 恢复显示
- container.style.removeProperty("display");
- }
- })();
- };
-
- // 绑定操作按钮事件
- (() => {
- if (action) {
- // 隐藏匿名操作按钮
- if (uid <= 0) {
- action.style.display = "none";
- return;
- }
-
- action.innerHTML = `屏蔽`;
- action.onclick = (e) => {
- const user = userModule.get(uid);
-
- if (e.ctrlKey === false) {
- userModule.view.details(uid, username, () => {
- execute();
- });
- return;
- }
-
- if (user) {
- userModule.remove(uid);
- } else {
- userModule.add(uid, username, [], filterModule.defaultMode);
- }
-
- execute(true);
- };
- }
- })();
-
- // 绑定事件
- item.nFilter = {
- pid,
- uid,
- username,
- container,
- title,
- author,
- subject,
- content: content.innerText,
- execute,
- };
- }
-
- // 等待过滤完成
- await item.nFilter.execute();
- };
-
- // 加载 UI
- const loadUI = () => {
- // 右上角菜单
- const result = (() => {
- let window;
-
- return ui.create(() => {
- if (window === undefined) {
- window = commonui.createCommmonWindow();
- }
-
- window._.addContent(null);
- window._.addTitle(`屏蔽`);
- window._.addContent(ui.content);
- window._.show();
- });
- })();
-
- // 加载失败
- if (result === false) {
- return;
- }
-
- // 模块
- ui.addModule("列表", listModule.view).toggle();
- ui.addModule("用户", userModule.view);
- ui.addModule("标记", tagModule.view);
- ui.addModule("关键字", keywordModule.view);
- ui.addModule("属地", locationModule.view);
- ui.addModule("通用设置", commonModule.view);
-
- // 绑定列表更新回调
- listModule.bindCallback(ui.update);
- };
-
- // 处理 mainMenu 模块
- const handleMenu = () => {
- let init = menuModule.init;
-
- // 劫持 init 函数,这个函数完成后才能添加 UI
- Object.defineProperty(menuModule, "init", {
- get: () => {
- return (...arguments) => {
- // 等待执行完毕
- init.apply(menuModule, arguments);
-
- // 加载 UI
- loadUI();
- };
- },
- set: (value) => {
- init = value;
- },
- });
-
- // 如果已经有模块,则直接加载 UI
- if (init) {
- loadUI();
- }
- };
-
- // 处理 topicArg 模块
- const handleTopicModule = async () => {
- let add = topicModule.add;
-
- // 劫持 add 函数,这是泥潭的主题添加事件
- Object.defineProperty(topicModule, "add", {
- get: () => {
- return async (...arguments) => {
- // 主题 ID
- const tid = arguments[8];
-
- // 先直接隐藏,等过滤完毕后再放出来
- (() => {
- // 主题标题
- const title = document.getElementById(arguments[1]);
-
- // 主题容器
- const container = title.closest("tr");
-
- // 隐藏元素
- container.style.display = "none";
- })();
-
- // 加入列表
- add.apply(topicModule, arguments);
-
- // 找到对应数据
- const topic = topicModule.data.find((item) => item[8] === tid);
-
- // 开始过滤
- await filterTopic(topic);
- };
- },
- set: (value) => {
- add = value;
- },
- });
-
- // 如果已经有数据,则直接过滤
- if (topicModule.data) {
- await Promise.all(Object.values(topicModule.data).map(filterTopic));
- }
- };
-
- // 处理 postArg 模块
- const handleReplyModule = async () => {
- let proc = replyModule.proc;
-
- // 劫持 proc 函数,这是泥潭的回复添加事件
- Object.defineProperty(replyModule, "proc", {
- get: () => {
- return async (...arguments) => {
- // 楼层号
- const index = arguments[0];
-
- // 先直接隐藏,等过滤完毕后再放出来
- (() => {
- // 判断是否是楼层
- const isFloor = typeof index === "number";
-
- // 评论额外标签
- const prefix = isFloor ? "" : "comment";
-
- // 用户容器
- const uInfoC = document.querySelector(
- `#${prefix}posterinfo${index}`
- );
-
- // 回复容器
- const container = isFloor
- ? uInfoC.closest("tr")
- : uInfoC.closest(".comment_c");
-
- // 隐藏元素
- container.style.display = "none";
- })();
-
- // 加入列表
- proc.apply(replyModule, arguments);
-
- // 找到对应数据
- const reply = replyModule.data[index];
-
- // 开始过滤
- await filterReply(reply);
- };
- },
- set: (value) => {
- proc = value;
- },
- });
-
- // 如果已经有数据,则直接过滤
- if (replyModule.data) {
- await Promise.all(Object.values(replyModule.data).map(filterReply));
- }
- };
-
- // 处理 commonui 模块
- const handleCommonui = () => {
- // 监听 mainMenu 模块,UI 需要等待这个模块加载完成
- (() => {
- if (commonui.mainMenu) {
- menuModule = commonui.mainMenu;
-
- handleMenu();
- return;
- }
-
- Object.defineProperty(commonui, "mainMenu", {
- get: () => menuModule,
- set: (value) => {
- menuModule = value;
-
- handleMenu();
- },
- });
- })();
-
- // 监听 topicArg 模块,这是泥潭的主题入口
- (() => {
- if (commonui.topicArg) {
- topicModule = commonui.topicArg;
-
- handleTopicModule();
- return;
- }
-
- Object.defineProperty(commonui, "topicArg", {
- get: () => topicModule,
- set: (value) => {
- topicModule = value;
-
- handleTopicModule();
- },
- });
- })();
-
- // 监听 postArg 模块,这是泥潭的回复入口
- (() => {
- if (commonui.postArg) {
- replyModule = commonui.postArg;
-
- handleReplyModule();
- return;
- }
-
- Object.defineProperty(commonui, "postArg", {
- get: () => replyModule,
- set: (value) => {
- replyModule = value;
-
- handleReplyModule();
- },
- });
- })();
- };
-
- // 前置过滤
- const handlePreFilter = () => {
- // 监听 commonui 模块,这是泥潭的主入口
- (() => {
- if (unsafeWindow.commonui) {
- commonui = unsafeWindow.commonui;
-
- handleCommonui();
- return;
- }
-
- Object.defineProperty(unsafeWindow, "commonui", {
- get: () => commonui,
- set: (value) => {
- commonui = value;
-
- handleCommonui();
- },
- });
- })();
- };
-
- // 普通过滤
- const handleFilter = () => {
- const runFilter = async () => {
- if (topicModule) {
- await Promise.all(
- Object.values(topicModule.data).map((item) => {
- if (item.executed) {
- return;
- }
-
- item.executed = true;
-
- filterTopic(item);
- })
- );
- }
-
- if (replyModule) {
- await Promise.all(
- Object.values(replyModule.data).map((item) => {
- if (item.executed) {
- return;
- }
-
- item.executed = true;
-
- filterReply(item);
- })
- );
- }
- };
-
- const hookFunction = (object, functionName, callback) => {
- ((originalFunction) => {
- object[functionName] = function () {
- const returnValue = originalFunction.apply(this, arguments);
-
- callback.apply(this, [returnValue, originalFunction, arguments]);
-
- return returnValue;
- };
- })(object[functionName]);
- };
-
- const initialized = {
- topicArg: false,
- postArg: false,
- };
-
- hookFunction(commonui, "eval", () => {
- if (Object.values(initialized).findIndex((item) => item === false) < 0) {
- return;
- }
-
- if (commonui.topicArg && initialized.topicArg === false) {
- hookFunction(commonui.topicArg, "add", runFilter);
-
- initialized.topicArg = true;
-
- topicModule = commonui.topicArg;
- }
-
- if (commonui.postArg && initialized.postArg === false) {
- hookFunction(commonui.postArg, "proc", runFilter);
-
- initialized.postArg = true;
-
- replyModule = commonui.postArg;
- }
- });
-
- runFilter();
- };
-
- // 主函数
- (() => {
- // 前置过滤
- if (preFilter) {
- handlePreFilter();
- return;
- }
-
- // 等待页面加载完毕后过滤
- unsafeWindow.addEventListener("load", () => {
- if (unsafeWindow.commonui === undefined) {
- return;
- }
-
- commonui = unsafeWindow.commonui;
-
- menuModule = commonui.mainMenu;
- topicModule = commonui.topicArg;
- replyModule = commonui.postArg;
-
- loadUI();
- handleFilter();
- });
- })();
- })();