mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-01-05 18:18:42 +08:00
add manager objects
This commit is contained in:
parent
bc1e3f26df
commit
52b2f635ad
@ -124,9 +124,9 @@ def on_websocket_connect(func: Callable[[aiocqhttp.Event], Awaitable[None]]) \
|
|||||||
|
|
||||||
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
|
from .message import message_preprocessor, Message, MessageSegment
|
||||||
from .plugin import (load_plugin, load_plugins, load_builtin_plugins,
|
from .plugin import (load_plugin, load_plugins, load_builtin_plugins,
|
||||||
get_loaded_plugins)
|
get_loaded_plugins)
|
||||||
from .message import message_preprocessor, Message, MessageSegment
|
|
||||||
from .command import on_command, CommandSession, CommandGroup
|
from .command import on_command, CommandSession, CommandGroup
|
||||||
from .natural_language import (on_natural_language, NLPSession, NLPResult,
|
from .natural_language import (on_natural_language, NLPSession, NLPResult,
|
||||||
IntentCommand)
|
IntentCommand)
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import asyncio
|
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import asyncio
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import (
|
from typing import (Tuple, Union, Callable, Iterable, Any, Optional, List, Dict,
|
||||||
Tuple, Union, Callable, Iterable, Any, Optional, List, Dict,
|
Awaitable)
|
||||||
Awaitable
|
|
||||||
)
|
|
||||||
|
|
||||||
from aiocqhttp import Event as CQEvent
|
from aiocqhttp import Event as CQEvent
|
||||||
|
|
||||||
@ -17,38 +15,22 @@ 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 (
|
from nonebot.typing import (CommandName_T, CommandArgs_T, Message_T, State_T,
|
||||||
CommandName_T, CommandArgs_T, Message_T, State_T, Filter_T
|
Filter_T)
|
||||||
)
|
|
||||||
|
|
||||||
# key: one segment of command name
|
|
||||||
# value: subtree or a leaf Command object
|
|
||||||
_registry = {} # type: Dict[str, Union[Dict, Command]]
|
|
||||||
|
|
||||||
# key: alias
|
|
||||||
# value: real command name
|
|
||||||
_aliases = {} # type: Dict[str, CommandName_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]
|
CommandHandler_T = Callable[['CommandSession'], Any]
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
__slots__ = ('name', 'func',
|
__slots__ = ('name', 'func', 'permission', 'only_to_me', 'privileged',
|
||||||
'permission',
|
|
||||||
'only_to_me',
|
|
||||||
'privileged',
|
|
||||||
'args_parser_func')
|
'args_parser_func')
|
||||||
|
|
||||||
def __init__(self, *,
|
def __init__(self, *, name: CommandName_T, func: CommandHandler_T,
|
||||||
name: CommandName_T,
|
permission: int, only_to_me: bool, privileged: bool):
|
||||||
func: CommandHandler_T,
|
|
||||||
permission: int,
|
|
||||||
only_to_me: bool,
|
|
||||||
privileged: bool):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.func = func
|
self.func = func
|
||||||
self.permission = permission
|
self.permission = permission
|
||||||
@ -56,8 +38,7 @@ class Command:
|
|||||||
self.privileged = privileged
|
self.privileged = privileged
|
||||||
self.args_parser_func: Optional[CommandHandler_T] = None
|
self.args_parser_func: Optional[CommandHandler_T] = None
|
||||||
|
|
||||||
async def run(self, session, *,
|
async def run(self, session, *, check_perm: bool = True,
|
||||||
check_perm: bool = True,
|
|
||||||
dry: bool = False) -> bool:
|
dry: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Run the command in a given session.
|
Run the command in a given session.
|
||||||
@ -94,15 +75,16 @@ class Command:
|
|||||||
if session.state['__validation_failure_num'] >= \
|
if session.state['__validation_failure_num'] >= \
|
||||||
config.MAX_VALIDATION_FAILURES:
|
config.MAX_VALIDATION_FAILURES:
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
session.finish(render_expression(
|
session.finish(
|
||||||
config.TOO_MANY_VALIDATION_FAILURES_EXPRESSION
|
render_expression(
|
||||||
|
config.
|
||||||
|
TOO_MANY_VALIDATION_FAILURES_EXPRESSION
|
||||||
), **session._current_send_kwargs)
|
), **session._current_send_kwargs)
|
||||||
|
|
||||||
failure_message = e.message
|
failure_message = e.message
|
||||||
if failure_message is None:
|
if failure_message is None:
|
||||||
failure_message = render_expression(
|
failure_message = render_expression(
|
||||||
config.DEFAULT_VALIDATION_FAILURE_EXPRESSION
|
config.DEFAULT_VALIDATION_FAILURE_EXPRESSION)
|
||||||
)
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
session.pause(failure_message,
|
session.pause(failure_message,
|
||||||
**session._current_send_kwargs)
|
**session._current_send_kwargs)
|
||||||
@ -133,6 +115,14 @@ class Command:
|
|||||||
return await perm.check_permission(session.bot, session.event,
|
return await perm.check_permission(session.bot, session.event,
|
||||||
self.permission)
|
self.permission)
|
||||||
|
|
||||||
|
def args_parser(self, parser_func: CommandHandler_T) -> CommandHandler_T:
|
||||||
|
"""
|
||||||
|
Decorator to register a function as the arguments parser of
|
||||||
|
the corresponding command.
|
||||||
|
"""
|
||||||
|
self.args_parser_func = parser_func
|
||||||
|
return parser_func
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Command, name={self.name.__repr__()}>'
|
return f'<Command, name={self.name.__repr__()}>'
|
||||||
|
|
||||||
@ -140,7 +130,238 @@ class Command:
|
|||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
def on_command(name: Union[str, CommandName_T], *,
|
class CommandManager:
|
||||||
|
"""Global Command Manager"""
|
||||||
|
_commands = {} # type: Dict[CommandName_T, Command]
|
||||||
|
_aliases = {} # type: Dict[str, Command]
|
||||||
|
_switches = {} # type: Dict[CommandName_T, bool]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.commands = CommandManager._commands.copy()
|
||||||
|
self.aliases = CommandManager._aliases.copy()
|
||||||
|
self.switches = CommandManager._switches.copy()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_command(cls, cmd_name: CommandName_T, cmd: Command) -> None:
|
||||||
|
"""Register a command
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Command name
|
||||||
|
cmd (Command): Command object
|
||||||
|
"""
|
||||||
|
if cmd_name in cls._commands:
|
||||||
|
warnings.warn(f"Command {cmd_name} already exists")
|
||||||
|
return
|
||||||
|
cls._switches[cmd_name] = True
|
||||||
|
cls._commands[cmd_name] = cmd
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reload_command(cls, cmd_name: CommandName_T, cmd: Command) -> None:
|
||||||
|
"""Reload a command
|
||||||
|
|
||||||
|
**Warning! Dangerous function**
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Command name
|
||||||
|
cmd (Command): Command object
|
||||||
|
"""
|
||||||
|
if cmd_name not in cls._commands:
|
||||||
|
warnings.warn(
|
||||||
|
f"Command {cmd_name} does not exist. Please use add_command instead"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
cls._commands[cmd_name] = cmd
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_command(cls, cmd_name: CommandName_T) -> bool:
|
||||||
|
"""Remove a command
|
||||||
|
|
||||||
|
**Warning! Dangerous function**
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Command name to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Success or not
|
||||||
|
"""
|
||||||
|
if cmd_name in cls._commands:
|
||||||
|
del cls._commands[cmd_name]
|
||||||
|
if cmd_name in cls._switches:
|
||||||
|
del cls._switches[cmd_name]
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def switch_command_global(cls,
|
||||||
|
cmd_name: CommandName_T,
|
||||||
|
state: Optional[bool] = None):
|
||||||
|
"""Change command state globally or simply switch it if `state` is None
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Command name
|
||||||
|
state (Optional[bool]): State to change to. Defaults to None.
|
||||||
|
"""
|
||||||
|
cls._switches[cmd_name] = not cls._switches[
|
||||||
|
cmd_name] if state is None else bool(state)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_aliases(cls, aliases: Union[Iterable[str], str], cmd: Command):
|
||||||
|
"""Register command alias(es)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aliases (Union[Iterable[str], str]): Command aliases
|
||||||
|
cmd_name (Command): Command
|
||||||
|
"""
|
||||||
|
if isinstance(aliases, str):
|
||||||
|
aliases = (aliases,)
|
||||||
|
for alias in aliases:
|
||||||
|
if not isinstance(alias, str):
|
||||||
|
warnings.warn(f"Alias {alias} is not a string! Ignored")
|
||||||
|
return
|
||||||
|
elif alias in cls._aliases:
|
||||||
|
warnings.warn(f"Alias {alias} already exists")
|
||||||
|
return
|
||||||
|
cls._aliases[alias] = cmd
|
||||||
|
|
||||||
|
def _add_command_to_tree(self, cmd_name: CommandName_T, cmd: Command,
|
||||||
|
tree: Dict[str, Union[Dict, Command]]) -> None:
|
||||||
|
"""Add command to the target command tree.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Name of the command
|
||||||
|
cmd (Command): Command object
|
||||||
|
tree (Dict[str, Union[Dict, Command]): Target command tree
|
||||||
|
"""
|
||||||
|
current_parent = tree
|
||||||
|
for parent_key in cmd_name[:-1]:
|
||||||
|
current_parent[parent_key] = current_parent.get(parent_key) or {}
|
||||||
|
current_parent = current_parent[parent_key]
|
||||||
|
# TODO: 支持test test.sub子命令
|
||||||
|
if not isinstance(current_parent, dict):
|
||||||
|
warnings.warn(f"{current_parent} is not a registry dict")
|
||||||
|
return
|
||||||
|
if cmd_name[-1] in current_parent:
|
||||||
|
warnings.warn(f"There is already a command named {cmd_name}")
|
||||||
|
return
|
||||||
|
current_parent[cmd_name[-1]] = cmd
|
||||||
|
|
||||||
|
def _generate_command_tree(self, commands: Dict[CommandName_T, Command]
|
||||||
|
) -> Dict[str, Union[Dict, Command]]:
|
||||||
|
"""Generate command tree from commands dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands (Dict[CommandName_T, Command]): Dictionary of commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Union[Dict, "Command"]]: Command tree
|
||||||
|
"""
|
||||||
|
cmd_tree = {} #type: Dict[str, Union[Dict, "Command"]]
|
||||||
|
for cmd_name, cmd in commands.items():
|
||||||
|
self._add_command_to_tree(cmd_name, cmd, cmd_tree)
|
||||||
|
return cmd_tree
|
||||||
|
|
||||||
|
def _find_command(self,
|
||||||
|
name: Union[str, CommandName_T]) -> Optional[Command]:
|
||||||
|
cmd_name = (name,) if isinstance(name, str) else name
|
||||||
|
if not cmd_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cmd_tree = self._generate_command_tree({
|
||||||
|
name: cmd
|
||||||
|
for name, cmd in self.commands.items()
|
||||||
|
if self.switches.get(name, True)
|
||||||
|
})
|
||||||
|
for part in cmd_name[:-1]:
|
||||||
|
if part not in cmd_tree or not isinstance(
|
||||||
|
cmd_tree[part], #type: ignore
|
||||||
|
dict):
|
||||||
|
return None
|
||||||
|
cmd_tree = cmd_tree[part] # type: ignore
|
||||||
|
|
||||||
|
cmd = cmd_tree.get(cmd_name[-1]) # type: ignore
|
||||||
|
return cmd if isinstance(cmd, Command) else None
|
||||||
|
|
||||||
|
def parse_command(self, bot: NoneBot, cmd_string: str
|
||||||
|
) -> Tuple[Optional[Command], Optional[str]]:
|
||||||
|
logger.debug(f'Parsing command: {repr(cmd_string)}')
|
||||||
|
|
||||||
|
matched_start = None
|
||||||
|
for start in bot.config.COMMAND_START:
|
||||||
|
# loop through COMMAND_START to find the longest matched start
|
||||||
|
curr_matched_start = None
|
||||||
|
if isinstance(start, type(re.compile(''))):
|
||||||
|
m = start.search(cmd_string)
|
||||||
|
if m and m.start(0) == 0:
|
||||||
|
curr_matched_start = m.group(0)
|
||||||
|
elif isinstance(start, str):
|
||||||
|
if cmd_string.startswith(start):
|
||||||
|
curr_matched_start = start
|
||||||
|
|
||||||
|
if curr_matched_start is not None and \
|
||||||
|
(matched_start is None or
|
||||||
|
len(curr_matched_start) > len(matched_start)):
|
||||||
|
# a longer start, use it
|
||||||
|
matched_start = curr_matched_start
|
||||||
|
|
||||||
|
if matched_start is None:
|
||||||
|
# it's not a command
|
||||||
|
logger.debug('It\'s not a command')
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
logger.debug(f'Matched command start: '
|
||||||
|
f'{matched_start}{"(empty)" if not matched_start else ""}')
|
||||||
|
full_command = cmd_string[len(matched_start):].lstrip()
|
||||||
|
|
||||||
|
if not full_command:
|
||||||
|
# command is empty
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
cmd_name_text, *cmd_remained = full_command.split(maxsplit=1)
|
||||||
|
|
||||||
|
cmd_name = None
|
||||||
|
for sep in bot.config.COMMAND_SEP:
|
||||||
|
# loop through COMMAND_SEP to find the most optimized split
|
||||||
|
curr_cmd_name = None
|
||||||
|
if isinstance(sep, type(re.compile(''))):
|
||||||
|
curr_cmd_name = tuple(sep.split(cmd_name_text))
|
||||||
|
elif isinstance(sep, str):
|
||||||
|
curr_cmd_name = tuple(cmd_name_text.split(sep))
|
||||||
|
|
||||||
|
if curr_cmd_name is not None and \
|
||||||
|
(not cmd_name or len(curr_cmd_name) > len(cmd_name)):
|
||||||
|
# a more optimized split, use it
|
||||||
|
cmd_name = curr_cmd_name
|
||||||
|
|
||||||
|
if not cmd_name:
|
||||||
|
cmd_name = (cmd_name_text,)
|
||||||
|
logger.debug(f'Split command name: {cmd_name}')
|
||||||
|
|
||||||
|
cmd = self._find_command(cmd_name) # type: ignore
|
||||||
|
if not cmd:
|
||||||
|
logger.debug(f'Command {cmd_name} not found. Try to match aliases')
|
||||||
|
cmd = self.aliases.get(cmd_name_text)
|
||||||
|
|
||||||
|
if not cmd:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
logger.debug(f'Command {cmd.name} found, function: {cmd.func}')
|
||||||
|
return cmd, ''.join(cmd_remained)
|
||||||
|
|
||||||
|
def switch_command(self,
|
||||||
|
cmd_name: CommandName_T,
|
||||||
|
state: Optional[bool] = None):
|
||||||
|
"""Change command state or simply switch it if `state` is None
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd_name (CommandName_T): Command name
|
||||||
|
state (Optional[bool]): State to change to. Defaults to None.
|
||||||
|
"""
|
||||||
|
self.switches[cmd_name] = not self.switches[
|
||||||
|
cmd_name] if state is None else bool(state)
|
||||||
|
|
||||||
|
|
||||||
|
def on_command(name: Union[str, CommandName_T],
|
||||||
|
*,
|
||||||
aliases: Union[Iterable[str], str] = (),
|
aliases: Union[Iterable[str], str] = (),
|
||||||
permission: int = perm.EVERYBODY,
|
permission: int = perm.EVERYBODY,
|
||||||
only_to_me: bool = True,
|
only_to_me: bool = True,
|
||||||
@ -157,7 +378,7 @@ def on_command(name: Union[str, CommandName_T], *,
|
|||||||
:param shell_like: use shell-like syntax to split arguments
|
:param shell_like: use shell-like syntax to split arguments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def deco(func: CommandHandler_T) -> CommandHandler_T:
|
def deco(func: CommandHandler_T) -> Command:
|
||||||
if not isinstance(name, (str, tuple)):
|
if not isinstance(name, (str, tuple)):
|
||||||
raise TypeError('the name of a command must be a str or tuple')
|
raise TypeError('the name of a command must be a str or tuple')
|
||||||
if not name:
|
if not name:
|
||||||
@ -165,63 +386,27 @@ def on_command(name: Union[str, CommandName_T], *,
|
|||||||
|
|
||||||
cmd_name = (name,) if isinstance(name, str) else name
|
cmd_name = (name,) if isinstance(name, str) else name
|
||||||
|
|
||||||
cmd = Command(name=cmd_name, func=func, permission=permission,
|
cmd = Command(name=cmd_name,
|
||||||
only_to_me=only_to_me, privileged=privileged)
|
func=func,
|
||||||
|
permission=permission,
|
||||||
def args_parser(parser_func: CommandHandler_T) -> CommandHandler_T:
|
only_to_me=only_to_me,
|
||||||
"""
|
privileged=privileged)
|
||||||
Decorator to register a function as the arguments parser of
|
|
||||||
the corresponding command.
|
|
||||||
"""
|
|
||||||
cmd.args_parser_func = parser_func
|
|
||||||
return parser_func
|
|
||||||
|
|
||||||
func.args_parser = args_parser
|
|
||||||
|
|
||||||
if shell_like:
|
if shell_like:
|
||||||
|
|
||||||
async def shell_like_args_parser(session):
|
async def shell_like_args_parser(session):
|
||||||
session.args['argv'] = shlex.split(session.current_arg)
|
session.args['argv'] = shlex.split(session.current_arg)
|
||||||
|
|
||||||
cmd.args_parser_func = shell_like_args_parser
|
cmd.args_parser_func = shell_like_args_parser
|
||||||
|
|
||||||
current_parent = _registry
|
CommandManager.add_command(cmd_name, cmd)
|
||||||
for parent_key in cmd_name[:-1]:
|
CommandManager.add_aliases(aliases, cmd)
|
||||||
current_parent[parent_key] = current_parent.get(parent_key) or {}
|
|
||||||
current_parent = current_parent[parent_key]
|
|
||||||
if not isinstance(current_parent, dict):
|
|
||||||
warnings.warn(f'{current_parent} is not a registry dict')
|
|
||||||
return func
|
|
||||||
if cmd_name[-1] in current_parent:
|
|
||||||
warnings.warn(f'There is already a command named {cmd_name}')
|
|
||||||
return func
|
|
||||||
current_parent[cmd_name[-1]] = cmd
|
|
||||||
|
|
||||||
nonlocal aliases
|
return cmd
|
||||||
if isinstance(aliases, str):
|
|
||||||
aliases = (aliases,)
|
|
||||||
for alias in aliases:
|
|
||||||
_aliases[alias] = cmd_name
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
return deco
|
return deco
|
||||||
|
|
||||||
|
|
||||||
def _find_command(name: Union[str, CommandName_T]) -> Optional[Command]:
|
|
||||||
cmd_name = (name,) if isinstance(name, str) else name
|
|
||||||
if not cmd_name:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cmd_tree = _registry
|
|
||||||
for part in cmd_name[:-1]:
|
|
||||||
if part not in cmd_tree or not isinstance(cmd_tree[part], dict):
|
|
||||||
return None
|
|
||||||
cmd_tree = cmd_tree[part]
|
|
||||||
|
|
||||||
cmd = cmd_tree.get(cmd_name[-1])
|
|
||||||
return cmd if isinstance(cmd, Command) else None
|
|
||||||
|
|
||||||
|
|
||||||
class _PauseException(Exception):
|
class _PauseException(Exception):
|
||||||
"""
|
"""
|
||||||
Raised by session.pause() indicating that the command session
|
Raised by session.pause() indicating that the command session
|
||||||
@ -262,13 +447,18 @@ class SwitchException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class CommandSession(BaseSession):
|
class CommandSession(BaseSession):
|
||||||
__slots__ = ('cmd',
|
__slots__ = ('cmd', 'current_key', 'current_arg_filters',
|
||||||
'current_key', 'current_arg_filters', '_current_send_kwargs',
|
'_current_send_kwargs', 'current_arg', '_current_arg_text',
|
||||||
'current_arg', '_current_arg_text', '_current_arg_images',
|
'_current_arg_images', '_state', '_last_interaction',
|
||||||
'_state', '_last_interaction', '_running', '_run_future')
|
'_running', '_run_future')
|
||||||
|
|
||||||
def __init__(self, bot: NoneBot, event: CQEvent, cmd: Command, *,
|
def __init__(self,
|
||||||
current_arg: str = '', args: Optional[CommandArgs_T] = None):
|
bot: NoneBot,
|
||||||
|
event: CQEvent,
|
||||||
|
cmd: Command,
|
||||||
|
*,
|
||||||
|
current_arg: Optional[str] = '',
|
||||||
|
args: Optional[CommandArgs_T] = None):
|
||||||
super().__init__(bot, event)
|
super().__init__(bot, event)
|
||||||
self.cmd = cmd # Command object
|
self.cmd = cmd # Command object
|
||||||
|
|
||||||
@ -281,7 +471,7 @@ class CommandSession(BaseSession):
|
|||||||
self._current_send_kwargs: Dict[str, Any] = {}
|
self._current_send_kwargs: Dict[str, Any] = {}
|
||||||
|
|
||||||
# initialize current argument
|
# initialize current argument
|
||||||
self.current_arg: str = '' # with potential CQ codes
|
self.current_arg: Optional[str] = '' # with potential CQ codes
|
||||||
self._current_arg_text = None
|
self._current_arg_text = None
|
||||||
self._current_arg_images = None
|
self._current_arg_images = None
|
||||||
self.refresh(event, current_arg=current_arg) # fill the above
|
self.refresh(event, current_arg=current_arg) # fill the above
|
||||||
@ -353,7 +543,8 @@ class CommandSession(BaseSession):
|
|||||||
"""
|
"""
|
||||||
if self._current_arg_images is None:
|
if self._current_arg_images is None:
|
||||||
self._current_arg_images = [
|
self._current_arg_images = [
|
||||||
s.data['url'] for s in Message(self.current_arg)
|
s.data['url']
|
||||||
|
for s in Message(self.current_arg)
|
||||||
if s.type == 'image' and 'url' in s.data
|
if s.type == 'image' and 'url' in s.data
|
||||||
]
|
]
|
||||||
return self._current_arg_images
|
return self._current_arg_images
|
||||||
@ -366,7 +557,8 @@ class CommandSession(BaseSession):
|
|||||||
"""
|
"""
|
||||||
return self.state.get('argv', [])
|
return self.state.get('argv', [])
|
||||||
|
|
||||||
def refresh(self, event: CQEvent, *, current_arg: str = '') -> None:
|
def refresh(self, event: CQEvent, *,
|
||||||
|
current_arg: Optional[str] = '') -> None:
|
||||||
"""
|
"""
|
||||||
Refill the session with a new message event.
|
Refill the session with a new message event.
|
||||||
|
|
||||||
@ -378,7 +570,9 @@ class CommandSession(BaseSession):
|
|||||||
self._current_arg_text = None
|
self._current_arg_text = None
|
||||||
self._current_arg_images = None
|
self._current_arg_images = None
|
||||||
|
|
||||||
def get(self, key: str, *,
|
def get(self,
|
||||||
|
key: str,
|
||||||
|
*,
|
||||||
prompt: Optional[Message_T] = None,
|
prompt: Optional[Message_T] = None,
|
||||||
arg_filters: Optional[List[Filter_T]] = None,
|
arg_filters: Optional[List[Filter_T]] = None,
|
||||||
**kwargs) -> Any:
|
**kwargs) -> Any:
|
||||||
@ -444,79 +638,8 @@ class CommandSession(BaseSession):
|
|||||||
raise SwitchException(new_message)
|
raise SwitchException(new_message)
|
||||||
|
|
||||||
|
|
||||||
def parse_command(bot: NoneBot,
|
async def handle_command(bot: NoneBot, event: CQEvent,
|
||||||
cmd_string: str) -> Tuple[Optional[Command], Optional[str]]:
|
manager: CommandManager) -> Optional[bool]:
|
||||||
"""
|
|
||||||
Parse a command string (typically from a message).
|
|
||||||
|
|
||||||
:param bot: NoneBot instance
|
|
||||||
:param cmd_string: command string
|
|
||||||
:return: (Command object, current arg string)
|
|
||||||
"""
|
|
||||||
logger.debug(f'Parsing command: {repr(cmd_string)}')
|
|
||||||
|
|
||||||
matched_start = None
|
|
||||||
for start in bot.config.COMMAND_START:
|
|
||||||
# loop through COMMAND_START to find the longest matched start
|
|
||||||
curr_matched_start = None
|
|
||||||
if isinstance(start, type(re.compile(''))):
|
|
||||||
m = start.search(cmd_string)
|
|
||||||
if m and m.start(0) == 0:
|
|
||||||
curr_matched_start = m.group(0)
|
|
||||||
elif isinstance(start, str):
|
|
||||||
if cmd_string.startswith(start):
|
|
||||||
curr_matched_start = start
|
|
||||||
|
|
||||||
if curr_matched_start is not None and \
|
|
||||||
(matched_start is None or
|
|
||||||
len(curr_matched_start) > len(matched_start)):
|
|
||||||
# a longer start, use it
|
|
||||||
matched_start = curr_matched_start
|
|
||||||
|
|
||||||
if matched_start is None:
|
|
||||||
# it's not a command
|
|
||||||
logger.debug('It\'s not a command')
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
logger.debug(f'Matched command start: '
|
|
||||||
f'{matched_start}{"(empty)" if not matched_start else ""}')
|
|
||||||
full_command = cmd_string[len(matched_start):].lstrip()
|
|
||||||
|
|
||||||
if not full_command:
|
|
||||||
# command is empty
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
cmd_name_text, *cmd_remained = full_command.split(maxsplit=1)
|
|
||||||
cmd_name = _aliases.get(cmd_name_text)
|
|
||||||
|
|
||||||
if not cmd_name:
|
|
||||||
for sep in bot.config.COMMAND_SEP:
|
|
||||||
# loop through COMMAND_SEP to find the most optimized split
|
|
||||||
curr_cmd_name = None
|
|
||||||
if isinstance(sep, type(re.compile(''))):
|
|
||||||
curr_cmd_name = tuple(sep.split(cmd_name_text))
|
|
||||||
elif isinstance(sep, str):
|
|
||||||
curr_cmd_name = tuple(cmd_name_text.split(sep))
|
|
||||||
|
|
||||||
if curr_cmd_name is not None and \
|
|
||||||
(not cmd_name or len(curr_cmd_name) > len(cmd_name)):
|
|
||||||
# a more optimized split, use it
|
|
||||||
cmd_name = curr_cmd_name
|
|
||||||
|
|
||||||
if not cmd_name:
|
|
||||||
cmd_name = (cmd_name_text,)
|
|
||||||
|
|
||||||
logger.debug(f'Split command name: {cmd_name}')
|
|
||||||
cmd = _find_command(cmd_name)
|
|
||||||
if not cmd:
|
|
||||||
logger.debug(f'Command {cmd_name} not found')
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
logger.debug(f'Command {cmd.name} found, function: {cmd.func}')
|
|
||||||
return cmd, ''.join(cmd_remained)
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_command(bot: NoneBot, event: CQEvent) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Handle a message as a command.
|
Handle a message as a command.
|
||||||
|
|
||||||
@ -524,13 +647,14 @@ async def handle_command(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
|
|
||||||
:param bot: NoneBot instance
|
:param bot: NoneBot instance
|
||||||
:param event: message event
|
:param event: message event
|
||||||
|
:param manager: command manager
|
||||||
:return: the message is handled as a command
|
:return: the message is handled as a command
|
||||||
"""
|
"""
|
||||||
cmd, current_arg = parse_command(bot, str(event.message).lstrip())
|
cmd, current_arg = manager.parse_command(bot, str(event.message).lstrip())
|
||||||
is_privileged_cmd = cmd and cmd.privileged
|
is_privileged_cmd = cmd and cmd.privileged
|
||||||
if is_privileged_cmd and cmd.only_to_me and not event['to_me']:
|
if is_privileged_cmd and cmd.only_to_me and not event['to_me']:
|
||||||
is_privileged_cmd = False
|
is_privileged_cmd = False
|
||||||
disable_interaction = is_privileged_cmd
|
disable_interaction = bool(is_privileged_cmd)
|
||||||
|
|
||||||
if is_privileged_cmd:
|
if is_privileged_cmd:
|
||||||
logger.debug(f'Command {cmd.name} is a privileged command')
|
logger.debug(f'Command {cmd.name} is a privileged command')
|
||||||
@ -551,10 +675,9 @@ async def handle_command(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
if session.running:
|
if session.running:
|
||||||
logger.warning(f'There is a session of command '
|
logger.warning(f'There is a session of command '
|
||||||
f'{session.cmd.name} running, notify the user')
|
f'{session.cmd.name} running, notify the user')
|
||||||
asyncio.ensure_future(send(
|
asyncio.ensure_future(
|
||||||
bot, event,
|
send(bot, event,
|
||||||
render_expression(bot.config.SESSION_RUNNING_EXPRESSION)
|
render_expression(bot.config.SESSION_RUNNING_EXPRESSION)))
|
||||||
))
|
|
||||||
# pretend we are successful, so that NLP won't handle it
|
# pretend we are successful, so that NLP won't handle it
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -582,16 +705,20 @@ async def handle_command(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
session = CommandSession(bot, event, cmd, current_arg=current_arg)
|
session = CommandSession(bot, event, cmd, current_arg=current_arg)
|
||||||
logger.debug(f'New session of command {session.cmd.name} created')
|
logger.debug(f'New session of command {session.cmd.name} created')
|
||||||
|
|
||||||
return await _real_run_command(session, ctx_id, check_perm=check_perm,
|
return await _real_run_command(session,
|
||||||
|
ctx_id,
|
||||||
|
check_perm=check_perm,
|
||||||
disable_interaction=disable_interaction)
|
disable_interaction=disable_interaction)
|
||||||
|
|
||||||
|
|
||||||
async def call_command(bot: NoneBot, event: CQEvent,
|
async def call_command(bot: NoneBot,
|
||||||
name: Union[str, CommandName_T], *,
|
event: CQEvent,
|
||||||
|
name: Union[str, CommandName_T],
|
||||||
|
*,
|
||||||
current_arg: str = '',
|
current_arg: str = '',
|
||||||
args: Optional[CommandArgs_T] = None,
|
args: Optional[CommandArgs_T] = None,
|
||||||
check_perm: bool = True,
|
check_perm: bool = True,
|
||||||
disable_interaction: bool = False) -> bool:
|
disable_interaction: bool = False) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
Call a command internally.
|
Call a command internally.
|
||||||
|
|
||||||
@ -612,22 +739,24 @@ async def call_command(bot: NoneBot, event: CQEvent,
|
|||||||
:param disable_interaction: disable the command's further interaction
|
:param disable_interaction: disable the command's further interaction
|
||||||
:return: the command is successfully called
|
:return: the command is successfully called
|
||||||
"""
|
"""
|
||||||
cmd = _find_command(name)
|
cmd = CommandManager()._find_command(name)
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return False
|
return False
|
||||||
session = CommandSession(bot, event, cmd,
|
session = CommandSession(bot,
|
||||||
current_arg=current_arg, args=args)
|
event,
|
||||||
return await _real_run_command(
|
cmd,
|
||||||
session, context_id(session.event),
|
current_arg=current_arg,
|
||||||
|
args=args)
|
||||||
|
return await _real_run_command(session,
|
||||||
|
context_id(session.event),
|
||||||
check_perm=check_perm,
|
check_perm=check_perm,
|
||||||
disable_interaction=disable_interaction
|
disable_interaction=disable_interaction)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _real_run_command(session: CommandSession,
|
async def _real_run_command(session: CommandSession,
|
||||||
ctx_id: str,
|
ctx_id: str,
|
||||||
disable_interaction: bool = False,
|
disable_interaction: bool = False,
|
||||||
**kwargs) -> bool:
|
**kwargs) -> Optional[bool]:
|
||||||
if not disable_interaction:
|
if not disable_interaction:
|
||||||
# override session only when interaction is not disabled
|
# override session only when interaction is not disabled
|
||||||
_sessions[ctx_id] = session
|
_sessions[ctx_id] = session
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Callable
|
from typing import Callable, Iterable
|
||||||
|
|
||||||
from aiocqhttp import Event as CQEvent
|
from aiocqhttp import Event as CQEvent
|
||||||
from aiocqhttp.message import *
|
from aiocqhttp.message import escape, unescape, Message, MessageSegment
|
||||||
|
|
||||||
from . import NoneBot
|
from . import NoneBot
|
||||||
from .command import handle_command, SwitchException
|
|
||||||
from .log import logger
|
from .log import logger
|
||||||
from .natural_language import handle_natural_language
|
from .natural_language import handle_natural_language
|
||||||
|
from .command import handle_command, SwitchException
|
||||||
|
from .plugin import PluginManager
|
||||||
|
|
||||||
_message_preprocessors = set()
|
_message_preprocessors = set()
|
||||||
|
|
||||||
@ -22,11 +24,12 @@ async def handle_message(bot: NoneBot, event: CQEvent) -> None:
|
|||||||
|
|
||||||
assert isinstance(event.message, Message)
|
assert isinstance(event.message, Message)
|
||||||
if not event.message:
|
if not event.message:
|
||||||
event.message.append(MessageSegment.text(''))
|
event.message.append(MessageSegment.text('')) # type: ignore
|
||||||
|
|
||||||
coros = []
|
coros = []
|
||||||
|
plugin_manager = PluginManager()
|
||||||
for preprocessor in _message_preprocessors:
|
for preprocessor in _message_preprocessors:
|
||||||
coros.append(preprocessor(bot, event))
|
coros.append(preprocessor(bot, event, plugin_manager))
|
||||||
if coros:
|
if coros:
|
||||||
await asyncio.wait(coros)
|
await asyncio.wait(coros)
|
||||||
|
|
||||||
@ -37,7 +40,7 @@ async def handle_message(bot: NoneBot, event: CQEvent) -> None:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
handled = await handle_command(bot, event)
|
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
|
||||||
@ -47,7 +50,7 @@ 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)
|
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')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Iterable, Optional, Callable, Union, NamedTuple
|
import warnings
|
||||||
|
from typing import Set, Iterable, Optional, Callable, Union, NamedTuple
|
||||||
|
|
||||||
from aiocqhttp import Event as CQEvent
|
from aiocqhttp import Event as CQEvent
|
||||||
|
|
||||||
@ -10,13 +11,10 @@ from .message import Message
|
|||||||
from .session import BaseSession
|
from .session import BaseSession
|
||||||
from .typing import CommandName_T, CommandArgs_T
|
from .typing import CommandName_T, CommandArgs_T
|
||||||
|
|
||||||
_nl_processors = set()
|
|
||||||
|
|
||||||
|
|
||||||
class NLProcessor:
|
class NLProcessor:
|
||||||
__slots__ = ('func', 'keywords', 'permission',
|
__slots__ = ('func', 'keywords', 'permission', 'only_to_me',
|
||||||
'only_to_me', 'only_short_message',
|
'only_short_message', 'allow_empty_message')
|
||||||
'allow_empty_message')
|
|
||||||
|
|
||||||
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,
|
||||||
@ -29,8 +27,80 @@ class NLProcessor:
|
|||||||
self.allow_empty_message = allow_empty_message
|
self.allow_empty_message = allow_empty_message
|
||||||
|
|
||||||
|
|
||||||
|
class NLPManager:
|
||||||
|
_nl_processors: Set[NLProcessor] = set()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.nl_processors = NLPManager._nl_processors.copy()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_nl_processor(cls, processor: NLProcessor) -> None:
|
||||||
|
"""Register a natural language processor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processor (NLProcessor): Processor object
|
||||||
|
"""
|
||||||
|
if processor in cls._nl_processors:
|
||||||
|
warnings.warn(f"NLProcessor {processor} already exists")
|
||||||
|
return
|
||||||
|
cls._nl_processors.add(processor)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove_nl_processor(cls, processor: NLProcessor) -> bool:
|
||||||
|
"""Remove a natural language processor globally
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processor (NLProcessor): Processor to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Success or not
|
||||||
|
"""
|
||||||
|
if processor in cls._nl_processors:
|
||||||
|
cls._nl_processors.remove(processor)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def switch_processor_global(cls,
|
||||||
|
processor: NLProcessor,
|
||||||
|
state: Optional[bool] = None) -> Optional[bool]:
|
||||||
|
"""Remove or add a processor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processor (NLProcessor): Processor object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if removed, False if added
|
||||||
|
"""
|
||||||
|
if processor in cls._nl_processors and not state:
|
||||||
|
cls._nl_processors.remove(processor)
|
||||||
|
return True
|
||||||
|
elif processor not in cls._nl_processors and state != False:
|
||||||
|
cls._nl_processors.add(processor)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def switch_processor(self,
|
||||||
|
processor: NLProcessor,
|
||||||
|
state: Optional[bool] = None) -> Optional[bool]:
|
||||||
|
"""Remove or add processor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processor (NLProcessor): Processor to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if removed, False if added
|
||||||
|
"""
|
||||||
|
if processor in self.nl_processors and not state:
|
||||||
|
self.nl_processors.remove(processor)
|
||||||
|
return True
|
||||||
|
elif processor not in self.nl_processors and state != False:
|
||||||
|
self.nl_processors.add(processor)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def on_natural_language(
|
def on_natural_language(
|
||||||
keywords: Union[Optional[Iterable], str, Callable] = None, *,
|
keywords: Union[Optional[Iterable], str, Callable] = None,
|
||||||
|
*,
|
||||||
permission: int = perm.EVERYBODY,
|
permission: int = perm.EVERYBODY,
|
||||||
only_to_me: bool = True,
|
only_to_me: bool = True,
|
||||||
only_short_message: bool = True,
|
only_short_message: bool = True,
|
||||||
@ -45,14 +115,16 @@ def on_natural_language(
|
|||||||
:param allow_empty_message: handle empty messages
|
:param allow_empty_message: handle empty messages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def deco(func: Callable) -> Callable:
|
def deco(func: Callable) -> NLProcessor:
|
||||||
nl_processor = NLProcessor(func=func, keywords=keywords,
|
nl_processor = NLProcessor(
|
||||||
|
func=func,
|
||||||
|
keywords=keywords, # type: ignore
|
||||||
permission=permission,
|
permission=permission,
|
||||||
only_to_me=only_to_me,
|
only_to_me=only_to_me,
|
||||||
only_short_message=only_short_message,
|
only_short_message=only_short_message,
|
||||||
allow_empty_message=allow_empty_message)
|
allow_empty_message=allow_empty_message)
|
||||||
_nl_processors.add(nl_processor)
|
NLPManager.add_nl_processor(nl_processor)
|
||||||
return func
|
return nl_processor
|
||||||
|
|
||||||
if isinstance(keywords, Callable):
|
if isinstance(keywords, Callable):
|
||||||
# here "keywords" is the function to be decorated
|
# here "keywords" is the function to be decorated
|
||||||
@ -71,8 +143,11 @@ class NLPSession(BaseSession):
|
|||||||
self.msg = msg
|
self.msg = msg
|
||||||
tmp_msg = Message(msg)
|
tmp_msg = Message(msg)
|
||||||
self.msg_text = tmp_msg.extract_plain_text()
|
self.msg_text = tmp_msg.extract_plain_text()
|
||||||
self.msg_images = [s.data['url'] for s in tmp_msg
|
self.msg_images = [
|
||||||
if s.type == 'image' and 'url' in s.data]
|
s.data['url']
|
||||||
|
for s in tmp_msg
|
||||||
|
if s.type == 'image' and 'url' in s.data
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class NLPResult(NamedTuple):
|
class NLPResult(NamedTuple):
|
||||||
@ -100,7 +175,8 @@ class IntentCommand(NamedTuple):
|
|||||||
current_arg: str = ''
|
current_arg: str = ''
|
||||||
|
|
||||||
|
|
||||||
async def handle_natural_language(bot: NoneBot, event: CQEvent) -> bool:
|
async def handle_natural_language(bot: NoneBot, event: CQEvent,
|
||||||
|
manager: NLPManager) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle a message as natural language.
|
Handle a message as natural language.
|
||||||
|
|
||||||
@ -108,6 +184,7 @@ async def handle_natural_language(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
|
|
||||||
:param bot: NoneBot instance
|
:param bot: NoneBot instance
|
||||||
:param event: message event
|
:param event: message event
|
||||||
|
:param manager: natural language processor manager
|
||||||
:return: the message is handled as natural language
|
:return: the message is handled as natural language
|
||||||
"""
|
"""
|
||||||
session = NLPSession(bot, event, str(event.message))
|
session = NLPSession(bot, event, str(event.message))
|
||||||
@ -117,7 +194,7 @@ async def handle_natural_language(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
msg_text_length = len(session.msg_text)
|
msg_text_length = len(session.msg_text)
|
||||||
|
|
||||||
futures = []
|
futures = []
|
||||||
for p in _nl_processors:
|
for p in manager.nl_processors:
|
||||||
if not p.allow_empty_message and not session.msg:
|
if not p.allow_empty_message and not session.msg:
|
||||||
# don't allow empty msg, but it is one, so skip to next
|
# don't allow empty msg, but it is one, so skip to next
|
||||||
continue
|
continue
|
||||||
@ -164,12 +241,12 @@ async def handle_natural_language(bot: NoneBot, event: CQEvent) -> bool:
|
|||||||
chosen_cmd = intent_commands[0]
|
chosen_cmd = intent_commands[0]
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'Intent command with highest confidence: {chosen_cmd}')
|
f'Intent command with highest confidence: {chosen_cmd}')
|
||||||
return await call_command(
|
return await call_command(bot,
|
||||||
bot, event, chosen_cmd.name,
|
event,
|
||||||
|
chosen_cmd.name,
|
||||||
args=chosen_cmd.args,
|
args=chosen_cmd.args,
|
||||||
current_arg=chosen_cmd.current_arg,
|
current_arg=chosen_cmd.current_arg,
|
||||||
check_perm=False
|
check_perm=False) # type: ignore
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.debug('No intent command has enough confidence')
|
logger.debug('No intent command has enough confidence')
|
||||||
return False
|
return False
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional, Callable, Union
|
from typing import List, Optional, Callable, Union
|
||||||
|
|
||||||
from aiocqhttp import Event as CQEvent
|
from aiocqhttp import Event as CQEvent
|
||||||
from aiocqhttp.bus import EventBus
|
from aiocqhttp.bus import EventBus
|
||||||
@ -10,20 +10,29 @@ from .session import BaseSession
|
|||||||
|
|
||||||
_bus = EventBus()
|
_bus = EventBus()
|
||||||
|
|
||||||
|
class EventHandler:
|
||||||
|
__slots__ = ('events', 'func')
|
||||||
|
|
||||||
|
def __init__(self, events: List[str], func: Callable):
|
||||||
|
self.events = events
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
|
||||||
def _make_event_deco(post_type: str) -> Callable:
|
def _make_event_deco(post_type: str) -> Callable:
|
||||||
def deco_deco(arg: Optional[Union[str, Callable]] = None,
|
def deco_deco(arg: Optional[Union[str, Callable]] = None,
|
||||||
*events: str) -> Callable:
|
*events: str) -> Callable:
|
||||||
def deco(func: Callable) -> Callable:
|
def deco(func: Callable) -> EventHandler:
|
||||||
if isinstance(arg, str):
|
if isinstance(arg, str):
|
||||||
for e in [arg] + list(events):
|
events_tmp = list(map(lambda x: f"{post_type}.{x}", [arg] + list(events)))
|
||||||
_bus.subscribe(f'{post_type}.{e}', func)
|
for e in events_tmp:
|
||||||
|
_bus.subscribe(e, func)
|
||||||
|
return EventHandler(events_tmp, func)
|
||||||
else:
|
else:
|
||||||
_bus.subscribe(post_type, func)
|
_bus.subscribe(post_type, func)
|
||||||
return func
|
return EventHandler([post_type], func)
|
||||||
|
|
||||||
if isinstance(arg, Callable):
|
if isinstance(arg, Callable):
|
||||||
return deco(arg)
|
return deco(arg) # type: ignore
|
||||||
return deco
|
return deco
|
||||||
|
|
||||||
return deco_deco
|
return deco_deco
|
||||||
|
@ -1,26 +1,111 @@
|
|||||||
import importlib
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Set, Optional
|
import warnings
|
||||||
|
import importlib
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, Set, Dict, Optional
|
||||||
|
|
||||||
from .log import logger
|
from .log import logger
|
||||||
|
from .command import Command, CommandManager
|
||||||
|
from .natural_language import NLProcessor, NLPManager
|
||||||
|
from .notice_request import _bus, EventHandler
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
__slots__ = ('module', 'name', 'usage')
|
__slots__ = ('module', 'name', 'usage', 'commands', 'nl_processors', 'event_handlers')
|
||||||
|
|
||||||
def __init__(self, module: Any,
|
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(),
|
||||||
|
nl_processors: Set[NLProcessor] = set(),
|
||||||
|
event_handlers: Set[EventHandler] = set()):
|
||||||
self.module = module
|
self.module = module
|
||||||
self.name = name
|
self.name = name
|
||||||
self.usage = usage
|
self.usage = usage
|
||||||
|
self.commands = commands
|
||||||
|
self.nl_processors = nl_processors
|
||||||
|
self.event_handlers = event_handlers
|
||||||
|
|
||||||
|
class PluginManager:
|
||||||
|
_plugins: Dict[str, Plugin] = {}
|
||||||
|
_anonymous_plugins: Set[Plugin] = set()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.cmd_manager = CommandManager()
|
||||||
|
self.nlp_manager = NLPManager()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_plugin(cls, plugin: Plugin) -> None:
|
||||||
|
"""Register a plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin (Plugin): Plugin object
|
||||||
|
"""
|
||||||
|
if plugin.name:
|
||||||
|
if plugin.name in cls._plugins:
|
||||||
|
warnings.warn(f"Plugin {plugin.name} already exists")
|
||||||
|
return
|
||||||
|
cls._plugins[plugin.name] = plugin
|
||||||
|
else:
|
||||||
|
cls._anonymous_plugins.add(plugin)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin(cls, name: str) -> Optional[Plugin]:
|
||||||
|
return cls._plugins.get(name)
|
||||||
|
|
||||||
|
# TODO: plugin重加载
|
||||||
|
@classmethod
|
||||||
|
def reload_plugin(cls, plugin: Plugin) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Plugin name
|
||||||
|
state (Optional[bool]): State to change to. Defaults to None.
|
||||||
|
"""
|
||||||
|
plugin = cls.get_plugin(name)
|
||||||
|
if not plugin:
|
||||||
|
warnings.warn(f"Plugin {name} not found")
|
||||||
|
return
|
||||||
|
for command in plugin.commands:
|
||||||
|
CommandManager.switch_command_global(command.name, state)
|
||||||
|
for nl_processor in plugin.nl_processors:
|
||||||
|
NLPManager.switch_processor_global(nl_processor, state)
|
||||||
|
for event_handler in plugin.event_handlers:
|
||||||
|
for event in event_handler.events:
|
||||||
|
if event_handler.func in _bus._subscribers[event] and not state:
|
||||||
|
_bus.unsubscribe(event, event_handler.func)
|
||||||
|
elif event_handler.func not in _bus._subscribers[event] and state != False:
|
||||||
|
_bus.subscribe(event, event_handler.func)
|
||||||
|
|
||||||
|
def switch_plugin(self, name: str, state: Optional[bool] = None) -> None:
|
||||||
|
"""Change plugin state or simply switch it if `state` is None
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Plugin name
|
||||||
|
state (Optional[bool]): State to change to. Defaults to None.
|
||||||
|
"""
|
||||||
|
plugin = self.get_plugin(name)
|
||||||
|
if not plugin:
|
||||||
|
warnings.warn(f"Plugin {name} not found")
|
||||||
|
return
|
||||||
|
for command in plugin.commands:
|
||||||
|
self.cmd_manager.switch_command(command.name, state)
|
||||||
|
for nl_processor in plugin.nl_processors:
|
||||||
|
self.nlp_manager.switch_processor(nl_processor, state)
|
||||||
|
# for event_handler in plugin.event_handlers:
|
||||||
|
# for event in event_handler.events:
|
||||||
|
# if event_handler.func in _bus._subscribers[event] and not state:
|
||||||
|
# _bus.unsubscribe(event, event_handler.func)
|
||||||
|
# elif event_handler.func not in _bus._subscribers[event] and state != False:
|
||||||
|
# _bus.subscribe(event, event_handler.func)
|
||||||
|
|
||||||
|
|
||||||
_plugins: Set[Plugin] = set()
|
def load_plugin(module_name: str) -> Optional[Plugin]:
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(module_name: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Load a module as a plugin.
|
Load a module as a plugin.
|
||||||
|
|
||||||
@ -31,16 +116,33 @@ def load_plugin(module_name: str) -> bool:
|
|||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
name = getattr(module, '__plugin_name__', None)
|
name = getattr(module, '__plugin_name__', None)
|
||||||
usage = getattr(module, '__plugin_usage__', None)
|
usage = getattr(module, '__plugin_usage__', None)
|
||||||
_plugins.add(Plugin(module, name, usage))
|
commands = set()
|
||||||
|
nl_processors = set()
|
||||||
|
event_handlers = set()
|
||||||
|
for attr in dir(module):
|
||||||
|
func = getattr(module, attr)
|
||||||
|
if isinstance(func, Command):
|
||||||
|
commands.add(func)
|
||||||
|
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}"')
|
logger.info(f'Succeeded to import "{module_name}"')
|
||||||
return True
|
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_name}", error: {e}')
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(plugin_dir: str, module_prefix: str) -> int:
|
# TODO: plugin重加载
|
||||||
|
def reload_plugin(module_name: str) -> Optional[Plugin]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugins(plugin_dir: str, module_prefix: str) -> Set[Plugin]:
|
||||||
"""
|
"""
|
||||||
Find all non-hidden modules or packages in a given directory,
|
Find all non-hidden modules or packages in a given directory,
|
||||||
and import them with the given module prefix.
|
and import them with the given module prefix.
|
||||||
@ -49,7 +151,7 @@ def load_plugins(plugin_dir: str, module_prefix: str) -> int:
|
|||||||
:param module_prefix: module prefix used while importing
|
:param module_prefix: module prefix used while importing
|
||||||
:return: number of plugins successfully loaded
|
:return: number of plugins successfully loaded
|
||||||
"""
|
"""
|
||||||
count = 0
|
count = set()
|
||||||
for name in os.listdir(plugin_dir):
|
for name in os.listdir(plugin_dir):
|
||||||
path = os.path.join(plugin_dir, name)
|
path = os.path.join(plugin_dir, name)
|
||||||
if os.path.isfile(path) and \
|
if os.path.isfile(path) and \
|
||||||
@ -64,12 +166,13 @@ def load_plugins(plugin_dir: str, module_prefix: str) -> int:
|
|||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if load_plugin(f'{module_prefix}.{m.group(1)}'):
|
result = load_plugin(f'{module_prefix}.{m.group(1)}')
|
||||||
count += 1
|
if result:
|
||||||
|
count.add(result)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def load_builtin_plugins() -> int:
|
def load_builtin_plugins() -> Set[Plugin]:
|
||||||
"""
|
"""
|
||||||
Load built-in plugins distributed along with "nonebot" package.
|
Load built-in plugins distributed along with "nonebot" package.
|
||||||
"""
|
"""
|
||||||
@ -83,4 +186,4 @@ def get_loaded_plugins() -> Set[Plugin]:
|
|||||||
|
|
||||||
:return: a set of Plugin objects
|
:return: a set of Plugin objects
|
||||||
"""
|
"""
|
||||||
return _plugins
|
return set(PluginManager._plugins.values()) | PluginManager._anonymous_plugins
|
||||||
|
Loading…
Reference in New Issue
Block a user