Bilibili哔哩哔哩互相关注检测脚本

检测互关的人

// ==UserScript==
// @name         Bilibili哔哩哔哩互相关注检测脚本
// @namespace    http://blog.853lab.com/
// @version      0.9
// @description  检测互关的人
// @author       Sonic853
// @include      https://space.bilibili.com/*
// @require      https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @run-at       document-end
// @license      MIT License
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// ==/UserScript==
//
// 律师函收到之日,即是我死期到来之时。
// 学写代码学到现在也不过是一枚棋子,随用随弃。
// :)
//

(function () {
  'use strict'


  const DEV_Log = Boolean(localStorage.getItem("Dev-853"))
  const localItem = "Lab8A"
  const NAME = "互关检测"
  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 = 500
    #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' && typeof GM_addStyle === 'undefined') {
    Console_error("GM is no Ready.")
  } else {
    Console_log("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 {
    // attribute 为0表示只关注了我,1表示我悄悄关注了他,2表示只有我关注了他,6表示互相关注
    // 其中悄悄关注无法知道他是否关注我,所以只有我关注了他才会是2,互相关注则是6
    /**
     * @type {Object.<string, {
     *    mid: number,
     *    attribute: number,
     *    mtime: number,
     *    tag: null,
     *    special: number,
     *    contract_info: {
     *     is_contractor: boolean,
     *     ts: number,
     *     is_contract: boolean,
     *     user_attr: number
     *    },
     *    uname: string,
     *    face: string,
     *    sign: string,
     *    face_nft: number,
     *    official_verify: {
     *     type: number,
     *     desc: string
     *    }
     *    vip: {
     *     vipType: number,
     *     vipDueDate: number,
     *     dueRemark: string,
     *     accessStatus: number,
     *     vipStatus: number,
     *     vipStatusWarn: string,
     *     themeType: number,
     *     label: {
     *      path: string,
     *      text: string,
     *      label_theme: string,
     *      text_color: string,
     *      bg_style: number,
     *      bg_color: string,
     *      border_color: string
     *     },
     *     avatar_subscript: number,
     *     nickname_color: string,
     *     avatar_subscript_url: string
     *    }
     *  }[]>} data
     */
    data
    constructor() {
      this.data = this.load()
    }
    load() {
      Console_log("正在加载数据")
      const defaultData = "{\"uid\":1,\"follow\":[],\"follow2\":[],\"unfollowed\":[],\"unfollowed2\":[],\"fans\":[],\"unfans\":[],\"whispers\":[],\"unwhispers\":[],\"whispersfollow\":[]}"
      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()

  const BilibiliFollowChecker = new class {
    /**
     * @type {number}
     */
    vmid = typeof bLab8A.data.uid == 'string' ? parseInt(bLab8A.data.uid) : bLab8A.data.uid
    /**
     * @type {number}
     */
    ps = 20
    /**
     * @type {number}
     */
    pn = 1
    /**
     * @type {number}
     */
    total = 0
    /**
     * @type {number}
     */
    fansTotal = 0
    /**
     * @type {number}
     */
    fansMaxpn = 50
    get stat() {
      return `https://api.bilibili.com/x/relation/stat?vmid=${this.vmid}&jsonp=jsonp`
    }
    get followUrl() {
      return `https://api.bilibili.com/x/relation/followings?vmid=${this.vmid}&pn=${this.pn}&ps=${this.ps}&order=desc&order_type=`
    }
    get followReferer() {
      return `https://space.bilibili.com/${this.vmid}/fans/follow`
    }
    get fansUrl() {
      return `https://api.bilibili.com/x/relation/followers?vmid=${this.vmid}&pn=${this.pn}&ps=${this.ps}&order=desc&order_type=attention`
    }
    get fansReferer() {
      return `https://space.bilibili.com/${this.vmid}/fans/fans`
    }
    get whispersUrl() {
      return `https://api.bilibili.com/x/relation/whispers?pn=${this.pn}&ps=${this.ps}`
    }
    get whispersReferer() {
      return `https://space.bilibili.com/${this.vmid}/fans/follow`
    }
    checkuid = 0
    get relationUrl() {
      return `https://api.bilibili.com/x/space/acc/relation?mid=${this.checkuid}`
    }
    get relationReferer() {
      return `https://space.bilibili.com/${this.checkuid}`
    }
    async getFollowings() {
      this.pn = 1
      Console_log(`正在获取第${this.pn}页关注列表`)
      /**
       * @type {{
       * code: number,
       * message: string,
       * ttl: number,
       * data: {
       *  list: {
       *    mid: number,
       *    attribute: number,
       *    mtime: number,
       *    tag: null,
       *    special: number,
       *    contract_info: {
       *     is_contractor: boolean,
       *     ts: number,
       *     is_contract: boolean,
       *     user_attr: number
       *    },
       *    uname: string,
       *    face: string,
       *    sign: string,
       *    face_nft: number,
       *    official_verify: {
       *     type: number,
       *     desc: string
       *    }
       *    vip: {
       *     vipType: number,
       *     vipDueDate: number,
       *     dueRemark: string,
       *     accessStatus: number,
       *     vipStatus: number,
       *     vipStatusWarn: string,
       *     themeType: number,
       *     label: {
       *      path: string,
       *      text: string,
       *      label_theme: string,
       *      text_color: string,
       *      bg_style: number,
       *      bg_color: string,
       *      border_color: string
       *     },
       *     avatar_subscript: number,
       *     nickname_color: string,
       *     avatar_subscript_url: string
       *    }
       *  }[],
       *  re_version: number,
       *  total: number
       * }
       * }}
       */
      const data = JSON.parse(await HTTPsend(this.followUrl, "GET", { "Referer": this.followReferer }))
      if (data && data.code === 0) {
        this.total = data.data.total
        /**
         * @type {{
         * mid: number,
         *    attribute: number,
         *    mtime: number,
         *    tag: null,
         *    special: number,
         *    contract_info: {
         *     is_contractor: boolean,
         *     ts: number,
         *     is_contract: boolean,
         *     user_attr: number
         *    },
         *    uname: string,
         *    face: string,
         *    sign: string,
         *    face_nft: number,
         *    official_verify: {
         *     type: number,
         *     desc: string
         *    }
         *    vip: {
         *     vipType: number,
         *     vipDueDate: number,
         *     dueRemark: string,
         *     accessStatus: number,
         *     vipStatus: number,
         *     vipStatusWarn: string,
         *     themeType: number,
         *     label: {
         *      path: string,
         *      text: string,
         *      label_theme: string,
         *      text_color: string,
         *      bg_style: number,
         *      bg_color: string,
         *      border_color: string
         *     },
         *     avatar_subscript: number,
         *     nickname_color: string,
         *     avatar_subscript_url: string
         *    }
         *  }[]}
         */
        let list = data.data.list
        if (list.length > 0 && list.length < this.total) {
          this.pn += 1
          for (; this.pn <= Math.ceil(this.total / this.ps); this.pn++) {
            await RList.Push()
            Console_log(`正在获取第${this.pn}页关注列表`)
            /**
             * @type {{
             * code: number,
             * message: string,
             * ttl: number,
             * data: {
             *  list: {
             *    mid: number,
             *    attribute: number,
             *    mtime: number,
             *    tag: null,
             *    special: number,
             *    contract_info: {
             *     is_contractor: boolean,
             *     ts: number,
             *     is_contract: boolean,
             *     user_attr: number
             *    },
             *    uname: string,
             *    face: string,
             *    sign: string,
             *    face_nft: number,
             *    official_verify: {
             *     type: number,
             *     desc: string
             *    }
             *    vip: {
             *     vipType: number,
             *     vipDueDate: number,
             *     dueRemark: string,
             *     accessStatus: number,
             *     vipStatus: number,
             *     vipStatusWarn: string,
             *     themeType: number,
             *     label: {
             *      path: string,
             *      text: string,
             *      label_theme: string,
             *      text_color: string,
             *      bg_style: number,
             *      bg_color: string,
             *      border_color: string
             *     },
             *     avatar_subscript: number,
             *     nickname_color: string,
             *     avatar_subscript_url: string
             *    }
             *  }[],
             *  re_version: number,
             *  total: number
             * }
             * }}
             */
            const data1 = JSON.parse(await HTTPsend(this.followUrl, "GET", { "Referer": this.followReferer }))
            if (data1 && data1.code === 0) {
              list = list.concat(data1.data.list)
            }
            else return Promise.reject(data1)
          }
        }
        return list
      } else {
        return Promise.reject(data)
      }
    }
    async getFollowers(page1) {
      this.pn = 1
      Console_log(`正在获取第${this.pn}页粉丝列表`)
      /**
       * @type {{
       * code: number,
       * message: string,
       * ttl: number,
       * data: {
       *  list: {
       *    mid: number,
       *    attribute: number,
       *    mtime: number,
       *    tag: null,
       *    special: number,
       *    contract_info: {
       *     is_contractor: boolean,
       *     ts: number,
       *     is_contract: boolean,
       *     user_attr: number
       *    },
       *    uname: string,
       *    face: string,
       *    sign: string,
       *    face_nft: number,
       *    official_verify: {
       *     type: number,
       *     desc: string
       *    }
       *    vip: {
       *     vipType: number,
       *     vipDueDate: number,
       *     dueRemark: string,
       *     accessStatus: number,
       *     vipStatus: number,
       *     vipStatusWarn: string,
       *     themeType: number,
       *     label: {
       *      path: string,
       *      text: string,
       *      label_theme: string,
       *      text_color: string,
       *      bg_style: number,
       *      bg_color: string,
       *      border_color: string
       *     },
       *     avatar_subscript: number,
       *     nickname_color: string,
       *     avatar_subscript_url: string
       *    }
       *  }[],
       *  re_version: number,
       *  total: number
       * }
       * }}
       */
      const data = JSON.parse(await HTTPsend(this.fansUrl, "GET", { "Referer": this.fansReferer }))
      if (data && data.code === 0) {
        this.fansTotal = data.data.total
        /**
         * @type {{
         * mid: number,
         *    attribute: number,
         *    mtime: number,
         *    tag: null,
         *    special: number,
         *    contract_info: {
         *     is_contractor: boolean,
         *     ts: number,
         *     is_contract: boolean,
         *     user_attr: number
         *    },
         *    uname: string,
         *    face: string,
         *    sign: string,
         *    face_nft: number,
         *    official_verify: {
         *     type: number,
         *     desc: string
         *    }
         *    vip: {
         *     vipType: number,
         *     vipDueDate: number,
         *     dueRemark: string,
         *     accessStatus: number,
         *     vipStatus: number,
         *     vipStatusWarn: string,
         *     themeType: number,
         *     label: {
         *      path: string,
         *      text: string,
         *      label_theme: string,
         *      text_color: string,
         *      bg_style: number,
         *      bg_color: string,
         *      border_color: string
         *     },
         *     avatar_subscript: number,
         *     nickname_color: string,
         *     avatar_subscript_url: string
         *    }
         *  }[]}
         */
        let list = data.data.list
        if (!page1 && list.length > 0 && list.length < this.fansTotal) {
          this.pn += 1
          let maxpn = Math.ceil(this.fansTotal / this.ps) > this.fansMaxpn ? this.fansMaxpn : Math.ceil(this.fansTotal / this.ps)
          for (; this.pn <= maxpn; this.pn++) {
            await RList.Push()
            Console_log(`正在获取第${this.pn}页粉丝列表`)
            const data1 = JSON.parse(await HTTPsend(this.fansUrl, "GET", { "Referer": this.fansReferer }))
            if (data1 && data1.code === 0) {
              list = list.concat(data1.data.list)
            }
            else return Promise.reject(data1)
          }
        }
        return list
      } else {
        return Promise.reject(data)
      }
    }
    async getWhispers() {
      /**
       * @type {{
       * code: number,
       * message: string,
       * ttl: number,
       * data: {
       *  black: number,
       *  follower: number,
       *  following: number,
       *  mid: number,
       *  whisper: number
       * }
       * }}
       */
      const status = JSON.parse(await HTTPsend(this.stat, "GET", { "Referer": this.whispersReferer }))
      /**
       * @type {{
       * mid: number,
       *    attribute: number,
       *    mtime: number,
       *    tag: null,
       *    special: number,
       *    contract_info: {
       *     is_contractor: boolean,
       *     ts: number,
       *     is_contract: boolean,
       *     user_attr: number
       *    },
       *    uname: string,
       *    face: string,
       *    sign: string,
       *    face_nft: number,
       *    official_verify: {
       *     type: number,
       *     desc: string
       *    }
       *    vip: {
       *     vipType: number,
       *     vipDueDate: number,
       *     dueRemark: string,
       *     accessStatus: number,
       *     vipStatus: number,
       *     vipStatusWarn: string,
       *     themeType: number,
       *     label: {
       *      path: string,
       *      text: string,
       *      label_theme: string,
       *      text_color: string,
       *      bg_style: number,
       *      bg_color: string,
       *      border_color: string
       *     },
       *     avatar_subscript: number,
       *     nickname_color: string,
       *     avatar_subscript_url: string
       *    }
       *  }[]}
       */
      let list = []
      if (status.code === 0) {
        if (status.data.whisper > 0) {
          for (this.pn = 1; this.pn <= Math.ceil(status.data.whisper / this.ps); this.pn++) {
            Console_log(`正在获取第${this.pn}页悄悄关注列表`)
            /**
             * @type {{
             * code: number,
             * message: string,
             * ttl: number,
             * data: {
             *  list: {
             *    mid: number,
             *    attribute: number,
             *    mtime: number,
             *    tag: null,
             *    special: number,
             *    contract_info: {
             *     is_contractor: boolean,
             *     ts: number,
             *     is_contract: boolean,
             *     user_attr: number
             *    },
             *    uname: string,
             *    face: string,
             *    sign: string,
             *    face_nft: number,
             *    official_verify: {
             *     type: number,
             *     desc: string
             *    }
             *    vip: {
             *     vipType: number,
             *     vipDueDate: number,
             *     dueRemark: string,
             *     accessStatus: number,
             *     vipStatus: number,
             *     vipStatusWarn: string,
             *     themeType: number,
             *     label: {
             *      path: string,
             *      text: string,
             *      label_theme: string,
             *      text_color: string,
             *      bg_style: number,
             *      bg_color: string,
             *      border_color: string
             *     },
             *     avatar_subscript: number,
             *     nickname_color: string,
             *     avatar_subscript_url: string
             *    }
             *  }[],
             *  re_version: number
             * }
             * }}
             */
            const data = JSON.parse(await HTTPsend(this.whispersUrl, "GET", { "Referer": this.whispersReferer }))
            if (data && data.code === 0) {
              list = list.concat(data.data.list)
            } else return Promise.reject(data)
          }
        }
        return list
      }
      else return Promise.reject(status)
    }
    /**
     * 检查是否已经关注
     * @param {number} uid
     */
    async checkFollow(uid) {
      Console_log(`正在检查${uid}是否已经关注`)
      this.checkuid = uid
      /**
       * @type {{
       *  code: number,
       *  message: string,
       *  ttl: number,
       *  data: {
       *   relation: {
       *    mid: number,
       *    attribute: number,
       *    mtime: number,
       *    tag: null,
       *    special: number,
       *   },
       *   be_relation: {
       *    mid: number,
       *    attribute: number,
       *    mtime: number,
       *    tag: null,
       *    special: number,
       *   }
       *  }
       * }}
       */
      const data = JSON.parse(await HTTPsend(this.relationUrl, "GET", { "Referer": this.relationReferer }))
      if (data && data.code === 0) {
        if (data.data.be_relation.mid === this.vmid) {
          return true
        }
        else return false
      }
      else return Promise.reject(data)
    }
  }

  const followingsDiff = async () => {
    if (bLab8A.data.follow.length === 0 && bLab8A.data.follow2.length === 0) {
      Console_log("没有关注列表")
      bLab8A.data.follow = await BilibiliFollowChecker.getFollowings()
      if (bLab8A.data.follow.length === 0) {
        return { newfollow: [], follow: [], follow2: [], unfollowed: [], unfollowed2: [] }
      }
      bLab8A.data.follow2 = bLab8A.data.follow.filter(item => {
        return item.attribute === 6
      })
      bLab8A.save(bLab8A.data)
      return { newfollow: bLab8A.data.follow, follow: bLab8A.data.follow, follow2: bLab8A.data.follow2, unfollowed: [], unfollowed2: [] }
    }
    else {
      let list = await BilibiliFollowChecker.getFollowings()
      if (list.length === 0) {
        return { newfollow: [], follow: [], follow2: [], unfollowed: bLab8A.data.follow, unfollowed2: [] }
      }
      let follow = []
      let follow2 = []
      let wht = []
      for (let i = 0; i < list.length; i++) {
        switch (list[i].attribute) {
          case 6:
            follow.push(list[i])
            follow2.push(list[i])
            break
          case 2:
            follow.push(list[i])
            break
          default:
            Console_log(list[i].attribute)
            wht.push(list[i])
            break
        }
      }
      if (wht.length > 0) {
        Console_log(`有${wht.length}个未知类型的关注`)
      }
      Console_log(follow.length)
      let newfollow = follow.filter(item => {
        return !bLab8A.data.follow.some(item2 => {
          return item.mid === item2.mid
        })
      })
      let unfollowed = bLab8A.data.follow.filter(item => {
        return !follow.some(item2 => {
          return item.mid === item2.mid
        })
      })
      let unfollowed2 = bLab8A.data.follow2.filter(item => {
        return !follow2.some(item2 => {
          return item.mid === item2.mid
        })
      })
      bLab8A.data.follow = follow
      bLab8A.data.follow2 = follow2
      bLab8A.data.unfollowed = bLab8A.data.unfollowed.concat(unfollowed)
      // 如果取消关注的人重新关注了,则把他从unfollowed中移除
      // 你觉得他们还会回来吗?嗯?
      bLab8A.data.unfollowed = bLab8A.data.unfollowed.filter(item => {
        return !follow.some(item2 => {
          return item.mid === item2.mid
        })
      })
      bLab8A.data.unfollowed2 = bLab8A.data.unfollowed2.concat(unfollowed2)
      bLab8A.data.unfollowed2 = bLab8A.data.unfollowed2.filter(item => {
        return !follow2.some(item2 => {
          return item.mid === item2.mid
        }) || !follow.some(item2 => {
          return item.mid === item2.mid
        })
      })
      bLab8A.save(bLab8A.data)
      return { newfollow, follow, follow2, unfollowed: bLab8A.data.unfollowed, unfollowed2: bLab8A.data.unfollowed2 }
    }
  }

  const fansCheck = async (page1) => {
    Console_log("开始检查粉丝")
    Console_log("由于粉丝列表最多只能获取到50页,所以这里只检查前50页")
    if (bLab8A.data.fans.length === 0) {
      bLab8A.data.fans = await BilibiliFollowChecker.getFollowers(page1)
      bLab8A.save(bLab8A.data)
      return { newfans: bLab8A.data.fans, fans: bLab8A.data.fans }
    }
    else {
      let fans = await BilibiliFollowChecker.getFollowers(page1)
      let newfans = []
      // 与bLab8A.data.fans比较,获取新增的粉丝
      newfans = fans.filter(item => {
        return !bLab8A.data.fans.some(item2 => {
          return item.mid === item2.mid
        })
      })
      // 将fans加入到bLab8A.data.fans中,同时避免重复加入
      for (let i = 0; i < fans.length; i++) {
        let isExist = false
        for (let j = 0; j < bLab8A.data.fans.length; j++) {
          if (fans[i].mid === bLab8A.data.fans[j].mid) {
            isExist = true
            break
          }
        }
        if (!isExist) {
          bLab8A.data.fans.push(fans[i])
        }
      }
      bLab8A.save(bLab8A.data)
      return { newfans, fans: bLab8A.data.fans }
    }
  }

  const whispersCheck = async () => {
    Console_log("开始检查悄悄关注")
    if (bLab8A.data.whispers.length === 0) {
      bLab8A.data.whispers = await BilibiliFollowChecker.getWhispers()
      // bLab8A.data.whispers.map(whispers => {
      //   Console_log(whispers.mid, whispers.uname, whispers.attribute)
      // })
      bLab8A.save(bLab8A.data)
      return { newwhispers: bLab8A.data.whispers, whispers: bLab8A.data.whispers }
    }
    else {
      let whispers = await BilibiliFollowChecker.getWhispers()
      let newwhispers = []
      let unwhispers = []
      // 与bLab8A.data.whispers比较,获取新增的悄悄关注
      newwhispers = whispers.filter(item => {
        return !bLab8A.data.whispers.some(item2 => {
          return item.mid === item2.mid
        })
      })
      // 与bLab8A.data.whispers比较,获取减少的悄悄关注
      unwhispers = bLab8A.data.whispers.filter(item => {
        return !whispers.some(item2 => {
          return item.mid === item2.mid
        })
      })
      // 将whispers加入到bLab8A.data.whispers中,同时避免重复加入
      for (let i = 0; i < whispers.length; i++) {
        let isExist = false
        for (let j = 0; j < bLab8A.data.whispers.length; j++) {
          if (whispers[i].mid === bLab8A.data.whispers[j].mid) {
            isExist = true
            break
          }
        }
        if (!isExist) {
          bLab8A.data.whispers.push(whispers[i])
        }
      }
      // 将unwhispers加入到bLab8A.data.unwhispers中,同时避免重复加入
      for (let i = 0; i < unwhispers.length; i++) {
        let isExist = false
        for (let j = 0; j < bLab8A.data.unwhispers.length; j++) {
          if (unwhispers[i].mid === bLab8A.data.unwhispers[j].mid) {
            isExist = true
            break
          }
        }
        if (!isExist) {
          bLab8A.data.unwhispers.push(unwhispers[i])
        }
      }
      // 从bLab8A.data.whispers中移除unwhispers
      bLab8A.data.whispers = bLab8A.data.whispers.filter(item => {
        return !unwhispers.some(item2 => {
          return item.mid === item2.mid
        })
      })
      // 如果悄悄关注变成了关注,则从bLab8A.data.unwhispers中移除
      bLab8A.data.unwhispers = bLab8A.data.unwhispers.filter(item => {
        return !bLab8A.data.follow.some(item2 => {
          return item.mid === item2.mid
        })
      })
      bLab8A.data.unwhispers = bLab8A.data.unwhispers.filter(item => {
        return !bLab8A.data.follow2.some(item2 => {
          return item.mid === item2.mid
        })
      })
      bLab8A.save(bLab8A.data)
      return { newwhispers, unwhispers: bLab8A.data.unwhispers, whispers: bLab8A.data.whispers }
    }
  }

  const whispersFollowCheck = async () => {
    for (let i = 0; i < bLab8A.data.whispers.length; i++) {
      const whisper = bLab8A.data.whispers[i]
      await RList.Push()
      await RList.Push()
      const isFollowed = await BilibiliFollowChecker.checkFollow(whisper.mid)
      Console_log(whisper.mid, whisper.uname, whisper.attribute, isFollowed ? "已关注我" : "未关注我")
      if (isFollowed) {
        // 将whisper加入到bLab8A.data.whispersfollow中,同时避免重复加入
        let isExist = false
        for (let j = 0; j < bLab8A.data.whispersfollow.length; j++) {
          if (whisper.mid === bLab8A.data.whispersfollow[j].mid) {
            isExist = true
            break
          }
        }
        if (!isExist) {
          bLab8A.data.whispersfollow.push(whisper)
        }
      }
    }
    bLab8A.save(bLab8A.data)
    return "悄悄关注检查完毕"
  }

  const saveDataBlob = async () => {
    const bLab8AData = new Blob([JSON.stringify(bLab8A.data)], { type: "application/json;charset=utf-8" })
    // const defaultData = "{\"follow\":[],\"follow2\":[],\"unfollowed\":[],\"unfollowed2\":[],\"fans\":[],\"unfans\":[],\"whispers\":[],\"unwhispers\":[],\"whispersfollow\":[]}"
    const date = Math.round(new Date() / 1000)
    saveAs(bLab8AData, `UID_${BilibiliFollowChecker.vmid}_f${bLab8A.data.follow.length}_2f${bLab8A.data.follow2.length}_uf${bLab8A.data.unfollowed.length}_2uf${bLab8A.data.unfollowed2.length}_${date}.json`)
    return "数据保存成功"
  }

  /**
   *
   * @param {string} file
   * @returns {string}
   */
  const loadDataBlob = (file) => {
    // const file = await openFile()
    const parseData = JSON.parse(file)
    // 逐个导入follow,follow2,unfollowed,unfollowed2,fans,unfans,whispers,unwhispers,whispersfollow数据,同时避免重复加入
    const keys = ["follow", "follow2", "unfollowed", "unfollowed2", "fans", "unfans", "whispers", "unwhispers", "whispersfollow"]
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      Console_log(parseData[key].length, key)
      let numbers = 0
      for (let j = 0; j < parseData[key].length; j++) {
        const item = parseData[key][j]
        let isExist = false
        for (let k = 0; k < bLab8A.data[key].length; k++) {
          if (item.mid === bLab8A.data[key][k].mid) {
            isExist = true
            break
          }
        }
        if (!isExist) {
          bLab8A.data[key].push(item)
          numbers++
        }
      }
      Console_log(`${key}有${numbers}条数据加入成功`)
    }
    bLab8A.save(bLab8A.data)
    return "数据加载成功"
  }

  /**
   *
   * @returns {Promise<string>}
   */
  const openFile = () => {
    return new Promise((resolve, reject) => {
      let fileInput = document.createElement("input")
      // 指定文件类型为json
      fileInput.type = 'file'
      fileInput.accept = 'application/json'
      // fileInput.setAttribute("accept", ".json")
      fileInput.style.backgroundColor = '#666'
      fileInput.style.position = 'fixed'
      fileInput.style.right = '0px'
      fileInput.style.bottom = '0px'
      fileInput.style.zIndex = '9999'
      fileInput.addEventListener('change', (e) => {
        const file = fileInput.files[0]
        Console_log(file);
        if (!file) {
          return reject("未选择文件")
        }
        let reader = new FileReader()
        reader.onload = (e1) => {
          let contents = e1.target.result
          resolve(contents)
          document.body.removeChild(fileInput)
        }
        reader.readAsText(file)
      })
      document.body.appendChild(fileInput)
    })
  }

  const cleanData = () => {
    if (confirm("确定要清空数据吗?")) {
      if (confirm("最后一次确认,确定要清空数据吗?")) {
        bLab8A.data = { uid: bLab8A.data.uid, follow: [], follow2: [], unfollowed: [], unfollowed2: [], fans: [], unfans: [], whispers: [], unwhispers: [], whispersfollow: [] }
        bLab8A.save(bLab8A.data)
      }
    }
  }

  const checkData = () => {
    // 数据去重
    const keys = ["follow", "follow2", "unfollowed", "unfollowed2", "fans", "unfans", "whispers", "unwhispers", "whispersfollow"]
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      let numbers = 0
      for (let j = 0; j < bLab8A.data[key].length; j++) {
        const item = bLab8A.data[key][j]
        let isExist = false
        for (let k = 0; k < bLab8A.data[key].length; k++) {
          if (j !== k && item.mid === bLab8A.data[key][k].mid) {
            isExist = true
            break
          }
        }
        if (isExist) {
          bLab8A.data[key].splice(j, 1)
          numbers++
        }
      }
      Console_log(`${key}有${numbers}条数据去重成功,剩余${bLab8A.data[key].length}条数据:`)
      console.log(bLab8A.data[key])
    }
    bLab8A.save(bLab8A.data)
  }

  let wanaStop = false

  const testC = async () => {
    let data = []
    for (let i = parseInt(prompt("Enter", 1)); i <= 703223001; i++) {
      if (wanaStop) break
      try {
        Console_log(`${i}/${703223001}`)
        if (await BilibiliFollowChecker.checkFollow(i)) {
          Console_log(`${i} 已关注我`)
          data.push(i)
        }
      }
      catch (e) {
        Console_log(e)
        break
      }
    }
    const Data = new Blob([JSON.stringify(data)], { type: "application/json;charset=utf-8" })
    saveAs(Data, `test${data.length}.json`)
  }

  GM_registerMenuCommand("获取关注差异", () => { followingsDiff().then(Console_log).catch(console.error) })
  GM_registerMenuCommand("检查粉丝", () => { fansCheck().then(Console_log).catch(console.error) })
  GM_registerMenuCommand("检查粉丝第一页", () => { fansCheck(true).then(Console_log).catch(console.error) })
  GM_registerMenuCommand("更新悄悄关注", () => { whispersCheck().then(Console_log).catch(console.error) })
  GM_registerMenuCommand("检查悄悄关注的人", () => { whispersFollowCheck().then(Console_log).catch(console.error) })
  GM_registerMenuCommand("导出数据", () => { saveDataBlob().then(Console_log).catch(console.error) })
  GM_registerMenuCommand("导入数据(点击后看右下角)", () => { openFile().then(e => { Console_log(loadDataBlob(e)) }).catch(console.error) })
  GM_registerMenuCommand("数据检查", () => { checkData() })
  GM_registerMenuCommand("清空数据", () => { cleanData() })
  GM_registerMenuCommand("你的UID", () => {
    const vmid = prompt("请输入你的UID", BilibiliFollowChecker.vmid)
    if (vmid) {
      BilibiliFollowChecker.vmid = parseInt(vmid)
      bLab8A.data.uid = parseInt(vmid)
      bLab8A.save(bLab8A.data)
    }
  })
  DEV_Log && GM_registerMenuCommand("测试", () => { testC() })
  DEV_Log && GM_registerMenuCommand("想停下来", () => { wanaStop = true })
  GM_registerMenuCommand("检查UID", () => { BilibiliFollowChecker.checkFollow(parseInt(prompt("输入UID", BilibiliFollowChecker.checkuid))).then(Console_log).catch(console.error) })
})()