blivechat/views/chat.py

259 lines
8.5 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-06-20 20:03:07 +08:00
import datetime
2019-05-22 01:11:23 +08:00
import enum
import json
2019-05-22 14:10:27 +08:00
import logging
2019-05-22 19:38:36 +08:00
import time
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
2019-05-22 14:10:27 +08:00
logger = logging.getLogger(__name__)
2019-05-22 01:11:23 +08:00
class Command(enum.IntEnum):
JOIN_ROOM = 0
ADD_TEXT = 1
ADD_GIFT = 2
2019-06-20 20:03:07 +08:00
ADD_MEMBER = 3
2019-05-22 01:11:23 +08:00
2019-07-01 18:32:54 +08:00
DEFAULT_AVATAR_URL = 'https://static.hdslb.com/images/member/noface.gif'
2019-05-26 17:14:59 +08:00
_http_session = aiohttp.ClientSession()
2019-05-22 15:42:45 +08:00
_avatar_url_cache: Dict[int, str] = {}
2019-07-01 18:32:54 +08:00
_last_fetch_avatar_time = datetime.datetime.now()
2019-06-20 20:03:07 +08:00
_last_avatar_failed_time = None
2019-07-01 18:32:54 +08:00
_uids_to_fetch_avatar = asyncio.Queue(15)
2019-05-22 15:42:45 +08:00
async def get_avatar_url(user_id):
if user_id in _avatar_url_cache:
return _avatar_url_cache[user_id]
2019-06-20 20:03:07 +08:00
2019-07-01 18:32:54 +08:00
global _last_avatar_failed_time, _last_fetch_avatar_time
cur_time = datetime.datetime.now()
# 防止获取头像频率太高被ban
if (cur_time - _last_fetch_avatar_time).total_seconds() < 0.2:
# 由_fetch_avatar_loop过一段时间再获取并缓存
try:
_uids_to_fetch_avatar.put_nowait(user_id)
except asyncio.QueueFull:
pass
return DEFAULT_AVATAR_URL
2019-06-20 20:03:07 +08:00
if _last_avatar_failed_time is not None:
2019-07-01 18:32:54 +08:00
if (cur_time - _last_avatar_failed_time).total_seconds() < 5 * 60 + 3:
# 5分钟以内被ban解封大约要15分钟
return DEFAULT_AVATAR_URL
2019-06-20 20:03:07 +08:00
else:
_last_avatar_failed_time = None
2019-07-01 18:32:54 +08:00
_last_fetch_avatar_time = cur_time
try:
async with _http_session.get('https://api.bilibili.com/x/space/acc/info',
params={'mid': user_id}) as r:
if r.status != 200: # 可能会被B站ban
logger.warning('获取头像失败status=%d %s uid=%d', r.status, r.reason, user_id)
_last_avatar_failed_time = cur_time
return DEFAULT_AVATAR_URL
data = await r.json()
except aiohttp.ClientConnectionError:
2019-07-01 18:32:54 +08:00
return DEFAULT_AVATAR_URL
2019-05-22 15:42:45 +08:00
url = data['data']['face']
if not url.endswith('noface.gif'):
2019-06-13 23:50:50 +08:00
url += '@48w_48h'
2019-05-22 15:42:45 +08:00
_avatar_url_cache[user_id] = url
2019-06-20 20:03:07 +08:00
if len(_avatar_url_cache) > 50000:
2019-05-22 15:42:45 +08:00
for _, key in zip(range(100), _avatar_url_cache):
del _avatar_url_cache[key]
return url
2019-07-01 18:32:54 +08:00
async def _fetch_avatar_loop():
while True:
try:
user_id = await _uids_to_fetch_avatar.get()
if user_id in _avatar_url_cache:
continue
# 延时长一些使实时弹幕有机会获取头像
await asyncio.sleep(0.4 - (datetime.datetime.now() - _last_fetch_avatar_time).total_seconds())
asyncio.ensure_future(get_avatar_url(user_id))
except:
pass
asyncio.ensure_future(_fetch_avatar_loop())
2019-05-22 01:11:23 +08:00
class Room(blivedm.BLiveClient):
def __init__(self, room_id):
2019-05-26 17:14:59 +08:00
super().__init__(room_id, session=_http_session)
2019-05-22 01:11:23 +08:00
self.clients: List['ChatHandler'] = []
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:
client.write_message(body)
2019-06-06 22:13:45 +08:00
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
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
2019-05-22 19:38:36 +08:00
self.send_message(Command.ADD_TEXT, {
2019-06-06 22:13:45 +08:00
'avatarUrl': await get_avatar_url(danmaku.uid),
'timestamp': danmaku.timestamp,
'authorName': danmaku.uname,
2019-05-22 20:54:42 +08:00
'authorType': author_type,
2019-06-12 19:20:17 +08:00
'content': danmaku.msg,
'privilegeType': danmaku.privilege_type,
'isGiftDanmaku': bool(danmaku.msg_type),
'authorLevel': danmaku.user_level,
'isNewbie': danmaku.urank < 10000,
2019-06-21 17:38:22 +08:00
'isMobileVerified': bool(danmaku.mobile_verify),
'medalLevel': 0 if danmaku.room_id != self.room_id else danmaku.medal_level
2019-05-22 19:38:36 +08:00
})
2019-05-22 01:11:23 +08:00
2019-06-06 22:13:45 +08:00
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
if gift.coin_type != 'gold': # 丢人
2019-05-22 19:38:36 +08:00
return
self.send_message(Command.ADD_GIFT, {
2019-07-01 18:32:54 +08:00
'avatarUrl': gift.face,
2019-06-12 19:20:17 +08:00
'timestamp': gift.timestamp,
2019-06-06 22:13:45 +08:00
'authorName': gift.uname,
'giftName': gift.gift_name,
'giftNum': gift.num,
'totalCoin': gift.total_coin
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-06-20 20:03:07 +08:00
self.send_message(Command.ADD_MEMBER, {
2019-06-06 22:13:45 +08:00
'avatarUrl': await get_avatar_url(message.uid),
2019-06-12 19:20:17 +08:00
'timestamp': message.start_time,
2019-06-06 22:13:45 +08:00
'authorName': message.username
2019-05-22 19:38:36 +08:00
})
2019-05-22 01:11:23 +08:00
class RoomManager:
def __init__(self):
self._rooms: Dict[int, Room] = {}
2019-06-06 22:13:45 +08:00
def add_client(self, room_id, client: 'ChatHandler'):
2019-05-22 01:11:23 +08:00
if room_id in self._rooms:
room = self._rooms[room_id]
else:
2019-05-22 14:10:27 +08:00
logger.info('创建房间%d', room_id)
2019-05-22 01:11:23 +08:00
room = Room(room_id)
self._rooms[room_id] = room
room.start()
room.clients.append(client)
2019-05-26 17:14:59 +08:00
if client.application.settings['debug']:
self.__send_test_message(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'):
if room_id not in self._rooms:
return
room = self._rooms[room_id]
room.clients.remove(client)
if not room.clients:
2019-05-22 14:10:27 +08:00
logger.info('移除房间%d', room_id)
2019-06-06 22:13:45 +08:00
room.stop_and_close()
2019-05-22 01:11:23 +08:00
del self._rooms[room_id]
2019-05-22 19:38:36 +08:00
# 测试用
@staticmethod
def __send_test_message(room):
2019-06-14 09:09:46 +08:00
base_data = {
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
'timestamp': time.time(),
2019-05-22 19:38:36 +08:00
'authorName': 'xfgryujk',
2019-06-14 09:09:46 +08:00
}
text_data = {
**base_data,
2019-05-22 20:54:42 +08:00
'authorType': 0,
2019-06-12 19:20:17 +08:00
'content': '我能吞下玻璃而不伤身体',
'privilegeType': 0,
'isGiftDanmaku': False,
'authorLevel': 20,
'isNewbie': False,
'isMobileVerified': True
2019-06-14 09:09:46 +08:00
}
vip_data = base_data
gift_data = {
**base_data,
2019-05-22 19:38:36 +08:00
'giftName': '礼花',
'giftNum': 1,
'totalCoin': 28000
2019-06-14 09:09:46 +08:00
}
room.send_message(Command.ADD_TEXT, text_data)
text_data['authorName'] = '主播'
text_data['authorType'] = 3
text_data['content'] = "I can eat glass, it doesn't hurt me."
room.send_message(Command.ADD_TEXT, text_data)
2019-06-20 20:03:07 +08:00
room.send_message(Command.ADD_MEMBER, vip_data)
2019-06-14 09:09:46 +08:00
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '节奏风暴'
gift_data['totalCoin'] = 100000
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '摩天大楼'
gift_data['totalCoin'] = 450000
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '小电视飞船'
gift_data['totalCoin'] = 1245000
room.send_message(Command.ADD_GIFT, gift_data)
2019-05-22 19:38:36 +08:00
2019-05-22 01:11:23 +08:00
room_manager = RoomManager()
# noinspection PyAbstractClass
class ChatHandler(tornado.websocket.WebSocketHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.room_id = None
2019-05-22 14:10:27 +08:00
def open(self):
logger.info('Websocket连接 %s', self.request.remote_ip)
2019-05-22 01:11:23 +08:00
def on_message(self, message):
if self.room_id is not None:
return
body = json.loads(message)
if body['cmd'] == Command.JOIN_ROOM:
2019-05-22 14:10:27 +08:00
self.room_id = int(body['data']['roomId'])
logger.info('客户端%s加入房间%d', self.request.remote_ip, self.room_id)
room_manager.add_client(self.room_id, self)
else:
logger.warning('未知的命令: %s data: %s', body['cmd'], body['data'])
2019-05-22 01:11:23 +08:00
def on_close(self):
2019-06-12 19:20:17 +08:00
logger.info('Websocket断开 %s room: %s', self.request.remote_ip, self.room_id)
2019-05-22 01:11:23 +08:00
if self.room_id is not None:
room_manager.del_client(self.room_id, self)
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)