NGA Likes Support

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

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

// ==UserScript==
// @name        NGA Likes Support
// @namespace   https://greasyfork.org/users/263018
// @version     1.4.2
// @author      snyssss
// @description 显示被点赞和粉丝数量,以及发帖数量、IP属地、曾用名

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_xmlhttpRequest
// @noframes
// ==/UserScript==

(async (ui) => {
  if (!ui) return;

  // KEY
  const SHOW_OLDNAME_ENABLE_KEY = "SHOW_OLDNAME_ENABLE";
  const SHOW_POSTNUM_ENABLE_KEY = "SHOW_POSTNUM_ENABLE";
  const SHOW_IPLOC_ENABLE_KEY = "SHOW_IPLOC_ENABLE";

  // 显示曾用名
  const showOldnameEnable = GM_getValue(SHOW_OLDNAME_ENABLE_KEY) || false;

  // 显示发帖数
  const showPostnumEnable = GM_getValue(SHOW_POSTNUM_ENABLE_KEY) || false;

  // 显示属地
  const showIpLocEnable = GM_getValue(SHOW_IPLOC_ENABLE_KEY) || false;

  // 钩子
  const hookFunction = (object, functionName, callback) => {
    ((originalFunction) => {
      object[functionName] = function () {
        const returnValue = originalFunction.apply(this, arguments);

        callback.apply(this, [returnValue, originalFunction, arguments]);

        return returnValue;
      };
    })(object[functionName]);
  };

  // IndexedDB 操作
  const db = await (async () => {
    // 常量
    const VERSION = 1;
    const DB_NAME = "NGA_CACHE_IPLOC";
    const TABLE_NAME = "ipLoc";

    // 是否支持
    const support = window.indexedDB !== undefined;

    // 不支持,直接返回
    if (support === false) {
      return {
        support,
      };
    }

    // 获取数据库实例
    const instance = await new Promise((resolve) => {
      // 打开 IndexedDB 数据库
      const request = window.indexedDB.open(DB_NAME, VERSION);

      // 如果数据库不存在则创建
      request.onupgradeneeded = (event) => {
        // 创建表
        const store = event.target.result.createObjectStore(TABLE_NAME, {
          keyPath: null,
          autoIncrement: true,
        });

        // 创建索引
        store.createIndex("uid", "uid");
      };

      // 成功后返回实例
      request.onsuccess = (event) => {
        resolve(event.target.result);
      };
    });

    // 缓存数据
    const save = (uid, ipLoc) =>
      new Promise((resolve, reject) => {
        // 创建事务
        const transaction = instance.transaction([TABLE_NAME], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(TABLE_NAME);

        // 获取索引
        const index = store.index("uid");

        // 查找最新的数据
        const request = index.openCursor(IDBKeyRange.only(uid), "prev");

        // 成功后处理数据
        request.onsuccess = (event) => {
          const cursor = event.target.result;

          // 如果属地没有变化则跳过
          if (cursor && cursor.value.ipLoc === ipLoc) {
            resolve();
            return;
          }

          // 插入数据
          const r = store.put({
            uid,
            ipLoc,
            timestamp: Date.now(),
          });

          r.onsuccess = () => {
            resolve();
          };

          r.onerror = () => {
            reject();
          };
        };

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

    // 读取数据
    const load = (uid, count) =>
      new Promise((resolve, reject) => {
        // 声明结果
        const result = [];

        // 创建事务
        const transaction = instance.transaction([TABLE_NAME], "readwrite");

        // 获取对象仓库
        const store = transaction.objectStore(TABLE_NAME);

        // 获取索引
        const index = store.index("uid");

        // 查找最新的数据
        const request = index.openCursor(IDBKeyRange.only(uid), "prev");

        // 成功后处理数据
        request.onsuccess = (event) => {
          const cursor = event.target.result;

          if (cursor && cursor.value) {
            if (
              result.length < count &&
              result.findIndex((item) => item.ipLoc === cursor.value.ipLoc) < 0
            ) {
              result.push(cursor.value);
            }

            cursor.continue();
          } else {
            resolve(result);
          }
        };

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

    return {
      support,
      save,
      load,
    };
  })();

  class UserInfo {
    execute(task) {
      task().finally(() => {
        if (this.waitingQueue.length) {
          const next = this.waitingQueue.shift();

          this.execute(next);
        } else {
          this.isRunning = false;
        }
      });
    }

    enqueue(task) {
      if (this.isRunning) {
        this.waitingQueue.push(task);
      } else {
        this.isRunning = true;

        this.execute(task);
      }
    }

    rearrange() {
      if (this.data) {
        const list = Object.values(this.children);

        for (let i = 0; i < list.length; i++) {
          if (list[i].source === undefined) {
            list[i].create(this.data);
          }

          Object.entries(this.container).forEach((item) => {
            list[i].clone(this.data, item);
          });
        }
      }
    }

    reload() {
      this.enqueue(async () => {
        this.data = await new Promise((resolve) => {
          fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${this.uid}`, {
            credentials: "omit",
          })
            .then((res) => res.blob())
            .then((blob) => {
              const reader = new FileReader();

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

                resolve(result.data[0]);
              };

              reader.readAsText(blob, "GBK");
            })
            .catch(() => {
              resolve({});
            });
        });

        if (this.data.usernameChanged && showOldnameEnable) {
          this.data.oldname = await new Promise((resolve) => {
            fetch(`/nuke.php?lite=js&__lib=ucp&__act=oldname&uid=${this.uid}`)
              .then((res) => res.blob())
              .then((blob) => {
                const reader = new FileReader();

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

                  resolve(result.data[0]);
                };

                reader.readAsText(blob, "GBK");
              })
              .catch(() => {
                resolve();
              });
          });
        }

        Object.values(this.children).forEach((item) => item.destroy());

        this.rearrange();
      });
    }

    constructor(id) {
      this.uid = id;

      this.waitingQueue = [];
      this.isRunning = false;

      this.container = {};
      this.children = {};

      this.reload();
    }
  }

  class UserInfoWidget {
    destroy() {
      if (this.source) {
        this.source = undefined;
      }

      if (this.target) {
        Object.values(this.target).forEach((item) => {
          if (item.parentNode) {
            item.parentNode.removeChild(item);
          }
        });
      }
    }

    clone(data, [argid, container]) {
      if (this.source) {
        if (this.target[argid] === undefined) {
          this.target[argid] = this.source.cloneNode(true);

          if (this.callback) {
            this.callback(data, this.target[argid]);
          }
        }

        const isSmall = container.classList.contains("posterInfoLine");

        if (isSmall) {
          const anchor = container.querySelector(".author ~ br");

          if (anchor) {
            anchor.parentNode.insertBefore(this.target[argid], anchor);
          }
        } else {
          container.appendChild(this.target[argid]);
        }
      }
    }

    constructor(func, callback) {
      this.create = (data) => {
        this.destroy();

        this.source = func(data);
        this.target = {};
      };

      this.callback = callback;
    }
  }

  ui.sn = ui.sn || {};
  ui.sn.userInfo = ui.sn.userInfo || {};

  ((info) => {
    const execute = (argid) => {
      const args = ui.postArg.data[argid];

      if (args.comment) return;

      const uid = +args.pAid;

      if (uid > 0) {
        if (info[uid] === undefined) {
          info[uid] = new UserInfo(uid);
        }

        if (document.contains(info[uid].container[argid]) === false) {
          info[uid].container[argid] =
            args.uInfoC.closest("tr").querySelector(".posterInfoLine") ||
            args.uInfoC.querySelector("div");
        }

        info[uid].enqueue(async () => {
          if (info[uid].children[8] === undefined) {
            info[uid].children[8] = new UserInfoWidget((data) => {
              const value =
                Object.values(data.more_info || {}).find(
                  (item) => item.type === 8
                )?.data || 0;

              const element = document.createElement("SPAN");

              element.className =
                "small_colored_text_btn stxt block_txt_c2 vertmod";
              element.style.cursor = "default";
              element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">⯅</span>&nbsp;${value}</span>`;

              return element;
            });
          }

          if (info[uid].children[16] === undefined) {
            info[uid].children[16] = new UserInfoWidget((data) => {
              const value = data.follow_by_num || 0;

              const element = document.createElement("SPAN");

              element.className =
                "small_colored_text_btn stxt block_txt_c2 vertmod";
              element.style.cursor = "default";
              element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>&nbsp;${value}</span>`;

              return element;
            });
          }

          info[uid].rearrange();

          const container = info[uid].container[argid];

          const isSmall = container.classList.contains("posterInfoLine");

          // 显示曾用名
          if (showOldnameEnable) {
            if (ui._w.__GP.admincheck) {
              return;
            }

            if (isSmall) {
              const anchor = [
                ...container.querySelectorAll("span.usercol"),
              ].pop().nextElementSibling;

              const uInfo = info[uid].data;

              if (anchor && uInfo && uInfo.oldname) {
                const element = document.createElement("SPAN");

                element.className = "usercol nobr";
                element.innerHTML = `
                      <span> · 曾用名 ${Object.values(uInfo.oldname)
                        .map(
                          (item) =>
                            `<span class="userval" title="${ui.time2dis(
                              item.time
                            )}">${item.username}</span>`
                        )
                        .join(", ")}</span>`;

                anchor.parentNode.insertBefore(element, anchor);
              }
            } else {
              const anchor = container.parentNode.querySelector(
                '.stat div[class="clear"]'
              ).parentNode;

              const uInfo = info[uid].data;

              if (anchor && uInfo && uInfo.oldname) {
                const element = document.createElement("DIV");

                element.innerHTML = `
                      <span>曾用名: ${Object.values(uInfo.oldname)
                        .map(
                          (item) =>
                            `<span class="userval" title="${ui.time2dis(
                              item.time
                            )}">${item.username}</span>`
                        )
                        .join(", ")}</span>`;

                anchor.parentNode.appendChild(element, anchor);
              }
            }
          }

          // 显示发帖数
          if (showPostnumEnable) {
            if (ui._w.__GP.admincheck) {
              return;
            }

            if (isSmall) {
              const anchor = [
                ...container.querySelectorAll("span.usercol"),
              ].pop().nextElementSibling;

              const uInfo = ui.userInfo.users[uid];

              if (anchor && uInfo) {
                const element = document.createElement("SPAN");

                element.className = "usercol nobr";
                element.innerHTML = `
                    <span> · 发帖 <span class="${
                      uInfo.postnum > 9999 ? "numeric" : "numericl"
                    } userval">${uInfo.postnum}</span></span>`;

                anchor.parentNode.insertBefore(element, anchor);
              }
            } else {
              const anchor = container.parentNode.querySelector(
                '.stat div[class="clear"]'
              );

              const uInfo = ui.userInfo.users[uid];

              if (anchor && uInfo) {
                const element = document.createElement("DIV");

                element.style =
                  "float:left;margin-right:3px;min-width:49%;*width:49%";
                element.innerHTML = `
                      <nobr>
                      <span>发帖: <span class="${
                        uInfo.postnum > 9999 ? "numeric" : "numericl"
                      } userval">${uInfo.postnum}</span></span>
                      </nobr>`;

                anchor.parentNode.insertBefore(element, anchor);
              }
            }
          }

          // 显示属地
          if (showIpLocEnable) {
            if (ui._w.__GP.admincheck) {
              return;
            }

            const data = await (async () => {
              const uInfo = info[uid].data;

              if (uInfo) {
                try {
                  if (db.support) {
                    await db.save(uid, uInfo.ipLoc);

                    return await db.load(uid, 3);
                  }
                } catch (e) {}

                return [{ ipLoc: uInfo.ipLoc }];
              }

              return [];
            })();

            if (isSmall) {
              const anchor = [
                ...container.querySelectorAll("span.usercol"),
              ].pop().nextElementSibling;

              if (anchor && data.length > 0) {
                const element = document.createElement("SPAN");

                element.className = "usercol nobr";
                element.innerHTML = `
                    <span> · 属地 ${Object.values(data)
                      .map(
                        (item) =>
                          `<span class="userval" title="${
                            item.timestamp
                              ? ui.time2dis(item.timestamp / 1000)
                              : ""
                          }">${item.ipLoc}</span>`
                      )
                      .join(", ")}</span>`;

                anchor.parentNode.insertBefore(element, anchor);
              }
            } else {
              const anchor = container.parentNode.querySelector(
                '.stat div[class="clear"]'
              );

              if (anchor && data.length > 0) {
                const element = document.createElement("DIV");

                element.style =
                  "float:left;margin-right:3px;min-width:49%;*width:49%";
                element.innerHTML = `
                    <nobr>
                      <span>属地: ${Object.values(data)
                        .map(
                          (item) =>
                            `<span class="userval" title="${
                              item.timestamp
                                ? ui.time2dis(item.timestamp / 1000)
                                : ""
                            }">${item.ipLoc}</span>`
                        )
                        .join(", ")}</span>
                    </nobr>`;

                anchor.parentNode.insertBefore(element, anchor);
              }
            }
          }
        });
      }
    };

    const refetch = (arguments) => {
      const anchor = arguments[0];

      const { tid, pid } = arguments[1];

      const target = anchor.parentNode.querySelector(".recommendvalue");

      if (!target) return;

      const observer = new MutationObserver(() => {
        observer.disconnect();

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

        fetch(url)
          .then((res) => res.blob())
          .then((blob) => {
            const getLastIndex = (content, position) => {
              if (position >= 0) {
                let nextIndex = position + 1;

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

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

                    if (nextIndex < 0) {
                      break;
                    }
                  }

                  nextIndex = nextIndex + 1;
                }
              }

              return -1;
            };

            const reader = new FileReader();

            reader.onload = async () => {
              const parser = new DOMParser();

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

              const html = doc.body.innerHTML;

              const verify = doc.querySelector("#m_posts");

              if (verify) {
                const str = `commonui.postArg.proc( 0`;

                const index = html.indexOf(str) + str.length;

                const lastIndex = getLastIndex(html, index);

                if (lastIndex >= 0) {
                  const matched = html
                    .substring(index, lastIndex)
                    .match(/'\d+,(\d+),(\d+)'/);

                  if (matched) {
                    const score = (matched[1] |= 0);
                    const score_2 = (matched[2] |= 0);
                    const recommend = score - score_2;

                    target.innerHTML = recommend > 0 ? recommend : 0;
                  }
                }
              }
            };

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

      observer.observe(target, {
        childList: true,
      });
    };

    if (ui.postArg) {
      Object.keys(ui.postArg.data).forEach((i) => execute(i));
    }

    // 绑定事件
    (() => {
      const initialized = {
        postDisp: false,
        postScoreAdd: false,
      };

      const hook = () => {
        if (
          Object.values(initialized).findIndex((item) => item === false) < 0
        ) {
          return;
        }

        if (ui.postDisp && initialized.postDisp === false) {
          hookFunction(
            ui,
            "postDisp",
            (returnValue, originalFunction, arguments) => execute(arguments[0])
          );

          initialized.postDisp = true;
        }

        if (ui.postScoreAdd && initialized.postScoreAdd === false) {
          hookFunction(
            ui,
            "postScoreAdd",
            (returnValue, originalFunction, arguments) => refetch(arguments)
          );

          initialized.postScoreAdd = true;
        }
      };

      hookFunction(ui, "eval", hook);

      hook();
    })();
  })(ui.sn.userInfo);

  // 菜单项
  (() => {
    // 显示曾用名
    if (showOldnameEnable) {
      GM_registerMenuCommand("显示曾用名:启用", () => {
        GM_setValue(SHOW_OLDNAME_ENABLE_KEY, false);
        location.reload();
      });
    } else {
      GM_registerMenuCommand("显示曾用名:禁用", () => {
        GM_setValue(SHOW_OLDNAME_ENABLE_KEY, true);
        location.reload();
      });
    }

    // 显示发帖数
    if (showPostnumEnable) {
      GM_registerMenuCommand("显示发帖数:启用", () => {
        GM_setValue(SHOW_POSTNUM_ENABLE_KEY, false);
        location.reload();
      });
    } else {
      GM_registerMenuCommand("显示发帖数:禁用", () => {
        GM_setValue(SHOW_POSTNUM_ENABLE_KEY, true);
        location.reload();
      });
    }

    // 显示属地
    if (showIpLocEnable) {
      GM_registerMenuCommand("显示属地:启用", () => {
        GM_setValue(SHOW_IPLOC_ENABLE_KEY, false);
        location.reload();
      });
    } else {
      GM_registerMenuCommand("显示属地:禁用", () => {
        GM_setValue(SHOW_IPLOC_ENABLE_KEY, true);
        location.reload();
      });
    }
  })();
})(commonui);