mirror of
https://github.com/xfgryujk/blivechat.git
synced 2024-12-26 21:00:15 +08:00
完成插件和blivechat连接
This commit is contained in:
parent
f886506c39
commit
b0b38bf0f0
@ -82,6 +82,25 @@ class PluginWsHandler(_PluginHandlerBase, tornado.websocket.WebSocketHandler):
|
|||||||
def on_close(self):
|
def on_close(self):
|
||||||
logger.info('plugin=%s disconnected', self.plugin.id)
|
logger.info('plugin=%s disconnected', self.plugin.id)
|
||||||
self.plugin.on_client_close(self)
|
self.plugin.on_client_close(self)
|
||||||
|
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
|
||||||
|
|
||||||
|
def on_message(self, message):
|
||||||
|
try:
|
||||||
|
body = json.loads(message)
|
||||||
|
cmd = int(body['cmd'])
|
||||||
|
|
||||||
|
if cmd == models.Command.HEARTBEAT:
|
||||||
|
self._refresh_receive_timeout_timer()
|
||||||
|
else:
|
||||||
|
logger.warning('plugin=%s unknown cmd=%d, body=%s', self.plugin.id, cmd, body)
|
||||||
|
|
||||||
|
except Exception: # noqa
|
||||||
|
logger.exception('plugin=%s on_message error, message=%s', self.plugin.id, message)
|
||||||
|
|
||||||
def send_cmd_data(self, cmd, data, extra: Optional[dict] = None):
|
def send_cmd_data(self, cmd, data, extra: Optional[dict] = None):
|
||||||
self.send_body_no_raise(make_message_body(cmd, data, extra))
|
self.send_body_no_raise(make_message_body(cmd, data, extra))
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = '0.0.1'
|
__version__ = '0.0.1'
|
||||||
|
|
||||||
|
from .handlers import *
|
||||||
|
from .client import *
|
||||||
|
from .exc import *
|
||||||
|
from .api import *
|
||||||
|
156
blcsdk/api.py
Normal file
156
blcsdk/api.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
__version__,
|
||||||
|
client as cli,
|
||||||
|
exc,
|
||||||
|
handlers,
|
||||||
|
models,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'init',
|
||||||
|
'shut_down',
|
||||||
|
'set_msg_handler',
|
||||||
|
'is_sdk_version_compatible',
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger('blcsdk')
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
_base_url = ''
|
||||||
|
"""HTTP API的URL"""
|
||||||
|
_token = ''
|
||||||
|
"""插件认证用的token"""
|
||||||
|
|
||||||
|
# 初始化消息
|
||||||
|
_init_future: Optional[asyncio.Future] = None
|
||||||
|
"""初始化消息的future"""
|
||||||
|
_init_msg: Optional[dict] = None
|
||||||
|
"""初始化消息,包含版本等信息"""
|
||||||
|
|
||||||
|
# 其他和blivechat通信用的对象
|
||||||
|
_http_session: Optional[aiohttp.ClientSession] = None
|
||||||
|
"""插件请求专用的HTTP客户端"""
|
||||||
|
_plugin_client: Optional[cli.BlcPluginClient] = None
|
||||||
|
"""插件客户端"""
|
||||||
|
_msg_handler: Optional[handlers.HandlerInterface] = None
|
||||||
|
"""插件消息处理器"""
|
||||||
|
_msg_handler_wrapper: Optional['_HandlerWrapper'] = None
|
||||||
|
"""用于SDK处理一些消息,然后转发给插件消息处理器"""
|
||||||
|
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
"""
|
||||||
|
初始化SDK
|
||||||
|
|
||||||
|
在调用除了set_msg_handler以外的其他接口之前必须先调用这个。如果抛出任何异常,应该退出当前程序
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
global _base_url, _token, _init_future, _init_msg, _http_session, _plugin_client, _msg_handler_wrapper
|
||||||
|
if _init_future is not None:
|
||||||
|
raise exc.InitError('Cannot call init() again')
|
||||||
|
_init_future = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
# 初始化环境变量信息
|
||||||
|
blc_port = int(os.environ['BLC_PORT'])
|
||||||
|
_base_url = f'http://localhost:{blc_port}'
|
||||||
|
blc_ws_url = f'ws://localhost:{blc_port}/api/plugin/websocket'
|
||||||
|
_token = os.environ['BLC_TOKEN']
|
||||||
|
|
||||||
|
_http_session = aiohttp.ClientSession(
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10),
|
||||||
|
headers={'Authorization': f'Bearer {_token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 连接blivechat
|
||||||
|
_msg_handler_wrapper = _HandlerWrapper()
|
||||||
|
_plugin_client = cli.BlcPluginClient(blc_ws_url, session=_http_session)
|
||||||
|
_plugin_client.set_handler(_msg_handler_wrapper)
|
||||||
|
_plugin_client.start()
|
||||||
|
|
||||||
|
# 等待初始化消息
|
||||||
|
_init_msg = await _init_future
|
||||||
|
logger.debug('SDK initialized, _init_msg=%s', _init_msg)
|
||||||
|
except exc.InitError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise exc.InitError(f'Error in init(): {e}') from e
|
||||||
|
|
||||||
|
|
||||||
|
async def shut_down():
|
||||||
|
"""退出程序之前建议调用"""
|
||||||
|
if _plugin_client is not None:
|
||||||
|
await _plugin_client.stop_and_close()
|
||||||
|
if _http_session is not None:
|
||||||
|
await _http_session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def set_msg_handler(handler: Optional[handlers.HandlerInterface]):
|
||||||
|
"""
|
||||||
|
设置消息处理器
|
||||||
|
|
||||||
|
注意消息处理器和网络协程运行在同一个协程,如果处理消息耗时太长会阻塞接收消息。如果是CPU密集型的任务,建议将消息推到线程池处理;
|
||||||
|
如果是IO密集型的任务,应该使用async函数,并且在handler里使用create_task创建新的协程
|
||||||
|
|
||||||
|
:param handler: 消息处理器
|
||||||
|
"""
|
||||||
|
global _msg_handler
|
||||||
|
_msg_handler = handler
|
||||||
|
|
||||||
|
|
||||||
|
class _HandlerWrapper(handlers.HandlerInterface):
|
||||||
|
"""用于SDK处理一些消息,然后转发给插件消息处理器"""
|
||||||
|
|
||||||
|
def handle(self, client: cli.BlcPluginClient, command: dict):
|
||||||
|
if not _init_future.done():
|
||||||
|
if command['cmd'] == models.Command.BLC_INIT:
|
||||||
|
_init_future.set_result(command['data'])
|
||||||
|
|
||||||
|
if _msg_handler is not None:
|
||||||
|
_msg_handler.handle(client, command)
|
||||||
|
|
||||||
|
def on_client_stopped(self, client: cli.BlcPluginClient, exception: Optional[Exception]):
|
||||||
|
if not _init_future.done():
|
||||||
|
if exception is not None:
|
||||||
|
_init_future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
_init_future.set_exception(exc.InitError('Connection closed before init msg'))
|
||||||
|
|
||||||
|
if _msg_handler is not None:
|
||||||
|
_msg_handler.on_client_stopped(client, exception)
|
||||||
|
|
||||||
|
|
||||||
|
def is_sdk_version_compatible():
|
||||||
|
"""
|
||||||
|
检查SDK版本和blivechat的版本是否兼容
|
||||||
|
|
||||||
|
如果不兼容,建议退出当前程序。如果继续执行有可能不能正常工作
|
||||||
|
"""
|
||||||
|
if _init_msg is None:
|
||||||
|
raise exc.SdkError('Please call init() first')
|
||||||
|
|
||||||
|
major_ver_pattern = r'(\d+)\.\d+\.\d+'
|
||||||
|
remote_ver = _init_msg['sdkVersion']
|
||||||
|
|
||||||
|
m = re.match(major_ver_pattern, remote_ver)
|
||||||
|
if m is None:
|
||||||
|
raise exc.SdkError(f"Bad remote version format: {remote_ver}")
|
||||||
|
remote_major_ver = m[1]
|
||||||
|
|
||||||
|
m = re.match(major_ver_pattern, __version__)
|
||||||
|
if m is None:
|
||||||
|
raise exc.SdkError(f"Bad local version format: {__version__}")
|
||||||
|
local_major_ver = m[1]
|
||||||
|
|
||||||
|
res = remote_major_ver == local_major_ver
|
||||||
|
if not res:
|
||||||
|
logger.warning('SDK version is not compatible, remote=%s, local=%s', remote_ver, __version__)
|
||||||
|
return res
|
222
blcsdk/client.py
222
blcsdk/client.py
@ -1 +1,223 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from . import handlers
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'BlcPluginClient',
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger('blcsdk')
|
||||||
|
|
||||||
|
|
||||||
|
class BlcPluginClient:
|
||||||
|
"""
|
||||||
|
blivechat插件服务的客户端
|
||||||
|
|
||||||
|
:param ws_url: blivechat消息转发服务WebSocket地址
|
||||||
|
:param session: 连接池
|
||||||
|
:param heartbeat_interval: 发送心跳包的间隔时间(秒)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ws_url: str,
|
||||||
|
*,
|
||||||
|
session: Optional[aiohttp.ClientSession] = None,
|
||||||
|
heartbeat_interval: float = 10,
|
||||||
|
):
|
||||||
|
self._ws_url = ws_url
|
||||||
|
|
||||||
|
if session is None:
|
||||||
|
self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||||
|
self._own_session = True
|
||||||
|
else:
|
||||||
|
self._session = session
|
||||||
|
self._own_session = False
|
||||||
|
assert self._session.loop is asyncio.get_event_loop() # noqa
|
||||||
|
|
||||||
|
self._heartbeat_interval = heartbeat_interval
|
||||||
|
|
||||||
|
self._handler: Optional[handlers.HandlerInterface] = None
|
||||||
|
"""消息处理器"""
|
||||||
|
|
||||||
|
# 在运行时初始化的字段
|
||||||
|
self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
|
||||||
|
"""WebSocket连接"""
|
||||||
|
self._network_future: Optional[asyncio.Future] = None
|
||||||
|
"""网络协程的future"""
|
||||||
|
self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
|
||||||
|
"""发心跳包定时器的handle"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""本客户端正在运行,注意调用stop后还没完全停止也算正在运行"""
|
||||||
|
return self._network_future is not None
|
||||||
|
|
||||||
|
def set_handler(self, handler: Optional['handlers.HandlerInterface']):
|
||||||
|
"""
|
||||||
|
设置消息处理器
|
||||||
|
|
||||||
|
注意消息处理器和网络协程运行在同一个协程,如果处理消息耗时太长会阻塞接收消息。如果是CPU密集型的任务,建议将消息推到线程池处理;
|
||||||
|
如果是IO密集型的任务,应该使用async函数,并且在handler里使用create_task创建新的协程
|
||||||
|
|
||||||
|
:param handler: 消息处理器
|
||||||
|
"""
|
||||||
|
self._handler = handler
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动本客户端"""
|
||||||
|
if self.is_running:
|
||||||
|
logger.warning('Plugin client is running, cannot start() again')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._network_future = asyncio.create_task(self._network_coroutine_wrapper())
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止本客户端"""
|
||||||
|
if not self.is_running:
|
||||||
|
logger.warning('Plugin client is stopped, cannot stop() again')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._network_future.cancel()
|
||||||
|
|
||||||
|
async def stop_and_close(self):
|
||||||
|
"""便利函数,停止本客户端并释放本客户端的资源,调用后本客户端将不可用"""
|
||||||
|
if self.is_running:
|
||||||
|
self.stop()
|
||||||
|
await self.join()
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def join(self):
|
||||||
|
"""等待本客户端停止"""
|
||||||
|
if not self.is_running:
|
||||||
|
logger.warning('Plugin client is stopped, cannot join()')
|
||||||
|
return
|
||||||
|
|
||||||
|
await asyncio.shield(self._network_future)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""释放本客户端的资源,调用后本客户端将不可用"""
|
||||||
|
if self.is_running:
|
||||||
|
logger.warning('Plugin is calling close(), but client is running')
|
||||||
|
|
||||||
|
# 如果session是自己创建的则关闭session
|
||||||
|
if self._own_session:
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def send_cmd_data(self, cmd: models.Command, data: dict):
|
||||||
|
"""
|
||||||
|
发送消息给服务器
|
||||||
|
|
||||||
|
:param cmd: 消息类型,见Command
|
||||||
|
:param data: 消息体JSON数据
|
||||||
|
"""
|
||||||
|
if self._websocket is None or self._websocket.closed:
|
||||||
|
raise ConnectionResetError('websocket is closed')
|
||||||
|
|
||||||
|
body = {'cmd': cmd, 'data': data}
|
||||||
|
await self._websocket.send_json(body)
|
||||||
|
|
||||||
|
async def _network_coroutine_wrapper(self):
|
||||||
|
"""负责处理网络协程的异常,网络协程具体逻辑在_network_coroutine里"""
|
||||||
|
exc = None
|
||||||
|
try:
|
||||||
|
await self._network_coroutine()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# 正常停止
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('_network_coroutine() finished with exception:')
|
||||||
|
exc = e
|
||||||
|
finally:
|
||||||
|
logger.debug('_network_coroutine() finished')
|
||||||
|
self._network_future = None
|
||||||
|
|
||||||
|
if self._handler is not None:
|
||||||
|
self._handler.on_client_stopped(self, exc)
|
||||||
|
|
||||||
|
async def _network_coroutine(self):
|
||||||
|
"""网络协程,负责连接服务器、接收消息、解包"""
|
||||||
|
try:
|
||||||
|
# 连接
|
||||||
|
async with self._session.ws_connect(
|
||||||
|
self._ws_url,
|
||||||
|
receive_timeout=self._heartbeat_interval + 5,
|
||||||
|
) as websocket:
|
||||||
|
self._websocket = websocket
|
||||||
|
await self._on_ws_connect()
|
||||||
|
|
||||||
|
# 处理消息
|
||||||
|
message: aiohttp.WSMessage
|
||||||
|
async for message in websocket:
|
||||||
|
self._on_ws_message(message)
|
||||||
|
finally:
|
||||||
|
self._websocket = None
|
||||||
|
await self._on_ws_close()
|
||||||
|
# 插件消息都是本地通信的,这里不可能是因为网络问题而掉线,所以不尝试重连
|
||||||
|
|
||||||
|
async def _on_ws_connect(self):
|
||||||
|
"""WebSocket连接成功"""
|
||||||
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
||||||
|
self._heartbeat_interval, self._on_send_heartbeat
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _on_ws_close(self):
|
||||||
|
"""WebSocket连接断开"""
|
||||||
|
if self._heartbeat_timer_handle is not None:
|
||||||
|
self._heartbeat_timer_handle.cancel()
|
||||||
|
self._heartbeat_timer_handle = None
|
||||||
|
|
||||||
|
def _on_send_heartbeat(self):
|
||||||
|
"""定时发送心跳包的回调"""
|
||||||
|
if self._websocket is None or self._websocket.closed:
|
||||||
|
self._heartbeat_timer_handle = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
||||||
|
self._heartbeat_interval, self._on_send_heartbeat
|
||||||
|
)
|
||||||
|
asyncio.create_task(self._send_heartbeat())
|
||||||
|
|
||||||
|
async def _send_heartbeat(self):
|
||||||
|
"""发送心跳包"""
|
||||||
|
try:
|
||||||
|
await self.send_cmd_data(models.Command.HEARTBEAT, {})
|
||||||
|
except (ConnectionResetError, aiohttp.ClientConnectionError) as e:
|
||||||
|
logger.warning('Plugin client _send_heartbeat() failed: %r', e)
|
||||||
|
except Exception: # noqa
|
||||||
|
logger.exception('Plugin client _send_heartbeat() failed:')
|
||||||
|
|
||||||
|
def _on_ws_message(self, message: aiohttp.WSMessage):
|
||||||
|
"""
|
||||||
|
收到WebSocket消息
|
||||||
|
|
||||||
|
:param message: WebSocket消息
|
||||||
|
"""
|
||||||
|
if message.type != aiohttp.WSMsgType.TEXT:
|
||||||
|
logger.warning('Unknown websocket message type=%s, data=%s', message.type, message.data)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = message.json()
|
||||||
|
self._handle_command(body)
|
||||||
|
except Exception:
|
||||||
|
logger.error('body=%s', message.data)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _handle_command(self, command: dict):
|
||||||
|
"""
|
||||||
|
处理业务消息
|
||||||
|
|
||||||
|
:param command: 业务消息
|
||||||
|
"""
|
||||||
|
if self._handler is not None:
|
||||||
|
try:
|
||||||
|
self._handler.handle(self, command)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('Plugin client _handle_command() failed, command=%s', command, exc_info=e)
|
||||||
|
13
blcsdk/exc.py
Normal file
13
blcsdk/exc.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
__all__ = (
|
||||||
|
'SdkError',
|
||||||
|
'InitError',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SdkError(Exception):
|
||||||
|
"""SDK错误的基类"""
|
||||||
|
|
||||||
|
|
||||||
|
class InitError(SdkError):
|
||||||
|
"""初始化失败"""
|
94
blcsdk/handlers.py
Normal file
94
blcsdk/handlers.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
from . import client as cli
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'HandlerInterface',
|
||||||
|
'BaseHandler',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HandlerInterface:
|
||||||
|
"""blivechat插件消息处理器接口"""
|
||||||
|
|
||||||
|
def handle(self, client: cli.BlcPluginClient, command: dict):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def on_client_stopped(self, client: cli.BlcPluginClient, exception: Optional[Exception]):
|
||||||
|
"""
|
||||||
|
当客户端停止时调用
|
||||||
|
|
||||||
|
这种情况说明blivechat已经退出了,或者插件被禁用了,因此重连基本会失败。这里唯一建议的操作是退出当前程序
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _make_msg_callback(method_name, message_cls):
|
||||||
|
def callback(self: 'BaseHandler', client: cli.BlcPluginClient, command: dict):
|
||||||
|
method = getattr(self, method_name)
|
||||||
|
msg = message_cls.from_command(command['data'])
|
||||||
|
extra = _get_extra(command)
|
||||||
|
return method(client, msg, extra)
|
||||||
|
return callback
|
||||||
|
|
||||||
|
|
||||||
|
def _get_extra(command: dict):
|
||||||
|
extra = command.get('extra', {})
|
||||||
|
room_key_dict = extra.get('roomKey', None)
|
||||||
|
if room_key_dict is not None:
|
||||||
|
extra['roomKey'] = models.RoomKey(
|
||||||
|
type=models.RoomKeyType(room_key_dict['type']),
|
||||||
|
value=room_key_dict['value'],
|
||||||
|
)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandler(HandlerInterface):
|
||||||
|
"""一个简单的消息处理器实现,带消息分发和消息类型转换。继承并重写_on_xxx方法即可实现自己的处理器"""
|
||||||
|
|
||||||
|
_CMD_CALLBACK_DICT: Dict[
|
||||||
|
int,
|
||||||
|
Optional[Callable[
|
||||||
|
['BaseHandler', cli.BlcPluginClient, dict],
|
||||||
|
Any
|
||||||
|
]]
|
||||||
|
] = {
|
||||||
|
# 收到弹幕
|
||||||
|
models.Command.ADD_TEXT: _make_msg_callback('_on_add_text', models.AddTextMsg),
|
||||||
|
# 有人送礼
|
||||||
|
models.Command.ADD_GIFT: _make_msg_callback('_on_add_gift', models.AddGiftMsg),
|
||||||
|
# 有人上舰
|
||||||
|
models.Command.ADD_MEMBER: _make_msg_callback('_on_add_member', models.AddMemberMsg),
|
||||||
|
# 醒目留言
|
||||||
|
models.Command.ADD_SUPER_CHAT: _make_msg_callback('_on_add_super_chat', models.AddSuperChatMsg),
|
||||||
|
# 删除醒目留言
|
||||||
|
models.Command.DEL_SUPER_CHAT: _make_msg_callback('_on_del_super_chat', models.DelSuperChatMsg),
|
||||||
|
# 更新翻译
|
||||||
|
models.Command.UPDATE_TRANSLATION: _make_msg_callback('_on_update_translation', models.UpdateTranslationMsg),
|
||||||
|
}
|
||||||
|
"""cmd -> 处理回调"""
|
||||||
|
|
||||||
|
def handle(self, client: cli.BlcPluginClient, command: dict):
|
||||||
|
cmd = command['cmd']
|
||||||
|
callback = self._CMD_CALLBACK_DICT.get(cmd, None)
|
||||||
|
if callback is not None:
|
||||||
|
callback(self, client, command)
|
||||||
|
|
||||||
|
def _on_add_text(self, client: cli.BlcPluginClient, message: models.AddTextMsg):
|
||||||
|
"""收到弹幕"""
|
||||||
|
|
||||||
|
def _on_add_gift(self, client: cli.BlcPluginClient, message: models.AddGiftMsg):
|
||||||
|
"""有人送礼"""
|
||||||
|
|
||||||
|
def _on_add_member(self, client: cli.BlcPluginClient, message: models.AddMemberMsg):
|
||||||
|
"""有人上舰"""
|
||||||
|
|
||||||
|
def _on_add_super_chat(self, client: cli.BlcPluginClient, message: models.AddSuperChatMsg):
|
||||||
|
"""醒目留言"""
|
||||||
|
|
||||||
|
def _on_del_super_chat(self, client: cli.BlcPluginClient, message: models.DelSuperChatMsg):
|
||||||
|
"""删除醒目留言"""
|
||||||
|
|
||||||
|
def _on_update_translation(self, client: cli.BlcPluginClient, message: models.UpdateTranslationMsg):
|
||||||
|
"""更新翻译"""
|
253
blcsdk/models.py
253
blcsdk/models.py
@ -1,6 +1,259 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'RoomKeyType',
|
||||||
|
'RoomKey',
|
||||||
|
'Command',
|
||||||
|
'AuthorType',
|
||||||
|
'GuardLevel',
|
||||||
|
'ContentType',
|
||||||
|
'AddTextMsg',
|
||||||
|
'AddGiftMsg',
|
||||||
|
'AddMemberMsg',
|
||||||
|
'AddSuperChatMsg',
|
||||||
|
'DelSuperChatMsg',
|
||||||
|
'UpdateTranslationMsg',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomKeyType(enum.IntEnum):
|
||||||
|
ROOM_ID = 1
|
||||||
|
AUTH_CODE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class RoomKey(NamedTuple):
|
||||||
|
"""用来标识一个房间"""
|
||||||
|
type: RoomKeyType
|
||||||
|
value: Union[int, str]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
res = str(self.value)
|
||||||
|
if self.type == RoomKeyType.AUTH_CODE:
|
||||||
|
# 身份码要脱敏
|
||||||
|
res = '***' + res[-3:]
|
||||||
|
return res
|
||||||
|
__repr__ = __str__
|
||||||
|
|
||||||
|
|
||||||
class Command(enum.IntEnum):
|
class Command(enum.IntEnum):
|
||||||
HEARTBEAT = 0
|
HEARTBEAT = 0
|
||||||
|
BLC_INIT = 1
|
||||||
|
|
||||||
|
ADD_TEXT = 20
|
||||||
|
ADD_GIFT = 21
|
||||||
|
ADD_MEMBER = 22
|
||||||
|
ADD_SUPER_CHAT = 23
|
||||||
|
DEL_SUPER_CHAT = 24
|
||||||
|
UPDATE_TRANSLATION = 25
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorType(enum.IntEnum):
|
||||||
|
NORMAL = 0
|
||||||
|
GUARD = 1
|
||||||
|
"""舰队"""
|
||||||
|
ADMIN = 2
|
||||||
|
"""房管"""
|
||||||
|
ROOM_OWNER = 3
|
||||||
|
"""主播"""
|
||||||
|
|
||||||
|
|
||||||
|
class GuardLevel(enum.IntEnum):
|
||||||
|
"""舰队等级"""
|
||||||
|
|
||||||
|
NONE = 0
|
||||||
|
LV3 = 1
|
||||||
|
"""总督"""
|
||||||
|
LV2 = 2
|
||||||
|
"""提督"""
|
||||||
|
LV1 = 3
|
||||||
|
"""舰长"""
|
||||||
|
|
||||||
|
|
||||||
|
class ContentType(enum.IntEnum):
|
||||||
|
TEXT = 0
|
||||||
|
EMOTICON = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AddTextMsg:
|
||||||
|
"""弹幕消息"""
|
||||||
|
|
||||||
|
avatar_url: str = ''
|
||||||
|
"""用户头像URL"""
|
||||||
|
timestamp: int = 0
|
||||||
|
"""时间戳(秒)"""
|
||||||
|
author_name: str = ''
|
||||||
|
"""用户名"""
|
||||||
|
author_type: int = AuthorType.NORMAL.value
|
||||||
|
"""用户类型,见AuthorType"""
|
||||||
|
content: str = ''
|
||||||
|
"""弹幕内容"""
|
||||||
|
privilege_type: int = GuardLevel.NONE.value
|
||||||
|
"""舰队等级,见GuardLevel"""
|
||||||
|
is_gift_danmaku: bool = False
|
||||||
|
"""是否礼物弹幕"""
|
||||||
|
author_level: int = 1
|
||||||
|
"""用户等级"""
|
||||||
|
is_newbie: bool = False
|
||||||
|
"""是否正式会员"""
|
||||||
|
is_mobile_verified: bool = True
|
||||||
|
"""是否绑定手机"""
|
||||||
|
medal_level: int = 0
|
||||||
|
"""勋章等级,如果没戴当前房间勋章则为0"""
|
||||||
|
id: str = ''
|
||||||
|
"""消息ID"""
|
||||||
|
translation: str = ''
|
||||||
|
"""弹幕内容翻译"""
|
||||||
|
content_type: int = ContentType.TEXT.value
|
||||||
|
"""内容类型,见ContentType"""
|
||||||
|
content_type_params: Union[dict, list] = dataclasses.field(default_factory=dict)
|
||||||
|
"""跟内容类型相关的参数"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: list):
|
||||||
|
content_type = data[13]
|
||||||
|
content_type_params = data[14]
|
||||||
|
if content_type == ContentType.EMOTICON:
|
||||||
|
content_type_params = {'url': content_type_params[0]}
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
avatar_url=data[0],
|
||||||
|
timestamp=data[1],
|
||||||
|
author_name=data[2],
|
||||||
|
author_type=data[3],
|
||||||
|
content=data[4],
|
||||||
|
privilege_type=data[5],
|
||||||
|
is_gift_danmaku=bool(data[6]),
|
||||||
|
author_level=data[7],
|
||||||
|
is_newbie=bool(data[8]),
|
||||||
|
is_mobile_verified=bool(data[9]),
|
||||||
|
medal_level=data[10],
|
||||||
|
id=data[11],
|
||||||
|
translation=data[12],
|
||||||
|
content_type=content_type,
|
||||||
|
content_type_params=content_type_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AddGiftMsg:
|
||||||
|
"""礼物消息"""
|
||||||
|
|
||||||
|
id: str = ''
|
||||||
|
"""消息ID"""
|
||||||
|
avatar_url: str = ''
|
||||||
|
"""用户头像URL"""
|
||||||
|
timestamp: int = 0
|
||||||
|
"""时间戳(秒)"""
|
||||||
|
author_name: str = ''
|
||||||
|
"""用户名"""
|
||||||
|
total_coin: int = 0
|
||||||
|
"""总价瓜子数,1000金瓜子 = 1元"""
|
||||||
|
gift_name: str = ''
|
||||||
|
"""礼物名"""
|
||||||
|
num: int = 0
|
||||||
|
"""数量"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: dict):
|
||||||
|
return cls(
|
||||||
|
id=data['id'],
|
||||||
|
avatar_url=data['avatarUrl'],
|
||||||
|
timestamp=data['timestamp'],
|
||||||
|
author_name=data['authorName'],
|
||||||
|
total_coin=data['totalCoin'],
|
||||||
|
gift_name=data['giftName'],
|
||||||
|
num=data['num'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AddMemberMsg:
|
||||||
|
"""上舰消息"""
|
||||||
|
|
||||||
|
id: str = ''
|
||||||
|
"""消息ID"""
|
||||||
|
avatar_url: str = ''
|
||||||
|
"""用户头像URL"""
|
||||||
|
timestamp: int = 0
|
||||||
|
"""时间戳(秒)"""
|
||||||
|
author_name: str = ''
|
||||||
|
"""用户名"""
|
||||||
|
privilege_type: int = GuardLevel.NONE.value
|
||||||
|
"""舰队等级,见GuardLevel"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: dict):
|
||||||
|
return cls(
|
||||||
|
id=data['id'],
|
||||||
|
avatar_url=data['avatarUrl'],
|
||||||
|
timestamp=data['timestamp'],
|
||||||
|
author_name=data['authorName'],
|
||||||
|
privilege_type=data['privilegeType'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AddSuperChatMsg:
|
||||||
|
"""醒目留言消息"""
|
||||||
|
|
||||||
|
id: str = ''
|
||||||
|
"""消息ID"""
|
||||||
|
avatar_url: str = ''
|
||||||
|
"""用户头像URL"""
|
||||||
|
timestamp: int = 0
|
||||||
|
"""时间戳(秒)"""
|
||||||
|
author_name: str = ''
|
||||||
|
"""用户名"""
|
||||||
|
price: int = 0
|
||||||
|
"""价格(元)"""
|
||||||
|
content: str = ''
|
||||||
|
"""内容"""
|
||||||
|
translation: str = ''
|
||||||
|
"""内容翻译"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: dict):
|
||||||
|
return cls(
|
||||||
|
id=data['id'],
|
||||||
|
avatar_url=data['avatarUrl'],
|
||||||
|
timestamp=data['timestamp'],
|
||||||
|
author_name=data['authorName'],
|
||||||
|
price=data['price'],
|
||||||
|
content=data['content'],
|
||||||
|
translation=data['translation'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class DelSuperChatMsg:
|
||||||
|
"""删除醒目留言消息"""
|
||||||
|
|
||||||
|
ids: List[str] = dataclasses.field(default_factory=list)
|
||||||
|
"""醒目留言ID数组"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: dict):
|
||||||
|
return cls(
|
||||||
|
ids=data['ids'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class UpdateTranslationMsg:
|
||||||
|
"""更新内容翻译消息"""
|
||||||
|
|
||||||
|
id: str = ''
|
||||||
|
"""消息ID"""
|
||||||
|
translation: str = ''
|
||||||
|
"""内容翻译"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_command(cls, data: list):
|
||||||
|
return cls(
|
||||||
|
id=data[0],
|
||||||
|
translation=data[1],
|
||||||
|
)
|
||||||
|
1
main.py
Normal file → Executable file
1
main.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
5
plugins/msg-logging/config.py
Normal file
5
plugins/msg-logging/config.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
|
||||||
|
BASE_PATH = os.path.realpath(os.getcwd())
|
||||||
|
LOG_PATH = os.path.join(BASE_PATH, 'log')
|
42
plugins/msg-logging/listener.py
Normal file
42
plugins/msg-logging/listener.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import __main__
|
||||||
|
import logging
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
import blcsdk
|
||||||
|
import blcsdk.models as sdk_models
|
||||||
|
from blcsdk import client as cli
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_msg_handler: Optional['MsgHandler'] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
global _msg_handler
|
||||||
|
_msg_handler = MsgHandler()
|
||||||
|
blcsdk.set_msg_handler(_msg_handler)
|
||||||
|
|
||||||
|
|
||||||
|
class MsgHandler(blcsdk.BaseHandler):
|
||||||
|
def on_client_stopped(self, client: cli.BlcPluginClient, exception: Optional[Exception]):
|
||||||
|
logger.info('blivechat disconnected')
|
||||||
|
__main__.start_shut_down()
|
||||||
|
|
||||||
|
def _on_add_text(self, client: cli.BlcPluginClient, message: sdk_models.AddTextMsg):
|
||||||
|
"""收到弹幕"""
|
||||||
|
|
||||||
|
def _on_add_gift(self, client: cli.BlcPluginClient, message: sdk_models.AddGiftMsg):
|
||||||
|
"""有人送礼"""
|
||||||
|
|
||||||
|
def _on_add_member(self, client: cli.BlcPluginClient, message: sdk_models.AddMemberMsg):
|
||||||
|
"""有人上舰"""
|
||||||
|
|
||||||
|
def _on_add_super_chat(self, client: cli.BlcPluginClient, message: sdk_models.AddSuperChatMsg):
|
||||||
|
"""醒目留言"""
|
||||||
|
|
||||||
|
def _on_del_super_chat(self, client: cli.BlcPluginClient, message: sdk_models.DelSuperChatMsg):
|
||||||
|
"""删除醒目留言"""
|
||||||
|
|
||||||
|
def _on_update_translation(self, client: cli.BlcPluginClient, message: sdk_models.UpdateTranslationMsg):
|
||||||
|
"""更新翻译"""
|
73
plugins/msg-logging/main.py
Normal file → Executable file
73
plugins/msg-logging/main.py
Normal file → Executable file
@ -1,14 +1,85 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
from typing import *
|
||||||
|
|
||||||
import blcsdk
|
import blcsdk
|
||||||
|
import config
|
||||||
|
import listener
|
||||||
|
|
||||||
|
logger = logging.getLogger('msg-logging')
|
||||||
|
|
||||||
|
shut_down_event: Optional[asyncio.Event] = None
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
print('hello world!', blcsdk.__version__)
|
try:
|
||||||
|
await init()
|
||||||
|
await run()
|
||||||
|
finally:
|
||||||
|
await shut_down()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
init_signal_handlers()
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
|
||||||
|
await blcsdk.init()
|
||||||
|
if not blcsdk.is_sdk_version_compatible():
|
||||||
|
raise RuntimeError('SDK version is not compatible')
|
||||||
|
|
||||||
|
await listener.init()
|
||||||
|
|
||||||
|
|
||||||
|
def init_signal_handlers():
|
||||||
|
global shut_down_event
|
||||||
|
shut_down_event = asyncio.Event()
|
||||||
|
|
||||||
|
signums = (signal.SIGINT, signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for signum in signums:
|
||||||
|
loop.add_signal_handler(signum, start_shut_down)
|
||||||
|
except NotImplementedError:
|
||||||
|
# 不太安全,但Windows只能用这个
|
||||||
|
for signum in signums:
|
||||||
|
signal.signal(signum, start_shut_down)
|
||||||
|
|
||||||
|
|
||||||
|
def start_shut_down(*_args):
|
||||||
|
shut_down_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging():
|
||||||
|
filename = os.path.join(config.LOG_PATH, 'msg-logging.log')
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
|
filename, encoding='utf-8', when='midnight', backupCount=7, delay=True
|
||||||
|
)
|
||||||
|
logging.basicConfig(
|
||||||
|
format='{asctime} {levelname} [{name}]: {message}',
|
||||||
|
style='{',
|
||||||
|
level=logging.INFO,
|
||||||
|
# level=logging.DEBUG,
|
||||||
|
handlers=[stream_handler, file_handler],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
logger.info('Running event loop')
|
||||||
|
await shut_down_event.wait()
|
||||||
|
logger.info('Start to shut down')
|
||||||
|
|
||||||
|
|
||||||
|
async def shut_down():
|
||||||
|
await blcsdk.shut_down()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(asyncio.run(main()))
|
sys.exit(asyncio.run(main()))
|
||||||
|
@ -10,7 +10,10 @@ import subprocess
|
|||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
import api.plugin
|
import api.plugin
|
||||||
|
import blcsdk
|
||||||
|
import blcsdk.models as sdk_models
|
||||||
import config
|
import config
|
||||||
|
import update
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -233,12 +236,20 @@ class Plugin:
|
|||||||
if self._client is client:
|
if self._client is client:
|
||||||
return
|
return
|
||||||
if self._client is not None:
|
if self._client is not None:
|
||||||
|
logger.info('plugin=%s closing old client', self._id)
|
||||||
self._client.close()
|
self._client.close()
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
def on_client_connect(self, client: 'api.plugin.PluginWsHandler'):
|
def on_client_connect(self, client: 'api.plugin.PluginWsHandler'):
|
||||||
self._set_client(client)
|
self._set_client(client)
|
||||||
|
|
||||||
|
# 发送初始化消息
|
||||||
|
self.send_cmd_data(sdk_models.Command.BLC_INIT, {
|
||||||
|
'blcVersion': update.VERSION,
|
||||||
|
'sdkVersion': blcsdk.__version__,
|
||||||
|
'pluginId': self._id,
|
||||||
|
})
|
||||||
|
|
||||||
def on_client_close(self, client: 'api.plugin.PluginWsHandler'):
|
def on_client_close(self, client: 'api.plugin.PluginWsHandler'):
|
||||||
if self._client is client:
|
if self._client is client:
|
||||||
self._set_client(None)
|
self._set_client(None)
|
||||||
|
Loading…
Reference in New Issue
Block a user