评论推土机

用于b站评论区屏蔽at信息的脚本

// ==UserScript==
// @name         评论推土机
// @namespace    tuituji
// @version      2.3
// @description  用于b站评论区屏蔽at信息的脚本
// @author       leostou
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/read/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @icon         https://static.hdslb.com/images/favicon.ico
// @license MIT
// ==/UserScript==

(function () {
  'use strict';
  let commentDom = null
  let commentOb = null
  let officialUrl = 'https://api.bilibili.com/x/space/acc/info?mid='
  let fansUrl = 'https://api.bilibili.com/x/relation/stat?vmid='
  let t = 0
  let maxT = 20
  let ready = false
  let timer = null

  /*可配置项*/
  let showTips = true //进入页面是否提示开启脚本,false为直接启动脚本
  let officialCheck = true // 是否开启认证校验,关闭则除白名单外的全部删除
  let bigUserFans = 200000 //认证校验开启时,非认证号粉丝量在200000以上的标记标记=>☆粉丝认证
  let smallUserFans = 2000//认证校验开启时,非认证号粉丝量在2000以上的标记标记=>☆未来可期
  let rightTagList = ['business', 'personal'] //头像右下角认证信息

  /*白名单,不过滤
  id: 白名单的id
  prev: 前缀
  reName: 重命名
  suffix: 后缀
  color: 匹配后的颜色
*/
  let whiteList = [{
    id: '',
    prev: '',
    reName: '',
    suffix: '',
    color: ''
  }, {
    id: '',
    prev: '',
    reName: '',
    suffix: '',
    color: ''
  }]

  /* 认证信息,认证校验开启时,非认证号粉丝量在200000以上的标记标记=>☆机构认证/☆知名认证
  type: 0 知名个人认证,1机构认证,10粉丝数大于2000,99粉丝数大于200000
  prev: 前缀
  suffix: 后缀
  color: 匹配后的颜色
*/
  let officialList = [{
    type: 0,
    prev: '',
    suffix: '[☆知名认证]\n',
    color: '#ffc62e',

  }, {
    type: 1,
    prev: '',
    suffix: '[☆机构认证]\n',
    color: '#3ec6f3',
  }, {
    type: 10,
    prev: '',
    suffix: '[☆未来可期]\n',
    color: '#95ddb2'
  }, {
    type: 99,
    prev: '',
    suffix: '[☆粉丝认证]\n',
    color: '#ff0000'
  }]

  const initOb = (dom) => {
    commentOb = new MutationObserver((mutations, ob) => {
      handlerComment(mutations[0].addedNodes)
    })
    commentOb.observe(dom, {
      childList: true
    })
  }

  const initTimer = () => {
    timer = setInterval(() => {
      // 10秒内没绑定成功关闭脚本
      if (ready || t >= maxT) {
        clearInterval(timer)
        timer = null
      } else {
        t++
        commentDom = document.querySelector('.comment-list')
        let list = commentDom ? commentDom.querySelectorAll('.list-item') : null
        handlerComment(list)
        if (list) {
          ready = true
          initOb(commentDom)
        }
      }
    }, 500)
  }

  // 评论列表绑定dom监听
  const handlerComment = (list) => {
    if (!list || list.length === 0) return
    Array.from(list).forEach(async item => {
      // 判断是否发言人是否有认证信息
      if (rightTagMatch(item)) return

      // 是否存在白名单
      if (whiteListMatch(item, 'dom')) return

      // 处理at逻辑
      atFn(item)
    })
  }

  const rightTagMatch = (dom) => {
    const rightTagClassName = dom.querySelector('span.bili-avatar-icon').className
    return rightTagList.some(item => {
      return rightTagClassName.includes(item)

    })
  }

  const whiteListMatch = (dom, type) => {
    let mid = type === 'dom' ? dom.querySelector('.user-face>a').dataset.usercardMid : dom.pathname.substr(1)
    if (whiteList.some(item => item.id === mid)) {
      type === 'dom' ? reWrite(dom.querySelector('.con .user>a'), '', mid) : reWrite(dom, '', mid)
      return true
    }
    return false
  }

  const atFn = async (dom) => {
    // 判断发言是否存在at
    let atReg = /@.*?\s/g
    let target = dom.querySelector('.con .text')
    if (atReg.test(target.innerText)) {
      // 是否校验认证用户

      const data = await textMatch(target)
      if (!data.some(item => item === true)) {
        dom.style.display = 'none'
      }
    }
  }

  const reWrite = (dom, type, id) => {
    let data
    if (type === 'official') {
      data = officialList.find(item => item.type === id)
    } else {
      data = whiteList.find(item => item.id === id)
    }
    let name = data.reName ? data.reName : dom.innerText
    dom.innerText = data.prev + name + data.suffix
    dom.style.color = data.color ? data.color : dom.style.color
  }

  const getInfo = (id, type) => {
    return new Promise((resolve, reject) => {
      let fullUrl = (type === 'official' ? officialUrl : fansUrl) + id
      GM_xmlhttpRequest({
        url: fullUrl,
        method: "GET",
        headers: {
          'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
        },
        onload: function (xhr) {
          resolve(xhr.responseText)
        }
      });
    })
  }

  const officialTest = (data) => {
    let res = JSON.parse(data)
    if (res && res.data) {
      return res.data.official.type
    }
    return -1
  }

  const fansTest = (data) => {
    let res = JSON.parse(data)
    if (res && res.data) {
      if (res.data.follower < smallUserFans) {
        return -1
      } else if (res.data.follower >= bigUserFans) {
        return 99
      } else {
        return 10
      }
    }
  }

  const textMatch = async (commentDom) => {
    let aTagArr = Array.from(commentDom.querySelectorAll('a'))
    return await Promise.all(aTagArr.map(aItem => new Promise(async (res, rej) => {
      if (whiteListMatch(aItem, 'atUser')) {
        res(true)
      }
      if (!officialCheck) {
        res(false)
      }
      let mid = aItem.pathname.substr(1)
      // 发送请求获取认证状态
      let type = -1
      const data = await getInfo(mid, 'official')
      type = officialTest(data)
      if (type === -1) {
        const data = await getInfo(mid, 'fans')
        type = fansTest(data)
      }
      if (textReplace(type, aItem)) {
        res(true)
      }
      res(false)
    })))
  }

  const textReplace = (type, aItem) => {
    // 认证条件判断成功
    if (type !== -1) {
      reWrite(aItem, 'official', type)
      return true
    }
    return false
  }

  const initTips = () => {
    if (!showTips) {
      initTimer()
      return
    }
    const box = document.createElement('div')
    box.style = 'position:fixed;width:200px;height:100px;left:-200px;top:500px;border-radius:10px;border:1px solid #37c8f7;background-color:#fff; text-align: center;transition :all linear 0.25s;opacity:.5;z-index:9999'
    const boxShow = () => {
      box.style.left = '0px'
      box.style.opacity = 1
    }
    const boxHide = () => {
      box.style.left = '-200px'
      box.style.opacity = '0'
    }

    const p = document.createElement('p')
    p.style = 'padding:10px'
    p.innerText = '是否开启坟头推土机脚本'

    const btnBox = document.createElement('div')
    btnBox.style.marginTop = '30px'

    let btnStyle = 'padding:5px 10px;border:1px solid #ccc;border-radius:5px;margin:5px;user-select:none;cursor:pointer'
    const btnCancel = document.createElement('span')
    btnCancel.innerText = '取消'
    btnCancel.style = btnStyle
    btnCancel.addEventListener('click', () => {
      boxHide()
      setTimeout(textBoxShow, 250)
    })

    const btnConfirm = document.createElement('span')
    btnConfirm.innerText = '确定'
    btnConfirm.style = btnStyle
    btnConfirm.style.backgroundColor = '#00a1d6'
    btnConfirm.style.color = '#fff'
    btnConfirm.addEventListener('click', () => {
      initTimer()
      boxHide()
    })

    const textBox = document.createElement('div')
    textBox.style = 'position:fixed;left:-60px;top:500px;background-color:#00a1d6;width:30px;padding:10px 10px 10px 0px;font-size:16px;color:#fff;font-weight:700;writing-mode: vertical-rl;transition :all linear 0.25s;opacity:0;cursor: pointer;border-radius: 0 10px 10px 0;'
    textBox.innerText = '评论推土机'
    textBox.addEventListener('click', () => {
      textBoxHide()
      setTimeout(boxShow, 250)
    })
    const textBoxHide = () => {
      textBox.style.left = '-50px'
      textBox.style.opacity = '0'
    }
    const textBoxShow = () => {
      textBox.style.left = '-10px'
      textBox.style.opacity = 1
    }
    const textBoxMouseOver = () => {
      textBox.style.transition = 'all .25s linear'
      textBox.style.left = '0px'
    }
    const textBoxMouseLeave = () => {
      textBox.style.transition = 'all .25s linear'
      textBox.style.left = '-10px'
    }
    textBox.addEventListener('mouseover', textBoxMouseOver)
    textBox.addEventListener('mouseleave', textBoxMouseLeave)

    document.body.appendChild(textBox)
    document.body.appendChild(box)
    box.appendChild(p)
    box.appendChild(btnBox)
    btnBox.appendChild(btnCancel)
    btnBox.appendChild(btnConfirm)

    document.querySelector('#app').addEventListener('click', (e) => {
      if (commentOb) return
      boxHide()
      setTimeout(textBoxShow, 250)
    }, false)

    setTimeout(() => {
      textBoxShow()
    })
  }

  initTips()
})()