// ==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()
})();