NGA Library

NGA 库,包括工具类、缓存、API

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/486070/1552669/NGA%20Library.js

  1. // ==UserScript==
  2. // @name NGA Library
  3. // @namespace https://greatest.deepsurf.us/users/263018
  4. // @version 1.2.1
  5. // @author snyssss
  6. // @description NGA 库,包括工具类、缓存、API
  7. // @license MIT
  8.  
  9. // @match *://bbs.nga.cn/*
  10. // @match *://ngabbs.com/*
  11. // @match *://nga.178.com/*
  12.  
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_registerMenuCommand
  16. // @grant unsafeWindow
  17. // ==/UserScript==
  18.  
  19. /**
  20. * 工具类
  21. */
  22. class Tools {
  23. /**
  24. * 返回当前值的类型
  25. * @param {*} value 值
  26. * @returns {String} 值的类型
  27. */
  28. static getType = (value) => {
  29. return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
  30. };
  31.  
  32. /**
  33. * 返回当前值是否为指定的类型
  34. * @param {*} value 值
  35. * @param {Array<String>} types 类型名称集合
  36. * @returns {Boolean} 值是否为指定的类型
  37. */
  38. static isType = (value, ...types) => {
  39. return types.includes(this.getType(value));
  40. };
  41.  
  42. /**
  43. * 拦截属性
  44. * @param {Object} target 目标对象
  45. * @param {String} property 属性或函数名称
  46. * @param {Function} beforeGet 获取属性前事件
  47. * @param {Function} beforeSet 设置属性前事件
  48. * @param {Function} afterGet 获取属性后事件
  49. * @param {Function} afterSet 设置属性前事件
  50. */
  51. static interceptProperty = (
  52. target,
  53. property,
  54. { beforeGet, beforeSet, afterGet, afterSet }
  55. ) => {
  56. // 判断目标对象是否存在
  57. if (target === undefined) {
  58. return;
  59. }
  60.  
  61. // 判断是否已被拦截
  62. const isIntercepted = (() => {
  63. const descriptor = Object.getOwnPropertyDescriptor(target, property);
  64.  
  65. if (descriptor && descriptor.get && descriptor.set) {
  66. return true;
  67. }
  68.  
  69. return false;
  70. })();
  71.  
  72. // 初始化目标对象的拦截列表
  73. target.interceptions = target.interceptions || {};
  74. target.interceptions[property] = target.interceptions[property] || {
  75. data: target[property],
  76. beforeGetQueue: [],
  77. beforeSetQueue: [],
  78. afterGetQueue: [],
  79. afterSetQueue: [],
  80. };
  81.  
  82. // 写入事件
  83. Object.entries({
  84. beforeGetQueue: beforeGet,
  85. beforeSetQueue: beforeSet,
  86. afterGetQueue: afterGet,
  87. afterSetQueue: afterSet,
  88. }).forEach(([queue, event]) => {
  89. if (event) {
  90. target.interceptions[property][queue].push(event);
  91. }
  92. });
  93.  
  94. // 拦截
  95. if (isIntercepted === false) {
  96. // 定义属性
  97. Object.defineProperty(target, property, {
  98. get: () => {
  99. // 获取事件
  100. const { data, beforeGetQueue, afterGetQueue } =
  101. target.interceptions[property];
  102.  
  103. // 如果是函数
  104. if (this.isType(data, "function")) {
  105. return (...args) => {
  106. try {
  107. // 执行前操作
  108. // 可以在这一步修改参数
  109. // 可以通过在这一步抛出来阻止执行
  110. if (beforeGetQueue) {
  111. beforeGetQueue.forEach((event) => {
  112. args = event.apply(target, args);
  113. });
  114. }
  115.  
  116. // 执行函数
  117. const result = data.apply(target, args);
  118.  
  119. // 执行后操作
  120. if (afterGetQueue) {
  121. // 返回的可能是一个 Promise
  122. const resultValue =
  123. result instanceof Promise
  124. ? result
  125. : Promise.resolve(result);
  126.  
  127. resultValue.then((value) => {
  128. afterGetQueue.forEach((event) => {
  129. event.apply(target, [value, args, data]);
  130. });
  131. });
  132. }
  133.  
  134. // 返回结果
  135. return result;
  136. } catch {
  137. return undefined;
  138. }
  139. };
  140. }
  141.  
  142. try {
  143. // 返回前操作
  144. // 可以在这一步修改返回结果
  145. // 可以通过在这一步抛出来返回 undefined
  146. let result = data;
  147.  
  148. if (beforeGetQueue) {
  149. beforeGetQueue.forEach((event) => {
  150. result = event.apply(target, [result]);
  151. });
  152. }
  153.  
  154. // 返回后操作
  155. // 实际上是在返回前完成的,并不能叫返回后操作,但是我们可以配合 afterGet 来操作处理后的数据
  156. if (afterGetQueue) {
  157. afterGetQueue.forEach((event) => {
  158. event.apply(target, [result, data]);
  159. });
  160. }
  161.  
  162. // 返回结果
  163. return result;
  164. } catch {
  165. return undefined;
  166. }
  167. },
  168. set: (value) => {
  169. // 获取事件
  170. const { data, beforeSetQueue, afterSetQueue } =
  171. target.interceptions[property];
  172.  
  173. // 声明结果
  174. let result = value;
  175.  
  176. try {
  177. // 写入前操作
  178. // 可以在这一步修改写入结果
  179. // 可以通过在这一步抛出来写入 undefined
  180. if (beforeSetQueue) {
  181. beforeSetQueue.forEach((event) => {
  182. result = event.apply(target, [data, result]);
  183. });
  184. }
  185.  
  186. // 写入可能的事件
  187. if (this.isType(data, "object")) {
  188. result.interceptions = data.interceptions;
  189. }
  190.  
  191. // 写入后操作
  192. if (afterSetQueue) {
  193. afterSetQueue.forEach((event) => {
  194. event.apply(target, [result, value]);
  195. });
  196. }
  197. } catch {
  198. result = undefined;
  199. } finally {
  200. // 写入结果
  201. target.interceptions[property].data = result;
  202.  
  203. // 返回结果
  204. return result;
  205. }
  206. },
  207. });
  208. }
  209.  
  210. // 如果已经有结果,则直接处理写入后操作
  211. if (Object.hasOwn(target, property)) {
  212. if (afterSet) {
  213. afterSet.apply(target, [target.interceptions[property].data]);
  214. }
  215. }
  216. };
  217.  
  218. /**
  219. * 合并数据
  220. * @param {*} target 目标对象
  221. * @param {Array} sources 来源对象集合
  222. * @returns 合并后的对象
  223. */
  224. static merge = (target, ...sources) => {
  225. for (const source of sources) {
  226. const targetType = this.getType(target);
  227. const sourceType = this.getType(source);
  228.  
  229. // 如果来源对象的类型与目标对象不一致,替换为来源对象
  230. if (sourceType !== targetType) {
  231. target = source;
  232. continue;
  233. }
  234.  
  235. // 如果来源对象是数组,直接合并
  236. if (targetType === "array") {
  237. target = [...target, ...source];
  238. continue;
  239. }
  240.  
  241. // 如果来源对象是对象,合并对象
  242. if (sourceType === "object") {
  243. for (const key in source) {
  244. if (Object.hasOwn(target, key)) {
  245. target[key] = this.merge(target[key], source[key]);
  246. } else {
  247. target[key] = source[key];
  248. }
  249. }
  250. continue;
  251. }
  252.  
  253. // 其他情况,更新值
  254. target = source;
  255. }
  256.  
  257. return target;
  258. };
  259.  
  260. /**
  261. * 数组排序
  262. * @param {Array} collection 数据集合
  263. * @param {Array<String | Function>} iterators 迭代器,要排序的属性名或排序函数
  264. */
  265. static sortBy = (collection, ...iterators) =>
  266. collection.slice().sort((a, b) => {
  267. for (let i = 0; i < iterators.length; i += 1) {
  268. const iteratee = iterators[i];
  269.  
  270. const valueA = this.isType(iteratee, "function")
  271. ? iteratee(a)
  272. : a[iteratee];
  273. const valueB = this.isType(iteratee, "function")
  274. ? iteratee(b)
  275. : b[iteratee];
  276.  
  277. if (valueA < valueB) {
  278. return -1;
  279. }
  280.  
  281. if (valueA > valueB) {
  282. return 1;
  283. }
  284. }
  285.  
  286. return 0;
  287. });
  288.  
  289. /**
  290. * 读取论坛数据
  291. * @param {Response} response 请求响应
  292. * @param {Boolean} toJSON 是否转为 JSON 格式
  293. */
  294. static readForumData = async (response, toJSON = true) => {
  295. return new Promise(async (resolve) => {
  296. const blob = await response.blob();
  297.  
  298. const reader = new FileReader();
  299.  
  300. reader.onload = () => {
  301. const text = reader.result.replace(
  302. "window.script_muti_get_var_store=",
  303. ""
  304. );
  305.  
  306. if (toJSON) {
  307. try {
  308. resolve(JSON.parse(text));
  309. } catch {
  310. resolve({});
  311. }
  312. return;
  313. }
  314.  
  315. resolve(text);
  316. };
  317.  
  318. reader.readAsText(blob, "GBK");
  319. });
  320. };
  321.  
  322. /**
  323. * 获取成对括号的内容
  324. * @param {String} content 内容
  325. * @param {String} keyword 起始位置关键字
  326. * @param {String} start 左括号
  327. * @param {String} end 右括号
  328. * @returns {String} 包含括号的内容
  329. */
  330. static searchPair = (content, keyword, start = "{", end = "}") => {
  331. // 获取成对括号的位置
  332. const findPairEndIndex = (content, position, start, end) => {
  333. if (position >= 0) {
  334. let nextIndex = position + 1;
  335.  
  336. while (nextIndex < content.length) {
  337. if (content[nextIndex] === end) {
  338. return nextIndex;
  339. }
  340.  
  341. if (content[nextIndex] === start) {
  342. nextIndex = findPairEndIndex(content, nextIndex, start, end);
  343.  
  344. if (nextIndex < 0) {
  345. break;
  346. }
  347. }
  348.  
  349. nextIndex = nextIndex + 1;
  350. }
  351. }
  352.  
  353. return -1;
  354. };
  355.  
  356. // 起始位置
  357. const str = keyword + start;
  358.  
  359. // 起始下标
  360. const index = content.indexOf(str) + str.length;
  361.  
  362. // 结尾下标
  363. const lastIndex = findPairEndIndex(content, index, start, end);
  364.  
  365. if (lastIndex > 0) {
  366. return start + content.substring(index, lastIndex) + end;
  367. }
  368.  
  369. return null;
  370. };
  371.  
  372. /**
  373. * 计算字符串的颜色
  374. *
  375. * 采用的是泥潭的颜色方案,参见 commonui.htmlName
  376. * @param {String} value 字符串
  377. * @returns {String} RGB代码
  378. */
  379. static generateColor(value) {
  380. const hash = (() => {
  381. let h = 5381;
  382.  
  383. for (var i = 0; i < value.length; i++) {
  384. h = ((h << 5) + h + value.charCodeAt(i)) & 0xffffffff;
  385. }
  386.  
  387. return h;
  388. })();
  389.  
  390. const hex = Math.abs(hash).toString(16) + "000000";
  391.  
  392. const hsv = [
  393. `0x${hex.substring(2, 4)}` / 255,
  394. `0x${hex.substring(2, 4)}` / 255 / 2 + 0.25,
  395. `0x${hex.substring(4, 6)}` / 255 / 2 + 0.25,
  396. ];
  397.  
  398. const rgb = ((h, s, v) => {
  399. const f = (n, k = (n + h / 60) % 6) =>
  400. v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
  401.  
  402. return [f(5), f(3), f(1)];
  403. })(hsv[0], hsv[1], hsv[2]);
  404.  
  405. return ["#", ...rgb].reduce((a, b) => {
  406. return a + ("0" + b.toString(16)).slice(-2);
  407. });
  408. }
  409.  
  410. /**
  411. * 添加样式
  412. *
  413. * @param {String} css 样式信息
  414. * @param {String} id 样式 ID
  415. * @returns {HTMLStyleElement} 样式元素
  416. */
  417. static addStyle(css, id = "s-" + Math.random().toString(36).slice(2)) {
  418. let element = document.getElementById(id);
  419.  
  420. if (element === null) {
  421. element = document.createElement("STYLE");
  422. element.id = id;
  423.  
  424. document.head.appendChild(element);
  425. }
  426.  
  427. element.textContent = css;
  428.  
  429. return element;
  430. }
  431.  
  432. /**
  433. * 计算时间是否为今天
  434. * @param {Date} date 时间
  435. * @returns {Boolean}
  436. */
  437. static dateIsToday(date) {
  438. const now = new Date();
  439.  
  440. return (
  441. date.getFullYear() === now.getFullYear() &&
  442. date.getMonth() === now.getMonth() &&
  443. date.getDate() === now.getDate()
  444. );
  445. }
  446.  
  447. /**
  448. * 计算时间差
  449. * @param {Date} start 开始时间
  450. * @param {Date} end 结束时间
  451. * @returns {object} 时间差
  452. */
  453. static dateDiff(start, end = new Date()) {
  454. if (start > end) {
  455. return dateDiff(end, start);
  456. }
  457.  
  458. const startYear = start.getFullYear();
  459. const startMonth = start.getMonth();
  460. const startDay = start.getDate();
  461.  
  462. const endYear = end.getFullYear();
  463. const endMonth = end.getMonth();
  464. const endDay = end.getDate();
  465.  
  466. const diff = {
  467. years: endYear - startYear,
  468. months: endMonth - startMonth,
  469. days: endDay - startDay,
  470. };
  471.  
  472. const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  473.  
  474. if (
  475. startYear % 400 === 0 ||
  476. (startYear % 100 !== 0 && startYear % 4 === 0)
  477. ) {
  478. daysInMonth[1] = 29;
  479. }
  480.  
  481. if (diff.months < 0) {
  482. diff.years -= 1;
  483. diff.months += 12;
  484. }
  485.  
  486. if (diff.days < 0) {
  487. if (diff.months === 0) {
  488. diff.years -= 1;
  489. diff.months = 11;
  490. } else {
  491. diff.months -= 1;
  492. }
  493.  
  494. diff.days += daysInMonth[startMonth];
  495. }
  496.  
  497. return diff;
  498. }
  499. }
  500.  
  501. /**
  502. * 简单队列
  503. */
  504. class Queue {
  505. /**
  506. * 任务队列
  507. */
  508. queue = {};
  509.  
  510. /**
  511. * 当前状态 - IDLE, RUNNING, PAUSED
  512. */
  513. state = "IDLE";
  514.  
  515. /**
  516. * 异常暂停时间
  517. */
  518. pauseTime = 1000 * 60 * 5;
  519.  
  520. /**
  521. * 添加任务
  522. * @param {string} key 标识
  523. * @param {() => Promise} task 任务
  524. */
  525. enqueue(key, task) {
  526. if (Object.hasOwn(this.queue, key)) {
  527. return;
  528. }
  529.  
  530. this.queue[key] = task;
  531. this.run();
  532. }
  533.  
  534. /**
  535. * 移除任务
  536. * @param {string} key 标识
  537. */
  538. dequeue(key) {
  539. if (Object.hasOwn(this.queue, key) === false) {
  540. return;
  541. }
  542.  
  543. delete this.queue[key];
  544. }
  545.  
  546. /**
  547. * 执行任务
  548. */
  549. run() {
  550. // 非空闲状态,直接返回
  551. if (this.state !== "IDLE") {
  552. return;
  553. }
  554.  
  555. // 获取任务队列标识
  556. const keys = Object.keys(this.queue);
  557.  
  558. // 任务队列为空,直接返回
  559. if (keys.length === 0) {
  560. return;
  561. }
  562.  
  563. // 标记为执行中
  564. this.state = "RUNNING";
  565.  
  566. // 取得第一个任务
  567. const key = keys[0];
  568.  
  569. // 执行任务
  570. this.queue[key]()
  571. .then(() => {
  572. // 移除任务
  573. this.dequeue(key);
  574. })
  575. .catch(async () => {
  576. // 标记为暂停
  577. this.state = "PAUSED";
  578.  
  579. // 等待指定时间
  580. await new Promise((resolve) => {
  581. setTimeout(resolve, this.pauseTime);
  582. });
  583. })
  584. .finally(() => {
  585. // 标记为空闲
  586. this.state = "IDLE";
  587.  
  588. // 执行下一个任务
  589. this.run();
  590. });
  591. }
  592. }
  593.  
  594. /**
  595. * 初始化缓存和 API
  596. */
  597. const initCacheAndAPI = (() => {
  598. // KEY
  599. const USER_AGENT_KEY = "USER_AGENT_KEY";
  600.  
  601. /**
  602. * 数据库名称
  603. */
  604. const name = "NGA_Storage";
  605.  
  606. /**
  607. * 模块列表
  608. */
  609. const modules = {
  610. TOPIC_NUM_CACHE: {
  611. keyPath: "uid",
  612. version: 1,
  613. indexes: ["timestamp"],
  614. expireTime: 1000 * 60 * 60,
  615. persistent: true,
  616. },
  617. USER_INFO_CACHE: {
  618. keyPath: "uid",
  619. version: 1,
  620. indexes: ["timestamp"],
  621. expireTime: 1000 * 60 * 60,
  622. persistent: false,
  623. },
  624. USER_IPLOC_CACHE: {
  625. keyPath: "uid",
  626. version: 1,
  627. indexes: ["timestamp"],
  628. expireTime: 1000 * 60 * 60,
  629. persistent: true,
  630. },
  631. PAGE_CACHE: {
  632. keyPath: "url",
  633. version: 1,
  634. indexes: ["timestamp"],
  635. expireTime: 1000 * 60 * 10,
  636. persistent: false,
  637. },
  638. FORUM_POSTED_CACHE: {
  639. keyPath: "url",
  640. version: 1,
  641. indexes: ["timestamp"],
  642. expireTime: 1000 * 60 * 60 * 24,
  643. persistent: true,
  644. },
  645. USER_NAME_CHANGED: {
  646. keyPath: "uid",
  647. version: 2,
  648. indexes: ["timestamp"],
  649. expireTime: 1000 * 60 * 60 * 24,
  650. persistent: true,
  651. },
  652. USER_STEAM_INFO: {
  653. keyPath: "uid",
  654. version: 3,
  655. indexes: ["timestamp"],
  656. expireTime: 1000 * 60 * 60 * 24,
  657. persistent: true,
  658. },
  659. USER_PSN_INFO: {
  660. keyPath: "uid",
  661. version: 3,
  662. indexes: ["timestamp"],
  663. expireTime: 1000 * 60 * 60 * 24,
  664. persistent: true,
  665. },
  666. USER_NINTENDO_INFO: {
  667. keyPath: "uid",
  668. version: 3,
  669. indexes: ["timestamp"],
  670. expireTime: 1000 * 60 * 60 * 24,
  671. persistent: true,
  672. },
  673. USER_GENSHIN_INFO: {
  674. keyPath: "uid",
  675. version: 3,
  676. indexes: ["timestamp"],
  677. expireTime: 1000 * 60 * 60 * 24,
  678. persistent: false,
  679. },
  680. USER_SKZY_INFO: {
  681. keyPath: "uid",
  682. version: 3,
  683. indexes: ["timestamp"],
  684. expireTime: 1000 * 60 * 60 * 24,
  685. persistent: false,
  686. },
  687. };
  688.  
  689. /**
  690. * IndexedDB
  691. *
  692. * 简单制造轮子,暂不打算引入 dexie.js,待其云方案正式推出后再考虑
  693. */
  694.  
  695. class DBStorage {
  696. /**
  697. * 当前实例
  698. */
  699. instance = null;
  700.  
  701. /**
  702. * 是否支持
  703. */
  704. isSupport() {
  705. return unsafeWindow.indexedDB !== undefined;
  706. }
  707.  
  708. /**
  709. * 打开数据库并创建表
  710. * @returns {Promise<IDBDatabase>} 实例
  711. */
  712. async open() {
  713. // 创建实例
  714. if (this.instance === null) {
  715. // 声明一个数组,用于等待全部表处理完毕
  716. const queue = [];
  717.  
  718. // 创建实例
  719. await new Promise((resolve, reject) => {
  720. // 版本
  721. const version = Object.values(modules)
  722. .map(({ version }) => version)
  723. .reduce((a, b) => Math.max(a, b), 0);
  724.  
  725. // 创建请求
  726. const request = unsafeWindow.indexedDB.open(name, version);
  727.  
  728. // 创建或者升级表
  729. request.onupgradeneeded = (event) => {
  730. this.instance = event.target.result;
  731.  
  732. const transaction = event.target.transaction;
  733. const oldVersion = event.oldVersion;
  734.  
  735. Object.entries(modules).forEach(([key, values]) => {
  736. if (values.version > oldVersion) {
  737. queue.push(this.createOrUpdateStore(key, values, transaction));
  738. }
  739. });
  740. };
  741.  
  742. // 成功后处理
  743. request.onsuccess = (event) => {
  744. this.instance = event.target.result;
  745. resolve();
  746. };
  747.  
  748. // 失败后处理
  749. request.onerror = () => {
  750. reject();
  751. };
  752. });
  753.  
  754. // 等待全部表处理完毕
  755. await Promise.all(queue);
  756. }
  757.  
  758. // 返回实例
  759. return this.instance;
  760. }
  761.  
  762. /**
  763. * 获取表
  764. * @param {String} name 表名
  765. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  766. * @param {String} mode 事务模式,默认为只读
  767. * @returns {Promise<IDBObjectStore>} 表
  768. */
  769. async getStore(name, transaction = null, mode = "readonly") {
  770. const db = await this.open();
  771.  
  772. if (transaction === null) {
  773. transaction = db.transaction(name, mode);
  774. }
  775.  
  776. return transaction.objectStore(name);
  777. }
  778.  
  779. /**
  780. * 创建或升级表
  781. * @param {String} name 表名
  782. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  783. * @returns {Promise}
  784. */
  785. async createOrUpdateStore(name, { keyPath, indexes }, transaction) {
  786. const db = transaction.db;
  787. const data = [];
  788.  
  789. // 检查是否存在表,如果存在,缓存数据并删除旧表
  790. if (db.objectStoreNames.contains(name)) {
  791. // 获取并缓存全部数据
  792. const result = await this.bulkGet(name, [], transaction);
  793.  
  794. if (result) {
  795. data.push(...result);
  796. }
  797.  
  798. // 删除旧表
  799. db.deleteObjectStore(name);
  800. }
  801.  
  802. // 创建表
  803. const store = db.createObjectStore(name, {
  804. keyPath,
  805. });
  806.  
  807. // 创建索引
  808. if (indexes) {
  809. indexes.forEach((index) => {
  810. store.createIndex(index, index);
  811. });
  812. }
  813.  
  814. // 迁移数据
  815. if (data.length > 0) {
  816. await this.bulkAdd(name, data, transaction);
  817. }
  818. }
  819.  
  820. /**
  821. * 清除指定表的数据
  822. * @param {String} name 表名
  823. * @param {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range 清除范围
  824. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  825. * @returns {Promise}
  826. */
  827. async clear(name, range = null, transaction = null) {
  828. // 获取表
  829. const store = await this.getStore(name, transaction, "readwrite");
  830.  
  831. // 清除全部数据
  832. if (range === null) {
  833. // 清空数据
  834. await new Promise((resolve, reject) => {
  835. // 创建请求
  836. const request = store.clear();
  837.  
  838. // 成功后处理
  839. request.onsuccess = (event) => {
  840. resolve(event.target.result);
  841. };
  842.  
  843. // 失败后处理
  844. request.onerror = (event) => {
  845. reject(event);
  846. };
  847. });
  848.  
  849. return;
  850. }
  851.  
  852. // 请求范围
  853. const request = range(store);
  854.  
  855. // 成功后删除数据
  856. request.onsuccess = (event) => {
  857. const cursor = event.target.result;
  858.  
  859. if (cursor) {
  860. store.delete(cursor.primaryKey);
  861.  
  862. cursor.continue();
  863. }
  864. };
  865. }
  866.  
  867. /**
  868. * 插入指定表的数据
  869. * @param {String} name 表名
  870. * @param {*} data 数据
  871. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  872. * @returns {Promise}
  873. */
  874. async add(name, data, transaction = null) {
  875. // 获取表
  876. const store = await this.getStore(name, transaction, "readwrite");
  877.  
  878. // 插入数据
  879. const result = await new Promise((resolve, reject) => {
  880. // 创建请求
  881. const request = store.add(data);
  882.  
  883. // 成功后处理
  884. request.onsuccess = (event) => {
  885. resolve(event.target.result);
  886. };
  887.  
  888. // 失败后处理
  889. request.onerror = (event) => {
  890. reject(event);
  891. };
  892. });
  893.  
  894. // 返回结果
  895. return result;
  896. }
  897.  
  898. /**
  899. * 删除指定表的数据
  900. * @param {String} name 表名
  901. * @param {String} key 主键
  902. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  903. * @returns {Promise}
  904. */
  905. async delete(name, key, transaction = null) {
  906. // 获取表
  907. const store = await this.getStore(name, transaction, "readwrite");
  908.  
  909. // 删除数据
  910. const result = await new Promise((resolve, reject) => {
  911. // 创建请求
  912. const request = store.delete(key);
  913.  
  914. // 成功后处理
  915. request.onsuccess = (event) => {
  916. resolve(event.target.result);
  917. };
  918.  
  919. // 失败后处理
  920. request.onerror = (event) => {
  921. reject(event);
  922. };
  923. });
  924.  
  925. // 返回结果
  926. return result;
  927. }
  928.  
  929. /**
  930. * 插入或修改指定表的数据
  931. * @param {String} name 表名
  932. * @param {*} data 数据
  933. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  934. * @returns {Promise}
  935. */
  936. async put(name, data, transaction = null) {
  937. // 获取表
  938. const store = await this.getStore(name, transaction, "readwrite");
  939.  
  940. // 插入或修改数据
  941. const result = await new Promise((resolve, reject) => {
  942. // 创建请求
  943. const request = store.put(data);
  944.  
  945. // 成功后处理
  946. request.onsuccess = (event) => {
  947. resolve(event.target.result);
  948. };
  949.  
  950. // 失败后处理
  951. request.onerror = (event) => {
  952. reject(event);
  953. };
  954. });
  955.  
  956. // 返回结果
  957. return result;
  958. }
  959.  
  960. /**
  961. * 获取指定表的数据
  962. * @param {String} name 表名
  963. * @param {String} key 主键
  964. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  965. * @returns {Promise} 数据
  966. */
  967. async get(name, key, transaction = null) {
  968. // 获取表
  969. const store = await this.getStore(name, transaction);
  970.  
  971. // 查询数据
  972. const result = await new Promise((resolve, reject) => {
  973. // 创建请求
  974. const request = store.get(key);
  975.  
  976. // 成功后处理
  977. request.onsuccess = (event) => {
  978. resolve(event.target.result);
  979. };
  980.  
  981. // 失败后处理
  982. request.onerror = (event) => {
  983. reject(event);
  984. };
  985. });
  986.  
  987. // 返回结果
  988. return result;
  989. }
  990.  
  991. /**
  992. * 批量插入指定表的数据
  993. * @param {String} name 表名
  994. * @param {Array} data 数据集合
  995. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  996. * @returns {Promise<number>} 成功数量
  997. */
  998. async bulkAdd(name, data, transaction = null) {
  999. // 等待操作结果
  1000. const result = await Promise.all(
  1001. data.map((item) =>
  1002. this.add(name, item, transaction)
  1003. .then(() => true)
  1004. .catch(() => false)
  1005. )
  1006. );
  1007.  
  1008. // 返回受影响的数量
  1009. return result.filter((item) => item).length;
  1010. }
  1011.  
  1012. /**
  1013. * 批量删除指定表的数据
  1014. * @param {String} name 表名
  1015. * @param {Array<String>} keys 主键集合,空则删除全部
  1016. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  1017. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  1018. */
  1019. async bulkDelete(name, keys = [], transaction = null) {
  1020. // 如果 keys 为空,删除全部数据
  1021. if (keys.length === 0) {
  1022. // 清空数据
  1023. await this.clear(name, null, transaction);
  1024.  
  1025. return -1;
  1026. }
  1027.  
  1028. // 等待操作结果
  1029. const result = await Promise.all(
  1030. keys.map((item) =>
  1031. this.delete(name, item, transaction)
  1032. .then(() => true)
  1033. .catch(() => false)
  1034. )
  1035. );
  1036.  
  1037. // 返回受影响的数量
  1038. return result.filter((item) => item).length;
  1039. }
  1040.  
  1041. /**
  1042. * 批量插入或修改指定表的数据
  1043. * @param {String} name 表名
  1044. * @param {Array} data 数据集合
  1045. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  1046. * @returns {Promise<number>} 成功数量
  1047. */
  1048. async bulkPut(name, data, transaction = null) {
  1049. // 等待操作结果
  1050. const result = await Promise.all(
  1051. data.map((item) =>
  1052. this.put(name, item, transaction)
  1053. .then(() => true)
  1054. .catch(() => false)
  1055. )
  1056. );
  1057.  
  1058. // 返回受影响的数量
  1059. return result.filter((item) => item).length;
  1060. }
  1061.  
  1062. /**
  1063. * 批量获取指定表的数据
  1064. * @param {String} name 表名
  1065. * @param {Array<String>} keys 主键集合,空则获取全部
  1066. * @param {IDBTransaction} transaction 事务,空则根据表名创建新事务
  1067. * @returns {Promise<Array>} 数据集合
  1068. */
  1069. async bulkGet(name, keys = [], transaction = null) {
  1070. // 如果 keys 为空,查询全部数据
  1071. if (keys.length === 0) {
  1072. // 获取表
  1073. const store = await this.getStore(name, transaction);
  1074.  
  1075. // 查询数据
  1076. const result = await new Promise((resolve, reject) => {
  1077. // 创建请求
  1078. const request = store.getAll();
  1079.  
  1080. // 成功后处理
  1081. request.onsuccess = (event) => {
  1082. resolve(event.target.result || []);
  1083. };
  1084.  
  1085. // 失败后处理
  1086. request.onerror = (event) => {
  1087. reject(event);
  1088. };
  1089. });
  1090.  
  1091. // 返回结果
  1092. return result;
  1093. }
  1094.  
  1095. // 返回符合的结果
  1096. const result = [];
  1097.  
  1098. await Promise.all(
  1099. keys.map((key) =>
  1100. this.get(name, key, transaction)
  1101. .then((item) => {
  1102. result.push(item);
  1103. })
  1104. .catch(() => {})
  1105. )
  1106. );
  1107.  
  1108. return result;
  1109. }
  1110. }
  1111.  
  1112. /**
  1113. * 油猴存储
  1114. *
  1115. * 虽然使用了不支持 Promise 的 GM_getValue 与 GM_setValue,但是为了配合 IndexedDB,统一视为 Promise
  1116. */
  1117. class GMStorage extends DBStorage {
  1118. /**
  1119. * 清除指定表的数据
  1120. * @param {String} name 表名
  1121. * @param {(store: IDBObjectStore) => IDBRequest<IDBCursor | null>} range 清除范围
  1122. * @returns {Promise}
  1123. */
  1124. async clear(name, range = null) {
  1125. // 如果支持 IndexedDB,使用 IndexedDB
  1126. if (super.isSupport()) {
  1127. return super.clear(name, range);
  1128. }
  1129.  
  1130. // 清除全部数据
  1131. GM_setValue(name, {});
  1132. }
  1133.  
  1134. /**
  1135. * 插入指定表的数据
  1136. * @param {String} name 表名
  1137. * @param {*} data 数据
  1138. * @returns {Promise}
  1139. */
  1140. async add(name, data) {
  1141. // 如果不在模块列表里,写入全部数据
  1142. if (Object.hasOwn(modules, name) === false) {
  1143. return GM_setValue(name, data);
  1144. }
  1145.  
  1146. // 如果支持 IndexedDB,使用 IndexedDB
  1147. if (super.isSupport()) {
  1148. return super.add(name, data);
  1149. }
  1150.  
  1151. // 获取对应的主键
  1152. const keyPath = modules[name].keyPath;
  1153. const key = data[keyPath];
  1154.  
  1155. // 如果数据中不包含主键,抛出异常
  1156. if (key === undefined) {
  1157. throw new Error();
  1158. }
  1159.  
  1160. // 获取全部数据
  1161. const values = GM_getValue(name, {});
  1162.  
  1163. // 如果对应主键已存在,抛出异常
  1164. if (Object.hasOwn(values, key)) {
  1165. throw new Error();
  1166. }
  1167.  
  1168. // 插入数据
  1169. values[key] = data;
  1170.  
  1171. // 保存数据
  1172. GM_setValue(name, values);
  1173. }
  1174.  
  1175. /**
  1176. * 删除指定表的数据
  1177. * @param {String} name 表名
  1178. * @param {String} key 主键
  1179. * @returns {Promise}
  1180. */
  1181. async delete(name, key) {
  1182. // 如果不在模块列表里,忽略 key,删除全部数据
  1183. if (Object.hasOwn(modules, name) === false) {
  1184. return GM_setValue(name, {});
  1185. }
  1186.  
  1187. // 如果支持 IndexedDB,使用 IndexedDB
  1188. if (super.isSupport()) {
  1189. return super.delete(name, key);
  1190. }
  1191.  
  1192. // 获取全部数据
  1193. const values = GM_getValue(name, {});
  1194.  
  1195. // 如果对应主键不存在,抛出异常
  1196. if (Object.hasOwn(values, key) === false) {
  1197. throw new Error();
  1198. }
  1199.  
  1200. // 删除数据
  1201. delete values[key];
  1202.  
  1203. // 保存数据
  1204. GM_setValue(name, values);
  1205. }
  1206.  
  1207. /**
  1208. * 插入或修改指定表的数据
  1209. * @param {String} name 表名
  1210. * @param {*} data 数据
  1211. * @returns {Promise}
  1212. */
  1213. async put(name, data) {
  1214. // 如果不在模块列表里,写入全部数据
  1215. if (Object.hasOwn(modules, name) === false) {
  1216. return GM_setValue(name, data);
  1217. }
  1218.  
  1219. // 如果支持 IndexedDB,使用 IndexedDB
  1220. if (super.isSupport()) {
  1221. return super.put(name, data);
  1222. }
  1223.  
  1224. // 获取对应的主键
  1225. const keyPath = modules[name].keyPath;
  1226. const key = data[keyPath];
  1227.  
  1228. // 如果数据中不包含主键,抛出异常
  1229. if (key === undefined) {
  1230. throw new Error();
  1231. }
  1232.  
  1233. // 获取全部数据
  1234. const values = GM_getValue(name, {});
  1235.  
  1236. // 插入或修改数据
  1237. values[key] = data;
  1238.  
  1239. // 保存数据
  1240. GM_setValue(name, values);
  1241. }
  1242.  
  1243. /**
  1244. * 获取指定表的数据
  1245. * @param {String} name 表名
  1246. * @param {String} key 主键
  1247. * @returns {Promise} 数据
  1248. */
  1249. async get(name, key) {
  1250. // 如果不在模块列表里,忽略 key,返回全部数据
  1251. if (Object.hasOwn(modules, name) === false) {
  1252. return GM_getValue(name);
  1253. }
  1254.  
  1255. // 如果支持 IndexedDB,使用 IndexedDB
  1256. if (super.isSupport()) {
  1257. return super.get(name, key);
  1258. }
  1259.  
  1260. // 获取全部数据
  1261. const values = GM_getValue(name, {});
  1262.  
  1263. // 如果对应主键不存在,抛出异常
  1264. if (Object.hasOwn(values, key) === false) {
  1265. throw new Error();
  1266. }
  1267.  
  1268. // 返回结果
  1269. return values[key];
  1270. }
  1271.  
  1272. /**
  1273. * 批量插入指定表的数据
  1274. * @param {String} name 表名
  1275. * @param {Array} data 数据集合
  1276. * @returns {Promise<number>} 成功数量
  1277. */
  1278. async bulkAdd(name, data) {
  1279. // 如果不在模块列表里,写入全部数据
  1280. if (Object.hasOwn(modules, name) === false) {
  1281. return GM_setValue(name, {});
  1282. }
  1283.  
  1284. // 如果支持 IndexedDB,使用 IndexedDB
  1285. if (super.isSupport()) {
  1286. return super.bulkAdd(name, data);
  1287. }
  1288.  
  1289. // 获取对应的主键
  1290. const keyPath = modules[name].keyPath;
  1291.  
  1292. // 获取全部数据
  1293. const values = GM_getValue(name, {});
  1294.  
  1295. // 添加数据
  1296. const result = data.map((item) => {
  1297. const key = item[keyPath];
  1298.  
  1299. // 如果数据中不包含主键,抛出异常
  1300. if (key === undefined) {
  1301. return false;
  1302. }
  1303.  
  1304. // 如果对应主键已存在,抛出异常
  1305. if (Object.hasOwn(values, key)) {
  1306. return false;
  1307. }
  1308.  
  1309. // 插入数据
  1310. values[key] = item;
  1311.  
  1312. return true;
  1313. });
  1314.  
  1315. // 保存数据
  1316. GM_setValue(name, values);
  1317.  
  1318. // 返回受影响的数量
  1319. return result.filter((item) => item).length;
  1320. }
  1321.  
  1322. /**
  1323. * 批量删除指定表的数据
  1324. * @param {String} name 表名
  1325. * @param {Array<String>} keys 主键集合,空则删除全部
  1326. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  1327. */
  1328. async bulkDelete(name, keys = []) {
  1329. // 如果不在模块列表里,忽略 keys,删除全部数据
  1330. if (Object.hasOwn(modules, name) === false) {
  1331. return GM_setValue(name, {});
  1332. }
  1333.  
  1334. // 如果支持 IndexedDB,使用 IndexedDB
  1335. if (super.isSupport()) {
  1336. return super.bulkDelete(name, keys);
  1337. }
  1338.  
  1339. // 如果 keys 为空,删除全部数据
  1340. if (keys.length === 0) {
  1341. await this.clear(name, null);
  1342.  
  1343. return -1;
  1344. }
  1345.  
  1346. // 获取全部数据
  1347. const values = GM_getValue(name, {});
  1348.  
  1349. // 删除数据
  1350. const result = keys.map((key) => {
  1351. // 如果对应主键不存在,抛出异常
  1352. if (Object.hasOwn(values, key) === false) {
  1353. return false;
  1354. }
  1355.  
  1356. // 删除数据
  1357. delete values[key];
  1358.  
  1359. return true;
  1360. });
  1361.  
  1362. // 保存数据
  1363. GM_setValue(name, values);
  1364.  
  1365. // 返回受影响的数量
  1366. return result.filter((item) => item).length;
  1367. }
  1368.  
  1369. /**
  1370. * 批量插入或修改指定表的数据
  1371. * @param {String} name 表名
  1372. * @param {Array} data 数据集合
  1373. * @returns {Promise<number>} 成功数量
  1374. */
  1375. async bulkPut(name, data) {
  1376. // 如果不在模块列表里,写入全部数据
  1377. if (Object.hasOwn(modules, name) === false) {
  1378. return GM_setValue(name, data);
  1379. }
  1380.  
  1381. // 如果支持 IndexedDB,使用 IndexedDB
  1382. if (super.isSupport()) {
  1383. return super.bulkPut(name, keys);
  1384. }
  1385.  
  1386. // 获取对应的主键
  1387. const keyPath = modules[name].keyPath;
  1388.  
  1389. // 获取全部数据
  1390. const values = GM_getValue(name, {});
  1391.  
  1392. // 添加数据
  1393. const result = data.map((item) => {
  1394. const key = item[keyPath];
  1395.  
  1396. // 如果数据中不包含主键,抛出异常
  1397. if (key === undefined) {
  1398. return false;
  1399. }
  1400.  
  1401. // 插入数据
  1402. values[key] = item;
  1403.  
  1404. return true;
  1405. });
  1406.  
  1407. // 保存数据
  1408. GM_setValue(name, values);
  1409.  
  1410. // 返回受影响的数量
  1411. return result.filter((item) => item).length;
  1412. }
  1413.  
  1414. /**
  1415. * 批量获取指定表的数据,如果不在模块列表里,返回全部数据
  1416. * @param {String} name 表名
  1417. * @param {Array<String>} keys 主键集合,空则获取全部
  1418. * @returns {Promise<Array>} 数据集合
  1419. */
  1420. async bulkGet(name, keys = []) {
  1421. // 如果不在模块列表里,忽略 keys,返回全部数据
  1422. if (Object.hasOwn(modules, name) === false) {
  1423. return GM_getValue(name);
  1424. }
  1425.  
  1426. // 如果支持 IndexedDB,使用 IndexedDB
  1427. if (super.isSupport()) {
  1428. return super.bulkGet(name, keys);
  1429. }
  1430.  
  1431. // 获取全部数据
  1432. const values = GM_getValue(name, {});
  1433.  
  1434. // 如果 keys 为空,返回全部数据
  1435. if (keys.length === 0) {
  1436. return Object.values(values);
  1437. }
  1438.  
  1439. // 返回符合的结果
  1440. const result = [];
  1441.  
  1442. keys.forEach((key) => {
  1443. if (Object.hasOwn(values, key)) {
  1444. result.push(values[key]);
  1445. }
  1446. });
  1447.  
  1448. return result;
  1449. }
  1450. }
  1451.  
  1452. /**
  1453. * 缓存管理
  1454. *
  1455. * 在存储的基础上,增加了过期时间和持久化选项,自动清理缓存
  1456. */
  1457. class Cache extends GMStorage {
  1458. /**
  1459. * 清除指定表的数据
  1460. * @param {String} name 表名
  1461. * @param {Boolean} onlyExpire 是否只清除超时数据
  1462. * @returns {Promise}
  1463. */
  1464. async clear(name, onlyExpire = false) {
  1465. // 如果不在模块里,直接清除
  1466. if (Object.hasOwn(modules, name) === false) {
  1467. return super.clear(name);
  1468. }
  1469.  
  1470. // 如果只清除超时数据为否,直接清除
  1471. if (onlyExpire === false) {
  1472. return super.clear(name);
  1473. }
  1474.  
  1475. // 读取模块配置
  1476. const { expireTime, persistent } = modules[name];
  1477.  
  1478. // 持久化
  1479. if (persistent) {
  1480. return;
  1481. }
  1482.  
  1483. // 清除超时数据
  1484. return super.clear(name, (store) =>
  1485. store
  1486. .index("timestamp")
  1487. .openKeyCursor(IDBKeyRange.upperBound(Date.now() - expireTime))
  1488. );
  1489. }
  1490.  
  1491. /**
  1492. * 插入指定表的数据,并增加 timestamp
  1493. * @param {String} name 表名
  1494. * @param {*} data 数据
  1495. * @returns {Promise}
  1496. */
  1497. async add(name, data) {
  1498. // 如果在模块里,增加 timestamp
  1499. if (Object.hasOwn(modules, name)) {
  1500. data.timestamp = data.timestamp || new Date().getTime();
  1501. }
  1502.  
  1503. return super.add(name, data);
  1504. }
  1505.  
  1506. /**
  1507. * 插入或修改指定表的数据,并增加 timestamp
  1508. * @param {String} name 表名
  1509. * @param {*} data 数据
  1510. * @returns {Promise}
  1511. */
  1512. async put(name, data) {
  1513. // 如果在模块里,增加 timestamp
  1514. if (Object.hasOwn(modules, name)) {
  1515. data.timestamp = data.timestamp || new Date().getTime();
  1516. }
  1517.  
  1518. return super.put(name, data);
  1519. }
  1520.  
  1521. /**
  1522. * 获取指定表的数据,并移除过期数据
  1523. * @param {String} name 表名
  1524. * @param {String} key 主键
  1525. * @returns {Promise} 数据
  1526. */
  1527. async get(name, key) {
  1528. // 获取数据
  1529. const value = await super.get(name, key).catch(() => null);
  1530.  
  1531. // 如果不在模块里,直接返回结果
  1532. if (Object.hasOwn(modules, name) === false) {
  1533. return value;
  1534. }
  1535.  
  1536. // 如果有结果的话,移除超时数据
  1537. if (value) {
  1538. // 读取模块配置
  1539. const { expireTime, persistent } = modules[name];
  1540.  
  1541. // 持久化或未超时
  1542. if (persistent || value.timestamp + expireTime > new Date().getTime()) {
  1543. return value;
  1544. }
  1545.  
  1546. // 移除超时数据
  1547. await super.delete(name, key);
  1548. }
  1549.  
  1550. return null;
  1551. }
  1552.  
  1553. /**
  1554. * 批量插入指定表的数据,并增加 timestamp
  1555. * @param {String} name 表名
  1556. * @param {Array} data 数据集合
  1557. * @returns {Promise<number>} 成功数量
  1558. */
  1559. async bulkAdd(name, data) {
  1560. // 如果在模块里,增加 timestamp
  1561. if (Object.hasOwn(modules, name)) {
  1562. data.forEach((item) => {
  1563. item.timestamp = item.timestamp || new Date().getTime();
  1564. });
  1565. }
  1566.  
  1567. return super.bulkAdd(name, data);
  1568. }
  1569.  
  1570. /**
  1571. * 批量删除指定表的数据
  1572. * @param {String} name 表名
  1573. * @param {Array<String>} keys 主键集合,空则删除全部
  1574. * @param {boolean} force 是否强制删除,否则只删除过期数据
  1575. * @returns {Promise<number>} 成功数量,删除全部时返回 -1
  1576. */
  1577. async bulkDelete(name, keys = [], force = false) {
  1578. // 如果不在模块里,强制删除
  1579. if (Object.hasOwn(modules, name) === false) {
  1580. force = true;
  1581. }
  1582.  
  1583. // 强制删除
  1584. if (force) {
  1585. return super.bulkDelete(name, keys);
  1586. }
  1587.  
  1588. // 批量获取指定表的数据,并移除过期数据
  1589. const result = this.bulkGet(name, keys);
  1590.  
  1591. // 返回成功数量
  1592. if (keys.length === 0) {
  1593. return -1;
  1594. }
  1595.  
  1596. return keys.length - result.length;
  1597. }
  1598.  
  1599. /**
  1600. * 批量插入或修改指定表的数据,并增加 timestamp
  1601. * @param {String} name 表名
  1602. * @param {Array} data 数据集合
  1603. * @returns {Promise<number>} 成功数量
  1604. */
  1605. async bulkPut(name, data) {
  1606. // 如果在模块里,增加 timestamp
  1607. if (Object.hasOwn(modules, name)) {
  1608. data.forEach((item) => {
  1609. item.timestamp = item.timestamp || new Date().getTime();
  1610. });
  1611. }
  1612.  
  1613. return super.bulkPut(name, data);
  1614. }
  1615.  
  1616. /**
  1617. * 批量获取指定表的数据,并移除过期数据
  1618. * @param {String} name 表名
  1619. * @param {Array<String>} keys 主键集合,空则获取全部
  1620. * @returns {Promise<Array>} 数据集合
  1621. */
  1622. async bulkGet(name, keys = []) {
  1623. // 获取数据
  1624. const values = await super.bulkGet(name, keys).catch(() => []);
  1625.  
  1626. // 如果不在模块里,直接返回结果
  1627. if (Object.hasOwn(modules, name) === false) {
  1628. return values;
  1629. }
  1630.  
  1631. // 读取模块配置
  1632. const { keyPath, expireTime, persistent } = modules[name];
  1633.  
  1634. // 筛选出超时数据
  1635. const result = [];
  1636. const expired = [];
  1637.  
  1638. values.forEach((value) => {
  1639. // 持久化或未超时
  1640. if (persistent || value.timestamp + expireTime > new Date().getTime()) {
  1641. result.push(value);
  1642. return;
  1643. }
  1644.  
  1645. // 记录超时数据
  1646. expired.push(value[keyPath]);
  1647. });
  1648.  
  1649. // 移除超时数据
  1650. await super.bulkDelete(name, expired);
  1651.  
  1652. // 返回结果
  1653. return result;
  1654. }
  1655. }
  1656.  
  1657. /**
  1658. * API
  1659. */
  1660. class API {
  1661. /**
  1662. * 缓存管理
  1663. */
  1664. cache;
  1665.  
  1666. /**
  1667. * 队列
  1668. */
  1669. queue;
  1670.  
  1671. /**
  1672. * 初始化并绑定缓存管理
  1673. * @param {Cache} cache 缓存管理
  1674. */
  1675. constructor(cache) {
  1676. this.cache = cache;
  1677. this.queue = new Queue();
  1678. }
  1679.  
  1680. /**
  1681. * 简单的统一请求
  1682. * @param {String} url 请求地址
  1683. * @param {Object} config 请求参数
  1684. * @param {Boolean} toJSON 是否转为 JSON 格式
  1685. */
  1686. async request(url, config = {}, toJSON = true) {
  1687. const userAgent =
  1688. (await this.cache.get(USER_AGENT_KEY)) || "Nga_Official";
  1689.  
  1690. const response = await fetch(url, {
  1691. headers: {
  1692. Referer: "",
  1693. "X-User-Agent": userAgent,
  1694. },
  1695. ...config,
  1696. });
  1697.  
  1698. const result = await Tools.readForumData(response, toJSON);
  1699.  
  1700. return result;
  1701. }
  1702.  
  1703. /**
  1704. * 获取用户主题数量
  1705. * @param {number} uid 用户 ID
  1706. */
  1707. async getTopicNum(uid) {
  1708. const name = "TOPIC_NUM_CACHE";
  1709. const { expireTime } = modules[name];
  1710.  
  1711. const api = `/thread.php?lite=js&authorid=${uid}`;
  1712.  
  1713. const cache = await this.cache.get(name, uid);
  1714.  
  1715. // 仍在缓存期间内,直接返回
  1716. if (cache) {
  1717. const expired = cache.timestamp + expireTime < new Date().getTime();
  1718.  
  1719. if (expired === false) {
  1720. return cache.count;
  1721. }
  1722. }
  1723.  
  1724. // 请求用户信息,获取发帖数量
  1725. const { posts } = await this.getUserInfo(uid);
  1726.  
  1727. // 发帖数量不准,且可能错误的返回 0
  1728. const value = posts || 0;
  1729.  
  1730. // 发帖数量在泥潭其他接口里命名为 postnum
  1731. const postnum = (() => {
  1732. if (value > 0) {
  1733. return value;
  1734. }
  1735.  
  1736. if (cache) {
  1737. return cache.postnum || 0;
  1738. }
  1739.  
  1740. return 0;
  1741. })();
  1742.  
  1743. // 当发帖数量发生变化时,再重新请求数据
  1744. const needRequest = (() => {
  1745. if (value > 0 && cache) {
  1746. return cache.postnum !== value;
  1747. }
  1748.  
  1749. return true;
  1750. })();
  1751.  
  1752. // 需要重新请求
  1753. if (needRequest) {
  1754. // 由于泥潭接口限制,同步使用队列请求数据
  1755. const task = () =>
  1756. new Promise(async (resolve, reject) => {
  1757. const result = await this.request(api);
  1758.  
  1759. // 服务器可能返回错误,遇到这种情况下,需要保留缓存
  1760. if (result.data && Number.isInteger(result.data.__ROWS)) {
  1761. this.cache.put(name, {
  1762. uid,
  1763. count: result.data.__ROWS,
  1764. rencentTopics: result.data.__T,
  1765. postnum,
  1766. });
  1767.  
  1768. resolve();
  1769. return;
  1770. }
  1771.  
  1772. reject();
  1773. });
  1774.  
  1775. // 先尝试请求一次,成功后直接返回结果,否则加入队列
  1776. try {
  1777. if (this.queue.state === "IDLE") {
  1778. await task();
  1779.  
  1780. const { count } = await this.cache.get(name, uid);
  1781.  
  1782. return count;
  1783. }
  1784.  
  1785. throw new Error();
  1786. } catch {
  1787. this.queue.enqueue(uid, task);
  1788. }
  1789. }
  1790.  
  1791. // 直接返回缓存结果
  1792. const count = cache ? cache.count : 0;
  1793. const rencentTopics = cache ? cache.rencentTopics : [];
  1794.  
  1795. // 更新缓存
  1796. this.cache.put(name, {
  1797. uid,
  1798. count,
  1799. rencentTopics,
  1800. postnum,
  1801. });
  1802.  
  1803. return count;
  1804. }
  1805.  
  1806. /**
  1807. * 获取用户近期主题
  1808. * @param {number} uid 用户 ID
  1809. */
  1810. async getTopicRencent(uid) {
  1811. const name = "TOPIC_NUM_CACHE";
  1812.  
  1813. // 请求用户主题数量
  1814. const count = await this.getTopicNum(uid);
  1815.  
  1816. // 如果存在结果,读取缓存
  1817. if (count > 0) {
  1818. const cache = await this.cache.get(name, uid);
  1819.  
  1820. if (cache) {
  1821. return cache.rencentTopics || [];
  1822. }
  1823. }
  1824.  
  1825. return [];
  1826. }
  1827.  
  1828. /**
  1829. * 获取用户信息
  1830. * @param {number} uid 用户 ID
  1831. */
  1832. async getUserInfo(uid) {
  1833. const name = "USER_INFO_CACHE";
  1834.  
  1835. const api = `nuke.php?func=ucp&uid=${uid}`;
  1836.  
  1837. const cache = await this.cache.get(name, uid);
  1838.  
  1839. if (cache) {
  1840. return cache.data;
  1841. }
  1842.  
  1843. const result = await this.request(api, {}, false);
  1844.  
  1845. const data = (() => {
  1846. const text = Tools.searchPair(result, `__UCPUSER =`);
  1847.  
  1848. if (text) {
  1849. try {
  1850. return JSON.parse(text);
  1851. } catch {
  1852. return null;
  1853. }
  1854. }
  1855.  
  1856. return null;
  1857. })();
  1858.  
  1859. if (data) {
  1860. this.cache.put(name, {
  1861. uid,
  1862. data,
  1863. });
  1864. }
  1865.  
  1866. return data || {};
  1867. }
  1868.  
  1869. /**
  1870. * 获取属地列表
  1871. * @param {number} uid 用户 ID
  1872. */
  1873. async getIpLocations(uid) {
  1874. const name = "USER_IPLOC_CACHE";
  1875. const { expireTime } = modules[name];
  1876.  
  1877. const cache = await this.cache.get(name, uid);
  1878.  
  1879. // 仍在缓存期间内,直接返回
  1880. if (cache) {
  1881. const expired = cache.timestamp + expireTime < new Date().getTime();
  1882.  
  1883. if (expired === false) {
  1884. return cache.data;
  1885. }
  1886. }
  1887.  
  1888. // 属地列表
  1889. const data = cache ? cache.data : [];
  1890.  
  1891. // 请求属地
  1892. const { ipLoc } = await this.getUserInfo(uid);
  1893.  
  1894. // 写入缓存
  1895. if (ipLoc) {
  1896. const index = data.findIndex((item) => {
  1897. return item.ipLoc === ipLoc;
  1898. });
  1899.  
  1900. if (index >= 0) {
  1901. data.splice(index, 1);
  1902. }
  1903.  
  1904. data.unshift({
  1905. ipLoc,
  1906. timestamp: new Date().getTime(),
  1907. });
  1908.  
  1909. this.cache.put(name, {
  1910. uid,
  1911. data,
  1912. });
  1913. }
  1914.  
  1915. // 返回结果
  1916. return data;
  1917. }
  1918.  
  1919. /**
  1920. * 获取帖子内容、用户信息(主要是发帖数量,常规的获取用户信息方法不一定有结果)、版面声望
  1921. * @param {number} tid 主题 ID
  1922. * @param {number} pid 回复 ID
  1923. */
  1924. async getPostInfo(tid, pid) {
  1925. const name = "PAGE_CACHE";
  1926.  
  1927. const api = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
  1928.  
  1929. const cache = await this.cache.get(name, api);
  1930.  
  1931. if (cache) {
  1932. return cache.data;
  1933. }
  1934.  
  1935. const result = await this.request(api, {}, false);
  1936.  
  1937. const parser = new DOMParser();
  1938.  
  1939. const doc = parser.parseFromString(result, "text/html");
  1940.  
  1941. // 声明返回值
  1942. const data = {
  1943. subject: "",
  1944. content: "",
  1945. userInfo: null,
  1946. reputation: NaN,
  1947. };
  1948.  
  1949. // 验证帖子正常
  1950. const verify = doc.querySelector("#m_posts");
  1951.  
  1952. if (verify) {
  1953. // 取得顶楼 UID
  1954. data.uid = (() => {
  1955. const ele = doc.querySelector("#postauthor0");
  1956.  
  1957. if (ele) {
  1958. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  1959.  
  1960. if (res) {
  1961. return res[1];
  1962. }
  1963. }
  1964.  
  1965. return 0;
  1966. })();
  1967.  
  1968. // 取得顶楼标题
  1969. data.subject = doc.querySelector("#postsubject0").innerHTML;
  1970.  
  1971. // 取得顶楼内容
  1972. data.content = doc.querySelector("#postcontent0").innerHTML;
  1973.  
  1974. // 非匿名用户可以继续取得用户信息和版面声望
  1975. if (data.uid > 0) {
  1976. // 取得用户信息
  1977. data.userInfo = (() => {
  1978. const text = Tools.searchPair(result, `"${data.uid}":`);
  1979.  
  1980. if (text) {
  1981. try {
  1982. return JSON.parse(text);
  1983. } catch {
  1984. return null;
  1985. }
  1986. }
  1987.  
  1988. return null;
  1989. })();
  1990.  
  1991. // 取得用户声望
  1992. data.reputation = (() => {
  1993. const reputations = (() => {
  1994. const text = Tools.searchPair(result, `"__REPUTATIONS":`);
  1995.  
  1996. if (text) {
  1997. try {
  1998. return JSON.parse(text);
  1999. } catch {
  2000. return null;
  2001. }
  2002. }
  2003.  
  2004. return null;
  2005. })();
  2006.  
  2007. if (reputations) {
  2008. for (let fid in reputations) {
  2009. return reputations[fid][data.uid] || 0;
  2010. }
  2011. }
  2012.  
  2013. return NaN;
  2014. })();
  2015. }
  2016. }
  2017.  
  2018. // 写入缓存
  2019. this.cache.put(name, {
  2020. url: api,
  2021. data,
  2022. });
  2023.  
  2024. // 返回结果
  2025. return data;
  2026. }
  2027.  
  2028. /**
  2029. * 获取版面信息
  2030. * @param {number} fid 版面 ID
  2031. */
  2032. async getForumInfo(fid) {
  2033. if (Number.isNaN(fid)) {
  2034. return null;
  2035. }
  2036.  
  2037. const api = `/thread.php?lite=js&fid=${fid}`;
  2038.  
  2039. const result = await this.request(api);
  2040.  
  2041. const info = result.data ? result.data.__F : null;
  2042.  
  2043. return info;
  2044. }
  2045.  
  2046. /**
  2047. * 获取版面发言记录
  2048. * @param {number} fid 版面 ID
  2049. * @param {number} uid 用户 ID
  2050. */
  2051. async getForumPosted(fid, uid) {
  2052. const name = "FORUM_POSTED_CACHE";
  2053. const { expireTime } = modules[name];
  2054.  
  2055. const api = `/thread.php?lite=js&authorid=${uid}&fid=${fid}`;
  2056.  
  2057. const cache = await this.cache.get(name, api);
  2058.  
  2059. if (cache) {
  2060. // 发言是无法撤销的,只要有记录就永远不需要再获取
  2061. // 手动处理没有记录的缓存数据
  2062. const expired = cache.timestamp + expireTime < new Date().getTime();
  2063.  
  2064. if (expired && cache.data === false) {
  2065. await this.cache.delete(name, api);
  2066. }
  2067.  
  2068. return cache.data;
  2069. }
  2070.  
  2071. let isComplete = false;
  2072. let isBusy = false;
  2073.  
  2074. const func = async (url) => {
  2075. if (isComplete || isBusy) {
  2076. return;
  2077. }
  2078.  
  2079. const result = await this.request(url, {}, false);
  2080.  
  2081. // 将所有匹配的 FID 写入缓存,即使并不在设置里
  2082. const matched = result.match(/"fid":(-?\d+),/g);
  2083.  
  2084. if (matched) {
  2085. const list = [
  2086. ...new Set(
  2087. matched.map((item) => parseInt(item.match(/-?\d+/)[0], 10))
  2088. ),
  2089. ];
  2090.  
  2091. list.forEach((item) => {
  2092. const key = api.replace(`&fid=${fid}`, `&fid=${item}`);
  2093.  
  2094. // 写入缓存
  2095. this.cache.put(name, {
  2096. url: key,
  2097. data: true,
  2098. });
  2099.  
  2100. // 已有结果,无需继续查询
  2101. if (fid === item) {
  2102. isComplete = true;
  2103. }
  2104. });
  2105. }
  2106.  
  2107. // 泥潭给版面查询接口增加了限制,经常会出现“服务器忙,请稍后重试”的错误
  2108. if (result.indexOf("服务器忙") > 0) {
  2109. isBusy = true;
  2110. }
  2111. };
  2112.  
  2113. // 先获取回复记录的第一页,顺便可以获取其他版面的记录
  2114. // 没有再通过版面接口获取,避免频繁出现“服务器忙,请稍后重试”的错误
  2115. await func(api.replace(`&fid=${fid}`, `&searchpost=1`));
  2116. await func(api + "&searchpost=1");
  2117. await func(api);
  2118.  
  2119. // 无论成功与否都写入缓存
  2120. if (isComplete === false) {
  2121. // 遇到服务器忙的情况,手动调整缓存时间至 1 小时
  2122. const timestamp = isBusy
  2123. ? new Date().getTime() - (expireTime - 1000 * 60 * 60)
  2124. : new Date().getTime();
  2125.  
  2126. // 写入失败缓存
  2127. this.cache.put(name, {
  2128. url: api,
  2129. data: false,
  2130. timestamp,
  2131. });
  2132. }
  2133.  
  2134. return isComplete;
  2135. }
  2136.  
  2137. /**
  2138. * 获取用户的曾用名
  2139. * @param {number} uid 用户 ID
  2140. */
  2141. async getUsernameChanged(uid) {
  2142. const name = "USER_NAME_CHANGED";
  2143. const { expireTime } = modules[name];
  2144.  
  2145. const api = `/nuke.php?lite=js&__lib=ucp&__act=oldname&uid=${uid}`;
  2146.  
  2147. const cache = await this.cache.get(name, uid);
  2148.  
  2149. // 仍在缓存期间内,直接返回
  2150. if (cache) {
  2151. const expired = cache.timestamp + expireTime < new Date().getTime();
  2152.  
  2153. if (expired === false) {
  2154. return cache.data;
  2155. }
  2156. }
  2157.  
  2158. // 请求用户信息
  2159. const { usernameChanged } = await this.getUserInfo(uid);
  2160.  
  2161. // 如果有修改记录
  2162. if (usernameChanged) {
  2163. // 请求数据
  2164. const result = await this.request(api);
  2165.  
  2166. // 取得结果
  2167. const data = result.data ? result.data[0] : null;
  2168.  
  2169. // 更新缓存
  2170. this.cache.put(name, {
  2171. uid,
  2172. data,
  2173. });
  2174.  
  2175. return data;
  2176. }
  2177.  
  2178. return null;
  2179. }
  2180.  
  2181. /**
  2182. * 获取用户绑定的 Steam 信息
  2183. * @param {number} uid 用户 ID
  2184. */
  2185. async getSteamInfo(uid) {
  2186. const name = "USER_STEAM_INFO";
  2187. const { expireTime } = modules[name];
  2188.  
  2189. const api = `/nuke.php?lite=js&__lib=steam&__act=steam_user_info&user_id=${uid}`;
  2190.  
  2191. const cache = await this.cache.get(name, uid);
  2192.  
  2193. // 仍在缓存期间内,直接返回
  2194. if (cache) {
  2195. const expired = cache.timestamp + expireTime < new Date().getTime();
  2196.  
  2197. if (expired === false) {
  2198. return cache.data;
  2199. }
  2200. }
  2201.  
  2202. // 请求数据
  2203. // Steam ID 64 位会超出 JavaScript Number 长度,需要手动处理
  2204. const result = await this.request(
  2205. api,
  2206. {
  2207. method: "POST",
  2208. },
  2209. false
  2210. );
  2211.  
  2212. // 先转换成 JSON
  2213. const resultJSON = JSON.parse(result);
  2214.  
  2215. // 取得结果
  2216. const data = resultJSON.data ? resultJSON.data[0] : null;
  2217.  
  2218. // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式
  2219. if (data.steam_user_id) {
  2220. const matched = result.match(/"steam_user_id":(\d+),/);
  2221.  
  2222. if (matched) {
  2223. data.steam_user_id = String(matched[1]);
  2224. }
  2225. }
  2226.  
  2227. // 更新缓存
  2228. this.cache.put(name, {
  2229. uid,
  2230. data,
  2231. });
  2232.  
  2233. return data;
  2234. }
  2235.  
  2236. /**
  2237. * 获取用户绑定的 PSN 信息
  2238. * @param {number} uid 用户 ID
  2239. */
  2240. async getPSNInfo(uid) {
  2241. const name = "USER_PSN_INFO";
  2242. const { expireTime } = modules[name];
  2243.  
  2244. const api = `/nuke.php?lite=js&__lib=psn&__act=psn_user_info&user_id=${uid}`;
  2245.  
  2246. const cache = await this.cache.get(name, uid);
  2247.  
  2248. // 仍在缓存期间内,直接返回
  2249. if (cache) {
  2250. const expired = cache.timestamp + expireTime < new Date().getTime();
  2251.  
  2252. if (expired === false) {
  2253. return cache.data;
  2254. }
  2255. }
  2256.  
  2257. // 请求数据
  2258. // PSN ID 64 位会超出 JavaScript Number 长度,需要手动处理
  2259. const result = await this.request(
  2260. api,
  2261. {
  2262. method: "POST",
  2263. },
  2264. false
  2265. );
  2266.  
  2267. // 先转换成 JSON
  2268. const resultJSON = JSON.parse(result);
  2269.  
  2270. // 取得结果
  2271. const data = resultJSON.data ? resultJSON.data[0] : null;
  2272.  
  2273. // 如果有绑定的数据,从原始数据中取得数据,并转为 String 格式
  2274. if (data.psn_user_id) {
  2275. const matched = result.match(/"psn_user_id":(\d+),/);
  2276.  
  2277. if (matched) {
  2278. data.psn_user_id = String(matched[1]);
  2279. }
  2280. }
  2281.  
  2282. // 更新缓存
  2283. this.cache.put(name, {
  2284. uid,
  2285. data,
  2286. });
  2287.  
  2288. return data;
  2289. }
  2290.  
  2291. /**
  2292. * 获取用户绑定的 NS 信息
  2293. * @param {number} uid 用户 ID
  2294. */
  2295. async getNintendoInfo(uid) {
  2296. const name = "USER_NINTENDO_INFO";
  2297. const { expireTime } = modules[name];
  2298.  
  2299. const api = `/nuke.php?lite=js&__lib=nintendo&__act=user_info&user_id=${uid}`;
  2300.  
  2301. const cache = await this.cache.get(name, uid);
  2302.  
  2303. // 仍在缓存期间内,直接返回
  2304. if (cache) {
  2305. const expired = cache.timestamp + expireTime < new Date().getTime();
  2306.  
  2307. if (expired === false) {
  2308. return cache.data;
  2309. }
  2310. }
  2311.  
  2312. // 请求数据
  2313. const result = await this.request(api, {
  2314. method: "POST",
  2315. });
  2316.  
  2317. // 取得结果
  2318. const data = result.data ? result.data[0] : null;
  2319.  
  2320. // 更新缓存
  2321. this.cache.put(name, {
  2322. uid,
  2323. data,
  2324. });
  2325.  
  2326. return data;
  2327. }
  2328.  
  2329. /**
  2330. * 获取用户绑定的原神信息
  2331. * @param {number} uid 用户 ID
  2332. */
  2333. async getGenshinInfo(uid) {
  2334. const name = "USER_GENSHIN_INFO";
  2335. const { expireTime } = modules[name];
  2336.  
  2337. const api = `/nuke.php?lite=js&__lib=genshin&__act=get_user&uid=${uid}`;
  2338.  
  2339. const cache = await this.cache.get(name, uid);
  2340.  
  2341. // 仍在缓存期间内,直接返回
  2342. if (cache) {
  2343. const expired = cache.timestamp + expireTime < new Date().getTime();
  2344.  
  2345. if (expired === false) {
  2346. return cache.data;
  2347. }
  2348. }
  2349.  
  2350. // 请求数据
  2351. const result = await this.request(api, {
  2352. method: "POST",
  2353. });
  2354.  
  2355. // 取得结果
  2356. const data = result.data ? result.data[0] : null;
  2357.  
  2358. // 更新缓存
  2359. this.cache.put(name, {
  2360. uid,
  2361. data,
  2362. });
  2363.  
  2364. return data;
  2365. }
  2366.  
  2367. /**
  2368. * 获取用户绑定的深空之眼信息
  2369. * @param {number} uid 用户 ID
  2370. */
  2371. async getSKZYInfo(uid) {
  2372. const name = "USER_SKZY_INFO";
  2373. const { expireTime } = modules[name];
  2374.  
  2375. const api = `/nuke.php?lite=js&__lib=auth_ys4fun&__act=skzy_user_game&user_id=${uid}`;
  2376.  
  2377. const cache = await this.cache.get(name, uid);
  2378.  
  2379. // 仍在缓存期间内,直接返回
  2380. if (cache) {
  2381. const expired = cache.timestamp + expireTime < new Date().getTime();
  2382.  
  2383. if (expired === false) {
  2384. return cache.data;
  2385. }
  2386. }
  2387.  
  2388. // 请求数据
  2389. const result = await this.request(api, {
  2390. method: "POST",
  2391. });
  2392.  
  2393. // 取得结果
  2394. const data = result.data ? result.data[0] : null;
  2395.  
  2396. // 更新缓存
  2397. this.cache.put(name, {
  2398. uid,
  2399. data,
  2400. });
  2401.  
  2402. return data;
  2403. }
  2404.  
  2405. /**
  2406. * 获取用户绑定的游戏信息
  2407. * @param {number} uid 用户 ID
  2408. */
  2409. async getUserGameInfo(uid) {
  2410. // 请求 Steam 信息
  2411. const steam = await this.getSteamInfo(uid);
  2412.  
  2413. // 请求 PSN 信息
  2414. const psn = await this.getPSNInfo(uid);
  2415.  
  2416. // 请求 NS 信息
  2417. const nintendo = await this.getNintendoInfo(uid);
  2418.  
  2419. // 请求原神信息
  2420. const genshin = await this.getGenshinInfo(uid);
  2421.  
  2422. // 请求深空之眼信息
  2423. const skzy = await this.getSKZYInfo(uid);
  2424.  
  2425. // 返回结果
  2426. return {
  2427. steam,
  2428. psn,
  2429. nintendo,
  2430. genshin,
  2431. skzy,
  2432. };
  2433. }
  2434. }
  2435.  
  2436. /**
  2437. * 注册脚本菜单
  2438. * @param {Cache} cache 缓存管理
  2439. */
  2440. const registerMenu = async (cache) => {
  2441. const data = (await cache.get(USER_AGENT_KEY)) || "Nga_Official";
  2442.  
  2443. GM_registerMenuCommand(`修改UA${data}`, () => {
  2444. const value = prompt("修改UA", data);
  2445.  
  2446. if (value) {
  2447. cache.put(USER_AGENT_KEY, value);
  2448. location.reload();
  2449. }
  2450. });
  2451. };
  2452.  
  2453. /**
  2454. * 自动清理缓存
  2455. * @param {Cache} cache 缓存管理
  2456. */
  2457. const autoClear = async (cache) =>
  2458. Promise.all(Object.keys(modules).map((name) => cache.clear(name, true)));
  2459.  
  2460. // 初始化事件
  2461. return () => {
  2462. // 防止重复初始化
  2463. if (unsafeWindow.NLibrary === undefined) {
  2464. // 初始化缓存和 API
  2465. const cache = new Cache();
  2466. const api = new API(cache);
  2467.  
  2468. // 自动清理缓存
  2469. autoClear(cache);
  2470.  
  2471. // 写入全局变量
  2472. unsafeWindow.NLibrary = {
  2473. cache,
  2474. api,
  2475. };
  2476. }
  2477.  
  2478. const { cache, api } = unsafeWindow.NLibrary;
  2479.  
  2480. // 注册脚本菜单
  2481. registerMenu(cache);
  2482.  
  2483. // 返回结果
  2484. return {
  2485. cache,
  2486. api,
  2487. };
  2488. };
  2489. })();