You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
blivechat/api/plugin.py

255 lines
8.6 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
import asyncio
import json
import logging
from typing import *
import tornado.web
import tornado.websocket
import api.base
import api.chat
import blcsdk.models as models
import config
import services.avatar
import services.chat
import services.plugin
logger = logging.getLogger(__name__)
class _AdminHandlerBase(api.base.ApiHandler):
def prepare(self):
cfg = config.get_config()
if not cfg.enable_admin_plugins:
raise tornado.web.HTTPError(403)
logger.info('client=%s requesting admin plugin, cls=%s', self.request.remote_ip, type(self).__name__)
super().prepare()
def _get_plugin(self):
plugin_id = self.json_args.get('pluginId', None)
if not isinstance(plugin_id, str) or plugin_id == '':
raise tornado.web.MissingArgumentError('pluginId')
plugin = services.plugin.get_plugin(plugin_id)
if plugin is None:
raise tornado.web.HTTPError(404, 'no plugin, plugin_id=%s', plugin_id)
return plugin
# 不继承_AdminHandlerBase为了忽略enable_admin_plugins
class PluginsHandler(api.base.ApiHandler):
async def get(self):
plugin_dicts = []
for plugin in services.plugin.iter_plugins():
plugin_cfg = plugin.config
plugin_dicts.append({
'id': plugin.id,
'name': plugin_cfg.name,
'version': plugin_cfg.version,
'author': plugin_cfg.author,
'description': plugin_cfg.description,
'enabled': plugin.enabled,
'isStarted': plugin.is_started,
'isConnected': plugin.is_connected,
})
self.write({'plugins': plugin_dicts})
class EnableHandler(_AdminHandlerBase):
async def post(self):
enabled = bool(self.json_args.get('enabled', False))
plugin = self._get_plugin()
old_enabled = plugin.enabled
is_switch_success = True
msg = ''
try:
plugin.enabled = enabled
except services.plugin.SwitchTooFrequently as e:
is_switch_success = False
msg = str(e)
plugin.enabled = old_enabled
except services.plugin.SwitchPluginError as e:
is_switch_success = False
msg = str(e)
self.write({
'enabled': plugin.enabled,
'isSwitchSuccess': is_switch_success,
'msg': msg
})
class OpenAdminUiHandler(_AdminHandlerBase):
async def post(self):
plugin = self._get_plugin()
plugin.send_cmd_data(models.Command.OPEN_PLUGIN_ADMIN_UI, {})
self.write({})
class _PluginApiHandlerBase(api.base.ApiHandler):
"""给插件用的接口"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.plugin: Optional[services.plugin.Plugin] = None
def prepare(self):
try:
auth = self.request.headers['Authorization']
if not auth.startswith('Bearer '):
raise ValueError(f'Bad authorization: {auth}')
token = auth[7:]
self.plugin = services.plugin.get_plugin_by_token(token)
if self.plugin is None:
raise ValueError(f'Token error: {token}')
except (KeyError, ValueError) as e:
logger.warning('client=%s failed to find plugin: %r', self.request.remote_ip, e)
raise tornado.web.HTTPError(403)
super().prepare()
def make_message_body(cmd, data, extra: Optional[dict] = None):
body = {'cmd': cmd, 'data': data}
if extra:
body['extra'] = extra
return json.dumps(body).encode('utf-8')
class PluginWsHandler(_PluginApiHandlerBase, tornado.websocket.WebSocketHandler):
HEARTBEAT_INTERVAL = 30
RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._heartbeat_timer_handle = None
self._receive_timeout_timer_handle = None
def open(self):
logger.info('plugin=%s connected, client=%s', self.plugin.id, self.request.remote_ip)
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
)
self._refresh_receive_timeout_timer()
self.plugin.on_client_connect(self)
def _on_send_heartbeat(self):
self.send_cmd_data(models.Command.HEARTBEAT, {})
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
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()
self._receive_timeout_timer_handle = asyncio.get_running_loop().call_later(
self.RECEIVE_TIMEOUT, self._on_receive_timeout
)
def _on_receive_timeout(self):
logger.info('plugin=%s timed out', self.plugin.id)
self._receive_timeout_timer_handle = None
self.close()
def on_close(self):
logger.info('plugin=%s disconnected', self.plugin.id)
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'])
data = body['data']
if cmd == models.Command.HEARTBEAT:
self._refresh_receive_timeout_timer()
elif cmd == models.Command.LOG_REQ:
logger.log(int(data['level']), '[%s] %s', self.plugin.id, data['msg'])
elif cmd == models.Command.ADD_TEXT_REQ:
self._on_add_text_req(data)
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 _on_add_text_req(self, data: dict):
room_key_dict = data['roomKey']
if room_key_dict is not None:
room_key = services.chat.RoomKey.from_dict(room_key_dict)
room = services.chat.client_room_manager.get_room(room_key)
if room is not None:
rooms = [room]
else:
rooms = []
else:
rooms = list(services.chat.client_room_manager.iter_rooms())
if not rooms:
return
author_name = str(data['authorName'])
if author_name == '':
author_name = self.plugin.id
uid = int(data['uid'])
avatar_url = str(data['avatarUrl'])
if avatar_url == '':
avatar_url = services.avatar.get_default_avatar_url(uid, author_name)
data_to_send = api.chat.make_text_message_data(
content=str(data['content']),
author_name=author_name,
uid=uid,
avatar_url=avatar_url,
author_type=int(data['authorType']),
privilege_type=int(data['guardLevel']),
medal_level=int(data['medalLevel']),
translation=str(data['translation']),
)
body_for_room = api.chat.make_message_body(api.chat.Command.ADD_TEXT, data_to_send)
for room in rooms:
room.send_body_no_raise(body_for_room)
extra = services.chat.make_plugin_msg_extra_from_client_room(room)
extra['isFromPlugin'] = True
services.plugin.broadcast_cmd_data(models.Command.ADD_TEXT, data_to_send, extra)
def send_cmd_data(self, cmd, data, extra: Optional[dict] = None):
self.send_body_no_raise(make_message_body(cmd, data, extra))
def send_body_no_raise(self, body: Union[bytes, str, Dict[str, Any]]):
try:
self.write_message(body)
except tornado.websocket.WebSocketClosedError:
self.close()
class RoomsHandler(_PluginApiHandlerBase):
async def get(self):
rooms = [
{
'roomId': live_client.room_id,
'roomKey': live_client.room_key.to_dict(),
}
for live_client in services.chat.iter_live_clients()
]
self.write({'rooms': rooms})
ROUTES = [
(r'/api/plugin/plugins', PluginsHandler),
(r'/api/plugin/enable_plugin', EnableHandler),
(r'/api/plugin/open_admin_ui', OpenAdminUiHandler),
(r'/api/plugin/websocket', PluginWsHandler),
(r'/api/plugin/rooms', RoomsHandler),
]