偷懒1号

b站自动获取直播列表,并发送特殊的查询,再执行相应的操作

当前为 2024-05-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         偷懒1号
// @namespace    https://gitee.com/Liwker
// @version      0.3
// @update       2024/5/21
// @description  b站自动获取直播列表,并发送特殊的查询,再执行相应的操作
// @author       Liwker 子木
// @match        https://space.bilibili.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(async function () {
  'use strict';
  // 留存数据
  const Data = {
    myUid: '',  // 自己的uid
    csrf: '',
    isRun: false, // 是否开始执行
    area_id: 321, // 直播区域,0 全部,321 原神
    livePage: 1,  // 查询区域列表
    content: [], // 发送的消息
    parent_area_id: 3, // 手游3 网游2 单机6
    parent_id_List: [2, 3, 6], // 目前已有的大分区
    wangyou_id_List: [329, 240],  // 网游的id
  }

  // 工具函数
  const Tools = {
    cookieToJson() {
      const cookieArr = document.cookie.split("; ")
      const obj = {} 
      for(const i of cookieArr) {
        const arr = i.split('=')
        obj[arr[0]] = arr[1]
      }
      return obj
    },
    // obj转url参数
    toQuery(params, url) {
      const arr = []
      for(const param in params) {
        arr.push(param + '=' + params[param])
      }
      const query = arr.join('&')
      if(url) {
        return url + '?' + query
      } {
        return query
      }
    },
    sleep(time) {
      return new Promise(resolve => setTimeout(resolve, time))
    }
  }


  // 创建AJAX请求
  /* option 参数
    type: 'get' or 'post',
    url: '',
    header: {"Content-Type": "application/x-www-form-urlencoded"} or '',
    body: 'msg[sender_uid]=292297687&msg[receiver_id]=647193094' or null,
  */
  function myAjax (option) {
    const xhr = new XMLHttpRequest()
    xhr.ontimeout = function () {
      alert("网络异常,请稍后重试!")
    }
    // 网络异常回调
    xhr.onerror = function () {
      alert("您的网络似乎出了一些问题!")
    }
    // xhr.open('post', 'https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?w_sender_uid=292297687&w_receiver_id=647193094&w_dev_id=23CC0EB2-8A23-4A59-B36F-047A60FA3F22&w_rid=6a507e3644f11685828e203925c28224&wts=1709401068', false)
    xhr.open(option.type, option.url, false)  // false 是同步请求
    if(option.header) { // 添加头
      for(const head in option.header) {
        xhr.setRequestHeader(head, option.header[head])
      }
    }
    // xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
    xhr.withCredentials = true // 设置为本页面的请求cookie

    // 发送请求
    // xhr.send(`msg[sender_uid]=292297687&msg[receiver_id]=647193094&msg[receiver_type]=1&msg[msg_type]=1&msg[msg_status]=0&msg[content]={"content":"hello"}&msg[timestamp]=1709401067&msg[new_face_version]=0&msg[dev_id]=23CC0EB2-8A23-4A59-B36F-047A60FA3F22&from_firework=0&build=0&mobi_app=web&csrf_token=f50ebc4374670d2bc9442ee7ff56e434&csrf=f50ebc4374670d2bc9442ee7ff56e434`)
    xhr.send(option.body || null)

    // 同步请求的响应
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        const res = xhr.response && JSON.parse(xhr.response)
        // console.log('xhr.response', res)
        return res
      } else {
        console.error('xhr.response', xhr.response)
      }
    } else {
      console.error('xhr', xhr)
    }
  }

  // 查询分区列表
  /* option 参数
    parent_area_id: 3,  // 3 手游分区
    area_id: 321, // 0全部,321原神
    page: 1,
  */
  function getList (option) {
    option.platform = 'web'

    const url = 'https://api.live.bilibili.com/xlive/web-interface/v1/second/getList'
    const ajaxOption = {
      type: 'get',
      url: Tools.toQuery(option, url),
      body: null,
    }
    const res = myAjax(ajaxOption)
    if(res && res.code == 0) {
      return res.data
    } else {
      console.error('获取直播列表失败', option, ajaxOption)
    }
  }

  // 查询主播
  /* option 参数
    search: xxxx, // 主播房间号
  */
  function searchAnchor (option) {
    option.search_type = 3

    const url = 'https://api.live.bilibili.com/xlive/mcn-interface/v1/mcn_mng/SearchAnchor'
    const ajaxOption = {
      type: 'get',
      url: Tools.toQuery(option, url),
      body: null,
    }
    const res = myAjax(ajaxOption)
    if(res && res.code == 0) {
      return res.data
    } else {
      console.error('查询主播房间号失败', option, ajaxOption)
    }
  }

  // 发送消息
  /* option 参数
    w_receiver_id: xxxx, // 接受者uid
    content: '' // 消息内容
  */
  function sendMsg (option) {
    const query = {
      w_sender_uid: Data.myUid,  // 发送者uid
      w_receiver_id: option.w_receiver_id,  // 接受者uid
      w_dev_id: '00B09F06-90C5-4E87-B905-F8337F2B5612',
      w_rid: 'fcf0e090a8f54f944957ceb3c1b6316a',
      wts: new Date().getTime()
    }
    const body = {
      'msg[sender_uid]': Data.myUid,  // 发送者uid
      'msg[receiver_id]': option.w_receiver_id, // 接受者uid
      'msg[receiver_type]': 1,
      'msg[msg_type]': 1,
      'msg[msg_status]': 0,
      'msg[content]': `{"content": "${option.content}"}`, // 消息内容
      'msg[timestamp]': query.wts,
      'msg[new_face_version]': 0,
      'msg[dev_id]': 'EE21C3A5-2CBA-4B6B-79D9-3811024DD56C764335infoc',
      csrf: Data.csrf,
      csrf_token: Data.csrf,
    }

    const url = 'https://api.vc.bilibili.com/web_im/v1/web_im/send_msg'
    const ajaxOption = {
      type: 'post',
      url: Tools.toQuery(query, url),
      header: {"Content-Type": "application/x-www-form-urlencoded"},
      body: Tools.toQuery(body),
    }
    const res = myAjax(ajaxOption)
    if((res && res.code == 0) || (res && res.message != '0')) {
      return res
    } else {
      console.error('发送消息失败', option, ajaxOption)
    }
  }

  // 创建消息El
  function addMsgEl (msg) {
    function addMsg(message, isLoading) {
      const liwkerMain = document.querySelector('div.liwkerMain')
      const msgEl = document.createElement('div')
      msgEl.classList.add('liwkerMsg')
      isLoading && msgEl.classList.add('liwkerLoading')
      msgEl.innerText = message
      msgEl.title = message
      liwkerMain.appendChild(msgEl)
    }
    // 移除已有的loading
    const liwkerLoading = document.querySelector('div.liwkerLoading')
    liwkerLoading && liwkerLoading.remove()
    if(msg) {
      addMsg(msg, false)
      // 添加加载消息
      addMsg('正在查找新主播...', true)
    } else {
      if(Data.isRun) {
        addMsg('正在查找新主播...', true)
      } else {
        addMsg('——————执行结束——————', true)
      }
    }

  }


  // 主函数
  async function main () {
    /* 步骤
      1. 查询分区主播列表
      2. 依次查询是否是新主播
      3. 给新主播发送消息
      4. 循环查询分区主播列表
    */

    let msgCount = 0  // 发送消息的人数
    addMsgEl()  // 开始加载信息
    while(Data.isRun) {
      // 获取直播列表
      const liveList = getList({
        parent_area_id: Data.parent_area_id,  // 3 手游分区
        area_id: Data.area_id, // 0全部,321原神
        page: Data.livePage,
      })
      if(!liveList) {
        Data.livePage = 1
        Data.isRun = false
        window.alert('获取直播列表失败')
        break
      } else if(liveList.list && liveList.list.length == 0) {
        // 结束
        Data.livePage = 1
        Data.isRun = false
        window.alert(`该分区已查询完毕!`)
        // 发送结束El
        break
      } else {
        Data.livePage++
      }
      // console.log(liveList)
      // 遍历直播查询
      for(const live of liveList.list) {
        if(!Data.isRun) break
        // console.log(live.roomid, live.title, live.uid) // 测试

        const liverInfo = searchAnchor({
          search: live.roomid
        })
        if(!liverInfo) {
          window.alert('查询房间号失败')
          break
        }
        if(liverInfo.items[0].is_new_anchor !== 1) {
          // 不是新主播
          // 休息,查询频率 2s
          await Tools.sleep(2000)
          // await Tools.sleep(500) // test
          continue
        } else {
          // 发送消息
          const res = sendMsg({
            w_receiver_id: live.uid, // 接受者uid
            // w_receiver_id: 647193094, // 测试
            content: Array.isArray(Data.content) ? Data.content[Math.floor(Math.random() * Data.content.length)] || '' : ''// 消息内容
          })
          msgCount++
          if(!res || res.message != 0) {
            addMsgEl(`${msgCount}. "${live.uname}"(${live.uid}) 发送消息失败!`)
            res.message && addMsgEl(`"${live.uname}":${res.message}`)
          } else {
            // 发送消息成功
            addMsgEl(`${msgCount}. "${live.uname}"(${live.uid}) 发送消息成功~`)
            // console.log('发送消息成功', live.uid, live.uname)
          }
          // 休息,发消息频率30s
          await Tools.sleep(30000)
          // await Tools.sleep(1000)  // test
        }
      }

      if(!Data.isRun) break
      // 休息
      await Tools.sleep(5000)
      // if(Data.livePage === 3) { Data.isRun = false }  // 测试
    }

    Data.isRun = false
    // 重置按钮
    const liBtnStart = document.querySelector('#liBtnStart')
    const liBtnEnd = document.querySelector('#liBtnEnd')
    liBtnStart.disabled = false
    liBtnStart.classList.remove('is-disabled')
    liBtnEnd.disabled = true
    liBtnEnd.classList.add('is-disabled')
    addMsgEl()  // 添加结束消息
  }


  /* 创建元素 */
  function createEl() {
    // 创建css
    const style = document.createElement('style')
    style.innerHTML = `
      #liwkerBtnShow {
        position: absolute;
        right: 15px;
        top: -40px;
        width: 80px;
      }
      #liwkerArea {
        width: 330px;
        height: 550px;
        position: fixed;
        top: 320px;
        right: 20px;
        box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
        background-color: #fff;
        border: 1px solid #ebeef5;
        transition: .3s;
        z-index: 999;
      }
      #liwkerArea > .top {
        width: 100%;
        height: 80px;
        background: #eee;
        display: flex;
        justify-content: space-evenly;
      }
      #liwkerArea > .top > .search {
        width: 220px;
        height: 100%;
      }
      #liwkerArea > .top > .search > #send_content {
        height: 35px;
      }
      #liwkerArea > .top > .search > select {
        width: 100%;
        height: 30px;
        margin-top: 7px;
      }

      .buttonBox {
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: space-evenly;
        align-items: end;
      }

      .liwkerMain {
        width: 100%;
        height: 465px;
        box-sizing: border-box;
        padding: 5px 15px;
        padding-left: 10px;
        overflow: auto;
      }
      .liwkerMsg {
        width: 285px;
        height: 24px;
        line-height: 24px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
    `
    document.querySelector('head').appendChild(style)

    // 创建box
    const liwkerArea = document.createElement('div')
    liwkerArea.id = "liwkerArea"
    liwkerArea.innerHTML = `
      <button id="liwkerBtnShow" type="button" class="el-button search-btn el-button--primary el-button--small">隐藏</button>
      <div class="top">
        <div class="search">
          <input type="text" id="send_content" autocomplete="off" placeholder="请输入发送的消息" class="el-input__inner">
          <select id="area_select">
            <option value="2">网游全部</option>
            <option value="3">手游全部</option>
            <option value="6">单机全部</option>
            <option value="321" selected>原神</option>
            <option value="549">崩坏:星穹铁道</option>
            <option value="329">无畏契约</option>
            <option value="240">APEX英雄</option>
            <option value="35">王者荣耀</option>
            <option value="256">和平精英</option>
            <option value="163">第五人格</option>
            <option value="395">LOL手游</option>
            <option value="255">明日方舟</option>
            <option value="872">来自星尘</option>
            <option value="571">蛋仔派对</option>
            <option value="822">元梦之星</option>
            <option value="874">鸣潮</option>
            <option value="777">晶核</option>
            <option value="36">阴阳师</option>
            <option value="343">DNF手游</option>
            <option value="662">绝区零</option>
          </select>
        </div>
        <div class="buttonBox">
          <button type="button" id="liBtnStart" class="el-button search-btn el-button--primary el-button--small">开始</button>
          <button type="button" id="liBtnEnd" class="el-button search-btn el-button--primary el-button--small">终止</button>
        </div>
      </div>
      <div class="liwkerMain">
        <!-- <div class="liwkerMsg">
          1. “高级的玫瑰”已成功发送消息
        </div> -->
      </div>
    `
    document.querySelector('body').appendChild(liwkerArea)

    // 绑定事件
    // 1. 展示/隐藏按钮
    const liwkerBtnShow = document.querySelector('#liwkerBtnShow')
    liwkerBtnShow.onclick = (e) => {
      const box = document.querySelector('#liwkerArea')
      const top = box.querySelector('.top')
      const main = box.querySelector('.liwkerMain')
      if(top.style.display !== 'none') {
        box.style.height = '0px'
        top.style.display = 'none'
        main.style.display = 'none'
        e.target.innerText = '显示'
      } else {
        box.style.height = '550px'
        top.style.display = 'flex'
        main.style.display = 'block'
        e.target.innerText = '隐藏'
      }
    }

    const send_content = document.querySelector('#send_content')
    const area_select = document.querySelector('#area_select')
    send_content.value = '主播你好呀~'
    
    // 2. 开始
    const liBtnStart = document.querySelector('#liBtnStart')
    const liBtnEnd = document.querySelector('#liBtnEnd')
    if(Data.isRun) {
      liBtnStart.disabled = true
      liBtnStart.classList.add('is-disabled')
      liBtnEnd.disabled = false
      liBtnEnd.classList.remove('is-disabled')
    } else {
      liBtnStart.disabled = false
      liBtnStart.classList.remove('is-disabled')
      liBtnEnd.disabled = true
      liBtnEnd.classList.add('is-disabled')
    }
    liBtnStart.onclick = () => {
      if(!send_content.value || send_content.value.trim() == '') {
        window.alert('请输入发送的消息')
        return
      }
      // 输入框消息更新
      Data.content = send_content.value.split('&')
      // 选择框更新 area_id & parent_area_id更新
      if(Data.parent_id_List.includes(Number(area_select.value))) {
        Data.parent_area_id = area_select.value
        Data.area_id = 0  // 全部子分区
      } else if(Data.wangyou_id_List.includes(Number(area_select.value))) {
        Data.parent_area_id = 2 // 网游
        Data.area_id = area_select.value
      } else {
        Data.parent_area_id = 3 // 手游
        Data.area_id = area_select.value
      }
      // 禁用按钮
      send_content.disabled = true
      send_content.classList.add('is-disabled')
      area_select.disabled = true
      liBtnStart.disabled = true
      liBtnStart.classList.add('is-disabled')
      liBtnEnd.disabled = false
      liBtnEnd.classList.remove('is-disabled')
      
      Data.isRun = true
      Data.livePage = 1
      // 执行操作
      main()
    }
    // 3. 终止
    liBtnEnd.onclick = () => {
      // 禁用按钮
      send_content.disabled = false
      send_content.classList.remove('is-disabled')
      area_select.disabled = false
      liBtnStart.disabled = false
      liBtnStart.classList.remove('is-disabled')
      liBtnEnd.disabled = true
      liBtnEnd.classList.add('is-disabled')

      Data.isRun = false
      Data.livePage = 1
    }
  }

  // 初始化
  function init () {
    const cookie = Tools.cookieToJson()
    Data.myUid = cookie.DedeUserID  // 自己的uid
    Data.csrf = cookie.bili_jct
    Data.isRun = false // 是否开始执行
    Data.livePage = 1 // 是否开始执行

    createEl()  // 创建元素
  }

  init()

})();