Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b483c6beab | ||
|
|
f748d35fe4 | ||
|
|
b98a8f6680 | ||
|
|
914a41700e | ||
|
|
085765b1a2 | ||
|
|
14c2fd48df | ||
|
|
8683c0bad7 | ||
|
|
a1189aea69 | ||
|
|
9ce1dd21aa | ||
|
|
1f89d0d1cf | ||
|
|
062e7ed1aa |
@@ -55,7 +55,7 @@
|
||||
npm i
|
||||
npm run build
|
||||
```
|
||||
2. 运行服务器(需要Python3.6以上版本):
|
||||
2. 运行服务器(需要Python3.8以上版本):
|
||||
```sh
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
|
||||
@@ -148,7 +148,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler): # noqa
|
||||
)
|
||||
|
||||
def _on_receive_timeout(self):
|
||||
logger.warning('client=%s timed out', self.request.remote_ip)
|
||||
logger.info('client=%s timed out', self.request.remote_ip)
|
||||
self._receive_timeout_timer_handle = None
|
||||
self.close()
|
||||
|
||||
@@ -308,8 +308,9 @@ class RoomInfoHandler(api.base.ApiHandler): # noqa
|
||||
async with utils.request.http_session.get(
|
||||
blivedm_client.ROOM_INIT_URL,
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
||||
' Chrome/102.0.0.0 Safari/537.36'
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Referer': f'https://live.bilibili.com/{room_id}'
|
||||
},
|
||||
params={
|
||||
'room_id': room_id
|
||||
|
||||
2
blivedm
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blivechat",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
||||
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 86 KiB |
@@ -107,7 +107,7 @@ export default class ChatClientDirect {
|
||||
|
||||
sendAuth() {
|
||||
let authParams = {
|
||||
uid: 0,
|
||||
uid: this.roomOwnerUid,
|
||||
roomid: this.roomId,
|
||||
protover: 3,
|
||||
platform: 'web',
|
||||
|
||||
@@ -10,8 +10,7 @@ const COMMAND_UPDATE_TRANSLATION = 7
|
||||
// const CONTENT_TYPE_TEXT = 0
|
||||
const CONTENT_TYPE_EMOTICON = 1
|
||||
|
||||
const HEARTBEAT_INTERVAL = 10 * 1000
|
||||
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + (5 * 1000)
|
||||
const RECEIVE_TIMEOUT = 15 * 1000
|
||||
|
||||
export default class ChatClientRelay {
|
||||
constructor(roomId, autoTranslate) {
|
||||
@@ -28,7 +27,6 @@ export default class ChatClientRelay {
|
||||
this.websocket = null
|
||||
this.retryCount = 0
|
||||
this.isDestroying = false
|
||||
this.heartbeatTimerId = null
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
@@ -66,16 +64,9 @@ export default class ChatClientRelay {
|
||||
}
|
||||
}
|
||||
}))
|
||||
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_HEARTBEAT
|
||||
}))
|
||||
}
|
||||
|
||||
refreshReceiveTimeoutTimer() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
@@ -95,10 +86,6 @@ export default class ChatClientRelay {
|
||||
|
||||
onWsClose() {
|
||||
this.websocket = null
|
||||
if (this.heartbeatTimerId) {
|
||||
window.clearInterval(this.heartbeatTimerId)
|
||||
this.heartbeatTimerId = null
|
||||
}
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
@@ -117,6 +104,10 @@ export default class ChatClientRelay {
|
||||
let { cmd, data } = JSON.parse(event.data)
|
||||
switch (cmd) {
|
||||
case COMMAND_HEARTBEAT: {
|
||||
// 不能由定时器触发发心跳包,因为浏览器会把不活动页面的定时器调到1分钟以上
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_HEARTBEAT
|
||||
}))
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_TEXT: {
|
||||
|
||||
@@ -2,4 +2,4 @@ aiohttp==3.7.4
|
||||
Brotli==1.0.9
|
||||
pycryptodome==3.10.1
|
||||
sqlalchemy==1.4.31
|
||||
tornado==6.1.0
|
||||
tornado==6.3.2
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 97 KiB |
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import urllib.parse
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
@@ -29,6 +31,14 @@ _uid_queue_to_fetch: Optional[asyncio.Queue] = None
|
||||
# 上次被B站ban时间
|
||||
_last_fetch_banned_time: Optional[datetime.datetime] = None
|
||||
|
||||
# wbi密码表
|
||||
WBI_KEY_INDEX_TABLE = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
|
||||
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13
|
||||
]
|
||||
# wbi鉴权口令
|
||||
_wbi_key = ''
|
||||
|
||||
|
||||
def init():
|
||||
cfg = config.get_config()
|
||||
@@ -45,6 +55,9 @@ async def get_avatar_url(user_id):
|
||||
|
||||
|
||||
async def get_avatar_url_or_none(user_id):
|
||||
if user_id == 0:
|
||||
return None
|
||||
|
||||
avatar_url = get_avatar_url_from_memory(user_id)
|
||||
if avatar_url is not None:
|
||||
return avatar_url
|
||||
@@ -146,16 +159,20 @@ async def _get_avatar_url_from_web_coroutine(user_id, future):
|
||||
|
||||
|
||||
async def _do_get_avatar_url_from_web(user_id):
|
||||
global _wbi_key
|
||||
if _wbi_key == '':
|
||||
# TODO 判断一下是否正在获取
|
||||
_wbi_key = await _get_wbi_key()
|
||||
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.bilibili.com/x/space/acc/info',
|
||||
'https://api.bilibili.com/x/space/wbi/acc/info',
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
||||
' Chrome/102.0.0.0 Safari/537.36'
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://space.bilibili.com',
|
||||
'Referer': f'https://space.bilibili.com/{user_id}/'
|
||||
},
|
||||
params={
|
||||
'mid': user_id
|
||||
}
|
||||
params=_add_wbi_sign({'mid': user_id}),
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('Failed to fetch avatar: status=%d %s uid=%d', r.status, r.reason, user_id)
|
||||
@@ -168,11 +185,78 @@ async def _do_get_avatar_url_from_web(user_id):
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
if data['code'] != 0:
|
||||
# 这里虽然失败但不会被ban一段时间
|
||||
logger.info('Failed to fetch avatar: code=%d %s uid=%d', data['code'], data['message'], user_id)
|
||||
if data['code'] == -403:
|
||||
_wbi_key = ''
|
||||
return None
|
||||
|
||||
avatar_url = process_avatar_url(data['data']['face'])
|
||||
update_avatar_cache(user_id, avatar_url)
|
||||
return avatar_url
|
||||
|
||||
|
||||
async def _get_wbi_key():
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.bilibili.com/nav',
|
||||
headers=utils.request.BILIBILI_COMMON_HEADERS,
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('Failed to get wbi key: status=%d %s', r.status, r.reason)
|
||||
return ''
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
logger.exception('Failed to get wbi key:')
|
||||
return ''
|
||||
|
||||
try:
|
||||
wbi_img = data['data']['wbi_img']
|
||||
img_key = wbi_img['img_url'].rpartition('/')[2].partition('.')[0]
|
||||
sub_key = wbi_img['sub_url'].rpartition('/')[2].partition('.')[0]
|
||||
except KeyError:
|
||||
logger.warning('Failed to get wbi key: data=%s', data)
|
||||
return ''
|
||||
|
||||
shuffled_key = img_key + sub_key
|
||||
wbi_key = []
|
||||
for index in WBI_KEY_INDEX_TABLE:
|
||||
if index < len(shuffled_key):
|
||||
wbi_key.append(shuffled_key[index])
|
||||
return ''.join(wbi_key)
|
||||
|
||||
|
||||
def _add_wbi_sign(params: dict):
|
||||
if _wbi_key == '':
|
||||
return params
|
||||
|
||||
wts = str(int(datetime.datetime.now().timestamp()))
|
||||
params_to_sign = {**params, 'wts': wts}
|
||||
|
||||
# 按key字典序排序
|
||||
params_to_sign = {
|
||||
key: params_to_sign[key]
|
||||
for key in sorted(params_to_sign.keys())
|
||||
}
|
||||
# 过滤一些字符
|
||||
for key, value in params_to_sign.items():
|
||||
value = ''.join(
|
||||
ch
|
||||
for ch in str(value)
|
||||
if ch not in "!'()*"
|
||||
)
|
||||
params_to_sign[key] = value
|
||||
|
||||
str_to_sign = urllib.parse.urlencode(params_to_sign) + _wbi_key
|
||||
w_rid = hashlib.md5(str_to_sign.encode('utf-8')).hexdigest()
|
||||
return {
|
||||
**params,
|
||||
'wts': wts,
|
||||
'w_rid': w_rid
|
||||
}
|
||||
|
||||
|
||||
def process_avatar_url(avatar_url):
|
||||
# 去掉协议,兼容HTTP、HTTPS
|
||||
m = re.fullmatch(r'(?:https?:)?(.*)', avatar_url)
|
||||
|
||||
@@ -263,7 +263,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
price=data['price'],
|
||||
message=data['message'],
|
||||
start_time=data['start_time'],
|
||||
id_=data['id'],
|
||||
id=data['id'],
|
||||
uid=data['uid'],
|
||||
uname=data['user_info']['uname'],
|
||||
face=data['user_info']['face'],
|
||||
|
||||
@@ -5,7 +5,7 @@ import aiohttp
|
||||
|
||||
import utils.request
|
||||
|
||||
VERSION = 'v1.6.1'
|
||||
VERSION = 'v1.6.2'
|
||||
|
||||
|
||||
def check_update():
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import aiohttp
|
||||
|
||||
# 不带这堆头部有时候也能成功请求,但是带上后成功的概率更高
|
||||
BILIBILI_COMMON_HEADERS = {
|
||||
'Origin': 'https://www.bilibili.com',
|
||||
'Referer': 'https://www.bilibili.com/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
||||
' Chrome/114.0.0.0 Safari/537.36'
|
||||
}
|
||||
|
||||
http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||
|
||||