NGA Likes Support

显示被点赞和粉丝数量,以及发帖数量、IP属地、曾用名

As of 2024-03-29. See the latest version.

  1. // ==UserScript==
  2. // @name NGA Likes Support
  3. // @namespace https://greasyfork.org/users/263018
  4. // @version 1.4.2
  5. // @author snyssss
  6. // @description 显示被点赞和粉丝数量,以及发帖数量、IP属地、曾用名
  7.  
  8. // @match *://bbs.nga.cn/*
  9. // @match *://ngabbs.com/*
  10. // @match *://nga.178.com/*
  11.  
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_xmlhttpRequest
  16. // @noframes
  17. // ==/UserScript==
  18.  
  19. (async (ui) => {
  20. if (!ui) return;
  21.  
  22. // KEY
  23. const SHOW_OLDNAME_ENABLE_KEY = "SHOW_OLDNAME_ENABLE";
  24. const SHOW_POSTNUM_ENABLE_KEY = "SHOW_POSTNUM_ENABLE";
  25. const SHOW_IPLOC_ENABLE_KEY = "SHOW_IPLOC_ENABLE";
  26.  
  27. // 显示曾用名
  28. const showOldnameEnable = GM_getValue(SHOW_OLDNAME_ENABLE_KEY) || false;
  29.  
  30. // 显示发帖数
  31. const showPostnumEnable = GM_getValue(SHOW_POSTNUM_ENABLE_KEY) || false;
  32.  
  33. // 显示属地
  34. const showIpLocEnable = GM_getValue(SHOW_IPLOC_ENABLE_KEY) || false;
  35.  
  36. // 钩子
  37. const hookFunction = (object, functionName, callback) => {
  38. ((originalFunction) => {
  39. object[functionName] = function () {
  40. const returnValue = originalFunction.apply(this, arguments);
  41.  
  42. callback.apply(this, [returnValue, originalFunction, arguments]);
  43.  
  44. return returnValue;
  45. };
  46. })(object[functionName]);
  47. };
  48.  
  49. // IndexedDB 操作
  50. const db = await (async () => {
  51. // 常量
  52. const VERSION = 1;
  53. const DB_NAME = "NGA_CACHE_IPLOC";
  54. const TABLE_NAME = "ipLoc";
  55.  
  56. // 是否支持
  57. const support = window.indexedDB !== undefined;
  58.  
  59. // 不支持,直接返回
  60. if (support === false) {
  61. return {
  62. support,
  63. };
  64. }
  65.  
  66. // 获取数据库实例
  67. const instance = await new Promise((resolve) => {
  68. // 打开 IndexedDB 数据库
  69. const request = window.indexedDB.open(DB_NAME, VERSION);
  70.  
  71. // 如果数据库不存在则创建
  72. request.onupgradeneeded = (event) => {
  73. // 创建表
  74. const store = event.target.result.createObjectStore(TABLE_NAME, {
  75. keyPath: null,
  76. autoIncrement: true,
  77. });
  78.  
  79. // 创建索引
  80. store.createIndex("uid", "uid");
  81. };
  82.  
  83. // 成功后返回实例
  84. request.onsuccess = (event) => {
  85. resolve(event.target.result);
  86. };
  87. });
  88.  
  89. // 缓存数据
  90. const save = (uid, ipLoc) =>
  91. new Promise((resolve, reject) => {
  92. // 创建事务
  93. const transaction = instance.transaction([TABLE_NAME], "readwrite");
  94.  
  95. // 获取对象仓库
  96. const store = transaction.objectStore(TABLE_NAME);
  97.  
  98. // 获取索引
  99. const index = store.index("uid");
  100.  
  101. // 查找最新的数据
  102. const request = index.openCursor(IDBKeyRange.only(uid), "prev");
  103.  
  104. // 成功后处理数据
  105. request.onsuccess = (event) => {
  106. const cursor = event.target.result;
  107.  
  108. // 如果属地没有变化则跳过
  109. if (cursor && cursor.value.ipLoc === ipLoc) {
  110. resolve();
  111. return;
  112. }
  113.  
  114. // 插入数据
  115. const r = store.put({
  116. uid,
  117. ipLoc,
  118. timestamp: Date.now(),
  119. });
  120.  
  121. r.onsuccess = () => {
  122. resolve();
  123. };
  124.  
  125. r.onerror = () => {
  126. reject();
  127. };
  128. };
  129.  
  130. // 失败后处理
  131. request.onerror = (event) => {
  132. reject(event.target.error);
  133. };
  134. });
  135.  
  136. // 读取数据
  137. const load = (uid, count) =>
  138. new Promise((resolve, reject) => {
  139. // 声明结果
  140. const result = [];
  141.  
  142. // 创建事务
  143. const transaction = instance.transaction([TABLE_NAME], "readwrite");
  144.  
  145. // 获取对象仓库
  146. const store = transaction.objectStore(TABLE_NAME);
  147.  
  148. // 获取索引
  149. const index = store.index("uid");
  150.  
  151. // 查找最新的数据
  152. const request = index.openCursor(IDBKeyRange.only(uid), "prev");
  153.  
  154. // 成功后处理数据
  155. request.onsuccess = (event) => {
  156. const cursor = event.target.result;
  157.  
  158. if (cursor && cursor.value) {
  159. if (
  160. result.length < count &&
  161. result.findIndex((item) => item.ipLoc === cursor.value.ipLoc) < 0
  162. ) {
  163. result.push(cursor.value);
  164. }
  165.  
  166. cursor.continue();
  167. } else {
  168. resolve(result);
  169. }
  170. };
  171.  
  172. // 失败后处理
  173. request.onerror = (event) => {
  174. reject(event.target.error);
  175. };
  176. });
  177.  
  178. return {
  179. support,
  180. save,
  181. load,
  182. };
  183. })();
  184.  
  185. class UserInfo {
  186. execute(task) {
  187. task().finally(() => {
  188. if (this.waitingQueue.length) {
  189. const next = this.waitingQueue.shift();
  190.  
  191. this.execute(next);
  192. } else {
  193. this.isRunning = false;
  194. }
  195. });
  196. }
  197.  
  198. enqueue(task) {
  199. if (this.isRunning) {
  200. this.waitingQueue.push(task);
  201. } else {
  202. this.isRunning = true;
  203.  
  204. this.execute(task);
  205. }
  206. }
  207.  
  208. rearrange() {
  209. if (this.data) {
  210. const list = Object.values(this.children);
  211.  
  212. for (let i = 0; i < list.length; i++) {
  213. if (list[i].source === undefined) {
  214. list[i].create(this.data);
  215. }
  216.  
  217. Object.entries(this.container).forEach((item) => {
  218. list[i].clone(this.data, item);
  219. });
  220. }
  221. }
  222. }
  223.  
  224. reload() {
  225. this.enqueue(async () => {
  226. this.data = await new Promise((resolve) => {
  227. fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${this.uid}`, {
  228. credentials: "omit",
  229. })
  230. .then((res) => res.blob())
  231. .then((blob) => {
  232. const reader = new FileReader();
  233.  
  234. reader.onload = () => {
  235. const text = reader.result;
  236. const result = JSON.parse(
  237. text.replace("window.script_muti_get_var_store=", "")
  238. );
  239.  
  240. resolve(result.data[0]);
  241. };
  242.  
  243. reader.readAsText(blob, "GBK");
  244. })
  245. .catch(() => {
  246. resolve({});
  247. });
  248. });
  249.  
  250. if (this.data.usernameChanged && showOldnameEnable) {
  251. this.data.oldname = await new Promise((resolve) => {
  252. fetch(`/nuke.php?lite=js&__lib=ucp&__act=oldname&uid=${this.uid}`)
  253. .then((res) => res.blob())
  254. .then((blob) => {
  255. const reader = new FileReader();
  256.  
  257. reader.onload = () => {
  258. const text = reader.result;
  259. const result = JSON.parse(
  260. text.replace("window.script_muti_get_var_store=", "")
  261. );
  262.  
  263. resolve(result.data[0]);
  264. };
  265.  
  266. reader.readAsText(blob, "GBK");
  267. })
  268. .catch(() => {
  269. resolve();
  270. });
  271. });
  272. }
  273.  
  274. Object.values(this.children).forEach((item) => item.destroy());
  275.  
  276. this.rearrange();
  277. });
  278. }
  279.  
  280. constructor(id) {
  281. this.uid = id;
  282.  
  283. this.waitingQueue = [];
  284. this.isRunning = false;
  285.  
  286. this.container = {};
  287. this.children = {};
  288.  
  289. this.reload();
  290. }
  291. }
  292.  
  293. class UserInfoWidget {
  294. destroy() {
  295. if (this.source) {
  296. this.source = undefined;
  297. }
  298.  
  299. if (this.target) {
  300. Object.values(this.target).forEach((item) => {
  301. if (item.parentNode) {
  302. item.parentNode.removeChild(item);
  303. }
  304. });
  305. }
  306. }
  307.  
  308. clone(data, [argid, container]) {
  309. if (this.source) {
  310. if (this.target[argid] === undefined) {
  311. this.target[argid] = this.source.cloneNode(true);
  312.  
  313. if (this.callback) {
  314. this.callback(data, this.target[argid]);
  315. }
  316. }
  317.  
  318. const isSmall = container.classList.contains("posterInfoLine");
  319.  
  320. if (isSmall) {
  321. const anchor = container.querySelector(".author ~ br");
  322.  
  323. if (anchor) {
  324. anchor.parentNode.insertBefore(this.target[argid], anchor);
  325. }
  326. } else {
  327. container.appendChild(this.target[argid]);
  328. }
  329. }
  330. }
  331.  
  332. constructor(func, callback) {
  333. this.create = (data) => {
  334. this.destroy();
  335.  
  336. this.source = func(data);
  337. this.target = {};
  338. };
  339.  
  340. this.callback = callback;
  341. }
  342. }
  343.  
  344. ui.sn = ui.sn || {};
  345. ui.sn.userInfo = ui.sn.userInfo || {};
  346.  
  347. ((info) => {
  348. const execute = (argid) => {
  349. const args = ui.postArg.data[argid];
  350.  
  351. if (args.comment) return;
  352.  
  353. const uid = +args.pAid;
  354.  
  355. if (uid > 0) {
  356. if (info[uid] === undefined) {
  357. info[uid] = new UserInfo(uid);
  358. }
  359.  
  360. if (document.contains(info[uid].container[argid]) === false) {
  361. info[uid].container[argid] =
  362. args.uInfoC.closest("tr").querySelector(".posterInfoLine") ||
  363. args.uInfoC.querySelector("div");
  364. }
  365.  
  366. info[uid].enqueue(async () => {
  367. if (info[uid].children[8] === undefined) {
  368. info[uid].children[8] = new UserInfoWidget((data) => {
  369. const value =
  370. Object.values(data.more_info || {}).find(
  371. (item) => item.type === 8
  372. )?.data || 0;
  373.  
  374. const element = document.createElement("SPAN");
  375.  
  376. element.className =
  377. "small_colored_text_btn stxt block_txt_c2 vertmod";
  378. element.style.cursor = "default";
  379. element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">⯅</span>&nbsp;${value}</span>`;
  380.  
  381. return element;
  382. });
  383. }
  384.  
  385. if (info[uid].children[16] === undefined) {
  386. info[uid].children[16] = new UserInfoWidget((data) => {
  387. const value = data.follow_by_num || 0;
  388.  
  389. const element = document.createElement("SPAN");
  390.  
  391. element.className =
  392. "small_colored_text_btn stxt block_txt_c2 vertmod";
  393. element.style.cursor = "default";
  394. element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>&nbsp;${value}</span>`;
  395.  
  396. return element;
  397. });
  398. }
  399.  
  400. info[uid].rearrange();
  401.  
  402. const container = info[uid].container[argid];
  403.  
  404. const isSmall = container.classList.contains("posterInfoLine");
  405.  
  406. // 显示曾用名
  407. if (showOldnameEnable) {
  408. if (ui._w.__GP.admincheck) {
  409. return;
  410. }
  411.  
  412. if (isSmall) {
  413. const anchor = [
  414. ...container.querySelectorAll("span.usercol"),
  415. ].pop().nextElementSibling;
  416.  
  417. const uInfo = info[uid].data;
  418.  
  419. if (anchor && uInfo && uInfo.oldname) {
  420. const element = document.createElement("SPAN");
  421.  
  422. element.className = "usercol nobr";
  423. element.innerHTML = `
  424. <span> · 曾用名 ${Object.values(uInfo.oldname)
  425. .map(
  426. (item) =>
  427. `<span class="userval" title="${ui.time2dis(
  428. item.time
  429. )}">${item.username}</span>`
  430. )
  431. .join(", ")}</span>`;
  432.  
  433. anchor.parentNode.insertBefore(element, anchor);
  434. }
  435. } else {
  436. const anchor = container.parentNode.querySelector(
  437. '.stat div[class="clear"]'
  438. ).parentNode;
  439.  
  440. const uInfo = info[uid].data;
  441.  
  442. if (anchor && uInfo && uInfo.oldname) {
  443. const element = document.createElement("DIV");
  444.  
  445. element.innerHTML = `
  446. <span>曾用名: ${Object.values(uInfo.oldname)
  447. .map(
  448. (item) =>
  449. `<span class="userval" title="${ui.time2dis(
  450. item.time
  451. )}">${item.username}</span>`
  452. )
  453. .join(", ")}</span>`;
  454.  
  455. anchor.parentNode.appendChild(element, anchor);
  456. }
  457. }
  458. }
  459.  
  460. // 显示发帖数
  461. if (showPostnumEnable) {
  462. if (ui._w.__GP.admincheck) {
  463. return;
  464. }
  465.  
  466. if (isSmall) {
  467. const anchor = [
  468. ...container.querySelectorAll("span.usercol"),
  469. ].pop().nextElementSibling;
  470.  
  471. const uInfo = ui.userInfo.users[uid];
  472.  
  473. if (anchor && uInfo) {
  474. const element = document.createElement("SPAN");
  475.  
  476. element.className = "usercol nobr";
  477. element.innerHTML = `
  478. <span> · 发帖 <span class="${
  479. uInfo.postnum > 9999 ? "numeric" : "numericl"
  480. } userval">${uInfo.postnum}</span></span>`;
  481.  
  482. anchor.parentNode.insertBefore(element, anchor);
  483. }
  484. } else {
  485. const anchor = container.parentNode.querySelector(
  486. '.stat div[class="clear"]'
  487. );
  488.  
  489. const uInfo = ui.userInfo.users[uid];
  490.  
  491. if (anchor && uInfo) {
  492. const element = document.createElement("DIV");
  493.  
  494. element.style =
  495. "float:left;margin-right:3px;min-width:49%;*width:49%";
  496. element.innerHTML = `
  497. <nobr>
  498. <span>发帖: <span class="${
  499. uInfo.postnum > 9999 ? "numeric" : "numericl"
  500. } userval">${uInfo.postnum}</span></span>
  501. </nobr>`;
  502.  
  503. anchor.parentNode.insertBefore(element, anchor);
  504. }
  505. }
  506. }
  507.  
  508. // 显示属地
  509. if (showIpLocEnable) {
  510. if (ui._w.__GP.admincheck) {
  511. return;
  512. }
  513.  
  514. const data = await (async () => {
  515. const uInfo = info[uid].data;
  516.  
  517. if (uInfo) {
  518. try {
  519. if (db.support) {
  520. await db.save(uid, uInfo.ipLoc);
  521.  
  522. return await db.load(uid, 3);
  523. }
  524. } catch (e) {}
  525.  
  526. return [{ ipLoc: uInfo.ipLoc }];
  527. }
  528.  
  529. return [];
  530. })();
  531.  
  532. if (isSmall) {
  533. const anchor = [
  534. ...container.querySelectorAll("span.usercol"),
  535. ].pop().nextElementSibling;
  536.  
  537. if (anchor && data.length > 0) {
  538. const element = document.createElement("SPAN");
  539.  
  540. element.className = "usercol nobr";
  541. element.innerHTML = `
  542. <span> · 属地 ${Object.values(data)
  543. .map(
  544. (item) =>
  545. `<span class="userval" title="${
  546. item.timestamp
  547. ? ui.time2dis(item.timestamp / 1000)
  548. : ""
  549. }">${item.ipLoc}</span>`
  550. )
  551. .join(", ")}</span>`;
  552.  
  553. anchor.parentNode.insertBefore(element, anchor);
  554. }
  555. } else {
  556. const anchor = container.parentNode.querySelector(
  557. '.stat div[class="clear"]'
  558. );
  559.  
  560. if (anchor && data.length > 0) {
  561. const element = document.createElement("DIV");
  562.  
  563. element.style =
  564. "float:left;margin-right:3px;min-width:49%;*width:49%";
  565. element.innerHTML = `
  566. <nobr>
  567. <span>属地: ${Object.values(data)
  568. .map(
  569. (item) =>
  570. `<span class="userval" title="${
  571. item.timestamp
  572. ? ui.time2dis(item.timestamp / 1000)
  573. : ""
  574. }">${item.ipLoc}</span>`
  575. )
  576. .join(", ")}</span>
  577. </nobr>`;
  578.  
  579. anchor.parentNode.insertBefore(element, anchor);
  580. }
  581. }
  582. }
  583. });
  584. }
  585. };
  586.  
  587. const refetch = (arguments) => {
  588. const anchor = arguments[0];
  589.  
  590. const { tid, pid } = arguments[1];
  591.  
  592. const target = anchor.parentNode.querySelector(".recommendvalue");
  593.  
  594. if (!target) return;
  595.  
  596. const observer = new MutationObserver(() => {
  597. observer.disconnect();
  598.  
  599. const url = pid ? `/read.php?pid=${pid}` : `/read.php?tid=${tid}`;
  600.  
  601. fetch(url)
  602. .then((res) => res.blob())
  603. .then((blob) => {
  604. const getLastIndex = (content, position) => {
  605. if (position >= 0) {
  606. let nextIndex = position + 1;
  607.  
  608. while (nextIndex < content.length) {
  609. if (content[nextIndex] === ")") {
  610. return nextIndex;
  611. }
  612.  
  613. if (content[nextIndex] === "(") {
  614. nextIndex = getLastIndex(content, nextIndex);
  615.  
  616. if (nextIndex < 0) {
  617. break;
  618. }
  619. }
  620.  
  621. nextIndex = nextIndex + 1;
  622. }
  623. }
  624.  
  625. return -1;
  626. };
  627.  
  628. const reader = new FileReader();
  629.  
  630. reader.onload = async () => {
  631. const parser = new DOMParser();
  632.  
  633. const doc = parser.parseFromString(reader.result, "text/html");
  634.  
  635. const html = doc.body.innerHTML;
  636.  
  637. const verify = doc.querySelector("#m_posts");
  638.  
  639. if (verify) {
  640. const str = `commonui.postArg.proc( 0`;
  641.  
  642. const index = html.indexOf(str) + str.length;
  643.  
  644. const lastIndex = getLastIndex(html, index);
  645.  
  646. if (lastIndex >= 0) {
  647. const matched = html
  648. .substring(index, lastIndex)
  649. .match(/'\d+,(\d+),(\d+)'/);
  650.  
  651. if (matched) {
  652. const score = (matched[1] |= 0);
  653. const score_2 = (matched[2] |= 0);
  654. const recommend = score - score_2;
  655.  
  656. target.innerHTML = recommend > 0 ? recommend : 0;
  657. }
  658. }
  659. }
  660. };
  661.  
  662. reader.readAsText(blob, "GBK");
  663. });
  664. });
  665.  
  666. observer.observe(target, {
  667. childList: true,
  668. });
  669. };
  670.  
  671. if (ui.postArg) {
  672. Object.keys(ui.postArg.data).forEach((i) => execute(i));
  673. }
  674.  
  675. // 绑定事件
  676. (() => {
  677. const initialized = {
  678. postDisp: false,
  679. postScoreAdd: false,
  680. };
  681.  
  682. const hook = () => {
  683. if (
  684. Object.values(initialized).findIndex((item) => item === false) < 0
  685. ) {
  686. return;
  687. }
  688.  
  689. if (ui.postDisp && initialized.postDisp === false) {
  690. hookFunction(
  691. ui,
  692. "postDisp",
  693. (returnValue, originalFunction, arguments) => execute(arguments[0])
  694. );
  695.  
  696. initialized.postDisp = true;
  697. }
  698.  
  699. if (ui.postScoreAdd && initialized.postScoreAdd === false) {
  700. hookFunction(
  701. ui,
  702. "postScoreAdd",
  703. (returnValue, originalFunction, arguments) => refetch(arguments)
  704. );
  705.  
  706. initialized.postScoreAdd = true;
  707. }
  708. };
  709.  
  710. hookFunction(ui, "eval", hook);
  711.  
  712. hook();
  713. })();
  714. })(ui.sn.userInfo);
  715.  
  716. // 菜单项
  717. (() => {
  718. // 显示曾用名
  719. if (showOldnameEnable) {
  720. GM_registerMenuCommand("显示曾用名:启用", () => {
  721. GM_setValue(SHOW_OLDNAME_ENABLE_KEY, false);
  722. location.reload();
  723. });
  724. } else {
  725. GM_registerMenuCommand("显示曾用名:禁用", () => {
  726. GM_setValue(SHOW_OLDNAME_ENABLE_KEY, true);
  727. location.reload();
  728. });
  729. }
  730.  
  731. // 显示发帖数
  732. if (showPostnumEnable) {
  733. GM_registerMenuCommand("显示发帖数:启用", () => {
  734. GM_setValue(SHOW_POSTNUM_ENABLE_KEY, false);
  735. location.reload();
  736. });
  737. } else {
  738. GM_registerMenuCommand("显示发帖数:禁用", () => {
  739. GM_setValue(SHOW_POSTNUM_ENABLE_KEY, true);
  740. location.reload();
  741. });
  742. }
  743.  
  744. // 显示属地
  745. if (showIpLocEnable) {
  746. GM_registerMenuCommand("显示属地:启用", () => {
  747. GM_setValue(SHOW_IPLOC_ENABLE_KEY, false);
  748. location.reload();
  749. });
  750. } else {
  751. GM_registerMenuCommand("显示属地:禁用", () => {
  752. GM_setValue(SHOW_IPLOC_ENABLE_KEY, true);
  753. location.reload();
  754. });
  755. }
  756. })();
  757. })(commonui);