NGA 库,包括工具类、缓存、API
Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greasyfork.org/scripts/486070/1552669/NGA%20Library.js
// ==UserScript== // @name NGA Library // @namespace https://greasyfork.org/users/263018 // @version 1.2.1 // @author snyssss // @description NGA 库,包括工具类、缓存、API // @license MIT // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant unsafeWindow // ==/UserScript== /** * 工具类 */ 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 } ) => { // 判断目标对象是否存在 if (target === undefined) { return; } // 判断是否已被拦截 const isIntercepted = (() => { const descriptor = Object.getOwnPropertyDescriptor(target, property); if (descriptor && descriptor.get && descriptor.set) { return true; } return false; })(); // 初始化目标对象的拦截列表 target.interceptions = target.interceptions || {}; target.interceptions[property] = target.interceptions[property] || { data: target[property], beforeGetQueue: [], beforeSetQueue: [], afterGetQueue: [], afterSetQueue: [], }; // 写入事件 Object.entries({ beforeGetQueue: beforeGet, beforeSetQueue: beforeSet, afterGetQueue: afterGet, afterSetQueue: afterSet, }).forEach(([queue, event]) => { if (event) { target.interceptions[property][queue].push(event); } }); // 拦截 if (isIntercepted === false) { // 定义属性 Object.defineProperty(target, property, { get: () => { // 获取事件 const { data, beforeGetQueue, afterGetQueue } = target.interceptions[property]; // 如果是函数 if (this.isType(data, "function")) { return (...args) => { try { // 执行前操作 // 可以在这一步修改参数 // 可以通过在这一步抛出来阻止执行 if (beforeGetQueue) { beforeGetQueue.forEach((event) => { args = event.apply(target, args); }); } // 执行函数 const r###lt = data.apply(target, args); // 执行后操作 if (afterGetQueue) { // 返回的可能是一个 Promise const r###ltValue = r###lt instanceof Promise ? r###lt : Promise.resolve(r###lt); r###ltValue.then((value) => { afterGetQueue.forEach((event) => { event.apply(target, [value, args, data]); }); }); } // 返回结果 return r###lt; } catch { return undefined; } }; } try { // 返回前操作 // 可以在这一步修改返回结果 // 可以通过在这一步抛出来返回 undefined let r###lt = data; if (beforeGetQueue) { beforeGetQueue.forEach((event) => { r###lt = event.apply(target, [r###lt]); }); } // 返回后操作 // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 afterGet 来操作处理后的数据 if (afterGetQueue) { afterGetQueue.forEach((event) => { event.apply(target, [r###lt, data]); }); } // 返回结果 return r###lt; } catch { return undefined; } }, set: (value) => { // 获取事件 const { data, beforeSetQueue, afterSetQueue } = target.interceptions[property]; // 声明结果 let r###lt = value; try { // 写入前操作 // 可以在这一步修改写入结果 // 可以通过在这一步抛出来写入 undefined if (beforeSetQueue) { beforeSetQueue.forEach((event) => { r###lt = event.apply(target, [data, r###lt]); }); } // 写入可能的事件 if (this.isType(data, "object")) { r###lt.interceptions = data.interceptions; } // 写入后操作 if (afterSetQueue) { afterSetQueue.forEach((event) => { event.apply(target, [r###lt, value]); }); } } catch { r###lt = undefined; } finally { // 写入结果 target.interceptions[property].data = r###lt; // 返回结果 return r###lt; } }, }); } // 如果已经有结果,则直接处理写入后操作 if (Object.hasOwn(target, property)) { if (afterSet) { afterSet.apply(target, [target.interceptions[property].data]); } } }; /** * 合并数据 * @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.r###lt.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 findPairEndIndex = (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 = findPairEndIndex(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 = findPairEndIndex(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 = ((h, s, v) => { const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); return [f(5), f(3), f(1)]; })(hsv[0], hsv[1], hsv[2]); return ["#", ...rgb].reduce((a, b) => { return a + ("0" + b.toString(16)).slice(-2); }); } /** * 添加样式 * * @param {String} css 样式信息 * @param {String} id 样式 ID * @returns {HTMLStyleElement} 样式元素 */ static addStyle(css, id = "s-" + Math.random().toString(36).slice(2)) { let element = document.getElementById(id); if (element === null) { element = document.createElement("STYLE"); element.id = id; document.head.appendChild(element); } element.textContent = css; return element; } /** * 计算时间是否为今天 * @param {Date} date 时间 * @returns {Boolean} */ static dateIsToday(date) { const now = new Date(); return ( date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ); } /** * 计算时间差 * @param {Date} start 开始时间 * @param {Date} end 结束时间 * @returns {object} 时间差 */ static dateDiff(start, end = new Date()) { if (start > end) { return dateDiff(end, start); } const startYear = start.getFullYear(); const startMonth = start.getMonth(); const startDay = start.getDate(); const endYear = end.getFullYear(); const endMonth = end.getMonth(); const endDay = end.getDate(); const diff = { years: endYear - startYear, months: endMonth - startMonth, days: endDay - startDay, }; const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if ( startYear % 400 === 0 || (startYear % 100 !== 0 && startYear % 4 === 0) ) { daysInMonth[1] = 29; } if (diff.months < 0) { diff.years -= 1; diff.months += 12; } if (diff.days < 0) { if (diff.months === 0) { diff.years -= 1; diff.months = 11; } else { diff.months -= 1; } diff.days += daysInMonth[startMonth]; } return diff; } } /** * 简单队列 */ class Queue { /** * 任务队列 */ queue = {}; /** * 当前状态 - IDLE, RUNNING, PAUSED */ state = "IDLE"; /** * 异常暂停时间 */ pauseTime = 1000 * 60 * 5; /** * 添加任务 * @param {string} key 标识 * @param {() => Promise} task 任务 */ enqueue(key, task) { if (Object.hasOwn(this.queue, key)) { return; } this.queue[key] = task; this.run(); } /** * 移除任务 * @param {string} key 标识 */ dequeue(key) { if (Object.hasOwn(this.queue, key) === false) { return; } delete this.queue[key]; } /** * 执行任务 */ run() { // 非空闲状态,直接返回 if (this.state !== "IDLE") { return; } // 获取任务队列标识 const keys = Object.keys(this.queue); // 任务队列为空,直接返回 if (keys.length === 0) { return; } // 标记为执行中 this.state = "RUNNING"; // 取得第一个任务 const key = keys[0]; // 执行任务 this.queue[key]() .then(() => { // 移除任务 this.dequeue(key); }) .catch(async () => { // 标记为暂停 this.state = "PAUSED"; // 等待指定时间 await new Promise((resolve) => { setTimeout(resolve, this.pauseTime); }); }) .finally(() => { // 标记为空闲 this.state = "IDLE"; // 执行下一个任务 this.run(); }); } } /** * 初始化缓存和 API */ const initCacheAndAPI = (() => { // KEY const USER_AGENT_KEY = "USER_AGENT_KEY"; /** * 数据库名称 */ const name = "NGA_Storage"; /** * 模块列表 */ const modules = { TOPIC_NUM_CACHE: { keyPath: "uid", version: 1, indexes: ["timestamp"], expireTime: 1000 * 60 * 60, persistent: true, }, USER_INFO_CACHE: { keyPath: "uid", version: 1, indexes: ["timestamp"], expireTime: 1000 * 60 * 60, persistent: false, }, USER_IPLOC_CACHE: { keyPath: "uid", version: 1, indexes: ["timestamp"], expireTime: 1000 * 60 * 60, persistent: true, }, PAGE_CACHE: { keyPath: "url", version: 1, indexes: ["timestamp"], expireTime: 1000 * 60 * 10, persistent: false, }, FORUM_POSTED_CACHE: { keyPath: "url", version: 1, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: true, }, USER_NAME_CHANGED: { keyPath: "uid", version: 2, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: true, }, USER_STEAM_INFO: { keyPath: "uid", version: 3, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: true, }, USER_PSN_INFO: { keyPath: "uid", version: 3, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: true, }, USER_NINTENDO_INFO: { keyPath: "uid", version: 3, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: true, }, USER_GENSHIN_INFO: { keyPath: "uid", version: 3, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: false, }, USER_SKZY_INFO: { keyPath: "uid", version: 3, indexes: ["timestamp"], expireTime: 1000 * 60 * 60 * 24, persistent: false, }, }; /** * IndexedDB * * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑 */ class DBStorage { /** * 当前实例 */ instance = null; /** * 是否支持 */ 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(modules) .map(({ version }) => version) .reduce((a, b) => Math.max(a, b), 0); // 创建请求 const request = unsafeWindow.indexedDB.open(name, version); // 创建或者升级表 request.onupgradeneeded = (event) => { this.instance = event.target.r###lt; const transaction = event.target.transaction; const oldVersion = event.oldVersion; Object.entries(modules).forEach(([key, values]) => { if (values.version > oldVersion) { queue.push(this.createOrUpdateStore(key, values, transaction)); } }); }; // 成功后处理 request.onsuccess = (event) => { this.instance = event.target.r###lt; 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 r###lt = await this.bulkGet(name, [], transaction); if (r###lt) { data.push(...r###lt); } // 删除旧表 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 {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range 清除范围 * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务 * @returns {Promise} */ async clear(name, range = null, transaction = null) { // 获取表 const store = await this.getStore(name, transaction, "readwrite"); // 清除全部数据 if (range === null) { // 清空数据 await new Promise((resolve, reject) => { // 创建请求 const request = store.clear(); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); return; } // 请求范围 const request = range(store); // 成功后删除数据 request.onsuccess = (event) => { const cursor = event.target.r###lt; if (cursor) { store.delete(cursor.primaryKey); cursor.continue(); } }; } /** * 插入指定表的数据 * @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 r###lt = await new Promise((resolve, reject) => { // 创建请求 const request = store.add(data); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); // 返回结果 return r###lt; } /** * 删除指定表的数据 * @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 r###lt = await new Promise((resolve, reject) => { // 创建请求 const request = store.delete(key); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); // 返回结果 return r###lt; } /** * 插入或修改指定表的数据 * @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 r###lt = await new Promise((resolve, reject) => { // 创建请求 const request = store.put(data); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); // 返回结果 return r###lt; } /** * 获取指定表的数据 * @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 r###lt = await new Promise((resolve, reject) => { // 创建请求 const request = store.get(key); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); // 返回结果 return r###lt; } /** * 批量插入指定表的数据 * @param {String} name 表名 * @param {Array} data 数据集合 * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务 * @returns {Promise<number>} 成功数量 */ async bulkAdd(name, data, transaction = null) { // 等待操作结果 const r###lt = await Promise.all( data.map((item) => this.add(name, item, transaction) .then(() => true) .catch(() => false) ) ); // 返回受影响的数量 return r###lt.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) { // 清空数据 await this.clear(name, null, transaction); return -1; } // 等待操作结果 const r###lt = await Promise.all( keys.map((item) => this.delete(name, item, transaction) .then(() => true) .catch(() => false) ) ); // 返回受影响的数量 return r###lt.filter((item) => item).length; } /** * 批量插入或修改指定表的数据 * @param {String} name 表名 * @param {Array} data 数据集合 * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务 * @returns {Promise<number>} 成功数量 */ async bulkPut(name, data, transaction = null) { // 等待操作结果 const r###lt = await Promise.all( data.map((item) => this.put(name, item, transaction) .then(() => true) .catch(() => false) ) ); // 返回受影响的数量 return r###lt.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 r###lt = await new Promise((resolve, reject) => { // 创建请求 const request = store.getAll(); // 成功后处理 request.onsuccess = (event) => { resolve(event.target.r###lt || []); }; // 失败后处理 request.onerror = (event) => { reject(event); }; }); // 返回结果 return r###lt; } // 返回符合的结果 const r###lt = []; await Promise.all( keys.map((key) => this.get(name, key, transaction) .then((item) => { r###lt.push(item); }) .catch(() => {}) ) ); return r###lt; } } /** * 油猴存储 * * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise */ class GMStorage extends DBStorage { /** * 清除指定表的数据 * @param {String} name 表名 * @param {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range 清除范围 * @returns {Promise} */ async clear(name, range = null) { // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.clear(name, range); } // 清除全部数据 GM_setValue(name, {}); } /** * 插入指定表的数据 * @param {String} name 表名 * @param {*} data 数据 * @returns {Promise} */ async add(name, data) { // 如果不在模块列表里,写入全部数据 if (Object.hasOwn(modules, name) === false) { return GM_setValue(name, data); } // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.add(name, data); } // 获取对应的主键 const keyPath = 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(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(modules, name) === false) { return GM_setValue(name, data); } // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.put(name, data); } // 获取对应的主键 const keyPath = 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(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(modules, name) === false) { return GM_setValue(name, {}); } // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.bulkAdd(name, data); } // 获取对应的主键 const keyPath = modules[name].keyPath; // 获取全部数据 const values = GM_getValue(name, {}); // 添加数据 const r###lt = 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 r###lt.filter((item) => item).length; } /** * 批量删除指定表的数据 * @param {String} name 表名 * @param {Array<String>} keys 主键集合,空则删除全部 * @returns {Promise<number>} 成功数量,删除全部时返回 -1 */ async bulkDelete(name, keys = []) { // 如果不在模块列表里,忽略 keys,删除全部数据 if (Object.hasOwn(modules, name) === false) { return GM_setValue(name, {}); } // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.bulkDelete(name, keys); } // 如果 keys 为空,删除全部数据 if (keys.length === 0) { await this.clear(name, null); return -1; } // 获取全部数据 const values = GM_getValue(name, {}); // 删除数据 const r###lt = keys.map((key) => { // 如果对应主键不存在,抛出异常 if (Object.hasOwn(values, key) === false) { return false; } // 删除数据 delete values[key]; return true; }); // 保存数据 GM_setValue(name, values); // 返回受影响的数量 return r###lt.filter((item) => item).length; } /** * 批量插入或修改指定表的数据 * @param {String} name 表名 * @param {Array} data 数据集合 * @returns {Promise<number>} 成功数量 */ async bulkPut(name, data) { // 如果不在模块列表里,写入全部数据 if (Object.hasOwn(modules, name) === false) { return GM_setValue(name, data); } // 如果支持 IndexedDB,使用 IndexedDB if (super.isSupport()) { return super.bulkPut(name, keys); } // 获取对应的主键 const keyPath = modules[name].keyPath; // 获取全部数据 const values = GM_getValue(name, {}); // 添加数据 const r###lt = data.map((item) => { const key = item[keyPath]; // 如果数据中不包含主键,抛出异常 if (key === undefined) { return false; } // 插入数据 values[key] = item; return true; }); // 保存数据 GM_setValue(name, values); // 返回受影响的数量 return r###lt.filter((item) => item).length; } /** * 批量获取指定表的数据,如果不在模块列表里,返回全部数据 * @param {String} name 表名 * @param {Array<String>} keys 主键集合,空则获取全部 * @returns {Promise<Array>} 数据集合 */ async bulkGet(name, keys = []) { // 如果不在模块列表里,忽略 keys,返回全部数据 if (Object.hasOwn(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 r###lt = []; keys.forEach((key) => { if (Object.hasOwn(values, key)) { r###lt.push(values[key]); } }); return r###lt; } } /** * 缓存管理 * * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存 */ class Cache extends GMStorage { /** * 清除指定表的数据 * @param {String} name 表名 * @param {Boolean} onlyExpire 是否只清除超时数据 * @returns {Promise} */ async clear(name, onlyExpire = false) { // 如果不在模块里,直接清除 if (Object.hasOwn(modules, name) === false) { return super.clear(name); } // 如果只清除超时数据为否,直接清除 if (onlyExpire === false) { return super.clear(name); } // 读取模块配置 const { expireTime, persistent } = modules[name]; // 持久化 if (persistent) { return; } // 清除超时数据 return super.clear(name, (store) => store .index("timestamp") .openKeyCursor(IDBKeyRange.upperBound(Date.now() - expireTime)) ); } /** * 插入指定表的数据,并增加 timestamp * @param {String} name 表名 * @param {*} data 数据 * @returns {Promise} */ async add(name, data) { // 如果在模块里,增加 timestamp if (Object.hasOwn(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(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(modules, name) === false) { return value; } // 如果有结果的话,移除超时数据 if (value) { // 读取模块配置 const { expireTime, persistent } = 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(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(modules, name) === false) { force = true; } // 强制删除 if (force) { return super.bulkDelete(name, keys); } // 批量获取指定表的数据,并移除过期数据 const r###lt = this.bulkGet(name, keys); // 返回成功数量 if (keys.length === 0) { return -1; } return keys.length - r###lt.length; } /** * 批量插入或修改指定表的数据,并增加 timestamp * @param {String} name 表名 * @param {Array} data 数据集合 * @returns {Promise<number>} 成功数量 */ async bulkPut(name, data) { // 如果在模块里,增加 timestamp if (Object.hasOwn(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(modules, name) === false) { return values; } // 读取模块配置 const { keyPath, expireTime, persistent } = modules[name]; // 筛选出超时数据 const r###lt = []; const expired = []; values.forEach((value) => { // 持久化或未超时 if (persistent || value.timestamp + expireTime > new Date().getTime()) { r###lt.push(value); return; } // 记录超时数据 expired.push(value[keyPath]); }); // 移除超时数据 await super.bulkDelete(name, expired); // 返回结果 return r###lt; } } /** * API */ class API { /** * 缓存管理 */ cache; /** * 队列 */ queue; /** * 初始化并绑定缓存管理 * @param {Cache} cache 缓存管理 */ constructor(cache) { this.cache = cache; this.queue = new Queue(); } /** * 简单的统一请求 * @param {String} url 请求地址 * @param {Object} config 请求参数 * @param {Boolean} toJSON 是否转为 JSON 格式 */ async request(url, config = {}, toJSON = true) { const userAgent = (await this.cache.get(USER_AGENT_KEY)) || "Nga_Official"; const response = await fetch(url, { headers: { Referer: "", "X-User-Agent": userAgent, }, ...config, }); const r###lt = await Tools.readForumData(response, toJSON); return r###lt; } /** * 获取用户主题数量 * @param {number} uid 用户 ID */ async getTopicNum(uid) { const name = "TOPIC_NUM_CACHE"; const { expireTime } = 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 { posts } = await this.getUserInfo(uid); // 发帖数量不准,且可能错误的返回 0 const value = posts || 0; // 发帖数量在泥潭其他接口里命名为 postnum const postnum = (() => { if (value > 0) { return value; } if (cache) { return cache.postnum || 0; } return 0; })(); // 当发帖数量发生变化时,再重新请求数据 const needRequest = (() => { if (value > 0 && cache) { return cache.postnum !== value; } return true; })(); // 需要重新请求 if (needRequest) { // 由于泥潭接口限制,同步使用队列请求数据 const task = () => new Promise(async (resolve, reject) => { const r###lt = await this.request(api); // 服务器可能返回错误,遇到这种情况下,需要保留缓存 if (r###lt.data && Number.isInteger(r###lt.data.__ROWS)) { this.cache.put(name, { uid, count: r###lt.data.__ROWS, rencentTopics: r###lt.data.__T, postnum, }); resolve(); return; } reject(); }); // 先尝试请求一次,成功后直接返回结果,否则加入队列 try { if (this.queue.state === "IDLE") { await task(); const { count } = await this.cache.get(name, uid); return count; } throw new Error(); } catch { this.queue.enqueue(uid, task); } } // 直接返回缓存结果 const count = cache ? cache.count : 0; const rencentTopics = cache ? cache.rencentTopics : []; // 更新缓存 this.cache.put(name, { uid, count, rencentTopics, postnum, }); return count; } /** * 获取用户近期主题 * @param {number} uid 用户 ID */ async getTopicRencent(uid) { const name = "TOPIC_NUM_CACHE"; // 请求用户主题数量 const count = await this.getTopicNum(uid); // 如果存在结果,读取缓存 if (count > 0) { const cache = await this.cache.get(name, uid); if (cache) { return cache.rencentTopics || []; } } return []; } /** * 获取用户信息 * @param {number} uid 用户 ID */ async getUserInfo(uid) { const name = "USER_INFO_CACHE"; const api = `nuke.php?func=ucp&uid=${uid}`; const cache = await this.cache.get(name, uid); if (cache) { return cache.data; } const r###lt = await this.request(api, {}, false); const data = (() => { const text = Tools.searchPair(r###lt, `__UCPUSER =`); if (text) { try { return JSON.parse(text); } catch { return null; } } return null; })(); if (data) { this.cache.put(name, { uid, data, }); } return data || {}; } /** * 获取属地列表 * @param {number} uid 用户 ID */ async getIpLocations(uid) { const name = "USER_IPLOC_CACHE"; const { expireTime } = modules[name]; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 属地列表 const data = cache ? cache.data : []; // 请求属地 const { ipLoc } = await this.getUserInfo(uid); // 写入缓存 if (ipLoc) { const index = data.findIndex((item) => { return item.ipLoc === ipLoc; }); if (index >= 0) { data.splice(index, 1); } data.unshift({ ipLoc, timestamp: new Date().getTime(), }); 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 r###lt = await this.request(api, {}, false); const parser = new DOMParser(); const doc = parser.parseFromString(r###lt, "text/html"); // 声明返回值 const data = { subject: "", content: "", userInfo: null, reputation: NaN, }; // 验证帖子正常 const verify = doc.querySelector("#m_posts"); if (verify) { // 取得顶楼 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(r###lt, `"${data.uid}":`); if (text) { try { return JSON.parse(text); } catch { return null; } } return null; })(); // 取得用户声望 data.reputation = (() => { const reputations = (() => { const text = Tools.searchPair(r###lt, `"__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 r###lt = await this.request(api); const info = r###lt.data ? r###lt.data.__F : null; return info; } /** * 获取版面发言记录 * @param {number} fid 版面 ID * @param {number} uid 用户 ID */ async getForumPosted(fid, uid) { const name = "FORUM_POSTED_CACHE"; const { expireTime } = 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 r###lt = await this.request(url, {}, false); // 将所有匹配的 FID 写入缓存,即使并不在设置里 const matched = r###lt.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 (r###lt.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; } /** * 获取用户的曾用名 * @param {number} uid 用户 ID */ async getUsernameChanged(uid) { const name = "USER_NAME_CHANGED"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=ucp&__act=oldname&uid=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求用户信息 const { usernameChanged } = await this.getUserInfo(uid); // 如果有修改记录 if (usernameChanged) { // 请求数据 const r###lt = await this.request(api); // 取得结果 const data = r###lt.data ? r###lt.data[0] : null; // 更新缓存 this.cache.put(name, { uid, data, }); return data; } return null; } /** * 获取用户绑定的 Steam 信息 * @param {number} uid 用户 ID */ async getSteamInfo(uid) { const name = "USER_STEAM_INFO"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=steam&__act=steam_user_info&user_id=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求数据 // Steam ID 64 位会超出 JavaScript Number 长度,需要手动处理 const r###lt = await this.request( api, { method: "POST", }, false ); // 先转换成 JSON const r###ltJSON = JSON.parse(r###lt); // 取得结果 const data = r###ltJSON.data ? r###ltJSON.data[0] : null; // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式 if (data.steam_user_id) { const matched = r###lt.match(/"steam_user_id":(\d+),/); if (matched) { data.steam_user_id = String(matched[1]); } } // 更新缓存 this.cache.put(name, { uid, data, }); return data; } /** * 获取用户绑定的 PSN 信息 * @param {number} uid 用户 ID */ async getPSNInfo(uid) { const name = "USER_PSN_INFO"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=psn&__act=psn_user_info&user_id=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求数据 // PSN ID 64 位会超出 JavaScript Number 长度,需要手动处理 const r###lt = await this.request( api, { method: "POST", }, false ); // 先转换成 JSON const r###ltJSON = JSON.parse(r###lt); // 取得结果 const data = r###ltJSON.data ? r###ltJSON.data[0] : null; // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式 if (data.psn_user_id) { const matched = r###lt.match(/"psn_user_id":(\d+),/); if (matched) { data.psn_user_id = String(matched[1]); } } // 更新缓存 this.cache.put(name, { uid, data, }); return data; } /** * 获取用户绑定的 NS 信息 * @param {number} uid 用户 ID */ async getNintendoInfo(uid) { const name = "USER_NINTENDO_INFO"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=nintendo&__act=user_info&user_id=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求数据 const r###lt = await this.request(api, { method: "POST", }); // 取得结果 const data = r###lt.data ? r###lt.data[0] : null; // 更新缓存 this.cache.put(name, { uid, data, }); return data; } /** * 获取用户绑定的原神信息 * @param {number} uid 用户 ID */ async getGenshinInfo(uid) { const name = "USER_GENSHIN_INFO"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=genshin&__act=get_user&uid=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求数据 const r###lt = await this.request(api, { method: "POST", }); // 取得结果 const data = r###lt.data ? r###lt.data[0] : null; // 更新缓存 this.cache.put(name, { uid, data, }); return data; } /** * 获取用户绑定的深空之眼信息 * @param {number} uid 用户 ID */ async getSKZYInfo(uid) { const name = "USER_SKZY_INFO"; const { expireTime } = modules[name]; const api = `/nuke.php?lite=js&__lib=auth_ys4fun&__act=skzy_user_game&user_id=${uid}`; const cache = await this.cache.get(name, uid); // 仍在缓存期间内,直接返回 if (cache) { const expired = cache.timestamp + expireTime < new Date().getTime(); if (expired === false) { return cache.data; } } // 请求数据 const r###lt = await this.request(api, { method: "POST", }); // 取得结果 const data = r###lt.data ? r###lt.data[0] : null; // 更新缓存 this.cache.put(name, { uid, data, }); return data; } /** * 获取用户绑定的游戏信息 * @param {number} uid 用户 ID */ async getUserGameInfo(uid) { // 请求 Steam 信息 const steam = await this.getSteamInfo(uid); // 请求 PSN 信息 const psn = await this.getPSNInfo(uid); // 请求 NS 信息 const nintendo = await this.getNintendoInfo(uid); // 请求原神信息 const genshin = await this.getGenshinInfo(uid); // 请求深空之眼信息 const skzy = await this.getSKZYInfo(uid); // 返回结果 return { steam, psn, nintendo, genshin, skzy, }; } } /** * 注册脚本菜单 * @param {Cache} cache 缓存管理 */ const registerMenu = async (cache) => { const data = (await cache.get(USER_AGENT_KEY)) || "Nga_Official"; GM_registerMenuCommand(`修改UA:${data}`, () => { const value = prompt("修改UA", data); if (value) { cache.put(USER_AGENT_KEY, value); location.reload(); } }); }; /** * 自动清理缓存 * @param {Cache} cache 缓存管理 */ const autoClear = async (cache) => Promise.all(Object.keys(modules).map((name) => cache.clear(name, true))); // 初始化事件 return () => { // 防止重复初始化 if (unsafeWindow.NLibrary === undefined) { // 初始化缓存和 API const cache = new Cache(); const api = new API(cache); // 自动清理缓存 autoClear(cache); // 写入全局变量 unsafeWindow.NLibrary = { cache, api, }; } const { cache, api } = unsafeWindow.NLibrary; // 注册脚本菜单 registerMenu(cache); // 返回结果 return { cache, api, }; }; })();