前端支持使用身份码

pull/109/head
John Smith 1 year ago
parent cad573312b
commit 68f8ab3a92

@ -63,7 +63,6 @@ def make_text_message_data(
translation: str = '',
content_type: int = ContentType.TEXT,
content_type_params: list = None,
text_emoticons: Iterable[Tuple[str, str]] = None
):
# 为了节省带宽用list而不是dict
return [
@ -98,7 +97,7 @@ def make_text_message_data(
# 14: contentTypeParams
content_type_params if content_type_params is not None else [],
# 15: textEmoticons
text_emoticons if text_emoticons is not None else [],
[], # 已废弃,保留
]
@ -295,13 +294,11 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
self.send_cmd_data(Command.ADD_TEXT, text_data)
text_data[4] = 'te[dog]st'
text_data[11] = uuid.uuid4().hex
text_data[15] = [('[dog]', 'http://i0.hdslb.com/bfs/live/4428c84e694fbf4e0ef6c06e958d9352c3582740.png')]
self.send_cmd_data(Command.ADD_TEXT, text_data)
text_data[2] = '主播'
text_data[3] = 3
text_data[4] = "I can eat glass, it doesn't hurt me."
text_data[11] = uuid.uuid4().hex
text_data[15] = []
self.send_cmd_data(Command.ADD_TEXT, text_data)
self.send_cmd_data(Command.ADD_MEMBER, member_data)
self.send_cmd_data(Command.ADD_SUPER_CHAT, sc_data)

@ -42,20 +42,14 @@ class BusinessError(Exception):
return self.data['code']
async def request_open_live_or_common_server(open_live_url, common_server_url, body: Union[dict, str, bytes]) -> dict:
async def request_open_live_or_common_server(open_live_url, common_server_url, body: dict) -> dict:
"""如果配置了开放平台,则直接请求,否则转发请求到公共服务器的内部接口"""
cfg = config.get_config()
if cfg.is_open_live_configured:
return await _request_open_live(open_live_url, body)
post_params = {'headers': {'Content-Type': 'application/json'}}
if isinstance(body, dict):
post_params['json'] = body
else:
post_params['data'] = body
req_ctx_mgr = utils.request.http_session.post(common_server_url, **post_params)
try:
req_ctx_mgr = utils.request.http_session.post(common_server_url, json=body)
return await _read_response(req_ctx_mgr)
except TransportError:
logger.exception('Request common server failed:')
@ -65,17 +59,11 @@ async def request_open_live_or_common_server(open_live_url, common_server_url, b
raise
async def _request_open_live(url, body: Union[dict, str, bytes]) -> dict:
async def _request_open_live(url, body: dict) -> dict:
cfg = config.get_config()
assert cfg.is_open_live_configured
if isinstance(body, dict):
body_bytes = json.dumps(body).encode('utf-8')
elif isinstance(body, str):
body_bytes = body.encode('utf-8')
else:
body_bytes = body
body_bytes = json.dumps(body).encode('utf-8')
headers = {
'x-bili-accesskeyid': cfg.open_live_access_key_id,
'x-bili-content-md5': hashlib.md5(body_bytes).hexdigest(),
@ -96,9 +84,9 @@ async def _request_open_live(url, body: Union[dict, str, bytes]) -> dict:
headers['Content-Type'] = 'application/json'
headers['Accept'] = 'application/json'
req_ctx_mgr = utils.request.http_session.post(url, headers=headers, data=body_bytes)
try:
req_ctx_mgr = utils.request.http_session.post(url, headers=headers, data=body_bytes)
return await _read_response(req_ctx_mgr)
except TransportError:
logger.exception('Request open live failed:')
@ -124,9 +112,13 @@ async def _read_response(req_ctx_mgr: AsyncContextManager[aiohttp.ClientResponse
class _OpenLiveHandlerBase(api.base.ApiHandler):
def prepare(self):
super().prepare()
# 做一些简单的检查
if not isinstance(self.json_args, dict):
raise tornado.web.MissingArgumentError('body')
if 'app_id' in self.json_args:
cfg = config.get_config()
self.json_args['app_id'] = cfg.open_live_app_id
logger.info('client=%s requesting open live, cls=%s', self.request.remote_ip, type(self).__name__)
@ -138,7 +130,7 @@ class _PublicHandlerBase(_OpenLiveHandlerBase):
async def post(self):
try:
res = await request_open_live_or_common_server(
self._OPEN_LIVE_URL, self._COMMON_SERVER_URL, self.request.body
self._OPEN_LIVE_URL, self._COMMON_SERVER_URL, self.json_args
)
except TransportError:
raise tornado.web.HTTPError(500)
@ -157,7 +149,7 @@ class _PrivateHandlerBase(_OpenLiveHandlerBase):
raise tornado.web.HTTPError(501)
try:
res = await _request_open_live(self._OPEN_LIVE_URL, self.request.body)
res = await _request_open_live(self._OPEN_LIVE_URL, self.json_args)
except TransportError:
raise tornado.web.HTTPError(500)
except BusinessError as e:

@ -13,8 +13,8 @@ const CONTENT_TYPE_EMOTICON = 1
const RECEIVE_TIMEOUT = 15 * 1000
export default class ChatClientRelay {
constructor(roomId, autoTranslate) {
this.roomId = roomId
constructor(roomKey, autoTranslate) {
this.roomKey = roomKey
this.autoTranslate = autoTranslate
this.onAddText = null
@ -58,7 +58,7 @@ export default class ChatClientRelay {
this.websocket.send(JSON.stringify({
cmd: COMMAND_JOIN_ROOM,
data: {
roomId: this.roomId,
roomKey: this.roomKey,
config: {
autoTranslate: this.autoTranslate
}

@ -9,9 +9,12 @@ export default {
home: {
roomIdEmpty: "Room ID can't be empty",
roomIdInteger: 'Room ID must be positive integer',
authCodeEmpty: "Identity code can't be empty",
general: 'General',
room: 'Room',
roomId: 'Room ID',
authCode: 'Identity code',
showDanmaku: 'Show messages',
showGift: 'Show Super Chats',
showGiftName: 'Show gift name',

@ -9,9 +9,12 @@ export default {
home: {
roomIdEmpty: 'ルームのIDを空白にすることはできません',
roomIdInteger: 'ルームは正の整数でなければなりません',
authCodeEmpty: 'アイデンティティコードを空白にすることはできません',
general: '常規',
room: 'ルーム',
roomId: 'ルームID',
authCode: 'アイデンティティコード',
showDanmaku: 'コメントを表示する',
showGift: 'スーパーチャットと新メンバーを表示する',
showGiftName: 'ギフト名を表示する',

@ -9,9 +9,12 @@ export default {
home: {
roomIdEmpty: '房间ID不能为空',
roomIdInteger: '房间ID必须为正整数',
authCodeEmpty: '身份码不能为空',
general: '常规',
room: '房间',
roomId: '房间ID',
authCode: '身份码',
showDanmaku: '显示弹幕',
showGift: '显示打赏和新舰长',
showGiftName: '显示礼物名',

@ -77,15 +77,22 @@ const router = new VueRouter({
props: route => ({ strConfig: route.query })
},
{
path: '/room/:roomId',
path: '/room/:roomKeyValue',
name: 'room',
component: Room,
props(route) {
let roomId = parseInt(route.params.roomId)
if (isNaN(roomId)) {
roomId = null
let roomKeyType = parseInt(route.query.roomKeyType) || 1
if (roomKeyType < 1 || roomKeyType > 2) {
roomKeyType = 1
}
return { roomId, strConfig: route.query }
let roomKeyValue = route.params.roomKeyValue
if (roomKeyType === 1) {
roomKeyValue = parseInt(roomKeyValue) || null
} else {
roomKeyValue = roomKeyValue || null
}
return { roomKeyType, roomKeyValue, strConfig: route.query }
}
},
{ path: '*', component: NotFound }

@ -1,16 +1,31 @@
<template>
<div>
<p>
<el-form :model="form" ref="form" label-width="150px" :rules="{
roomId: [
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
]
}">
<el-form :model="form" ref="form" label-width="150px">
<el-tabs type="border-card">
<el-tab-pane :label="$t('home.general')">
<el-form-item :label="$t('home.roomId')" required prop="roomId">
<el-input v-model.number="form.roomId" type="number" min="1"></el-input>
<el-form-item :label="$t('home.room')" required :prop="form.roomKeyType === 1 ? 'roomId' : 'authCode'">
<el-row>
<el-col :span="6">
<el-select v-model="form.roomKeyType" style="width: 100%">
<el-option :label="$t('home.authCode')" :value="2"></el-option>
<el-option :label="$t('home.roomId')" :value="1"></el-option>
</el-select>
</el-col>
<el-col :span="18">
<el-input v-if="form.roomKeyType === 1"
v-model.number="form.roomId" type="number" min="1" :rules="[
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
]"
></el-input>
<el-input v-else
v-model.number="form.authCode" :rules="[
{required: true, message: $t('home.authCodeEmpty'), trigger: 'blur'}
]"
></el-input>
</el-col>
</el-row>
</el-form-item>
<el-row :gutter="20">
<el-col :xs="24" :sm="8">
@ -184,11 +199,20 @@ export default {
},
form: {
...chatConfig.getLocalConfig(),
roomId: parseInt(window.localStorage.roomId || '1')
roomKeyType: parseInt(window.localStorage.roomKeyType || '2'),
roomId: parseInt(window.localStorage.roomId || '1'),
authCode: window.localStorage.authCode || '',
}
}
},
computed: {
roomKeyValue() {
if (this.form.roomKeyType === 1) {
return this.form.roomId
} else {
return this.form.authCode
}
},
roomUrl() {
return this.getRoomUrl(false)
},
@ -206,7 +230,9 @@ export default {
},
watch: {
roomUrl: _.debounce(function() {
window.localStorage.roomKeyType = this.form.roomKeyType
window.localStorage.roomId = this.form.roomId
window.localStorage.authCode = this.form.authCode
chatConfig.setLocalConfig(this.form)
}, 500)
},
@ -256,13 +282,13 @@ export default {
},
enterRoom() {
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
window.open(this.roomUrl, `room ${this.roomKeyValue}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
},
enterTestRoom() {
window.open(this.getRoomUrl(true), 'test room', 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
},
getRoomUrl(isTestRoom) {
if (!isTestRoom && this.form.roomId === '') {
if (!isTestRoom && !this.roomKeyValue) {
return ''
}
@ -272,12 +298,13 @@ export default {
lang: this.$i18n.locale
}
delete query.roomId
delete query.authCode
let resolved
if (isTestRoom) {
resolved = this.$router.resolve({ name: 'test_room', query })
} else {
resolved = this.$router.resolve({ name: 'room', params: { roomId: this.form.roomId }, query })
resolved = this.$router.resolve({ name: 'room', params: { roomKeyValue: this.roomKeyValue }, query })
}
return `${window.location.protocol}//${window.location.host}${resolved.href}`
},
@ -314,7 +341,9 @@ export default {
chatConfig.sanitizeConfig(cfg)
this.form = {
...cfg,
roomId: this.form.roomId
roomKeyType: this.form.roomKeyType,
roomId: this.form.roomId,
authCode: this.form.authCode
}
}
}

@ -21,8 +21,12 @@ export default {
ChatRenderer
},
props: {
roomId: {
roomKeyType: {
type: Number,
default: 1
},
roomKeyValue: {
type: [Number, String],
default: null
},
strConfig: {
@ -139,13 +143,20 @@ export default {
}
},
initChatClient() {
if (this.roomId === null) {
if (this.roomKeyValue === null) {
this.chatClient = new ChatClientTest()
} else if (this.config.relayMessagesByServer) {
let roomKey = {
type: this.roomKeyType,
value: this.roomKeyValue
}
this.chatClient = new ChatClientRelay(roomKey, this.config.autoTranslate)
} else {
if (!this.config.relayMessagesByServer) {
this.chatClient = new ChatClientDirect(this.roomId)
if (this.roomKeyType === 1) {
this.chatClient = new ChatClientDirect(this.roomKeyValue)
} else {
this.chatClient = new ChatClientRelay(this.roomId, this.config.autoTranslate)
// TODO authCode
// this.chatClient = new ChatClientDirect(this.roomKeyValue)
}
}
this.chatClient.onAddText = this.onAddText

@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
import asyncio
import base64
import binascii
import enum
import json
import logging
import uuid
from typing import *
@ -11,8 +8,8 @@ from typing import *
import api.chat
import api.open_live as api_open_live
import blivedm.blivedm as blivedm
import blivedm.blivedm.models.open_live as dm_open_models
import blivedm.blivedm.models.web as dm_web_models
import blivedm.blivedm.models.pb as dm_pb_models
import config
import services.avatar
import services.translate
@ -368,106 +365,13 @@ class ClientRoom:
class LiveMsgHandler(blivedm.BaseHandler):
# 重新定义XXX_callback是为了减少对字段名的依赖防止B站改字段名
def __danmu_msg_callback(self, client: LiveClientType, command: dict):
info = command['info']
dm_v2 = command.get('dm_v2', '')
proto: Optional[dm_pb_models.SimpleDm] = None
if dm_v2 != '':
try:
proto = dm_pb_models.SimpleDm.loads(base64.b64decode(dm_v2))
except (binascii.Error, KeyError, TypeError, ValueError):
pass
if proto is not None:
face = proto.user.face
else:
face = ''
if len(info[3]) != 0:
medal_level = info[3][0]
medal_room_id = info[3][3]
else:
medal_level = 0
medal_room_id = 0
message = dm_web_models.DanmakuMessage(
timestamp=info[0][4],
msg_type=info[0][9],
dm_type=info[0][12],
emoticon_options=info[0][13],
mode_info=info[0][15],
msg=info[1],
uid=info[2][0],
uname=info[2][1],
face=face,
admin=info[2][2],
urank=info[2][5],
mobile_verify=info[2][6],
medal_level=medal_level,
medal_room_id=medal_room_id,
user_level=info[4][0],
privilege_type=info[7],
)
return self._on_danmaku(client, message)
def __send_gift_callback(self, client: LiveClientType, command: dict):
data = command['data']
message = dm_web_models.GiftMessage(
gift_name=data['giftName'],
num=data['num'],
uname=data['uname'],
face=data['face'],
uid=data['uid'],
timestamp=data['timestamp'],
coin_type=data['coin_type'],
total_coin=data['total_coin'],
)
return self._on_gift(client, message)
def __guard_buy_callback(self, client: LiveClientType, command: dict):
data = command['data']
message = dm_web_models.GuardBuyMessage(
uid=data['uid'],
username=data['username'],
guard_level=data['guard_level'],
start_time=data['start_time'],
)
return self._on_buy_guard(client, message)
def __super_chat_message_callback(self, client: LiveClientType, command: dict):
data = command['data']
message = dm_web_models.SuperChatMessage(
price=data['price'],
message=data['message'],
start_time=data['start_time'],
id=data['id'],
uid=data['uid'],
uname=data['user_info']['uname'],
face=data['user_info']['face'],
)
return self._on_super_chat(client, message)
_CMD_CALLBACK_DICT = {
**blivedm.BaseHandler._CMD_CALLBACK_DICT,
'DANMU_MSG': __danmu_msg_callback,
'SEND_GIFT': __send_gift_callback,
'GUARD_BUY': __guard_buy_callback,
'SUPER_CHAT_MESSAGE': __super_chat_message_callback
}
def on_client_stopped(self, client: LiveClientType, exception: Optional[Exception]):
_live_client_manager.del_live_client(client.room_key)
def _on_danmaku(self, client: LiveClientType, message: dm_web_models.DanmakuMessage):
def _on_danmaku(self, client: WebLiveClient, message: dm_web_models.DanmakuMessage):
asyncio.create_task(self.__on_danmaku(client, message))
async def __on_danmaku(self, client: LiveClientType, message: dm_web_models.DanmakuMessage):
async def __on_danmaku(self, client: WebLiveClient, message: dm_web_models.DanmakuMessage):
avatar_url = message.face
if avatar_url != '':
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
@ -497,8 +401,6 @@ class LiveMsgHandler(blivedm.BaseHandler):
content_type = api.chat.ContentType.TEXT
content_type_params = None
text_emoticons = self._parse_text_emoticons(message)
need_translate = (
content_type != api.chat.ContentType.EMOTICON and self._need_translate(message.msg, room, client)
)
@ -529,30 +431,12 @@ class LiveMsgHandler(blivedm.BaseHandler):
translation=translation,
content_type=content_type,
content_type_params=content_type_params,
text_emoticons=text_emoticons,
))
if need_translate:
await self._translate_and_response(message.msg, room.room_key, msg_id)
@staticmethod
def _parse_text_emoticons(message: dm_web_models.DanmakuMessage):
try:
extra = json.loads(message.mode_info['extra'])
# {"[dog]":{"emoticon_id":208,"emoji":"[dog]","descript":"[dog]","url":"http://i0.hdslb.com/bfs/live/4428c8
# 4e694fbf4e0ef6c06e958d9352c3582740.png","width":20,"height":20,"emoticon_unique":"emoji_208","count":1}}
emoticons = extra['emots']
if emoticons is None:
return []
res = [
(emoticon['descript'], emoticon['url'])
for emoticon in emoticons.values()
]
return res
except (json.JSONDecodeError, TypeError, KeyError):
return []
def _on_gift(self, client: LiveClientType, message: dm_web_models.GiftMessage):
def _on_gift(self, client: WebLiveClient, message: dm_web_models.GiftMessage):
avatar_url = services.avatar.process_avatar_url(message.face)
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
@ -574,11 +458,11 @@ class LiveMsgHandler(blivedm.BaseHandler):
'num': message.num
})
def _on_buy_guard(self, client: LiveClientType, message: dm_web_models.GuardBuyMessage):
def _on_buy_guard(self, client: WebLiveClient, message: dm_web_models.GuardBuyMessage):
asyncio.create_task(self.__on_buy_guard(client, message))
@staticmethod
async def __on_buy_guard(client: LiveClientType, message: dm_web_models.GuardBuyMessage):
async def __on_buy_guard(client: WebLiveClient, message: dm_web_models.GuardBuyMessage):
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
avatar_url = await services.avatar.get_avatar_url(message.uid)
@ -594,7 +478,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
'privilegeType': message.guard_level
})
def _on_super_chat(self, client: LiveClientType, message: dm_web_models.SuperChatMessage):
def _on_super_chat(self, client: WebLiveClient, message: dm_web_models.SuperChatMessage):
avatar_url = services.avatar.process_avatar_url(message.face)
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
@ -629,7 +513,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
message.message, room.room_key, msg_id, services.translate.Priority.HIGH
))
def _on_super_chat_delete(self, client: LiveClientType, message: dm_web_models.SuperChatDeleteMessage):
def _on_super_chat_delete(self, client: WebLiveClient, message: dm_web_models.SuperChatDeleteMessage):
room = client_room_manager.get_room(client.room_key)
if room is None:
return
@ -667,4 +551,140 @@ class LiveMsgHandler(blivedm.BaseHandler):
)
)
# TODO 开放平台消息处理
#
# 开放平台消息
#
def _on_open_live_danmaku(self, client: OpenLiveClient, message: dm_open_models.DanmakuMessage):
avatar_url = message.uface
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
room = client_room_manager.get_room(client.room_key)
if room is None:
return
if message.uid == client.room_owner_uid:
author_type = 3 # 主播
elif message.guard_level != 0: # 1总督2提督3舰长
author_type = 1 # 舰队
else:
author_type = 0
if message.dm_type == 1:
content_type = api.chat.ContentType.EMOTICON
content_type_params = api.chat.make_emoticon_params(message.emoji_img_url)
else:
content_type = api.chat.ContentType.TEXT
content_type_params = None
need_translate = (
content_type != api.chat.ContentType.EMOTICON and self._need_translate(message.msg, room, client)
)
if need_translate:
translation = services.translate.get_translation_from_cache(message.msg)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
room.send_cmd_data(api.chat.Command.ADD_TEXT, api.chat.make_text_message_data(
avatar_url=avatar_url,
timestamp=message.timestamp,
author_name=message.uname,
author_type=author_type,
content=message.msg,
privilege_type=message.guard_level,
medal_level=0 if not message.fans_medal_wearing_status else message.fans_medal_level,
id_=message.msg_id,
translation=translation,
content_type=content_type,
content_type_params=content_type_params,
))
if need_translate:
asyncio.create_task(self._translate_and_response(message.msg, room.room_key, message.msg_id))
def _on_open_live_gift(self, client: OpenLiveClient, message: dm_open_models.GiftMessage):
avatar_url = services.avatar.process_avatar_url(message.uface)
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
# 丢人
if not message.paid:
return
room = client_room_manager.get_room(client.room_key)
if room is None:
return
room.send_cmd_data(api.chat.Command.ADD_GIFT, {
'id': message.msg_id,
'avatarUrl': avatar_url,
'timestamp': message.timestamp,
'authorName': message.uname,
'totalCoin': message.price,
'giftName': message.gift_name,
'num': message.gift_num
})
def _on_open_live_buy_guard(self, client: OpenLiveClient, message: dm_open_models.GuardBuyMessage):
avatar_url = message.user_info.uface
services.avatar.update_avatar_cache_if_expired(message.user_info.uid, avatar_url)
room = client_room_manager.get_room(client.room_key)
if room is None:
return
room.send_cmd_data(api.chat.Command.ADD_MEMBER, {
'id': message.msg_id,
'avatarUrl': avatar_url,
'timestamp': message.timestamp,
'authorName': message.user_info.uname,
'privilegeType': message.guard_level
})
def _on_open_live_super_chat(self, client: OpenLiveClient, message: dm_open_models.SuperChatMessage):
avatar_url = services.avatar.process_avatar_url(message.uface)
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
room = client_room_manager.get_room(client.room_key)
if room is None:
return
need_translate = self._need_translate(message.message, room, client)
if need_translate:
translation = services.translate.get_translation_from_cache(message.message)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
msg_id = str(message.message_id)
room.send_cmd_data(api.chat.Command.ADD_SUPER_CHAT, {
'id': msg_id,
'avatarUrl': avatar_url,
'timestamp': message.start_time,
'authorName': message.uname,
'price': message.rmb,
'content': message.message,
'translation': translation
})
if need_translate:
asyncio.create_task(self._translate_and_response(
message.message, room.room_key, msg_id, services.translate.Priority.HIGH
))
def _on_open_live_super_chat_delete(self, client: OpenLiveClient, message: dm_open_models.SuperChatDeleteMessage):
room = client_room_manager.get_room(client.room_key)
if room is None:
return
room.send_cmd_data(api.chat.Command.DEL_SUPER_CHAT, {
'ids': list(map(str, message.message_ids))
})

Loading…
Cancel
Save