blivechat/api/chat.py

515 lines
17 KiB
Python
Raw Normal View History

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
2019-05-22 01:11:23 +08:00
from typing import *
2019-05-22 15:42:45 +08:00
import aiohttp
2019-05-22 01:11:23 +08:00
import tornado.websocket
import blivedm.blivedm as blivedm
2020-02-06 17:39:56 +08:00
import config
2020-02-03 16:18:21 +08:00
import models.avatar
2020-02-06 17:39:56 +08:00
import models.translate
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
2019-05-26 17:14:59 +08:00
_http_session = aiohttp.ClientSession()
2020-02-03 16:18:21 +08:00
room_manager: Optional['RoomManager'] = None
def init():
global room_manager
room_manager = RoomManager()
2019-07-01 18:32:54 +08:00
2019-05-22 01:11:23 +08:00
class Room(blivedm.BLiveClient):
2020-01-12 23:02:15 +08:00
# 重新定义parse_XXX是为了减少对字段名的依赖防止B站改字段名
def __parse_danmaku(self, command):
info = command['info']
if info[3]:
room_id = info[3][3]
medal_level = info[3][0]
else:
room_id = medal_level = 0
return self._on_receive_danmaku(blivedm.DanmakuMessage(
None, None, None, info[0][4], None, None, info[0][9], None,
info[1],
info[2][0], info[2][1], info[2][2], None, None, info[2][5], info[2][6], None,
medal_level, None, None, room_id, None, None,
info[4][0], None, None,
None, None,
info[7]
))
def __parse_gift(self, command):
data = command['data']
return self._on_receive_gift(blivedm.GiftMessage(
data['giftName'], data['num'], data['uname'], data['face'], None,
2020-02-03 16:18:21 +08:00
data['uid'], data['timestamp'], None, None,
2020-01-12 23:02:15 +08:00
None, None, None, data['coin_type'], data['total_coin']
))
def __parse_buy_guard(self, command):
data = command['data']
return self._on_buy_guard(blivedm.GuardBuyMessage(
2020-07-19 21:33:26 +08:00
data['uid'], data['username'], data['guard_level'], None, None,
2020-01-12 23:02:15 +08:00
None, None, data['start_time'], None
))
def __parse_super_chat(self, command):
data = command['data']
return self._on_super_chat(blivedm.SuperChatMessage(
data['price'], data['message'], None, data['start_time'],
None, None, data['id'], None,
2020-02-03 16:18:21 +08:00
None, data['uid'], data['user_info']['uname'],
2020-01-12 23:02:15 +08:00
data['user_info']['face'], None,
None, None,
None, None, None,
None
))
2020-01-12 22:46:02 +08:00
_COMMAND_HANDLERS = {
**blivedm.BLiveClient._COMMAND_HANDLERS,
2020-01-12 23:02:15 +08:00
'DANMU_MSG': __parse_danmaku,
'SEND_GIFT': __parse_gift,
'GUARD_BUY': __parse_buy_guard,
'SUPER_CHAT_MESSAGE': __parse_super_chat
2020-01-12 22:46:02 +08:00
}
2019-05-22 01:11:23 +08:00
def __init__(self, room_id):
2019-09-17 23:08:03 +08:00
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
2019-05-22 01:11:23 +08:00
self.clients: List['ChatHandler'] = []
2020-02-06 17:39:56 +08:00
self.auto_translate_count = 0
2019-06-06 22:13:45 +08:00
def stop_and_close(self):
2019-06-21 17:38:22 +08:00
if self.is_running:
future = self.stop()
future.add_done_callback(lambda _future: asyncio.ensure_future(self.close()))
else:
asyncio.ensure_future(self.close())
2019-05-22 20:54:42 +08:00
2019-05-22 01:11:23 +08:00
def send_message(self, cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
for client in self.clients:
2019-12-08 17:55:58 +08:00
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
room_manager.del_client(self.room_id, client)
2019-05-22 01:11:23 +08:00
2020-02-06 17:39:56 +08:00
def send_message_if(self, can_send_func: Callable[['ChatHandler'], bool], cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
for client in filter(can_send_func, self.clients):
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
room_manager.del_client(self.room_id, client)
2020-02-06 17:39:56 +08:00
2019-06-06 22:13:45 +08:00
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
2019-07-04 19:12:24 +08:00
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
async def __on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
2019-06-06 22:13:45 +08:00
if danmaku.uid == self.room_owner_uid:
2019-05-22 20:54:42 +08:00
author_type = 3 # 主播
2019-06-06 22:13:45 +08:00
elif danmaku.admin:
2019-05-22 20:54:42 +08:00
author_type = 2 # 房管
2019-06-06 22:13:45 +08:00
elif danmaku.privilege_type != 0: # 1总督2提督3舰长
2019-05-22 20:54:42 +08:00
author_type = 1 # 舰队
else:
author_type = 0
2020-02-06 17:39:56 +08:00
need_translate = self._need_translate(danmaku.msg)
if need_translate:
translation = models.translate.get_translation_from_cache(danmaku.msg)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
id_ = uuid.uuid4().hex
2020-02-05 17:28:10 +08:00
# 为了节省带宽用list而不是dict
self.send_message(Command.ADD_TEXT, make_text_message(
2020-02-05 17:28:10 +08:00
await models.avatar.get_avatar_url(danmaku.uid),
2020-05-20 21:20:08 +08:00
int(danmaku.timestamp / 1000),
2020-02-05 17:28:10 +08:00
danmaku.uname,
author_type,
danmaku.msg,
danmaku.privilege_type,
danmaku.msg_type,
2020-02-05 17:28:10 +08:00
danmaku.user_level,
danmaku.urank < 10000,
danmaku.mobile_verify,
2020-02-06 17:39:56 +08:00
0 if danmaku.room_id != self.room_id else danmaku.medal_level,
id_,
translation
))
2019-05-22 01:11:23 +08:00
2020-02-06 17:39:56 +08:00
if need_translate:
await self._translate_and_response(danmaku.msg, id_)
2019-06-06 22:13:45 +08:00
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
avatar_url = models.avatar.process_avatar_url(gift.face)
2020-02-03 16:18:21 +08:00
models.avatar.update_avatar_cache(gift.uid, avatar_url)
2019-06-06 22:13:45 +08:00
if gift.coin_type != 'gold': # 丢人
2019-05-22 19:38:36 +08:00
return
2020-02-06 17:39:56 +08:00
id_ = uuid.uuid4().hex
2019-05-22 19:38:36 +08:00
self.send_message(Command.ADD_GIFT, {
2020-02-06 17:39:56 +08:00
'id': id_,
2020-02-03 16:18:21 +08:00
'avatarUrl': avatar_url,
2019-06-12 19:20:17 +08:00
'timestamp': gift.timestamp,
2019-06-06 22:13:45 +08:00
'authorName': gift.uname,
'totalCoin': gift.total_coin,
'giftName': gift.gift_name,
'num': gift.num
2019-05-22 19:38:36 +08:00
})
2019-06-06 22:13:45 +08:00
async def _on_buy_guard(self, message: blivedm.GuardBuyMessage):
2019-07-04 19:12:24 +08:00
asyncio.ensure_future(self.__on_buy_guard(message))
async def __on_buy_guard(self, message: blivedm.GuardBuyMessage):
2020-02-06 17:39:56 +08:00
id_ = uuid.uuid4().hex
2019-06-20 20:03:07 +08:00
self.send_message(Command.ADD_MEMBER, {
2020-02-06 17:39:56 +08:00
'id': id_,
2020-02-05 17:28:10 +08:00
'avatarUrl': await models.avatar.get_avatar_url(message.uid),
2019-06-12 19:20:17 +08:00
'timestamp': message.start_time,
2020-07-19 21:33:26 +08:00
'authorName': message.username,
'privilegeType': message.guard_level
2019-05-22 19:38:36 +08:00
})
2019-09-23 22:22:27 +08:00
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
avatar_url = models.avatar.process_avatar_url(message.face)
2020-02-03 16:18:21 +08:00
models.avatar.update_avatar_cache(message.uid, avatar_url)
2020-02-06 17:39:56 +08:00
need_translate = self._need_translate(message.message)
if need_translate:
translation = models.translate.get_translation_from_cache(message.message)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
id_ = str(message.id)
2019-09-23 22:22:27 +08:00
self.send_message(Command.ADD_SUPER_CHAT, {
2020-02-06 17:39:56 +08:00
'id': id_,
2020-02-03 16:18:21 +08:00
'avatarUrl': avatar_url,
2019-09-23 22:22:27 +08:00
'timestamp': message.start_time,
'authorName': message.uname,
'price': message.price,
'content': message.message,
2020-02-06 17:39:56 +08:00
'translation': translation
2019-09-23 22:22:27 +08:00
})
2020-02-06 17:39:56 +08:00
if need_translate:
asyncio.ensure_future(self._translate_and_response(message.message, id_))
2019-09-23 22:22:27 +08:00
async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
self.send_message(Command.ADD_SUPER_CHAT, {
2020-02-06 17:39:56 +08:00
'ids': list(map(str, message.ids))
2019-09-23 22:22:27 +08:00
})
2020-02-06 17:39:56 +08:00
def _need_translate(self, text):
cfg = config.get_config()
2020-02-06 17:39:56 +08:00
return (
cfg.enable_translate
and (not cfg.allow_translate_rooms or self.room_id in cfg.allow_translate_rooms)
2020-02-06 17:39:56 +08:00
and self.auto_translate_count > 0
and models.translate.need_translate(text)
)
async def _translate_and_response(self, text, msg_id):
translation = await models.translate.translate(text)
if translation is None:
return
self.send_message_if(
lambda client: client.auto_translate,
2020-09-12 11:33:11 +08:00
Command.UPDATE_TRANSLATION, make_translation_message(
2020-02-06 17:39:56 +08:00
msg_id,
translation
2020-09-12 11:33:11 +08:00
)
2020-02-06 17:39:56 +08:00
)
2019-05-22 01:11:23 +08:00
def make_text_message(avatar_url, timestamp, author_name, author_type, content, privilege_type,
is_gift_danmaku, author_level, is_newbie, is_mobile_verified, medal_level,
id_, translation):
return [
# 0: avatarUrl
avatar_url,
# 1: timestamp
timestamp,
# 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
id_,
# 12: translation
translation
]
2020-09-12 11:33:11 +08:00
def make_translation_message(msg_id, translation):
return [
# 0: id
msg_id,
# 1: translation
translation
]
2019-05-22 01:11:23 +08:00
class RoomManager:
def __init__(self):
self._rooms: Dict[int, Room] = {}
2019-10-06 16:20:48 +08:00
async def add_client(self, room_id, client: 'ChatHandler'):
if room_id not in self._rooms:
if not await self._add_room(room_id):
client.close()
return
room = self._rooms.get(room_id, None)
if room is None:
return
2019-05-22 01:11:23 +08:00
room.clients.append(client)
2019-10-06 16:20:48 +08:00
logger.info('%d clients in room %s', len(room.clients), room_id)
2020-02-06 17:39:56 +08:00
if client.auto_translate:
room.auto_translate_count += 1
2019-05-22 01:11:23 +08:00
await client.on_join_room()
2019-05-22 19:38:36 +08:00
2019-05-22 01:11:23 +08:00
def del_client(self, room_id, client: 'ChatHandler'):
room = self._rooms.get(room_id, None)
if room is None:
2019-05-22 01:11:23 +08:00
return
try:
room.clients.remove(client)
except ValueError:
# _add_room未完成没有执行到room.clients.append
pass
else:
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
room.auto_translate_count = max(0, room.auto_translate_count - 1)
2019-05-22 01:11:23 +08:00
if not room.clients:
2019-10-06 16:20:48 +08:00
self._del_room(room_id)
2019-05-22 01:11:23 +08:00
2019-10-06 16:20:48 +08:00
async def _add_room(self, room_id):
if room_id in self._rooms:
return True
logger.info('Creating room %d', room_id)
self._rooms[room_id] = room = Room(room_id)
2019-10-06 16:20:48 +08:00
if await room.init_room():
room.start()
logger.info('%d rooms', len(self._rooms))
2019-10-06 16:20:48 +08:00
return True
else:
self._del_room(room_id)
return False
def _del_room(self, room_id):
room = self._rooms.get(room_id, None)
if room is None:
2019-10-06 16:20:48 +08:00
return
logger.info('Removing room %d', room_id)
for client in room.clients:
client.close()
room.stop_and_close()
self._rooms.pop(room_id, None)
logger.info('%d rooms', len(self._rooms))
2019-05-22 19:38:36 +08:00
2019-05-22 01:11:23 +08:00
# noinspection PyAbstractClass
class ChatHandler(tornado.websocket.WebSocketHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
2020-02-05 17:28:10 +08:00
self._close_on_timeout_future = 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):
2019-07-18 09:08:03 +08:00
logger.info('Websocket connected %s', self.request.remote_ip)
2020-02-05 17:28:10 +08:00
self._close_on_timeout_future = asyncio.ensure_future(self._close_on_timeout())
2019-05-22 14:10:27 +08:00
2020-02-05 17:28:10 +08:00
async def _close_on_timeout(self):
try:
# 超过一定时间还没加入房间则断开
await asyncio.sleep(10)
logger.warning('Client %s joining room timed out', self.request.remote_ip)
self.close()
except (asyncio.CancelledError, tornado.websocket.WebSocketClosedError):
2019-10-06 16:20:48 +08:00
pass
2020-02-05 17:28:10 +08:00
def on_message(self, message):
try:
body = json.loads(message)
cmd = body['cmd']
if cmd == Command.HEARTBEAT:
2019-10-06 16:20:48 +08:00
return
2020-02-05 17:28:10 +08:00
elif cmd == Command.JOIN_ROOM:
if self.has_joined_room:
return
self.room_id = int(body['data']['roomId'])
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
2020-02-06 17:39:56 +08:00
try:
cfg = body['data']['config']
self.auto_translate = cfg['autoTranslate']
except KeyError:
pass
2020-02-05 17:28:10 +08:00
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
self._close_on_timeout_future.cancel()
self._close_on_timeout_future = None
else:
logger.warning('Unknown cmd, client: %s, cmd: %d, body: %s', self.request.remote_ip, cmd, body)
except:
logger.exception('on_message error, client: %s, message: %s', self.request.remote_ip, message)
2019-05-22 01:11:23 +08:00
def on_close(self):
2019-10-06 16:20:48 +08:00
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, str(self.room_id))
2020-02-05 17:28:10 +08:00
if self.has_joined_room:
2019-05-22 01:11:23 +08:00
room_manager.del_client(self.room_id, self)
2020-02-05 17:28:10 +08:00
if self._close_on_timeout_future is not None:
self._close_on_timeout_future.cancel()
self._close_on_timeout_future = None
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
@property
def has_joined_room(self):
return self.room_id is not None
def send_message(self, cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
try:
self.write_message(body)
except tornado.websocket.WebSocketClosedError:
self.on_close()
async def on_join_room(self):
if self.application.settings['debug']:
await self.send_test_message()
# 不允许自动翻译的提示
if self.auto_translate:
cfg = config.get_config()
if cfg.allow_translate_rooms and self.room_id not in cfg.allow_translate_rooms:
self.send_message(Command.ADD_TEXT, make_text_message(
models.avatar.DEFAULT_AVATAR_URL,
int(time.time()),
'blivechat',
2,
2020-08-22 18:38:52 +08:00
'Translation is not allowed in this room. Please download to use translation',
0,
False,
60,
False,
True,
0,
uuid.uuid4().hex,
''
))
2019-10-06 16:20:48 +08:00
# 测试用
2020-02-05 17:28:10 +08:00
async def send_test_message(self):
2019-10-06 16:20:48 +08:00
base_data = {
2020-02-05 17:28:10 +08:00
'avatarUrl': await models.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',
}
text_data = make_text_message(
2020-02-05 17:28:10 +08:00
base_data['avatarUrl'],
base_data['timestamp'],
base_data['authorName'],
0,
'我能吞下玻璃而不伤身体',
0,
False,
2020-02-05 17:28:10 +08:00
20,
False,
True,
2020-02-05 17:28:10 +08:00
0,
2020-02-06 17:39:56 +08:00
uuid.uuid4().hex,
''
)
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,
'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
}
self.send_message(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
2019-10-06 16:20:48 +08:00
self.send_message(Command.ADD_TEXT, text_data)
self.send_message(Command.ADD_MEMBER, member_data)
self.send_message(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'] = '敏捷的棕色狐狸跳过了懒狗'
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
2020-02-05 17:28:10 +08:00
# self.send_message(Command.DEL_SUPER_CHAT, {'ids': [sc_data['id']]})
2019-10-06 16:20:48 +08:00
self.send_message(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
gift_data['giftName'] = '小电视飞船'
2019-10-06 16:20:48 +08:00
self.send_message(Command.ADD_GIFT, gift_data)