米哈游玩家指示器

B站评论区自动标注米哈游玩家,依据是动态里是否有米哈游游戏的相关内容。灵感来自于原神玩家指示器。

当前为 2022-09-11 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name             米哈游玩家指示器
// @namespace        http://853lab.com/
// @version          0.6
// @description      B站评论区自动标注米哈游玩家,依据是动态里是否有米哈游游戏的相关内容。灵感来自于原神玩家指示器。
// @author           Sonic853
// @match            https://www.bilibili.com/video/*
// @icon             https://static.hdslb.com/images/favicon.ico
// @run-at           document-end
// @license          MIT License
// @grant            GM_setValue
// @grant            GM_getValue
// @grant            GM_registerMenuCommand
// @grant            GM_xmlhttpRequest
// @original-author  xulaupuz
// @original-license MIT
// @original-script  https://greasyfork.org/zh-CN/scripts/450720-%E5%8E%9F%E7%A5%9E%E7%8E%A9%E5%AE%B6%E6%8C%87%E7%A4%BA%E5%99%A8
// ==/UserScript==
// 凭实力走在对立面,
// 这就是我的觉悟。
// https://h.bilibili.com/67313825
// So FUCK YOU, miHoYo!

(async function () {
  'use strict'

  const DEV_Log = Boolean(localStorage.getItem("Dev-853"))
  const localItem = "miHoYoCheck"
  const NAME = "miHoYoCheck"
  const D = () => {
    return new Date().toLocaleTimeString()
  }
  const Console_log = function (...text) {
    let d = new Date().toLocaleTimeString()
    console.log(`[${NAME}][${d}]: `, ...text)
  }
  const Console_Devlog = function (...text) {
    let d = new Date().toLocaleTimeString()
    DEV_Log && (console.log(`[${NAME}][${d}]: `, ...text))
  }
  const Console_error = function (...text) {
    let d = new Date().toLocaleTimeString()
    console.error(`[${NAME}][${d}]: `, ...text)
  }

  const snooze = ms => new Promise(resolve => setTimeout(resolve, ms))

  const RList = new class {
    time = 200
    #list = -1
    async Push() {
      this.#list++
      await snooze(this.#list * this.time)
      Promise.resolve().finally(() => {
        setTimeout(() => { this.#list-- }, (this.#list + 1) * this.time)
      })
    }
  }

  if (typeof GM_xmlhttpRequest === 'undefined'
    && typeof GM_registerMenuCommand === 'undefined'
    && typeof GM_setValue === 'undefined'
    && typeof GM_getValue === 'undefined') {
    console.error(`[${NAME}][${D()}]: `, "GM is no Ready.")
  } else {
    console.log(`[${NAME}][${D()}]: `, "GM is Ready.")
  }

  /**
   *
   * @param {string} url
   * @param {string} method
   * @param {Object.<string, any>} headers
   * @param {string} responseType
   * @param {*} successHandler
   * @param {*} errorHandler
   * @returns
   */
  let HTTPsend = function (url, method, headers, responseType, successHandler, errorHandler) {
    Console_Devlog(url)
    if (typeof GM_xmlhttpRequest != 'undefined') {
      return new Promise((rl, rj) => {
        try {
          GM_xmlhttpRequest({
            method,
            url,
            headers,
            responseType,
            onerror: function (response) {
              Console_Devlog(response.status)
              errorHandler && errorHandler(response.status)
              rj(response.status)
            },
            onload: function (response) {
              let status
              if (response.readyState == 4) { // `DONE`
                status = response.status
                if (status == 200) {
                  Console_Devlog(response.response)
                  successHandler && successHandler(response.response)
                  rl(response.response)
                } else {
                  Console_Devlog(status)
                  errorHandler && errorHandler(status)
                  rj(status)
                }
              }
            },
          })
        } catch (error) {
          rj(error)
        }
      })
    } else {
      return new Promise((rl, rj) => {
        try {
          let xhr = new XMLHttpRequest()
          xhr.open(method, url, true)
          xhr.withCredentials = true
          xhr.responseType = responseType
          xhr.onreadystatechange = function () {
            let status
            if (xhr.readyState == 4) { // `DONE`
              status = xhr.status
              if (status == 200) {
                Console_log(xhr.response)
                successHandler && successHandler(xhr.response)
                rl(xhr.response)
              } else {
                Console_log(status)
                errorHandler && errorHandler(status)
                rj(status)
              }
            }
          }
          xhr.send()
        } catch (error) {
          rj(error)
        }
      })
    }
  }

  let BLab8A = class {
    /**
     * @type {Object.<string, {
     * name: string,
     * uid: string
     *  }[]>} data
     */
    data
    constructor() {
      this.data = this.load()
    }
    load() {
      Console_log("正在加载数据")
      const defaultData = "{\"unknown\":[],\"miHoYo\":[],\"no_miHoYo\":[]}"
      if (typeof GM_getValue !== 'undefined') {
        let gdata = GM_getValue(localItem, JSON.parse(defaultData))
        return gdata
      } else {
        let ldata = JSON.parse(localStorage.getItem(localItem) === null ? defaultData : localStorage.getItem(localItem))
        return ldata
      }
    }
    save(d) {
      Console_log("正在保存数据")
      d === undefined ? (d = this.data) : (this.data = d)
      typeof GM_getValue != 'undefined' ? GM_setValue(localItem, d) : localStorage.setItem(localItem, JSON.stringify(d))
      return this
    }
  }
  let bLab8A = new BLab8A()


  GM_registerMenuCommand("清空插件所有数据", () => {
    if (!confirm("确定要清空数据吗?")) return
    bLab8A.data.miHoYo = []
    bLab8A.data.no_miHoYo = []
    bLab8A.save()
    Console_log("数据已清空")
  })
  GM_registerMenuCommand("清空插件里的米哈游玩家数据", () => {
    if (!confirm("确定要清空数据吗?")) return
    bLab8A.data.miHoYo = []
    bLab8A.save()
    Console_log("数据已清空")
  })
  GM_registerMenuCommand("清空插件里的非米哈游玩家数据", () => {
    if (!confirm("确定要清空数据吗?")) return
    bLab8A.data.no_miHoYo = []
    bLab8A.save()
    Console_log("数据已清空")
  })

  class Checker {
    running = false
    /**
       * @type {{
     * name: string,
     * uid: string,
     * dom: HTMLElement
     * }[]}
     */
    list = []
    // 原神 关键词 有待改进,因为会出现 还原神作 等误判断情况
    /**
     * 关键词检查
     */
    keywords = [
      "玩原神",
      "原神玩家",
      "#原神#",
      "【原神",
      "《原神",
      "[原神",
      "米哈游",
      "崩坏三",
      "崩坏3",
      "崩坏学园",
      "崩坏学院",
      "miHoYo",
      "崩坏星穹铁道",
      "崩坏:星穹铁道",
      "崩坏:星穹铁道",
      "未定事件簿",
      "绝区零",
      "米游社",
      // "鹿鸣",
    ]
    soeWithKeywords = [
      "原神",
    ]
    /**
     * 米哈游以及涉及米哈游的帐号
     */
    uids = [
      "256667467",
      "318432901",
      "133934",
      "27534330",
      "358367842",
      "1340190821",
      "401742377",
      "33307860",
      "52957002",
      "510189715",
      "488836173",
      "1636034895",
      "1340190821",
      "436175352",
    ]
    tag = "[米哈游玩家]"
    dynamicURL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space"
    get is_new() {
      return document.querySelectorAll(".reply-list").length != 0
    }
    get getDom() {
      if (this.is_new) {
        return document.querySelectorAll(".reply-list")
        // sub-reply-list
      }
      else {
        return document.querySelectorAll(".comment-list")
        // reply-box
      }
    }
    /**
     * @param {HTMLElement} node 
     * @returns {{
     * name: string,
     * uid: string,
     * dom: HTMLElement
     * }}
     */
    getUser(node) {
      if (this.is_new) {
        // console.log(node)
        return {
          name: node.innerText,
          uid: node.getAttribute("data-user-id"),
          dom: node
        }
      }
      else {
        /** @type {HTMLAnchorElement} */
        let child = node.children[0]
        if (child.href == undefined) {
          for (let _child of node.children) {
            if (_child.href != undefined) {
              child = _child
              break
            }
          }
        }
        if (child === undefined && child.href == undefined) return
        // console.log(child)
        let uid = child.getAttribute("data-usercard-mid") === null ? child.href.replace(/[^\d]/g, "") : child.getAttribute("data-usercard-mid")
        return {
          name: child.innerText,
          uid,
          dom: child
        }
      }
    }
    getUserList() {
      /**
       * @type {{
       * name: string,
       * uid: string,
       * dom: HTMLElement
       * }[]}
       */
      let list = []
      if (this.is_new) {
        let users = document.querySelectorAll(".user-name")
        let subuser = document.querySelectorAll(".sub-user-name")
        for (let user of users) {
          let u = this.getUser(user)
          if (u != undefined
            && !list.some(e => e.uid == u.uid)) {
            {
              list.push(u)
            }
          }
        }
        for (let user of subuser) {
          let u = this.getUser(user)
          if (u != undefined
            && !list.some(e => e.uid == u.uid)) {
            {
              list.push(u)
            }
          }
        }
      }
      else {
        let users = document.querySelectorAll(".user")
        for (let user of users) {
          let u = this.getUser(user)
          if (u != undefined
            && !list.some(e => e.uid == u.uid)) {
            {
              list.push(u)
            }
          }
        }
      }
      return list
    }
    /**
     * @param {string} uid 
     * @param {string} offset
     */
    async getUserDynamic(uid, offset = "") {
      // ?offset=&host_mid=
      if (uid == undefined) return
      /**
       * @type {{
       * code: number,
       * message: string,
       * ttl: number,
       * data: {
       *  has_more: boolean,
       *  items: {
       *   id_str: string,
       *   modules: {
       *    module_dynamic: {
       *     desc?: {
       *      rich_text_nodes: {
       *       orig_text: string,
       *       text: string,
       *       type: string
       *      }[],
       *      text: string
       *     },
       *     major?: {
       *      archive: {
       *       aid: string,
       *       title: string,
       *       desc: string,
       *      }
       *     }
       *    }
       *   },
       *   orig?: {
       *    modules: {
       *     module_author: {
       *      mid: string,
       *      name: string
       *     },
       *     module_dynamic: {
       *      desc?: {
       *       rich_text_nodes: {
       *        orig_text: string,
       *        text: string,
       *        type: string
       *       }[],
       *       text: string
       *      },
       *      major?: {
       *       archive: {
       *        aid: string,
       *        title: string,
       *        desc: string,
       *       }
       *      }
       *     }
       *    },
       *    type: string
       *   },
       *   type: string
       *  }[],
       *  offset: string,
       *  update_baseline: string,
       *  update_num: number
       * }
       * }}
       */
      let data = JSON.parse(await HTTPsend(`${this.dynamicURL}?offset=${offset}&host_mid=${uid}`,
        "GET",
        {
          "Referer": `https://space.bilibili.com/${uid}/dynamic`,
        }
      ))
      if (data.code != 0) {
        console.error(`[${NAME}][${D()}]: `, data)
        return
      }
      let items = data.data.items
      return {
        items,
        offset: data.data.offset,
      }
    }
    /**
     * @param {{
       *   id_str: string,
     *   modules: {
     *    module_dynamic: {
     *     desc?: {
     *      rich_text_nodes: {
     *       orig_text: string,
     *       text: string,
     *       type: string
     *      }[],
     *      text: string
     *     },
     *     major?: {
     *      archive: {
     *       aid: string,
     *       title: string,
     *       desc: string,
     *      }
     *     }
     *    }
     *   },
     *   orig?: {
     *    modules: {
     *     module_author: {
     *      mid: string,
     *      name: string
     *     },
     *     module_dynamic: {
     *      desc?: {
     *       rich_text_nodes: {
     *        orig_text: string,
     *        text: string,
     *        type: string
     *       }[],
     *       text: string
     *      },
     *      major?: {
     *       archive: {
     *        aid: string,
     *        title: string,
     *        desc: string,
     *       }
     *      }
     *     }
     *    },
     *    type: string
     *   },
     *   type: string
     *  }[]} items
     */
    checkDynamicsIncludeKeyword(items) {
      for (let item of items) {
        if (this.checkDynamicIncludeKeyword(item)) {
          return true
        }
      }
      return false
    }
    /**
     * @param {{
       *   id_str: string,
     *   modules: {
     *    module_dynamic: {
     *     desc?: {
     *      rich_text_nodes: {
     *       orig_text: string,
     *       text: string,
     *       type: string
     *      }[],
     *      text: string
     *     },
     *     major?: {
     *      archive: {
     *       aid: string,
     *       title: string,
     *       desc: string,
     *      }
     *     }
     *    }
     *   },
     *   orig?: {
     *    modules: {
     *     module_author: {
     *      mid: string,
     *      name: string
     *     },
     *     module_dynamic: {
     *      desc?: {
     *       rich_text_nodes: {
     *        orig_text: string,
     *        text: string,
     *        type: string
     *       }[],
     *       text: string
     *      },
     *      major?: {
     *       archive: {
     *        aid: string,
     *        title: string,
     *        desc: string,
     *       }
     *      }
     *     }
     *    },
     *    type: string
     *   },
     *   type: string
     *  }} item
     */
    checkDynamicIncludeKeyword(item) {
      if (item == undefined) return false
      // for (let item of items) {
      if (item.modules.module_dynamic.desc != null) {
        if (this.checkKeyword(item.modules.module_dynamic.desc.text)) {
          return true
        }
      }
      switch (item.type) {
        case "DYNAMIC_TYPE_FORWARD":
          {
            if (item.orig != undefined) {
              for (let uid of this.uids) {
                if (item.orig.modules.module_author.mid == uid) {
                  return true
                }
              }
              switch (item.orig.type) {
                case "DYNAMIC_TYPE_DRAW":
                case "DYNAMIC_TYPE_COMMON_SQUARE":
                case "DYNAMIC_TYPE_WORD":
                  {
                    if (item.orig.modules.module_dynamic.desc != null) {
                      if (this.checkKeyword(item.orig.modules.module_dynamic.desc.text)) {
                        return true
                      }
                    }
                  }
                  break
                case "DYNAMIC_TYPE_AV":
                  {
                    if (item.orig.modules.module_dynamic.desc != null) {
                      if (this.checkKeyword(item.orig.modules.module_dynamic.desc.text)) {
                        return true
                      }
                    }
                    if (item.orig.modules.module_dynamic.major != null) {
                      Console_Devlog(item.orig.modules.module_dynamic.major)
                      if (this.checkKeyword(item.orig.modules.module_dynamic.major?.archive?.title)
                      || this.checkKeyword(item.orig.modules.module_dynamic.major?.archive?.desc)) {
                        return true
                      }
                    }
                  }
                  break
                case "DYNAMIC_TYPE_NONE":
                  {
                  }
                  break
              }
            }
          }
          break
        case "DYNAMIC_TYPE_AV":
          {
            if (item.modules.module_dynamic.major != null) {
              if (this.checkKeyword(item.modules.module_dynamic.major.archive.title)
              || this.checkKeyword(item.modules.module_dynamic.major.archive.desc)) {
                return true
              }
            }
          }
          break
        case "DYNAMIC_TYPE_DRAW":
        case "DYNAMIC_TYPE_WORD":
          {
          }
          break
        default:
          break
      }
      // }
      return false
    }
    checkKeyword(text) {
      if (text != null) {
        for (let keyword of this.keywords) {
          if (text.includes(keyword)) {
            return true
          }
        }
        for (let keyword of this.soeWithKeywords) {
          if (text.startsWith(keyword) || text.endsWith(keyword)) {
            return true
          }
        }
      }
      return false
    }
    async checkUser() {
      if (this.running) return
      if (this.list.length == 0) return
      this.running = true
      // 复制一份 this.list
      let list = this.list.slice()
      for (let user of list) {
        if (bLab8A.data.miHoYo.some(e => e.uid == user.uid)) {
          Console_log("已知的米哈游玩家", user)
          if (user.dom != undefined) {
            this.insertSpan(user.dom, this.tag)
          }
          continue
        }
        else if (bLab8A.data.no_miHoYo.some(e => e.uid == user.uid)) {
          Console_log("已知的非米哈游玩家", user)
          continue
        }
        Console_Devlog(`检查用户 ${user.name}`)
        await RList.Push()
        let dynamic = await this.getUserDynamic(user.uid)
        if (dynamic == undefined) continue
        if (dynamic.items.length == 0) continue
        let has_miHoYo = this.checkDynamicsIncludeKeyword(dynamic.items)
        if (has_miHoYo) {
          Console_log("米哈游玩家", user)
          bLab8A.data.miHoYo.push({
            uid: user.uid,
            name: user.name,
          })
          this.insertSpan(user.dom, this.tag)
        }
        else {
          await RList.Push()
          dynamic = await this.getUserDynamic(user.uid, dynamic.offset)
          if (dynamic != undefined
            && dynamic.items.length !== 0) {
            has_miHoYo = this.checkDynamicsIncludeKeyword(dynamic.items)
            if (has_miHoYo) {
              Console_log("米哈游玩家", user)
              bLab8A.data.miHoYo.push({
                uid: user.uid,
                name: user.name,
              })
              this.insertSpan(user.dom, this.tag)
            }
            else {
              Console_log("非米哈游玩家", user)
              bLab8A.data.no_miHoYo.push({
                uid: user.uid,
                name: user.name,
              })
            }
          }
          else {
            Console_log("非米哈游玩家", user)
            bLab8A.data.no_miHoYo.push({
              uid: user.uid,
              name: user.name,
            })
          }
        }
        bLab8A.save()
      }
      this.running = false
      // 删除已经检查过的用户
      for (let user of list) {
        this.list.splice(this.list.indexOf(user), 1)
      }
      if (this.list.length != 0) {
        this.checkUser()
      }
    }
    /**
     * @param {HTMLElement} dom 
     * @param {string} text 
     */
    insertSpan(dom, text) {
      if (dom != undefined) {
        if (dom.querySelector("span.check_tag") != null) {
          return
        }
        let span = document.createElement("span")
        span.classList.add("check_tag")
        span.style.color = "red"
        span.title = `非准确结果,是否为${text}请自行判断`
        span.innerText = text
        dom.appendChild(span)
      }
    }
  }
  let checker = new Checker()

  /**
   * @param {MutationRecord[]} mutationList 
   * @param {MutationObserver} observer 
   */
  let callback = (mutationList, observer) => {
    Console_Devlog(`[${NAME}][${D()}]: `, "callback", mutationList)
    let list = checker.getUserList()
    for (let item of list) {
      if (!checker.list.some(e => e.dom == item.dom)) {
        checker.list.push(item)
      }
    }
    checker.checkUser()
    // mutationList.forEach(mutation => {
    //   if (mutation.type == "childList") {
    //     // [0].addedNodes[0].children[1].children[0].children[0].href
    //     // mutation.addedNodes.forEach(node => {
    //     //   if (checker.is_new) {
    //     //     try {

    //     //     } catch (error) {

    //     //     }
    //     //   }
    //     //   else {
    //     //     try {
    //     //       console.log(node.childNodes[1].childNodes[0].childNodes[0].href)
    //     //     } catch (error) {

    //     //     }
    //     //   }
    //     // })
    //   }
    // })
  }

  /** @type {MutationObserverInit} */
  let observerOption = {
    childList: true,
    subtree: true,
  }

  let observer = new MutationObserver(callback)
  // while 检测 checker.getDom
  while (checker.getDom.length == 0) {
    console.log(`[${NAME}][${D()}]: `, "寻找中")
    await RList.Push()
  }
  for (let _dom of checker.getDom) {
    observer.observe(_dom, observerOption)
  }
  // observer.observe(checker.getDom[0], observerOption)
  Console_Devlog(`[${NAME}][${D()}]: `, "开始监听")

  // 先获取一次列表
  let list = checker.getUserList()
  for (let item of list) {
    if (!checker.list.some(e => e.dom == item.dom)) {
      checker.list.push(item)
    }
  }
  checker.checkUser()
})()