move decorators to plugin module

This commit is contained in:
yanyongyu 2020-04-20 13:50:38 +08:00
parent 9fbd09331c
commit 4f9a9136f9
19 changed files with 327 additions and 259 deletions

View File

@ -15,12 +15,16 @@ else:
class NoneBot(CQHttp): class NoneBot(CQHttp):
def __init__(self, config_object: Optional[Any] = None): def __init__(self, config_object: Optional[Any] = None):
if config_object is None: if config_object is None:
from . import default_config as config_object from . import default_config as config_object
config_dict = {k: v for k, v in config_object.__dict__.items() config_dict = {
if k.isupper() and not k.startswith('_')} k: v
for k, v in config_object.__dict__.items()
if k.isupper() and not k.startswith('_')
}
logger.debug(f'Loaded configurations: {config_dict}') logger.debug(f'Loaded configurations: {config_dict}')
super().__init__(message_class=aiocqhttp.message.Message, super().__init__(message_class=aiocqhttp.message.Message,
**{k.lower(): v for k, v in config_dict.items()}) **{k.lower(): v for k, v in config_dict.items()})
@ -43,8 +47,11 @@ class NoneBot(CQHttp):
async def _(event: aiocqhttp.Event): async def _(event: aiocqhttp.Event):
asyncio.create_task(handle_notice_or_request(self, event)) asyncio.create_task(handle_notice_or_request(self, event))
def run(self, host: Optional[str] = None, port: Optional[int] = None, def run(self,
*args, **kwargs) -> None: host: Optional[str] = None,
port: Optional[int] = None,
*args,
**kwargs) -> None:
host = host or self.config.HOST host = host or self.config.HOST
port = port or self.config.PORT port = port or self.config.PORT
if 'debug' not in kwargs: if 'debug' not in kwargs:
@ -99,8 +106,8 @@ def get_bot() -> NoneBot:
return _bot return _bot
def run(host: Optional[str] = None, port: Optional[int] = None, def run(host: Optional[str] = None, port: Optional[int] = None, *args,
*args, **kwargs) -> None: **kwargs) -> None:
"""Run the NoneBot instance.""" """Run the NoneBot instance."""
get_bot().run(host=host, port=port, *args, **kwargs) get_bot().run(host=host, port=port, *args, **kwargs)
@ -125,31 +132,40 @@ def on_websocket_connect(func: Callable[[aiocqhttp.Event], Awaitable[None]]) \
from .exceptions import * from .exceptions import *
from .message import message_preprocessor, Message, MessageSegment from .message import message_preprocessor, Message, MessageSegment
from .plugin import (load_plugin, load_plugins, load_builtin_plugins, from .plugin import (on_command, on_natural_language, on_notice, on_request,
load_plugin, load_plugins, load_builtin_plugins,
get_loaded_plugins) get_loaded_plugins)
from .command import on_command, CommandSession, CommandGroup from .command import CommandSession, CommandGroup
from .natural_language import (on_natural_language, NLPSession, NLPResult, from .natural_language import NLPSession, NLPResult, IntentCommand
IntentCommand) from .notice_request import NoticeSession, RequestSession
from .notice_request import (on_notice, NoticeSession,
on_request, RequestSession)
from .helpers import context_id from .helpers import context_id
__all__ = [ __all__ = [
'NoneBot', 'scheduler', 'init', 'get_bot', 'run', 'NoneBot',
'scheduler',
'on_startup', 'on_websocket_connect', 'init',
'get_bot',
'run',
'on_startup',
'on_websocket_connect',
'CQHttpError', 'CQHttpError',
'load_plugin',
'load_plugin', 'load_plugins', 'load_builtin_plugins', 'load_plugins',
'load_builtin_plugins',
'get_loaded_plugins', 'get_loaded_plugins',
'message_preprocessor',
'message_preprocessor', 'Message', 'MessageSegment', 'Message',
'MessageSegment',
'on_command', 'CommandSession', 'CommandGroup', 'on_command',
'CommandSession',
'on_natural_language', 'NLPSession', 'NLPResult', 'IntentCommand', 'CommandGroup',
'on_notice', 'NoticeSession', 'on_request', 'RequestSession', 'on_natural_language',
'NLPSession',
'NLPResult',
'IntentCommand',
'on_notice',
'NoticeSession',
'on_request',
'RequestSession',
'context_id', 'context_id',
] ]

View File

@ -4,6 +4,7 @@ from .command import CommandSession
class ParserExit(RuntimeError): class ParserExit(RuntimeError):
def __init__(self, status=0, message=None): def __init__(self, status=0, message=None):
self.status = status self.status = status
self.message = message self.message = message

View File

@ -15,20 +15,17 @@ from nonebot.helpers import context_id, send, render_expression
from nonebot.log import logger from nonebot.log import logger
from nonebot.message import Message from nonebot.message import Message
from nonebot.session import BaseSession from nonebot.session import BaseSession
from nonebot.typing import (CommandName_T, CommandArgs_T, Message_T, State_T, from nonebot.typing import (CommandName_T, CommandArgs_T, CommandHandler_T,
Filter_T) Message_T, State_T, Filter_T)
# key: context id # key: context id
# value: CommandSession object # value: CommandSession object
_sessions = {} # type: Dict[str, "CommandSession"] _sessions = {} # type: Dict[str, "CommandSession"]
CommandHandler_T = Callable[['CommandSession'], Any]
class Command: class Command:
__slots__ = ('name', 'func', 'permission', 'only_to_me', 'privileged', __slots__ = ('name', 'func', 'permission', 'only_to_me', 'privileged',
'args_parser_func', '__name__', '__qualname__', '__doc__', 'args_parser_func')
'__annotations__', '__dict__')
def __init__(self, *, name: CommandName_T, func: CommandHandler_T, def __init__(self, *, name: CommandName_T, func: CommandHandler_T,
permission: int, only_to_me: bool, privileged: bool): permission: int, only_to_me: bool, privileged: bool):
@ -361,55 +358,6 @@ class CommandManager:
cmd_name] if state is None else bool(state) cmd_name] if state is None else bool(state)
def on_command(name: Union[str, CommandName_T],
*,
aliases: Union[Iterable[str], str] = (),
permission: int = perm.EVERYBODY,
only_to_me: bool = True,
privileged: bool = False,
shell_like: bool = False) -> Callable:
"""
Decorator to register a function as a command.
:param name: command name (e.g. 'echo' or ('random', 'number'))
:param aliases: aliases of command name, for convenient access
:param permission: permission required by the command
:param only_to_me: only handle messages to me
:param privileged: can be run even when there is already a session
:param shell_like: use shell-like syntax to split arguments
"""
def deco(func: CommandHandler_T) -> Command:
if not isinstance(name, (str, tuple)):
raise TypeError('the name of a command must be a str or tuple')
if not name:
raise ValueError('the name of a command must not be empty')
cmd_name = (name,) if isinstance(name, str) else name
cmd = Command(name=cmd_name,
func=func,
permission=permission,
only_to_me=only_to_me,
privileged=privileged)
if shell_like:
async def shell_like_args_parser(session):
session.args['argv'] = shlex.split(session.current_arg)
cmd.args_parser_func = shell_like_args_parser
CommandManager.add_command(cmd_name, cmd)
CommandManager.add_aliases(aliases, cmd)
update_wrapper(wrapper=cmd, wrapped=func) # type: ignore
return cmd
return deco
class _PauseException(Exception): class _PauseException(Exception):
""" """
Raised by session.pause() indicating that the command session Raised by session.pause() indicating that the command session

View File

@ -4,5 +4,6 @@ from nonebot.typing import Message_T
class ValidateError(ValueError): class ValidateError(ValueError):
def __init__(self, message: Optional[Message_T] = None): def __init__(self, message: Optional[Message_T] = None):
self.message = message self.message = message

View File

@ -11,8 +11,8 @@ def handle_cancellation(session: CommandSession):
def control(value): def control(value):
if _is_cancellation(value) is True: if _is_cancellation(value) is True:
session.finish(render_expression( session.finish(
session.bot.config.SESSION_CANCEL_EXPRESSION)) render_expression(session.bot.config.SESSION_CANCEL_EXPRESSION))
return value return value
return control return control

View File

@ -15,13 +15,15 @@ def _simple_chinese_to_bool(text: str) -> Optional[bool]:
""" """
text = text.strip().lower().replace(' ', '') \ text = text.strip().lower().replace(' ', '') \
.rstrip(',.!?~,。!?~了的呢吧呀啊呗啦') .rstrip(',.!?~,。!?~了的呢吧呀啊呗啦')
if text in {'', '', '', '', '', '', '', if text in {
'ok', 'okay', 'yeah', 'yep', '', '', '', '', '', '', '', 'ok', 'okay', 'yeah', 'yep',
'当真', '当然', '必须', '可以', '肯定', '没错', '确定', '确认'}: '当真', '当然', '必须', '可以', '肯定', '没错', '确定', '确认'
}:
return True return True
if text in {'', '不要', '不用', '不是', '', '不好', '不对', '不行', '', if text in {
'no', 'nono', 'nonono', 'nope', '不ok', '不可以', '不能', '', '不要', '不用', '不是', '', '不好', '不对', '不行', '', 'no', 'nono',
'不可以'}: 'nonono', 'nope', '不ok', '不可以', '不能', '不可以'
}:
return False return False
return None return None
@ -31,8 +33,8 @@ def _split_nonempty_lines(text: str) -> List[str]:
def _split_nonempty_stripped_lines(text: str) -> List[str]: def _split_nonempty_stripped_lines(text: str) -> List[str]:
return list(filter(lambda x: x, return list(filter(lambda x: x, map(lambda x: x.strip(),
map(lambda x: x.strip(), text.splitlines()))) text.splitlines())))
simple_chinese_to_bool = _simple_chinese_to_bool simple_chinese_to_bool = _simple_chinese_to_bool

View File

@ -14,8 +14,11 @@ def _extract_text(arg: Message_T) -> str:
def _extract_image_urls(arg: Message_T) -> List[str]: def _extract_image_urls(arg: Message_T) -> List[str]:
"""Extract all image urls from a message-like object.""" """Extract all image urls from a message-like object."""
arg_as_msg = Message(arg) arg_as_msg = Message(arg)
return [s.data['url'] for s in arg_as_msg return [
if s.type == 'image' and 'url' in s.data] s.data['url']
for s in arg_as_msg
if s.type == 'image' and 'url' in s.data
]
def _extract_numbers(arg: Message_T) -> List[float]: def _extract_numbers(arg: Message_T) -> List[float]:

View File

@ -6,6 +6,7 @@ from nonebot.typing import Filter_T
class BaseValidator: class BaseValidator:
def __init__(self, message=None): def __init__(self, message=None):
self.message = message self.message = message
@ -69,8 +70,7 @@ def match_regex(pattern: str, message=None, *, flags=0,
return validate return validate
def ensure_true(bool_func: Callable[[Any], bool], def ensure_true(bool_func: Callable[[Any], bool], message=None) -> Filter_T:
message=None) -> Filter_T:
""" """
Validate any object to ensure the result of applying Validate any object to ensure the result of applying
a boolean function to it is True. a boolean function to it is True.

View File

@ -45,6 +45,4 @@ TOO_MANY_VALIDATION_FAILURES_EXPRESSION: Expression_T = \
SESSION_CANCEL_EXPRESSION: Expression_T = '好的' SESSION_CANCEL_EXPRESSION: Expression_T = '好的'
APSCHEDULER_CONFIG: Dict[str, Any] = { APSCHEDULER_CONFIG: Dict[str, Any] = {'apscheduler.timezone': 'Asia/Shanghai'}
'apscheduler.timezone': 'Asia/Shanghai'
}

View File

@ -10,8 +10,8 @@ from .message import escape
from .typing import Message_T, Expression_T from .typing import Message_T, Expression_T
def context_id(event: CQEvent, *, def context_id(event: CQEvent, *, mode: str = 'default',
mode: str = 'default', use_hash: bool = False) -> str: use_hash: bool = False) -> str:
""" """
Calculate a unique id representing the context of the given event. Calculate a unique id representing the context of the given event.
@ -48,8 +48,10 @@ def context_id(event: CQEvent, *,
return ctx_id return ctx_id
async def send(bot: NoneBot, event: CQEvent, async def send(bot: NoneBot,
message: Message_T, *, event: CQEvent,
message: Message_T,
*,
ensure_private: bool = False, ensure_private: bool = False,
ignore_failure: bool = True, ignore_failure: bool = True,
**kwargs) -> Any: **kwargs) -> Any:
@ -65,8 +67,10 @@ async def send(bot: NoneBot, event: CQEvent,
return None return None
def render_expression(expr: Expression_T, *args, def render_expression(expr: Expression_T,
escape_args: bool = True, **kwargs) -> str: *args,
escape_args: bool = True,
**kwargs) -> str:
""" """
Render an expression to message string. Render an expression to message string.
@ -82,8 +86,8 @@ def render_expression(expr: Expression_T, *args,
expr = random.choice(expr) expr = random.choice(expr)
if escape_args: if escape_args:
return expr.format( return expr.format(
*[escape(s) if isinstance(s, str) else s for s in args], *[escape(s) if isinstance(s, str) else s for s in args], **{
**{k: escape(v) if isinstance(v, str) else v k: escape(v) if isinstance(v, str) else v
for k, v in kwargs.items()} for k, v in kwargs.items()
) })
return expr.format(*args, **kwargs) return expr.format(*args, **kwargs)

View File

@ -10,7 +10,6 @@ import sys
logger = logging.getLogger('nonebot') logger = logging.getLogger('nonebot')
default_handler = logging.StreamHandler(sys.stdout) default_handler = logging.StreamHandler(sys.stdout)
default_handler.setFormatter(logging.Formatter( default_handler.setFormatter(
'[%(asctime)s %(name)s] %(levelname)s: %(message)s' logging.Formatter('[%(asctime)s %(name)s] %(levelname)s: %(message)s'))
))
logger.addHandler(default_handler) logger.addHandler(default_handler)

View File

@ -57,7 +57,8 @@ async def handle_message(bot: NoneBot, event: CQEvent) -> None:
while True: while True:
try: try:
handled = await handle_command(bot, event, plugin_manager.cmd_manager) handled = await handle_command(bot, event,
plugin_manager.cmd_manager)
break break
except SwitchException as e: except SwitchException as e:
# we are sure that there is no session existing now # we are sure that there is no session existing now
@ -67,7 +68,8 @@ async def handle_message(bot: NoneBot, event: CQEvent) -> None:
logger.info(f'Message {event.message_id} is handled as a command') logger.info(f'Message {event.message_id} is handled as a command')
return return
handled = await handle_natural_language(bot, event, plugin_manager.nlp_manager) handled = await handle_natural_language(bot, event,
plugin_manager.nlp_manager)
if handled: if handled:
logger.info(f'Message {event.message_id} is handled ' logger.info(f'Message {event.message_id} is handled '
f'as natural language') f'as natural language')
@ -121,8 +123,8 @@ def _check_calling_me_nickname(bot: NoneBot, event: CQEvent) -> None:
else: else:
nicknames = filter(lambda n: n, bot.config.NICKNAME) nicknames = filter(lambda n: n, bot.config.NICKNAME)
nickname_regex = '|'.join(nicknames) nickname_regex = '|'.join(nicknames)
m = re.search(rf'^({nickname_regex})([\s,]*|$)', m = re.search(rf'^({nickname_regex})([\s,]*|$)', first_text,
first_text, re.IGNORECASE) re.IGNORECASE)
if m: if m:
nickname = m.group(1) nickname = m.group(1)
logger.debug(f'User is calling me {nickname}') logger.debug(f'User is calling me {nickname}')

View File

@ -15,8 +15,7 @@ from .typing import CommandName_T, CommandArgs_T
class NLProcessor: class NLProcessor:
__slots__ = ('func', 'keywords', 'permission', 'only_to_me', __slots__ = ('func', 'keywords', 'permission', 'only_to_me',
'only_short_message', 'allow_empty_message', '__name__', '__qualname__', '__doc__', 'only_short_message', 'allow_empty_message')
'__annotations__', '__dict__')
def __init__(self, *, func: Callable, keywords: Optional[Iterable], def __init__(self, *, func: Callable, keywords: Optional[Iterable],
permission: int, only_to_me: bool, only_short_message: bool, permission: int, only_to_me: bool, only_short_message: bool,
@ -101,44 +100,6 @@ class NLPManager:
return False return False
def on_natural_language(
keywords: Union[Optional[Iterable], str, Callable] = None,
*,
permission: int = perm.EVERYBODY,
only_to_me: bool = True,
only_short_message: bool = True,
allow_empty_message: bool = False) -> Callable:
"""
Decorator to register a function as a natural language processor.
:param keywords: keywords to respond to, if None, respond to all messages
:param permission: permission required by the processor
:param only_to_me: only handle messages to me
:param only_short_message: only handle short messages
:param allow_empty_message: handle empty messages
"""
def deco(func: Callable) -> NLProcessor:
nl_processor = NLProcessor(
func=func,
keywords=keywords, # type: ignore
permission=permission,
only_to_me=only_to_me,
only_short_message=only_short_message,
allow_empty_message=allow_empty_message)
NLPManager.add_nl_processor(nl_processor)
update_wrapper(wrapper=nl_processor, wrapped=func) # type: ignore
return nl_processor
if isinstance(keywords, Callable):
# here "keywords" is the function to be decorated
return on_natural_language()(keywords)
else:
if isinstance(keywords, str):
keywords = (keywords,)
return deco
class NLPSession(BaseSession): class NLPSession(BaseSession):
__slots__ = ('msg', 'msg_text', 'msg_images') __slots__ = ('msg', 'msg_text', 'msg_images')

View File

@ -11,41 +11,15 @@ from .session import BaseSession
_bus = EventBus() _bus = EventBus()
class EventHandler: class EventHandler:
__slots__ = ('events', 'func', '__name__', '__qualname__', '__doc__', __slots__ = ('events', 'func')
'__annotations__', '__dict__')
def __init__(self, events: List[str], func: Callable): def __init__(self, events: List[str], func: Callable):
self.events = events self.events = events
self.func = func self.func = func
def _make_event_deco(post_type: str) -> Callable:
def deco_deco(arg: Optional[Union[str, Callable]] = None,
*events: str) -> Callable:
def deco(func: Callable) -> EventHandler:
if isinstance(arg, str):
events_tmp = list(map(lambda x: f"{post_type}.{x}", [arg] + list(events)))
for e in events_tmp:
_bus.subscribe(e, func)
handler = EventHandler(events_tmp, func)
return update_wrapper(handler, func) # type: ignore
else:
_bus.subscribe(post_type, func)
handler = EventHandler([post_type], func)
return update_wrapper(handler, func) # type: ignore
if isinstance(arg, Callable):
return deco(arg) # type: ignore
return deco
return deco_deco
on_notice = _make_event_deco('notice')
on_request = _make_event_deco('request')
class NoticeSession(BaseSession): class NoticeSession(BaseSession):
__slots__ = () __slots__ = ()
@ -66,12 +40,13 @@ class RequestSession(BaseSession):
:param remark: remark of friend (only works in friend request) :param remark: remark of friend (only works in friend request)
""" """
try: try:
await self.bot.call_action( await self.bot.call_action(action='.handle_quick_operation_async',
action='.handle_quick_operation_async',
self_id=self.event.self_id, self_id=self.event.self_id,
context=self.event, context=self.event,
operation={'approve': True, 'remark': remark} operation={
) 'approve': True,
'remark': remark
})
except CQHttpError: except CQHttpError:
pass pass
@ -82,12 +57,13 @@ class RequestSession(BaseSession):
:param reason: reason to reject (only works in group request) :param reason: reason to reject (only works in group request)
""" """
try: try:
await self.bot.call_action( await self.bot.call_action(action='.handle_quick_operation_async',
action='.handle_quick_operation_async',
self_id=self.event.self_id, self_id=self.event.self_id,
context=self.event, context=self.event,
operation={'approve': False, 'reason': reason} operation={
) 'approve': False,
'reason': reason
})
except CQHttpError: except CQHttpError:
pass pass

View File

@ -88,8 +88,7 @@ async def _check(bot: NoneBot, min_event: _MinEvent,
self_id=min_event.self_id, self_id=min_event.self_id,
group_id=min_event.group_id, group_id=min_event.group_id,
user_id=min_event.user_id, user_id=min_event.user_id,
no_cache=True no_cache=True)
)
if member_info: if member_info:
if member_info['role'] == 'owner': if member_info['role'] == 'owner':
permission |= IS_GROUP_OWNER permission |= IS_GROUP_OWNER

View File

@ -1,20 +1,29 @@
import os import os
import re import re
import shlex
import warnings import warnings
import importlib import importlib
from types import ModuleType from types import ModuleType
from typing import Any, Set, Dict, Optional from typing import Any, Set, Dict, Union, Optional, Iterable, Callable
from .log import logger from .log import logger
from nonebot import permission as perm
from .command import Command, CommandManager from .command import Command, CommandManager
from .natural_language import NLProcessor, NLPManager from .natural_language import NLProcessor, NLPManager
from .notice_request import _bus, EventHandler from .notice_request import _bus, EventHandler
from .typing import CommandName_T, CommandHandler_T
_tmp_command: Set[Command] = set()
_tmp_nl_processor: Set[NLProcessor] = set()
_tmp_event_handler: Set[EventHandler] = set()
class Plugin: class Plugin:
__slots__ = ('module', 'name', 'usage', 'commands', 'nl_processors', 'event_handlers') __slots__ = ('module', 'name', 'usage', 'commands', 'nl_processors',
'event_handlers')
def __init__(self, module: ModuleType, def __init__(self,
module: ModuleType,
name: Optional[str] = None, name: Optional[str] = None,
usage: Optional[Any] = None, usage: Optional[Any] = None,
commands: Set[Command] = set(), commands: Set[Command] = set(),
@ -27,40 +36,58 @@ class Plugin:
self.nl_processors = nl_processors self.nl_processors = nl_processors
self.event_handlers = event_handlers self.event_handlers = event_handlers
class PluginManager: class PluginManager:
_plugins: Dict[str, Plugin] = {} _plugins: Dict[str, Plugin] = {}
_anonymous_plugins: Set[Plugin] = set()
def __init__(self): def __init__(self):
self.cmd_manager = CommandManager() self.cmd_manager = CommandManager()
self.nlp_manager = NLPManager() self.nlp_manager = NLPManager()
@classmethod @classmethod
def add_plugin(cls, plugin: Plugin) -> None: def add_plugin(cls, module_path: str, plugin: Plugin) -> None:
"""Register a plugin """Register a plugin
Args: Args:
name (str): module path
plugin (Plugin): Plugin object plugin (Plugin): Plugin object
""" """
if plugin.name: if module_path in cls._plugins:
if plugin.name in cls._plugins: warnings.warn(f"Plugin {module_path} already exists")
warnings.warn(f"Plugin {plugin.name} already exists")
return return
cls._plugins[plugin.name] = plugin cls._plugins[module_path] = plugin
else:
cls._anonymous_plugins.add(plugin)
@classmethod @classmethod
def get_plugin(cls, name: str) -> Optional[Plugin]: def get_plugin(cls, module_path: str) -> Optional[Plugin]:
return cls._plugins.get(name) """Get plugin object by plugin path
# TODO: plugin重加载 Args:
@classmethod name (str): plugin path
def reload_plugin(cls, plugin: Plugin) -> None:
pass Returns:
Optional[Plugin]: Plugin object
"""
return cls._plugins.get(module_path, None)
@classmethod @classmethod
def switch_plugin_global(cls, name: str, state: Optional[bool] = None) -> None: def remove_plugin(cls, module_path: str) -> bool:
plugin = cls.get_plugin(module_path)
if not plugin:
warnings.warn(f"Plugin {module_path} not exists")
return False
for command in plugin.commands:
CommandManager.remove_command(command.name)
for nl_processor in plugin.nl_processors:
NLPManager.remove_nl_processor(nl_processor)
for event_handler in plugin.event_handlers:
for event in event_handler.events:
_bus.unsubscribe(event, event_handler.func)
del cls._plugins[module_path]
return True
@classmethod
def switch_plugin_global(cls, name: str,
state: Optional[bool] = None) -> None:
"""Change plugin state globally or simply switch it if `state` is None """Change plugin state globally or simply switch it if `state` is None
Args: Args:
@ -79,11 +106,13 @@ class PluginManager:
for event in event_handler.events: for event in event_handler.events:
if event_handler.func in _bus._subscribers[event] and not state: if event_handler.func in _bus._subscribers[event] and not state:
_bus.unsubscribe(event, event_handler.func) _bus.unsubscribe(event, event_handler.func)
elif event_handler.func not in _bus._subscribers[event] and state != False: elif event_handler.func not in _bus._subscribers[
event] and state != False:
_bus.subscribe(event, event_handler.func) _bus.subscribe(event, event_handler.func)
@classmethod @classmethod
def switch_command_global(cls, name: str, state: Optional[bool] = None) -> None: def switch_command_global(cls, name: str,
state: Optional[bool] = None) -> None:
"""Change plugin command state globally or simply switch it if `state` is None """Change plugin command state globally or simply switch it if `state` is None
Args: Args:
@ -98,7 +127,8 @@ class PluginManager:
CommandManager.switch_command_global(command.name, state) CommandManager.switch_command_global(command.name, state)
@classmethod @classmethod
def switch_nlprocessor_global(cls, name: str, state: Optional[bool] = None) -> None: def switch_nlprocessor_global(cls, name: str,
state: Optional[bool] = None) -> None:
"""Change plugin nlprocessor state globally or simply switch it if `state` is None """Change plugin nlprocessor state globally or simply switch it if `state` is None
Args: Args:
@ -113,7 +143,8 @@ class PluginManager:
NLPManager.switch_nlprocessor_global(processor, state) NLPManager.switch_nlprocessor_global(processor, state)
@classmethod @classmethod
def switch_eventhandler_global(cls, name: str, state: Optional[bool] = None) -> None: def switch_eventhandler_global(cls, name: str,
state: Optional[bool] = None) -> None:
"""Change plugin event handler state globally or simply switch it if `state` is None """Change plugin event handler state globally or simply switch it if `state` is None
Args: Args:
@ -128,7 +159,8 @@ class PluginManager:
for event in event_handler.events: for event in event_handler.events:
if event_handler.func in _bus._subscribers[event] and not state: if event_handler.func in _bus._subscribers[event] and not state:
_bus.unsubscribe(event, event_handler.func) _bus.unsubscribe(event, event_handler.func)
elif event_handler.func not in _bus._subscribers[event] and state != False: elif event_handler.func not in _bus._subscribers[
event] and state != False:
_bus.subscribe(event, event_handler.func) _bus.subscribe(event, event_handler.func)
def switch_plugin(self, name: str, state: Optional[bool] = None) -> None: def switch_plugin(self, name: str, state: Optional[bool] = None) -> None:
@ -166,7 +198,8 @@ class PluginManager:
for command in plugin.commands: for command in plugin.commands:
self.cmd_manager.switch_command(command.name, state) self.cmd_manager.switch_command(command.name, state)
def switch_nlprocessor(self, name: str, state: Optional[bool] = None) -> None: def switch_nlprocessor(self, name: str,
state: Optional[bool] = None) -> None:
"""Change plugin nlprocessor state or simply switch it if `state` is None """Change plugin nlprocessor state or simply switch it if `state` is None
Args: Args:
@ -181,41 +214,42 @@ class PluginManager:
self.nlp_manager.switch_nlprocessor(processor, state) self.nlp_manager.switch_nlprocessor(processor, state)
def load_plugin(module_name: str) -> Optional[Plugin]: def load_plugin(module_path: str) -> Optional[Plugin]:
""" """Load a module as a plugin
Load a module as a plugin.
:param module_name: name of module to import Args:
:return: successful or not module_path (str): path of module to import
Returns:
Optional[Plugin]: Plugin object loaded
""" """
# Make sure tmp is clean
_tmp_command.clear()
_tmp_nl_processor.clear()
_tmp_event_handler.clear()
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_path)
name = getattr(module, '__plugin_name__', None) name = getattr(module, '__plugin_name__', None)
usage = getattr(module, '__plugin_usage__', None) usage = getattr(module, '__plugin_usage__', None)
commands = set() commands = _tmp_command.copy()
nl_processors = set() nl_processors = _tmp_nl_processor.copy()
event_handlers = set() event_handlers = _tmp_event_handler.copy()
for attr in dir(module): plugin = Plugin(module, name, usage, commands, nl_processors,
func = getattr(module, attr) event_handlers)
if isinstance(func, Command): PluginManager.add_plugin(module_path, plugin)
commands.add(func) logger.info(f'Succeeded to import "{module_path}"')
elif isinstance(func, NLProcessor):
nl_processors.add(func)
elif isinstance(func, EventHandler):
event_handlers.add(func)
plugin = Plugin(module, name, usage, commands, nl_processors, event_handlers)
PluginManager.add_plugin(plugin)
logger.info(f'Succeeded to import "{module_name}"')
return plugin return plugin
except Exception as e: except Exception as e:
logger.error(f'Failed to import "{module_name}", error: {e}') logger.error(f'Failed to import "{module_path}", error: {e}')
logger.exception(e) logger.exception(e)
return None return None
# TODO: plugin重加载 def reload_plugin(module_path: str) -> Optional[Plugin]:
def reload_plugin(module_name: str) -> Optional[Plugin]: result = PluginManager.remove_plugin(module_path)
pass if not result:
return None
return load_plugin(module_path)
def load_plugins(plugin_dir: str, module_prefix: str) -> Set[Plugin]: def load_plugins(plugin_dir: str, module_prefix: str) -> Set[Plugin]:
@ -262,4 +296,121 @@ def get_loaded_plugins() -> Set[Plugin]:
:return: a set of Plugin objects :return: a set of Plugin objects
""" """
return set(PluginManager._plugins.values()) | PluginManager._anonymous_plugins return set(PluginManager._plugins.values())
def on_command(name: Union[str, CommandName_T],
*,
aliases: Union[Iterable[str], str] = (),
permission: int = perm.EVERYBODY,
only_to_me: bool = True,
privileged: bool = False,
shell_like: bool = False) -> Callable:
"""
Decorator to register a function as a command.
:param name: command name (e.g. 'echo' or ('random', 'number'))
:param aliases: aliases of command name, for convenient access
:param permission: permission required by the command
:param only_to_me: only handle messages to me
:param privileged: can be run even when there is already a session
:param shell_like: use shell-like syntax to split arguments
"""
def deco(func: CommandHandler_T) -> CommandHandler_T:
if not isinstance(name, (str, tuple)):
raise TypeError('the name of a command must be a str or tuple')
if not name:
raise ValueError('the name of a command must not be empty')
cmd_name = (name,) if isinstance(name, str) else name
cmd = Command(name=cmd_name,
func=func,
permission=permission,
only_to_me=only_to_me,
privileged=privileged)
if shell_like:
async def shell_like_args_parser(session):
session.args['argv'] = shlex.split(session.current_arg)
cmd.args_parser_func = shell_like_args_parser
CommandManager.add_command(cmd_name, cmd)
CommandManager.add_aliases(aliases, cmd)
_tmp_command.add(cmd)
func.args_parser = cmd.args_parser
return func
return deco
def on_natural_language(
keywords: Union[Optional[Iterable], str, Callable] = None,
*,
permission: int = perm.EVERYBODY,
only_to_me: bool = True,
only_short_message: bool = True,
allow_empty_message: bool = False) -> Callable:
"""
Decorator to register a function as a natural language processor.
:param keywords: keywords to respond to, if None, respond to all messages
:param permission: permission required by the processor
:param only_to_me: only handle messages to me
:param only_short_message: only handle short messages
:param allow_empty_message: handle empty messages
"""
def deco(func: Callable) -> Callable:
nl_processor = NLProcessor(
func=func,
keywords=keywords, # type: ignore
permission=permission,
only_to_me=only_to_me,
only_short_message=only_short_message,
allow_empty_message=allow_empty_message)
NLPManager.add_nl_processor(nl_processor)
_tmp_nl_processor.add(nl_processor)
return func
if isinstance(keywords, Callable):
# here "keywords" is the function to be decorated
return on_natural_language()(keywords)
else:
if isinstance(keywords, str):
keywords = (keywords,)
return deco
def _make_event_deco(post_type: str) -> Callable:
def deco_deco(arg: Optional[Union[str, Callable]] = None,
*events: str) -> Callable:
def deco(func: Callable) -> Callable:
if isinstance(arg, str):
events_tmp = list(
map(lambda x: f"{post_type}.{x}", [arg] + list(events)))
for e in events_tmp:
_bus.subscribe(e, func)
handler = EventHandler(events_tmp, func)
else:
_bus.subscribe(post_type, func)
handler = EventHandler([post_type], func)
_tmp_event_handler.add(handler)
return func
if isinstance(arg, Callable):
return deco(arg) # type: ignore
return deco
return deco_deco
on_notice = _make_event_deco('notice')
on_request = _make_event_deco('request')

View File

@ -5,6 +5,7 @@ except ImportError:
AsyncIOScheduler = None AsyncIOScheduler = None
if AsyncIOScheduler: if AsyncIOScheduler:
class Scheduler(AsyncIOScheduler): class Scheduler(AsyncIOScheduler):
pass pass
else: else:

View File

@ -24,7 +24,9 @@ class BaseSession:
def self_id(self) -> int: def self_id(self) -> int:
return self.event.self_id return self.event.self_id
async def send(self, message: Message_T, *, async def send(self,
message: Message_T,
*,
at_sender: bool = False, at_sender: bool = False,
ensure_private: bool = False, ensure_private: bool = False,
ignore_failure: bool = True, ignore_failure: bool = True,
@ -38,7 +40,10 @@ class BaseSession:
:param ignore_failure: if any CQHttpError raised, ignore it :param ignore_failure: if any CQHttpError raised, ignore it
:return: the result returned by CQHTTP :return: the result returned by CQHTTP
""" """
return await send(self.bot, self.event, message, return await send(self.bot,
self.event,
message,
at_sender=at_sender, at_sender=at_sender,
ensure_private=ensure_private, ensure_private=ensure_private,
ignore_failure=ignore_failure, **kwargs) ignore_failure=ignore_failure,
**kwargs)

View File

@ -5,5 +5,6 @@ Message_T = Union[str, Dict[str, Any], List[Dict[str, Any]]]
Expression_T = Union[str, Sequence[str], Callable] Expression_T = Union[str, Sequence[str], Callable]
CommandName_T = Tuple[str, ...] CommandName_T = Tuple[str, ...]
CommandArgs_T = Dict[str, Any] CommandArgs_T = Dict[str, Any]
CommandHandler_T = Callable[["CommandSession"], Any]
State_T = Dict[str, Any] State_T = Dict[str, Any]
Filter_T = Callable[[Any], Union[Any, Awaitable[Any]]] Filter_T = Callable[[Any], Union[Any, Awaitable[Any]]]