2019-05-22 01:11:23 +08:00
|
|
|
# -*- coding: utf-8 -*-
|
2019-05-22 14:10:27 +08:00
|
|
|
import asyncio
|
2019-05-22 01:11:23 +08:00
|
|
|
import enum
|
|
|
|
import json
|
2019-05-22 14:10:27 +08:00
|
|
|
import logging
|
2020-02-05 17:28:10 +08:00
|
|
|
import random
|
2019-05-22 19:38:36 +08:00
|
|
|
import time
|
2020-02-06 17:39:56 +08:00
|
|
|
import uuid
|
2022-02-19 14:58:29 +08:00
|
|
|
from typing import *
|
2019-05-22 01:11:23 +08:00
|
|
|
|
2019-05-22 15:42:45 +08:00
|
|
|
import aiohttp
|
2019-05-22 01:11:23 +08:00
|
|
|
import tornado.websocket
|
|
|
|
|
2020-09-13 21:24:20 +08:00
|
|
|
import api.base
|
2021-12-19 18:46:32 +08:00
|
|
|
import blivedm.blivedm.client as blivedm_client
|
2020-02-06 17:39:56 +08:00
|
|
|
import config
|
2022-02-15 00:18:46 +08:00
|
|
|
import services.avatar
|
|
|
|
import services.chat
|
|
|
|
import services.translate
|
|
|
|
import utils.request
|
2019-05-22 01:11:23 +08:00
|
|
|
|
2019-05-22 14:10:27 +08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2019-05-22 01:11:23 +08:00
|
|
|
|
|
|
|
class Command(enum.IntEnum):
|
2019-09-23 22:22:27 +08:00
|
|
|
HEARTBEAT = 0
|
|
|
|
JOIN_ROOM = 1
|
|
|
|
ADD_TEXT = 2
|
|
|
|
ADD_GIFT = 3
|
|
|
|
ADD_MEMBER = 4
|
|
|
|
ADD_SUPER_CHAT = 5
|
|
|
|
DEL_SUPER_CHAT = 6
|
2020-02-06 17:39:56 +08:00
|
|
|
UPDATE_TRANSLATION = 7
|
2019-05-22 01:11:23 +08:00
|
|
|
|
|
|
|
|
2021-12-26 23:34:45 +08:00
|
|
|
class ContentType(enum.IntEnum):
|
|
|
|
TEXT = 0
|
|
|
|
EMOTICON = 1
|
|
|
|
|
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
def make_message_body(cmd, data):
|
|
|
|
return json.dumps(
|
|
|
|
{
|
|
|
|
'cmd': cmd,
|
|
|
|
'data': data
|
|
|
|
}
|
|
|
|
).encode('utf-8')
|
|
|
|
|
|
|
|
|
2022-02-23 23:15:21 +08:00
|
|
|
def make_text_message_data(
|
|
|
|
avatar_url: str = services.avatar.DEFAULT_AVATAR_URL,
|
|
|
|
timestamp: int = None,
|
|
|
|
author_name: str = '',
|
|
|
|
author_type: int = 0,
|
|
|
|
content: str = '',
|
|
|
|
privilege_type: int = 0,
|
|
|
|
is_gift_danmaku: bool = False,
|
|
|
|
author_level: int = 1,
|
|
|
|
is_newbie: bool = False,
|
|
|
|
is_mobile_verified: bool = True,
|
|
|
|
medal_level: int = 0,
|
|
|
|
id_: str = None,
|
|
|
|
translation: str = '',
|
|
|
|
content_type: int = ContentType.TEXT,
|
|
|
|
content_type_params: list = None,
|
2023-08-27 00:23:52 +08:00
|
|
|
text_emoticons: Iterable[Tuple[str, str]] = None
|
2022-02-23 23:15:21 +08:00
|
|
|
):
|
|
|
|
# 为了节省带宽用list而不是dict
|
2020-08-16 12:13:53 +08:00
|
|
|
return [
|
|
|
|
# 0: avatarUrl
|
|
|
|
avatar_url,
|
|
|
|
# 1: timestamp
|
2022-02-23 23:15:21 +08:00
|
|
|
timestamp if timestamp is not None else int(time.time()),
|
2020-08-16 12:13:53 +08:00
|
|
|
# 2: authorName
|
|
|
|
author_name,
|
|
|
|
# 3: authorType
|
|
|
|
author_type,
|
|
|
|
# 4: content
|
|
|
|
content,
|
|
|
|
# 5: privilegeType
|
|
|
|
privilege_type,
|
|
|
|
# 6: isGiftDanmaku
|
|
|
|
1 if is_gift_danmaku else 0,
|
|
|
|
# 7: authorLevel
|
|
|
|
author_level,
|
|
|
|
# 8: isNewbie
|
|
|
|
1 if is_newbie else 0,
|
|
|
|
# 9: isMobileVerified
|
|
|
|
1 if is_mobile_verified else 0,
|
|
|
|
# 10: medalLevel
|
|
|
|
medal_level,
|
|
|
|
# 11: id
|
2022-02-23 23:15:21 +08:00
|
|
|
id_ if id_ is not None else uuid.uuid4().hex,
|
2020-08-16 12:13:53 +08:00
|
|
|
# 12: translation
|
2021-12-26 23:34:45 +08:00
|
|
|
translation,
|
|
|
|
# 13: contentType
|
|
|
|
content_type,
|
|
|
|
# 14: contentTypeParams
|
|
|
|
content_type_params if content_type_params is not None else [],
|
2023-08-27 00:23:52 +08:00
|
|
|
# 15: textEmoticons
|
|
|
|
text_emoticons if text_emoticons is not None else [],
|
2021-12-26 23:34:45 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def make_emoticon_params(url):
|
|
|
|
return [
|
|
|
|
# 0: url
|
|
|
|
url,
|
2020-08-16 12:13:53 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
def make_translation_message_data(msg_id, translation):
|
2020-09-12 11:33:11 +08:00
|
|
|
return [
|
|
|
|
# 0: id
|
|
|
|
msg_id,
|
|
|
|
# 1: translation
|
2021-12-26 23:34:45 +08:00
|
|
|
translation,
|
2020-09-12 11:33:11 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-02-15 00:18:46 +08:00
|
|
|
class ChatHandler(tornado.websocket.WebSocketHandler): # noqa
|
2021-02-01 23:49:31 +08:00
|
|
|
HEARTBEAT_INTERVAL = 10
|
|
|
|
RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5
|
|
|
|
|
2019-05-22 01:11:23 +08:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-02-01 23:49:31 +08:00
|
|
|
self._heartbeat_timer_handle = None
|
|
|
|
self._receive_timeout_timer_handle = None
|
|
|
|
|
2019-05-22 01:11:23 +08:00
|
|
|
self.room_id = None
|
2020-02-06 17:39:56 +08:00
|
|
|
self.auto_translate = False
|
2019-05-22 01:11:23 +08:00
|
|
|
|
2019-05-22 14:10:27 +08:00
|
|
|
def open(self):
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.info('client=%s connected', self.request.remote_ip)
|
2023-07-29 12:48:57 +08:00
|
|
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
2021-02-01 23:49:31 +08:00
|
|
|
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
|
|
|
)
|
|
|
|
self._refresh_receive_timeout_timer()
|
2019-05-22 14:10:27 +08:00
|
|
|
|
2021-02-01 23:49:31 +08:00
|
|
|
def _on_send_heartbeat(self):
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.HEARTBEAT, {})
|
2023-07-29 12:48:57 +08:00
|
|
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
2021-02-01 23:49:31 +08:00
|
|
|
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
|
|
|
)
|
|
|
|
|
|
|
|
def _refresh_receive_timeout_timer(self):
|
|
|
|
if self._receive_timeout_timer_handle is not None:
|
|
|
|
self._receive_timeout_timer_handle.cancel()
|
2023-07-29 12:48:57 +08:00
|
|
|
self._receive_timeout_timer_handle = asyncio.get_running_loop().call_later(
|
2021-02-01 23:49:31 +08:00
|
|
|
self.RECEIVE_TIMEOUT, self._on_receive_timeout
|
|
|
|
)
|
|
|
|
|
|
|
|
def _on_receive_timeout(self):
|
2023-07-06 23:26:44 +08:00
|
|
|
logger.info('client=%s timed out', self.request.remote_ip)
|
2021-02-01 23:49:31 +08:00
|
|
|
self._receive_timeout_timer_handle = None
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
def on_close(self):
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.info('client=%s disconnected, room=%s', self.request.remote_ip, str(self.room_id))
|
2021-02-01 23:49:31 +08:00
|
|
|
if self.has_joined_room:
|
2022-02-19 14:58:29 +08:00
|
|
|
services.chat.client_room_manager.del_client(self.room_id, self)
|
2021-02-01 23:49:31 +08:00
|
|
|
if self._heartbeat_timer_handle is not None:
|
|
|
|
self._heartbeat_timer_handle.cancel()
|
|
|
|
self._heartbeat_timer_handle = None
|
|
|
|
if self._receive_timeout_timer_handle is not None:
|
|
|
|
self._receive_timeout_timer_handle.cancel()
|
|
|
|
self._receive_timeout_timer_handle = None
|
2020-02-05 17:28:10 +08:00
|
|
|
|
|
|
|
def on_message(self, message):
|
|
|
|
try:
|
|
|
|
body = json.loads(message)
|
|
|
|
cmd = body['cmd']
|
2022-02-19 14:58:29 +08:00
|
|
|
|
2020-02-05 17:28:10 +08:00
|
|
|
if cmd == Command.HEARTBEAT:
|
2023-08-26 16:42:01 +08:00
|
|
|
# 超时没有加入房间也断开
|
|
|
|
if self.has_joined_room:
|
|
|
|
self._refresh_receive_timeout_timer()
|
2022-02-19 14:58:29 +08:00
|
|
|
|
2020-02-05 17:28:10 +08:00
|
|
|
elif cmd == Command.JOIN_ROOM:
|
|
|
|
if self.has_joined_room:
|
|
|
|
return
|
2021-02-01 23:49:31 +08:00
|
|
|
self._refresh_receive_timeout_timer()
|
|
|
|
|
2020-02-05 17:28:10 +08:00
|
|
|
self.room_id = int(body['data']['roomId'])
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.info('client=%s joining room %d', self.request.remote_ip, self.room_id)
|
2020-02-06 17:39:56 +08:00
|
|
|
try:
|
|
|
|
cfg = body['data']['config']
|
2022-02-19 14:58:29 +08:00
|
|
|
self.auto_translate = bool(cfg['autoTranslate'])
|
2020-02-06 17:39:56 +08:00
|
|
|
except KeyError:
|
|
|
|
pass
|
2020-02-05 17:28:10 +08:00
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
services.chat.client_room_manager.add_client(self.room_id, self)
|
2023-07-29 12:48:57 +08:00
|
|
|
asyncio.create_task(self._on_joined_room())
|
2022-02-19 14:58:29 +08:00
|
|
|
|
2020-02-05 17:28:10 +08:00
|
|
|
else:
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.warning('client=%s unknown cmd=%d, body=%s', self.request.remote_ip, cmd, body)
|
|
|
|
|
2022-02-15 00:18:46 +08:00
|
|
|
except Exception: # noqa
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.exception('client=%s on_message error, message=%s', self.request.remote_ip, message)
|
2019-05-22 01:11:23 +08:00
|
|
|
|
2019-05-26 17:14:59 +08:00
|
|
|
# 跨域测试用
|
|
|
|
def check_origin(self, origin):
|
|
|
|
if self.application.settings['debug']:
|
|
|
|
return True
|
|
|
|
return super().check_origin(origin)
|
2019-10-06 16:20:48 +08:00
|
|
|
|
2020-08-16 12:13:53 +08:00
|
|
|
@property
|
|
|
|
def has_joined_room(self):
|
|
|
|
return self.room_id is not None
|
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
def send_cmd_data(self, cmd, data):
|
|
|
|
self.send_body_no_raise(make_message_body(cmd, data))
|
|
|
|
|
|
|
|
def send_body_no_raise(self, body: Union[bytes, str, Dict[str, Any]]):
|
2020-08-16 12:13:53 +08:00
|
|
|
try:
|
|
|
|
self.write_message(body)
|
|
|
|
except tornado.websocket.WebSocketClosedError:
|
2021-02-01 23:49:31 +08:00
|
|
|
self.close()
|
2020-08-16 12:13:53 +08:00
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
async def _on_joined_room(self):
|
2022-02-27 22:05:37 +08:00
|
|
|
if self.settings['debug']:
|
2022-02-19 14:58:29 +08:00
|
|
|
await self._send_test_message()
|
2020-08-16 12:13:53 +08:00
|
|
|
|
|
|
|
# 不允许自动翻译的提示
|
|
|
|
if self.auto_translate:
|
|
|
|
cfg = config.get_config()
|
|
|
|
if cfg.allow_translate_rooms and self.room_id not in cfg.allow_translate_rooms:
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.ADD_TEXT, make_text_message_data(
|
2021-12-26 23:34:45 +08:00
|
|
|
author_name='blivechat',
|
|
|
|
author_type=2,
|
|
|
|
content='Translation is not allowed in this room. Please download to use translation',
|
|
|
|
author_level=60,
|
2020-08-16 12:13:53 +08:00
|
|
|
))
|
|
|
|
|
2019-10-06 16:20:48 +08:00
|
|
|
# 测试用
|
2022-02-19 14:58:29 +08:00
|
|
|
async def _send_test_message(self):
|
2019-10-06 16:20:48 +08:00
|
|
|
base_data = {
|
2022-02-15 00:18:46 +08:00
|
|
|
'avatarUrl': await services.avatar.get_avatar_url(300474),
|
2020-05-20 21:20:08 +08:00
|
|
|
'timestamp': int(time.time()),
|
2019-10-06 16:20:48 +08:00
|
|
|
'authorName': 'xfgryujk',
|
|
|
|
}
|
2022-02-19 14:58:29 +08:00
|
|
|
text_data = make_text_message_data(
|
2021-12-26 23:34:45 +08:00
|
|
|
avatar_url=base_data['avatarUrl'],
|
|
|
|
timestamp=base_data['timestamp'],
|
|
|
|
author_name=base_data['authorName'],
|
|
|
|
content='我能吞下玻璃而不伤身体',
|
2022-02-23 23:15:21 +08:00
|
|
|
author_level=60,
|
2020-08-16 12:13:53 +08:00
|
|
|
)
|
2020-02-06 17:39:56 +08:00
|
|
|
member_data = {
|
|
|
|
**base_data,
|
2020-07-19 21:33:26 +08:00
|
|
|
'id': uuid.uuid4().hex,
|
|
|
|
'privilegeType': 3
|
2020-02-06 17:39:56 +08:00
|
|
|
}
|
2019-10-06 16:20:48 +08:00
|
|
|
gift_data = {
|
|
|
|
**base_data,
|
2020-02-06 17:39:56 +08:00
|
|
|
'id': uuid.uuid4().hex,
|
2020-03-01 14:14:19 +08:00
|
|
|
'totalCoin': 450000,
|
|
|
|
'giftName': '摩天大楼',
|
|
|
|
'num': 1
|
2019-10-06 16:20:48 +08:00
|
|
|
}
|
|
|
|
sc_data = {
|
|
|
|
**base_data,
|
2020-02-06 17:39:56 +08:00
|
|
|
'id': str(random.randint(1, 65535)),
|
2020-02-05 17:28:10 +08:00
|
|
|
'price': 30,
|
2019-10-06 16:20:48 +08:00
|
|
|
'content': 'The quick brown fox jumps over the lazy dog',
|
2020-02-06 17:39:56 +08:00
|
|
|
'translation': ''
|
2019-10-06 16:20:48 +08:00
|
|
|
}
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.ADD_TEXT, text_data)
|
2023-08-27 00:23:52 +08:00
|
|
|
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)
|
2020-02-05 17:28:10 +08:00
|
|
|
text_data[2] = '主播'
|
|
|
|
text_data[3] = 3
|
|
|
|
text_data[4] = "I can eat glass, it doesn't hurt me."
|
2020-02-06 17:39:56 +08:00
|
|
|
text_data[11] = uuid.uuid4().hex
|
2023-08-27 00:23:52 +08:00
|
|
|
text_data[15] = []
|
2022-02-19 14:58:29 +08:00
|
|
|
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)
|
2020-02-06 17:39:56 +08:00
|
|
|
sc_data['id'] = str(random.randint(1, 65535))
|
2019-10-06 16:20:48 +08:00
|
|
|
sc_data['price'] = 100
|
|
|
|
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.ADD_SUPER_CHAT, sc_data)
|
2022-03-16 22:20:41 +08:00
|
|
|
# self.send_cmd_data(Command.DEL_SUPER_CHAT, {'ids': [sc_data['id']]})
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.ADD_GIFT, gift_data)
|
2020-02-06 17:39:56 +08:00
|
|
|
gift_data['id'] = uuid.uuid4().hex
|
2019-10-06 16:20:48 +08:00
|
|
|
gift_data['totalCoin'] = 1245000
|
2020-03-01 14:14:19 +08:00
|
|
|
gift_data['giftName'] = '小电视飞船'
|
2022-02-19 14:58:29 +08:00
|
|
|
self.send_cmd_data(Command.ADD_GIFT, gift_data)
|
2020-09-13 21:24:20 +08:00
|
|
|
|
|
|
|
|
2022-02-15 00:18:46 +08:00
|
|
|
class RoomInfoHandler(api.base.ApiHandler): # noqa
|
2020-09-13 21:24:20 +08:00
|
|
|
async def get(self):
|
|
|
|
room_id = int(self.get_query_argument('roomId'))
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.info('client=%s getting room info, room=%d', self.request.remote_ip, room_id)
|
2020-09-13 21:24:20 +08:00
|
|
|
room_id, owner_uid = await self._get_room_info(room_id)
|
|
|
|
host_server_list = await self._get_server_host_list(room_id)
|
|
|
|
if owner_uid == 0:
|
|
|
|
# 缓存3分钟
|
|
|
|
self.set_header('Cache-Control', 'private, max-age=180')
|
|
|
|
else:
|
|
|
|
# 缓存1天
|
|
|
|
self.set_header('Cache-Control', 'private, max-age=86400')
|
|
|
|
self.write({
|
|
|
|
'roomId': room_id,
|
|
|
|
'ownerUid': owner_uid,
|
|
|
|
'hostServerList': host_server_list
|
|
|
|
})
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
async def _get_room_info(room_id):
|
|
|
|
try:
|
2022-02-15 00:18:46 +08:00
|
|
|
async with utils.request.http_session.get(
|
2022-06-18 21:31:25 +08:00
|
|
|
blivedm_client.ROOM_INIT_URL,
|
|
|
|
headers={
|
2022-09-28 00:11:29 +08:00
|
|
|
**utils.request.BILIBILI_COMMON_HEADERS,
|
|
|
|
'Origin': 'https://live.bilibili.com',
|
|
|
|
'Referer': f'https://live.bilibili.com/{room_id}'
|
2022-06-18 21:31:25 +08:00
|
|
|
},
|
|
|
|
params={
|
|
|
|
'room_id': room_id
|
|
|
|
}
|
2022-02-15 00:18:46 +08:00
|
|
|
) as res:
|
2020-09-13 21:24:20 +08:00
|
|
|
if res.status != 200:
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.warning('room=%d _get_room_info failed: %d %s', room_id,
|
2020-09-13 21:24:20 +08:00
|
|
|
res.status, res.reason)
|
|
|
|
return room_id, 0
|
|
|
|
data = await res.json()
|
2021-03-28 18:30:31 +08:00
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.exception('room=%d _get_room_info failed', room_id)
|
2020-09-13 21:24:20 +08:00
|
|
|
return room_id, 0
|
|
|
|
|
|
|
|
if data['code'] != 0:
|
2022-02-19 14:58:29 +08:00
|
|
|
logger.warning('room=%d _get_room_info failed: %s', room_id, data['message'])
|
2020-09-13 21:24:20 +08:00
|
|
|
return room_id, 0
|
|
|
|
|
|
|
|
room_info = data['data']['room_info']
|
|
|
|
return room_info['room_id'], room_info['uid']
|
|
|
|
|
2022-02-19 14:58:29 +08:00
|
|
|
@staticmethod
|
|
|
|
async def _get_server_host_list(_room_id):
|
2020-09-13 21:24:20 +08:00
|
|
|
# 连接其他host必须要key
|
2022-02-19 14:58:29 +08:00
|
|
|
return blivedm_client.DEFAULT_DANMAKU_SERVER_LIST
|
2020-09-13 21:24:20 +08:00
|
|
|
|
|
|
|
|
2022-02-15 00:18:46 +08:00
|
|
|
class AvatarHandler(api.base.ApiHandler): # noqa
|
2020-09-13 21:24:20 +08:00
|
|
|
async def get(self):
|
|
|
|
uid = int(self.get_query_argument('uid'))
|
2022-02-15 00:18:46 +08:00
|
|
|
avatar_url = await services.avatar.get_avatar_url_or_none(uid)
|
2020-09-13 21:24:20 +08:00
|
|
|
if avatar_url is None:
|
2022-02-15 00:18:46 +08:00
|
|
|
avatar_url = services.avatar.DEFAULT_AVATAR_URL
|
2020-09-13 21:24:20 +08:00
|
|
|
# 缓存3分钟
|
|
|
|
self.set_header('Cache-Control', 'private, max-age=180')
|
|
|
|
else:
|
|
|
|
# 缓存1天
|
|
|
|
self.set_header('Cache-Control', 'private, max-age=86400')
|
|
|
|
self.write({
|
|
|
|
'avatarUrl': avatar_url
|
|
|
|
})
|