mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-09-07 04:26:45 +00:00
Rename package to "nonebot"
This commit is contained in:
169
nonebot/__init__.py
Normal file
169
nonebot/__init__.py
Normal file
@ -0,0 +1,169 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
import aiocqhttp.message
|
||||
from aiocqhttp import CQHttp
|
||||
|
||||
from .log import logger
|
||||
from .sched import Scheduler
|
||||
|
||||
if Scheduler:
|
||||
scheduler = Scheduler()
|
||||
else:
|
||||
scheduler = None
|
||||
|
||||
|
||||
class NoneBot(CQHttp):
|
||||
def __init__(self, config_object: Optional[Any] = None):
|
||||
if config_object is None:
|
||||
from . import default_config as config_object
|
||||
|
||||
config_dict = {k: v for k, v in config_object.__dict__.items()
|
||||
if k.isupper() and not k.startswith('_')}
|
||||
logger.debug(f'Loaded configurations: {config_dict}')
|
||||
super().__init__(message_class=aiocqhttp.message.Message,
|
||||
**{k.lower(): v for k, v in config_dict.items()})
|
||||
|
||||
self.config = config_object
|
||||
self.asgi.debug = self.config.DEBUG
|
||||
|
||||
from .message import handle_message
|
||||
from .notice_request import handle_notice_or_request
|
||||
|
||||
@self.on_message
|
||||
async def _(ctx):
|
||||
asyncio.ensure_future(handle_message(self, ctx))
|
||||
|
||||
@self.on_notice
|
||||
async def _(ctx):
|
||||
asyncio.ensure_future(handle_notice_or_request(self, ctx))
|
||||
|
||||
@self.on_request
|
||||
async def _(ctx):
|
||||
asyncio.ensure_future(handle_notice_or_request(self, ctx))
|
||||
|
||||
def run(self, host: Optional[str] = None, port: Optional[int] = None,
|
||||
*args, **kwargs) -> None:
|
||||
host = host or self.config.HOST
|
||||
port = port or self.config.PORT
|
||||
if 'debug' not in kwargs:
|
||||
kwargs['debug'] = self.config.DEBUG
|
||||
|
||||
logger.info(f'Running on {host}:{port}')
|
||||
super().run(host=host, port=port, loop=asyncio.get_event_loop(),
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
_bot: Optional[NoneBot] = None
|
||||
|
||||
|
||||
def init(config_object: Optional[Any] = None) -> None:
|
||||
"""
|
||||
Initialize NoneBot instance.
|
||||
|
||||
This function must be called at the very beginning of code,
|
||||
otherwise the get_bot() function will return None and nothing
|
||||
is gonna work properly.
|
||||
|
||||
:param config_object: configuration object
|
||||
"""
|
||||
global _bot
|
||||
_bot = NoneBot(config_object)
|
||||
|
||||
if _bot.config.DEBUG:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if scheduler and not scheduler.running:
|
||||
scheduler.configure(_bot.config.APSCHEDULER_CONFIG)
|
||||
scheduler.start()
|
||||
|
||||
|
||||
def get_bot() -> NoneBot:
|
||||
"""
|
||||
Get the NoneBot instance.
|
||||
|
||||
The result is ensured to be not None, otherwise an exception will
|
||||
be raised.
|
||||
|
||||
:raise ValueError: instance not initialized
|
||||
"""
|
||||
if _bot is None:
|
||||
raise ValueError('NoneBot instance has not been initialized')
|
||||
return _bot
|
||||
|
||||
|
||||
def run(host: Optional[str] = None, port: Optional[int] = None,
|
||||
*args, **kwargs) -> None:
|
||||
"""Run the NoneBot instance."""
|
||||
get_bot().run(host=host, port=port, *args, **kwargs)
|
||||
|
||||
|
||||
_plugins = set()
|
||||
|
||||
|
||||
def load_plugin(module_name: str) -> bool:
|
||||
"""
|
||||
Load a module as a plugin.
|
||||
|
||||
:param module_name: name of module to import
|
||||
:return: successful or not
|
||||
"""
|
||||
try:
|
||||
_plugins.add(importlib.import_module(module_name))
|
||||
logger.info(f'Succeeded to import "{module_name}"')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to import "{module_name}", error: {e}')
|
||||
logger.exception(e)
|
||||
return False
|
||||
|
||||
|
||||
def load_plugins(plugin_dir: str, module_prefix: str) -> int:
|
||||
"""
|
||||
Find all non-hidden modules or packages in a given directory,
|
||||
and import them with the given module prefix.
|
||||
|
||||
:param plugin_dir: plugin directory to search
|
||||
:param module_prefix: module prefix used while importing
|
||||
:return: number of plugins successfully loaded
|
||||
"""
|
||||
count = 0
|
||||
for name in os.listdir(plugin_dir):
|
||||
path = os.path.join(plugin_dir, name)
|
||||
if os.path.isfile(path) and \
|
||||
(name.startswith('_') or not name.endswith('.py')):
|
||||
continue
|
||||
if os.path.isdir(path) and \
|
||||
(name.startswith('_') or not os.path.exists(
|
||||
os.path.join(path, '__init__.py'))):
|
||||
continue
|
||||
|
||||
m = re.match(r'([_A-Z0-9a-z]+)(.py)?', name)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
if load_plugin(f'{module_prefix}.{m.group(1)}'):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def load_builtin_plugins() -> int:
|
||||
"""
|
||||
Load built-in plugins distributed along with "nonebot" package.
|
||||
"""
|
||||
plugin_dir = os.path.join(os.path.dirname(__file__), 'plugins')
|
||||
return load_plugins(plugin_dir, 'nonebot.plugins')
|
||||
|
||||
|
||||
from .exceptions import *
|
||||
from .message import message_preprocessor, Message, MessageSegment
|
||||
from .command import on_command, CommandSession, CommandGroup
|
||||
from .natural_language import on_natural_language, NLPSession, NLPResult
|
||||
from .notice_request import (on_notice, NoticeSession,
|
||||
on_request, RequestSession)
|
44
nonebot/argparse.py
Normal file
44
nonebot/argparse.py
Normal file
@ -0,0 +1,44 @@
|
||||
from argparse import *
|
||||
|
||||
from .command import CommandSession
|
||||
|
||||
|
||||
class ParserExit(RuntimeError):
|
||||
def __init__(self, status=0, message=None):
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
|
||||
class ArgumentParser(ArgumentParser):
|
||||
"""
|
||||
An ArgumentParser wrapper that avoid printing messages to
|
||||
standard I/O.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = kwargs.pop('session', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _print_message(self, *args, **kwargs):
|
||||
# do nothing
|
||||
pass
|
||||
|
||||
def exit(self, status=0, message=None):
|
||||
raise ParserExit(status=status, message=message)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
def finish(msg):
|
||||
if self.session and isinstance(self.session, CommandSession):
|
||||
self.session.finish(msg)
|
||||
|
||||
if not args:
|
||||
finish(self.usage)
|
||||
else:
|
||||
try:
|
||||
return super().parse_args(args=args, namespace=namespace)
|
||||
except ParserExit as e:
|
||||
if e.status == 0:
|
||||
# --help
|
||||
finish(self.usage)
|
||||
else:
|
||||
finish('参数不足或不正确,请使用 --help 参数查询使用帮助')
|
616
nonebot/command.py
Normal file
616
nonebot/command.py
Normal file
@ -0,0 +1,616 @@
|
||||
import asyncio
|
||||
import re
|
||||
import shlex
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Tuple, Union, Callable, Iterable, Any, Optional, List
|
||||
)
|
||||
|
||||
from . import NoneBot, permission as perm
|
||||
from .helpers import context_id, send, render_expression
|
||||
from .log import logger
|
||||
from .message import Message
|
||||
from .session import BaseSession
|
||||
from .typing import (
|
||||
Context_T, CommandName_T, CommandArgs_T, Message_T
|
||||
)
|
||||
|
||||
# Key: str (one segment of command name)
|
||||
# Value: subtree or a leaf Command object
|
||||
_registry = {}
|
||||
|
||||
# Key: str
|
||||
# Value: tuple that identifies a command
|
||||
_aliases = {}
|
||||
|
||||
# Key: context id
|
||||
# Value: CommandSession object
|
||||
_sessions = {}
|
||||
|
||||
|
||||
class Command:
|
||||
__slots__ = ('name', 'func', 'permission',
|
||||
'only_to_me', 'privileged', 'args_parser_func')
|
||||
|
||||
def __init__(self, *, name: CommandName_T, func: Callable,
|
||||
permission: int, only_to_me: bool, privileged: bool):
|
||||
self.name = name
|
||||
self.func = func
|
||||
self.permission = permission
|
||||
self.only_to_me = only_to_me
|
||||
self.privileged = privileged
|
||||
self.args_parser_func = None
|
||||
|
||||
async def run(self, session, *,
|
||||
check_perm: bool = True,
|
||||
dry: bool = False) -> bool:
|
||||
"""
|
||||
Run the command in a given session.
|
||||
|
||||
:param session: CommandSession object
|
||||
:param check_perm: should check permission before running
|
||||
:param dry: just check any prerequisite, without actually running
|
||||
:return: the command is finished (or can be run, given dry == True)
|
||||
"""
|
||||
has_perm = await self._check_perm(session) if check_perm else True
|
||||
if self.func and has_perm:
|
||||
if dry:
|
||||
return True
|
||||
if self.args_parser_func:
|
||||
await self.args_parser_func(session)
|
||||
await self.func(session)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _check_perm(self, session) -> bool:
|
||||
"""
|
||||
Check if the session has sufficient permission to
|
||||
call the command.
|
||||
|
||||
:param session: CommandSession object
|
||||
:return: the session has the permission
|
||||
"""
|
||||
return await perm.check_permission(session.bot, session.ctx,
|
||||
self.permission)
|
||||
|
||||
|
||||
class CommandFunc:
|
||||
__slots__ = ('cmd', 'func')
|
||||
|
||||
def __init__(self, cmd: Command, func: Callable):
|
||||
self.cmd = cmd
|
||||
self.func = func
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
def args_parser(self, parser_func: Callable):
|
||||
"""
|
||||
Decorator to register a function as the arguments parser of
|
||||
the corresponding command.
|
||||
"""
|
||||
self.cmd.args_parser_func = parser_func
|
||||
return parser_func
|
||||
|
||||
|
||||
def on_command(name: Union[str, CommandName_T], *,
|
||||
aliases: Iterable[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: Callable) -> Callable:
|
||||
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
|
||||
|
||||
current_parent = _registry
|
||||
for parent_key in cmd_name[:-1]:
|
||||
current_parent[parent_key] = current_parent.get(parent_key) or {}
|
||||
current_parent = current_parent[parent_key]
|
||||
current_parent[cmd_name[-1]] = cmd
|
||||
|
||||
for alias in aliases:
|
||||
_aliases[alias] = cmd_name
|
||||
|
||||
return CommandFunc(cmd, func)
|
||||
|
||||
return deco
|
||||
|
||||
|
||||
class CommandGroup:
|
||||
"""
|
||||
Group a set of commands with same name prefix.
|
||||
"""
|
||||
__slots__ = ('basename', 'permission', 'only_to_me', 'privileged',
|
||||
'shell_like')
|
||||
|
||||
def __init__(self, name: Union[str, CommandName_T],
|
||||
permission: Optional[int] = None, *,
|
||||
only_to_me: Optional[bool] = None,
|
||||
privileged: Optional[bool] = None,
|
||||
shell_like: Optional[bool] = None):
|
||||
self.basename = (name,) if isinstance(name, str) else name
|
||||
self.permission = permission
|
||||
self.only_to_me = only_to_me
|
||||
self.privileged = privileged
|
||||
self.shell_like = shell_like
|
||||
|
||||
def command(self, name: Union[str, CommandName_T], *,
|
||||
aliases: Optional[Iterable[str]] = None,
|
||||
permission: Optional[int] = None,
|
||||
only_to_me: Optional[bool] = None,
|
||||
privileged: Optional[bool] = None,
|
||||
shell_like: Optional[bool] = None) -> Callable:
|
||||
sub_name = (name,) if isinstance(name, str) else name
|
||||
name = self.basename + sub_name
|
||||
|
||||
kwargs = {}
|
||||
if aliases is not None:
|
||||
kwargs['aliases'] = aliases
|
||||
if permission is not None:
|
||||
kwargs['permission'] = permission
|
||||
elif self.permission is not None:
|
||||
kwargs['permission'] = self.permission
|
||||
if only_to_me is not None:
|
||||
kwargs['only_to_me'] = only_to_me
|
||||
elif self.only_to_me is not None:
|
||||
kwargs['only_to_me'] = self.only_to_me
|
||||
if privileged is not None:
|
||||
kwargs['privileged'] = privileged
|
||||
elif self.privileged is not None:
|
||||
kwargs['privileged'] = self.privileged
|
||||
if shell_like is not None:
|
||||
kwargs['shell_like'] = shell_like
|
||||
elif self.shell_like is not None:
|
||||
kwargs['shell_like'] = self.shell_like
|
||||
return on_command(name, **kwargs)
|
||||
|
||||
|
||||
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 _FurtherInteractionNeeded(Exception):
|
||||
"""
|
||||
Raised by session.pause() indicating that the command should
|
||||
enter interactive mode to ask the user for some arguments.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _FinishException(Exception):
|
||||
"""
|
||||
Raised by session.finish() indicating that the command session
|
||||
should be stopped and removed.
|
||||
"""
|
||||
|
||||
def __init__(self, result: bool = True):
|
||||
"""
|
||||
:param result: succeeded to call the command
|
||||
"""
|
||||
self.result = result
|
||||
|
||||
|
||||
class SwitchException(Exception):
|
||||
"""
|
||||
Raised by session.switch() indicating that the command session
|
||||
should be stopped and replaced with a new one (going through
|
||||
handle_message() again).
|
||||
|
||||
Since the new context message will go through handle_message()
|
||||
again, the later function should be notified. So this exception
|
||||
is designed to be propagated to handle_message().
|
||||
"""
|
||||
|
||||
def __init__(self, new_ctx_message: Message):
|
||||
"""
|
||||
:param new_ctx_message: new message which should be placed in context
|
||||
"""
|
||||
self.new_ctx_message = new_ctx_message
|
||||
|
||||
|
||||
class CommandSession(BaseSession):
|
||||
__slots__ = ('cmd', 'current_key', 'current_arg', 'current_arg_text',
|
||||
'current_arg_images', 'args', '_last_interaction', '_running')
|
||||
|
||||
def __init__(self, bot: NoneBot, ctx: Context_T, cmd: Command, *,
|
||||
current_arg: str = '', args: Optional[CommandArgs_T] = None):
|
||||
super().__init__(bot, ctx)
|
||||
self.cmd = cmd # Command object
|
||||
self.current_key = None # current key that the command handler needs
|
||||
self.current_arg = None # current argument (with potential CQ codes)
|
||||
self.current_arg_text = None # current argument without any CQ codes
|
||||
self.current_arg_images = None # image urls in current argument
|
||||
self.refresh(ctx, current_arg=current_arg)
|
||||
self.args = args or {}
|
||||
self._last_interaction = None # last interaction time of this session
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
@running.setter
|
||||
def running(self, value) -> None:
|
||||
if self._running is True and value is False:
|
||||
# change status from running to not running, record the time
|
||||
self._last_interaction = datetime.now()
|
||||
self._running = value
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if the session is expired or not."""
|
||||
if self.bot.config.SESSION_EXPIRE_TIMEOUT and \
|
||||
self._last_interaction and \
|
||||
datetime.now() - self._last_interaction > \
|
||||
self.bot.config.SESSION_EXPIRE_TIMEOUT:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_first_run(self) -> bool:
|
||||
return self._last_interaction is None
|
||||
|
||||
@property
|
||||
def argv(self) -> List[str]:
|
||||
"""
|
||||
Shell-like argument list.
|
||||
Only available while shell_like is True in on_command decorator.
|
||||
"""
|
||||
return self.get_optional('argv', [])
|
||||
|
||||
def refresh(self, ctx: Context_T, *, current_arg: str = '') -> None:
|
||||
"""
|
||||
Refill the session with a new message context.
|
||||
|
||||
:param ctx: new message context
|
||||
:param current_arg: new command argument as a string
|
||||
"""
|
||||
self.ctx = ctx
|
||||
self.current_arg = current_arg
|
||||
current_arg_as_msg = Message(current_arg)
|
||||
self.current_arg_text = current_arg_as_msg.extract_plain_text()
|
||||
self.current_arg_images = [s.data['url'] for s in current_arg_as_msg
|
||||
if s.type == 'image' and 'url' in s.data]
|
||||
|
||||
def get(self, key: Any, *,
|
||||
prompt: Optional[Message_T] = None, **kwargs) -> Any:
|
||||
"""
|
||||
Get an argument with a given key.
|
||||
|
||||
If the argument does not exist in the current session,
|
||||
a FurtherInteractionNeeded exception will be raised,
|
||||
and the caller of the command will know it should keep
|
||||
the session for further interaction with the user.
|
||||
|
||||
:param key: argument key
|
||||
:param prompt: prompt to ask the user
|
||||
:return: the argument value
|
||||
"""
|
||||
value = self.get_optional(key)
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
self.current_key = key
|
||||
# ask the user for more information
|
||||
self.pause(prompt, **kwargs)
|
||||
|
||||
def get_optional(self, key: Any,
|
||||
default: Optional[Any] = None) -> Optional[Any]:
|
||||
"""Simply get a argument with given key."""
|
||||
return self.args.get(key, default)
|
||||
|
||||
def pause(self, message: Optional[Message_T] = None, **kwargs) -> None:
|
||||
"""Pause the session for further interaction."""
|
||||
if message:
|
||||
asyncio.ensure_future(self.send(message, **kwargs))
|
||||
raise _FurtherInteractionNeeded
|
||||
|
||||
def finish(self, message: Optional[Message_T] = None, **kwargs) -> None:
|
||||
"""Finish the session."""
|
||||
if message:
|
||||
asyncio.ensure_future(self.send(message, **kwargs))
|
||||
raise _FinishException
|
||||
|
||||
def switch(self, new_ctx_message: Message_T) -> None:
|
||||
"""
|
||||
Finish the session and switch to a new (fake) message context.
|
||||
|
||||
The user may send another command (or another intention as natural
|
||||
language) when interacting with the current session. In this case,
|
||||
the session may not understand what the user is saying, so it
|
||||
should call this method and pass in that message, then NoneBot will
|
||||
handle the situation properly.
|
||||
"""
|
||||
if self.is_first_run:
|
||||
# if calling this method during first run,
|
||||
# we think the command is not handled
|
||||
raise _FinishException(result=False)
|
||||
|
||||
if not isinstance(new_ctx_message, Message):
|
||||
new_ctx_message = Message(new_ctx_message)
|
||||
raise SwitchException(new_ctx_message)
|
||||
|
||||
|
||||
def parse_command(bot: NoneBot,
|
||||
cmd_string: str) -> Tuple[Optional[Command], Optional[str]]:
|
||||
"""
|
||||
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: {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, ctx: Context_T) -> bool:
|
||||
"""
|
||||
Handle a message as a command.
|
||||
|
||||
This function is typically called by "handle_message".
|
||||
|
||||
:param bot: NoneBot instance
|
||||
:param ctx: message context
|
||||
:return: the message is handled as a command
|
||||
"""
|
||||
cmd, current_arg = parse_command(bot, str(ctx['message']).lstrip())
|
||||
is_privileged_cmd = cmd and cmd.privileged
|
||||
if is_privileged_cmd and cmd.only_to_me and not ctx['to_me']:
|
||||
is_privileged_cmd = False
|
||||
disable_interaction = is_privileged_cmd
|
||||
|
||||
if is_privileged_cmd:
|
||||
logger.debug(f'Command {cmd.name} is a privileged command')
|
||||
|
||||
ctx_id = context_id(ctx)
|
||||
|
||||
if not is_privileged_cmd:
|
||||
# wait for 1.5 seconds (at most) if the current session is running
|
||||
retry = 5
|
||||
while retry > 0 and \
|
||||
_sessions.get(ctx_id) and _sessions[ctx_id].running:
|
||||
retry -= 1
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
check_perm = True
|
||||
session = _sessions.get(ctx_id) if not is_privileged_cmd else None
|
||||
if session:
|
||||
if session.running:
|
||||
logger.warning(f'There is a session of command '
|
||||
f'{session.cmd.name} running, notify the user')
|
||||
asyncio.ensure_future(send(
|
||||
bot, ctx,
|
||||
render_expression(bot.config.SESSION_RUNNING_EXPRESSION)
|
||||
))
|
||||
# pretend we are successful, so that NLP won't handle it
|
||||
return True
|
||||
|
||||
if session.is_valid:
|
||||
logger.debug(f'Session of command {session.cmd.name} exists')
|
||||
session.refresh(ctx, current_arg=str(ctx['message']))
|
||||
# there is no need to check permission for existing session
|
||||
check_perm = False
|
||||
else:
|
||||
# the session is expired, remove it
|
||||
logger.debug(f'Session of command {session.cmd.name} is expired')
|
||||
if ctx_id in _sessions:
|
||||
del _sessions[ctx_id]
|
||||
session = None
|
||||
|
||||
if not session:
|
||||
if not cmd:
|
||||
logger.debug('Not a known command, ignored')
|
||||
return False
|
||||
if cmd.only_to_me and not ctx['to_me']:
|
||||
logger.debug('Not to me, ignored')
|
||||
return False
|
||||
session = CommandSession(bot, ctx, cmd, current_arg=current_arg)
|
||||
logger.debug(f'New session of command {session.cmd.name} created')
|
||||
|
||||
return await _real_run_command(session, ctx_id, check_perm=check_perm,
|
||||
disable_interaction=disable_interaction)
|
||||
|
||||
|
||||
async def call_command(bot: NoneBot, ctx: Context_T,
|
||||
name: Union[str, CommandName_T], *,
|
||||
current_arg: str = '',
|
||||
args: Optional[CommandArgs_T] = None,
|
||||
check_perm: bool = True,
|
||||
disable_interaction: bool = False) -> bool:
|
||||
"""
|
||||
Call a command internally.
|
||||
|
||||
This function is typically called by some other commands
|
||||
or "handle_natural_language" when handling NLPResult object.
|
||||
|
||||
Note: If disable_interaction is not True, after calling this function,
|
||||
any previous command session will be overridden, even if the command
|
||||
being called here does not need further interaction (a.k.a asking
|
||||
the user for more info).
|
||||
|
||||
:param bot: NoneBot instance
|
||||
:param ctx: message context
|
||||
:param name: command name
|
||||
:param current_arg: command current argument string
|
||||
:param args: command args
|
||||
:param check_perm: should check permission before running command
|
||||
:param disable_interaction: disable the command's further interaction
|
||||
:return: the command is successfully called
|
||||
"""
|
||||
cmd = _find_command(name)
|
||||
if not cmd:
|
||||
return False
|
||||
session = CommandSession(bot, ctx, cmd, current_arg=current_arg, args=args)
|
||||
return await _real_run_command(session, context_id(session.ctx),
|
||||
check_perm=check_perm,
|
||||
disable_interaction=disable_interaction)
|
||||
|
||||
|
||||
async def _real_run_command(session: CommandSession,
|
||||
ctx_id: str,
|
||||
disable_interaction: bool = False,
|
||||
**kwargs) -> bool:
|
||||
if not disable_interaction:
|
||||
# override session only when interaction is not disabled
|
||||
_sessions[ctx_id] = session
|
||||
try:
|
||||
logger.debug(f'Running command {session.cmd.name}')
|
||||
session.running = True
|
||||
future = asyncio.ensure_future(session.cmd.run(session, **kwargs))
|
||||
timeout = None
|
||||
if session.bot.config.SESSION_RUN_TIMEOUT:
|
||||
timeout = session.bot.config.SESSION_RUN_TIMEOUT.total_seconds()
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout)
|
||||
handled = future.result()
|
||||
except asyncio.TimeoutError:
|
||||
handled = True
|
||||
except (_FurtherInteractionNeeded,
|
||||
_FinishException,
|
||||
SwitchException) as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f'An exception occurred while '
|
||||
f'running command {session.cmd.name}:')
|
||||
logger.exception(e)
|
||||
handled = True
|
||||
raise _FinishException(handled)
|
||||
except _FurtherInteractionNeeded:
|
||||
session.running = False
|
||||
if disable_interaction:
|
||||
# if the command needs further interaction, we view it as failed
|
||||
return False
|
||||
logger.debug(f'Further interaction needed for '
|
||||
f'command {session.cmd.name}')
|
||||
# return True because this step of the session is successful
|
||||
return True
|
||||
except (_FinishException, SwitchException) as e:
|
||||
session.running = False
|
||||
logger.debug(f'Session of command {session.cmd.name} finished')
|
||||
if not disable_interaction and ctx_id in _sessions:
|
||||
# the command is finished, remove the session,
|
||||
# but if interaction is disabled during this command call,
|
||||
# we leave the _sessions untouched.
|
||||
del _sessions[ctx_id]
|
||||
|
||||
if isinstance(e, _FinishException):
|
||||
return e.result
|
||||
elif isinstance(e, SwitchException):
|
||||
# we are guaranteed that the session is not first run here,
|
||||
# which means interaction is definitely enabled,
|
||||
# so we can safely touch _sessions here.
|
||||
if ctx_id in _sessions:
|
||||
# make sure there is no session waiting
|
||||
del _sessions[ctx_id]
|
||||
logger.debug(f'Session of command {session.cmd.name} switching, '
|
||||
f'new context message: {e.new_ctx_message}')
|
||||
raise e # this is intended to be propagated to handle_message()
|
||||
|
||||
|
||||
def kill_current_session(bot: NoneBot, ctx: Context_T) -> None:
|
||||
"""
|
||||
Force kill current session of the given context,
|
||||
despite whether it is running or not.
|
||||
|
||||
:param bot: NoneBot instance
|
||||
:param ctx: message context
|
||||
"""
|
||||
ctx_id = context_id(ctx)
|
||||
if ctx_id in _sessions:
|
||||
del _sessions[ctx_id]
|
40
nonebot/default_config.py
Normal file
40
nonebot/default_config.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
Default configurations.
|
||||
|
||||
Any derived configurations must import everything from this module
|
||||
at the very beginning of their code, and then set their own value
|
||||
to override the default one.
|
||||
|
||||
For example:
|
||||
|
||||
>>> from nonebot.default_config import *
|
||||
>>> PORT = 9090
|
||||
>>> DEBUG = False
|
||||
>>> SUPERUSERS.add(123456)
|
||||
>>> NICKNAME = '小明'
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Container, Union, Iterable, Pattern, Optional, Dict, Any
|
||||
|
||||
from .typing import Expression_T
|
||||
|
||||
API_ROOT: str = ''
|
||||
ACCESS_TOKEN: str = ''
|
||||
SECRET: str = ''
|
||||
HOST: str = '127.0.0.1'
|
||||
PORT: int = 8080
|
||||
DEBUG: bool = True
|
||||
|
||||
SUPERUSERS: Container[int] = set()
|
||||
NICKNAME: Union[str, Iterable[str]] = ''
|
||||
COMMAND_START: Iterable[Union[str, Pattern]] = {'/', '!', '/', '!'}
|
||||
COMMAND_SEP: Iterable[Union[str, Pattern]] = {'/', '.'}
|
||||
SESSION_EXPIRE_TIMEOUT: Optional[timedelta] = timedelta(minutes=5)
|
||||
SESSION_RUN_TIMEOUT: Optional[timedelta] = None
|
||||
SESSION_RUNNING_EXPRESSION: Expression_T = '您有命令正在执行,请稍后再试'
|
||||
SHORT_MESSAGE_MAX_LENGTH: int = 50
|
||||
|
||||
APSCHEDULER_CONFIG: Dict[str, Any] = {
|
||||
'apscheduler.timezone': 'Asia/Shanghai'
|
||||
}
|
1
nonebot/exceptions.py
Normal file
1
nonebot/exceptions.py
Normal file
@ -0,0 +1 @@
|
||||
from aiocqhttp import Error as CQHttpError
|
81
nonebot/helpers.py
Normal file
81
nonebot/helpers.py
Normal file
@ -0,0 +1,81 @@
|
||||
import hashlib
|
||||
import random
|
||||
from typing import Sequence, Callable
|
||||
|
||||
from . import NoneBot
|
||||
from .exceptions import CQHttpError
|
||||
from .message import escape
|
||||
from .typing import Context_T, Message_T, Expression_T
|
||||
|
||||
|
||||
def context_id(ctx: Context_T, *,
|
||||
mode: str = 'default', use_hash: bool = False) -> str:
|
||||
"""
|
||||
Calculate a unique id representing the current context.
|
||||
|
||||
mode:
|
||||
default: one id for one context
|
||||
group: one id for one group or discuss
|
||||
user: one id for one user
|
||||
|
||||
:param ctx: the context dict
|
||||
:param mode: unique id mode: "default", "group", or "user"
|
||||
:param use_hash: use md5 to hash the id or not
|
||||
"""
|
||||
ctx_id = ''
|
||||
if mode == 'default':
|
||||
if ctx.get('group_id'):
|
||||
ctx_id += f'/group/{ctx["group_id"]}'
|
||||
elif ctx.get('discuss_id'):
|
||||
ctx_id += f'/discuss/{ctx["discuss_id"]}'
|
||||
if ctx.get('user_id'):
|
||||
ctx_id += f'/user/{ctx["user_id"]}'
|
||||
elif mode == 'group':
|
||||
if ctx.get('group_id'):
|
||||
ctx_id += f'/group/{ctx["group_id"]}'
|
||||
elif ctx.get('discuss_id'):
|
||||
ctx_id += f'/discuss/{ctx["discuss_id"]}'
|
||||
elif mode == 'user':
|
||||
if ctx.get('user_id'):
|
||||
ctx_id += f'/user/{ctx["user_id"]}'
|
||||
|
||||
if ctx_id and use_hash:
|
||||
ctx_id = hashlib.md5(ctx_id.encode('ascii')).hexdigest()
|
||||
return ctx_id
|
||||
|
||||
|
||||
async def send(bot: NoneBot, ctx: Context_T,
|
||||
message: Message_T, *,
|
||||
ensure_private: bool = False,
|
||||
ignore_failure: bool = True,
|
||||
**kwargs) -> None:
|
||||
"""Send a message ignoring failure by default."""
|
||||
try:
|
||||
if ensure_private:
|
||||
ctx = ctx.copy()
|
||||
ctx['message_type'] = 'private'
|
||||
await bot.send(ctx, message, **kwargs)
|
||||
except CQHttpError:
|
||||
if not ignore_failure:
|
||||
raise
|
||||
|
||||
|
||||
def render_expression(expr: Expression_T, *,
|
||||
escape_args: bool = True, **kwargs) -> str:
|
||||
"""
|
||||
Render an expression to message string.
|
||||
|
||||
:param expr: expression to render
|
||||
:param escape_args: should escape arguments or not
|
||||
:param kwargs: keyword arguments used in str.format()
|
||||
:return: the rendered message
|
||||
"""
|
||||
if isinstance(expr, Callable):
|
||||
expr = expr(**kwargs)
|
||||
elif isinstance(expr, Sequence) and not isinstance(expr, str):
|
||||
expr = random.choice(expr)
|
||||
if escape_args:
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, str):
|
||||
kwargs[k] = escape(v)
|
||||
return expr.format(**kwargs)
|
16
nonebot/log.py
Normal file
16
nonebot/log.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
Provide logger object.
|
||||
|
||||
Any other modules in "nonebot" should use "logger" from this module
|
||||
to log messages.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger('nonebot')
|
||||
default_handler = logging.StreamHandler(sys.stdout)
|
||||
default_handler.setFormatter(logging.Formatter(
|
||||
'[%(asctime)s %(name)s] %(levelname)s: %(message)s'
|
||||
))
|
||||
logger.addHandler(default_handler)
|
68
nonebot/message.py
Normal file
68
nonebot/message.py
Normal file
@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
from typing import Callable
|
||||
|
||||
from aiocqhttp.message import *
|
||||
|
||||
from . import NoneBot
|
||||
from .command import handle_command, SwitchException
|
||||
from .log import logger
|
||||
from .natural_language import handle_natural_language
|
||||
from .typing import Context_T
|
||||
|
||||
_message_preprocessors = set()
|
||||
|
||||
|
||||
def message_preprocessor(func: Callable) -> Callable:
|
||||
_message_preprocessors.add(func)
|
||||
return func
|
||||
|
||||
|
||||
async def handle_message(bot: NoneBot, ctx: Context_T) -> None:
|
||||
_log_message(ctx)
|
||||
|
||||
coros = []
|
||||
for processor in _message_preprocessors:
|
||||
coros.append(processor(bot, ctx))
|
||||
if coros:
|
||||
await asyncio.wait(coros)
|
||||
|
||||
if 'to_me' not in ctx:
|
||||
if ctx['message_type'] != 'private':
|
||||
# group or discuss
|
||||
ctx['to_me'] = False
|
||||
first_message_seg = ctx['message'][0]
|
||||
if first_message_seg == MessageSegment.at(ctx['self_id']):
|
||||
ctx['to_me'] = True
|
||||
del ctx['message'][0]
|
||||
if not ctx['message']:
|
||||
ctx['message'].append(MessageSegment.text(''))
|
||||
else:
|
||||
ctx['to_me'] = True
|
||||
|
||||
while True:
|
||||
try:
|
||||
handled = await handle_command(bot, ctx)
|
||||
break
|
||||
except SwitchException as e:
|
||||
# we are sure that there is no session existing now
|
||||
ctx['message'] = e.new_ctx_message
|
||||
ctx['to_me'] = True
|
||||
if handled:
|
||||
logger.info(f'Message {ctx["message_id"]} is handled as a command')
|
||||
return
|
||||
|
||||
handled = await handle_natural_language(bot, ctx)
|
||||
if handled:
|
||||
logger.info(f'Message {ctx["message_id"]} is handled '
|
||||
f'as natural language')
|
||||
return
|
||||
|
||||
|
||||
def _log_message(ctx: Context_T) -> None:
|
||||
msg_from = f'{ctx["user_id"]}'
|
||||
if ctx['message_type'] == 'group':
|
||||
msg_from += f'@[群:{ctx["group_id"]}]'
|
||||
elif ctx['message_type'] == 'discuss':
|
||||
msg_from += f'@[讨论组:{ctx["discuss_id"]}]'
|
||||
logger.info(f'Message {ctx["message_id"]} from {msg_from}: '
|
||||
f'{ctx["message"]}')
|
158
nonebot/natural_language.py
Normal file
158
nonebot/natural_language.py
Normal file
@ -0,0 +1,158 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Iterable, Optional, Callable, Union, NamedTuple
|
||||
|
||||
from . import NoneBot, permission as perm
|
||||
from .command import call_command
|
||||
from .log import logger
|
||||
from .message import Message
|
||||
from .session import BaseSession
|
||||
from .typing import Context_T, CommandName_T, CommandArgs_T
|
||||
|
||||
_nl_processors = set()
|
||||
|
||||
|
||||
class NLProcessor:
|
||||
__slots__ = ('func', 'keywords', 'permission',
|
||||
'only_to_me', 'only_short_message',
|
||||
'allow_empty_message')
|
||||
|
||||
def __init__(self, *, func: Callable, keywords: Optional[Iterable],
|
||||
permission: int, only_to_me: bool, only_short_message: bool,
|
||||
allow_empty_message: bool):
|
||||
self.func = func
|
||||
self.keywords = keywords
|
||||
self.permission = permission
|
||||
self.only_to_me = only_to_me
|
||||
self.only_short_message = only_short_message
|
||||
self.allow_empty_message = allow_empty_message
|
||||
|
||||
|
||||
def on_natural_language(keywords: Union[Optional[Iterable], 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,
|
||||
permission=permission,
|
||||
only_to_me=only_to_me,
|
||||
only_short_message=only_short_message,
|
||||
allow_empty_message=allow_empty_message)
|
||||
_nl_processors.add(nl_processor)
|
||||
return func
|
||||
|
||||
if isinstance(keywords, Callable):
|
||||
# here "keywords" is the function to be decorated
|
||||
return on_natural_language()(keywords)
|
||||
else:
|
||||
return deco
|
||||
|
||||
|
||||
class NLPSession(BaseSession):
|
||||
__slots__ = ('msg', 'msg_text', 'msg_images')
|
||||
|
||||
def __init__(self, bot: NoneBot, ctx: Context_T, msg: str):
|
||||
super().__init__(bot, ctx)
|
||||
self.msg = msg
|
||||
tmp_msg = Message(msg)
|
||||
self.msg_text = tmp_msg.extract_plain_text()
|
||||
self.msg_images = [s.data['url'] for s in tmp_msg
|
||||
if s.type == 'image' and 'url' in s.data]
|
||||
|
||||
|
||||
class NLPResult(NamedTuple):
|
||||
confidence: float
|
||||
cmd_name: Union[str, CommandName_T]
|
||||
cmd_args: Optional[CommandArgs_T] = None
|
||||
|
||||
|
||||
async def handle_natural_language(bot: NoneBot, ctx: Context_T) -> bool:
|
||||
"""
|
||||
Handle a message as natural language.
|
||||
|
||||
This function is typically called by "handle_message".
|
||||
|
||||
:param bot: NoneBot instance
|
||||
:param ctx: message context
|
||||
:return: the message is handled as natural language
|
||||
"""
|
||||
msg = str(ctx['message'])
|
||||
if bot.config.NICKNAME:
|
||||
# check if the user is calling me with my nickname
|
||||
if isinstance(bot.config.NICKNAME, str) or \
|
||||
not isinstance(bot.config.NICKNAME, Iterable):
|
||||
nicknames = (bot.config.NICKNAME,)
|
||||
else:
|
||||
nicknames = filter(lambda n: n, bot.config.NICKNAME)
|
||||
nickname_regex = '|'.join(nicknames)
|
||||
m = re.search(rf'^({nickname_regex})([\s,,]|$)', msg, re.IGNORECASE)
|
||||
if m:
|
||||
nickname = m.group(1)
|
||||
logger.debug(f'User is calling me {nickname}')
|
||||
ctx['to_me'] = True
|
||||
msg = msg[m.end():]
|
||||
|
||||
session = NLPSession(bot, ctx, msg)
|
||||
|
||||
# use msg_text here because CQ code "share" may be very long,
|
||||
# at the same time some plugins may want to handle it
|
||||
msg_text_length = len(session.msg_text)
|
||||
|
||||
futures = []
|
||||
for p in _nl_processors:
|
||||
if not p.allow_empty_message and not session.msg:
|
||||
# don't allow empty msg, but it is one, so skip to next
|
||||
continue
|
||||
|
||||
if p.only_short_message and \
|
||||
msg_text_length > bot.config.SHORT_MESSAGE_MAX_LENGTH:
|
||||
continue
|
||||
|
||||
if p.only_to_me and not ctx['to_me']:
|
||||
continue
|
||||
|
||||
should_run = await perm.check_permission(bot, ctx, p.permission)
|
||||
if should_run and p.keywords:
|
||||
for kw in p.keywords:
|
||||
if kw in session.msg_text:
|
||||
break
|
||||
else:
|
||||
# no keyword matches
|
||||
should_run = False
|
||||
|
||||
if should_run:
|
||||
futures.append(asyncio.ensure_future(p.func(session)))
|
||||
|
||||
if futures:
|
||||
# wait for possible results, and sort them by confidence
|
||||
results = []
|
||||
for fut in futures:
|
||||
try:
|
||||
results.append(await fut)
|
||||
except Exception as e:
|
||||
logger.error('An exception occurred while running '
|
||||
'some natural language processor:')
|
||||
logger.exception(e)
|
||||
results = sorted(filter(lambda r: r, results),
|
||||
key=lambda r: r.confidence, reverse=True)
|
||||
logger.debug(f'NLP results: {results}')
|
||||
if results and results[0].confidence >= 60.0:
|
||||
# choose the result with highest confidence
|
||||
logger.debug(f'NLP result with highest confidence: {results[0]}')
|
||||
return await call_command(bot, ctx, results[0].cmd_name,
|
||||
args=results[0].cmd_args,
|
||||
check_perm=False)
|
||||
else:
|
||||
logger.debug('No NLP result having enough confidence')
|
||||
return False
|
109
nonebot/notice_request.py
Normal file
109
nonebot/notice_request.py
Normal file
@ -0,0 +1,109 @@
|
||||
from typing import Optional, Callable, Union
|
||||
|
||||
from aiocqhttp.bus import EventBus
|
||||
|
||||
from . import NoneBot
|
||||
from .exceptions import CQHttpError
|
||||
from .log import logger
|
||||
from .session import BaseSession
|
||||
from .typing import Context_T
|
||||
|
||||
_bus = EventBus()
|
||||
|
||||
|
||||
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):
|
||||
for e in [arg] + list(events):
|
||||
_bus.subscribe(f'{post_type}.{e}', func)
|
||||
else:
|
||||
_bus.subscribe(post_type, func)
|
||||
return func
|
||||
|
||||
if isinstance(arg, Callable):
|
||||
return deco(arg)
|
||||
return deco
|
||||
|
||||
return deco_deco
|
||||
|
||||
|
||||
on_notice = _make_event_deco('notice')
|
||||
on_request = _make_event_deco('request')
|
||||
|
||||
|
||||
class NoticeSession(BaseSession):
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, bot: NoneBot, ctx: Context_T):
|
||||
super().__init__(bot, ctx)
|
||||
|
||||
|
||||
class RequestSession(BaseSession):
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, bot: NoneBot, ctx: Context_T):
|
||||
super().__init__(bot, ctx)
|
||||
|
||||
async def approve(self, remark: str = '') -> None:
|
||||
"""
|
||||
Approve the request.
|
||||
|
||||
:param remark: remark of friend (only works in friend request)
|
||||
"""
|
||||
try:
|
||||
await self.bot.call_action(
|
||||
action='.handle_quick_operation_async',
|
||||
self_id=self.ctx.get('self_id'),
|
||||
context=self.ctx,
|
||||
operation={'approve': True, 'remark': remark}
|
||||
)
|
||||
except CQHttpError:
|
||||
pass
|
||||
|
||||
async def reject(self, reason: str = '') -> None:
|
||||
"""
|
||||
Reject the request.
|
||||
|
||||
:param reason: reason to reject (only works in group request)
|
||||
"""
|
||||
try:
|
||||
await self.bot.call_action(
|
||||
action='.handle_quick_operation_async',
|
||||
self_id=self.ctx.get('self_id'),
|
||||
context=self.ctx,
|
||||
operation={'approve': False, 'reason': reason}
|
||||
)
|
||||
except CQHttpError:
|
||||
pass
|
||||
|
||||
|
||||
async def handle_notice_or_request(bot: NoneBot, ctx: Context_T) -> None:
|
||||
post_type = ctx['post_type'] # "notice" or "request"
|
||||
detail_type = ctx[f'{post_type}_type']
|
||||
event = f'{post_type}.{detail_type}'
|
||||
if ctx.get('sub_type'):
|
||||
event += f'.{ctx["sub_type"]}'
|
||||
|
||||
if post_type == 'notice':
|
||||
_log_notice(ctx)
|
||||
session = NoticeSession(bot, ctx)
|
||||
else: # must be 'request'
|
||||
_log_request(ctx)
|
||||
session = RequestSession(bot, ctx)
|
||||
|
||||
logger.debug(f'Emitting event: {event}')
|
||||
try:
|
||||
await _bus.emit(event, session)
|
||||
except Exception as e:
|
||||
logger.error(f'An exception occurred while handling event {event}:')
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def _log_notice(ctx: Context_T) -> None:
|
||||
logger.info(f'Notice: {ctx}')
|
||||
|
||||
|
||||
def _log_request(ctx: Context_T) -> None:
|
||||
logger.info(f'Request: {ctx}')
|
103
nonebot/permission.py
Normal file
103
nonebot/permission.py
Normal file
@ -0,0 +1,103 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from aiocache import cached
|
||||
|
||||
from . import NoneBot
|
||||
from .exceptions import CQHttpError
|
||||
from .typing import Context_T
|
||||
|
||||
PRIVATE_FRIEND = 0x0001
|
||||
PRIVATE_GROUP = 0x0002
|
||||
PRIVATE_DISCUSS = 0x0004
|
||||
PRIVATE_OTHER = 0x0008
|
||||
PRIVATE = 0x000F
|
||||
DISCUSS = 0x00F0
|
||||
GROUP_MEMBER = 0x0100
|
||||
GROUP_ADMIN = 0x0200
|
||||
GROUP_OWNER = 0x0400
|
||||
GROUP = 0x0F00
|
||||
SUPERUSER = 0xF000
|
||||
EVERYBODY = 0xFFFF
|
||||
|
||||
IS_NOBODY = 0x0000
|
||||
IS_PRIVATE_FRIEND = PRIVATE_FRIEND
|
||||
IS_PRIVATE_GROUP = PRIVATE_GROUP
|
||||
IS_PRIVATE_DISCUSS = PRIVATE_DISCUSS
|
||||
IS_PRIVATE_OTHER = PRIVATE_OTHER
|
||||
IS_PRIVATE = PRIVATE
|
||||
IS_DISCUSS = DISCUSS
|
||||
IS_GROUP_MEMBER = GROUP_MEMBER
|
||||
IS_GROUP_ADMIN = GROUP_MEMBER | GROUP_ADMIN
|
||||
IS_GROUP_OWNER = GROUP_ADMIN | GROUP_OWNER
|
||||
IS_GROUP = GROUP
|
||||
IS_SUPERUSER = 0xFFFF
|
||||
|
||||
_min_context_fields = (
|
||||
'self_id',
|
||||
'message_type',
|
||||
'sub_type',
|
||||
'user_id',
|
||||
'discuss_id',
|
||||
'group_id',
|
||||
'anonymous',
|
||||
)
|
||||
|
||||
_MinContext = namedtuple('MinContext', _min_context_fields)
|
||||
|
||||
|
||||
async def check_permission(bot: NoneBot, ctx: Context_T,
|
||||
permission_required: int) -> bool:
|
||||
"""
|
||||
Check if the context has the permission required.
|
||||
|
||||
:param bot: NoneBot instance
|
||||
:param ctx: message context
|
||||
:param permission_required: permission required
|
||||
:return: the context has the permission
|
||||
"""
|
||||
min_ctx_kwargs = {}
|
||||
for field in _min_context_fields:
|
||||
if field in ctx:
|
||||
min_ctx_kwargs[field] = ctx[field]
|
||||
else:
|
||||
min_ctx_kwargs[field] = None
|
||||
min_ctx = _MinContext(**min_ctx_kwargs)
|
||||
return await _check(bot, min_ctx, permission_required)
|
||||
|
||||
|
||||
@cached(ttl=2 * 60) # cache the result for 2 minute
|
||||
async def _check(bot: NoneBot, min_ctx: _MinContext,
|
||||
permission_required: int) -> bool:
|
||||
permission = 0
|
||||
if min_ctx.user_id in bot.config.SUPERUSERS:
|
||||
permission |= IS_SUPERUSER
|
||||
if min_ctx.message_type == 'private':
|
||||
if min_ctx.sub_type == 'friend':
|
||||
permission |= IS_PRIVATE_FRIEND
|
||||
elif min_ctx.sub_type == 'group':
|
||||
permission |= IS_PRIVATE_GROUP
|
||||
elif min_ctx.sub_type == 'discuss':
|
||||
permission |= IS_PRIVATE_DISCUSS
|
||||
elif min_ctx.sub_type == 'other':
|
||||
permission |= IS_PRIVATE_OTHER
|
||||
elif min_ctx.message_type == 'group':
|
||||
permission |= IS_GROUP_MEMBER
|
||||
if not min_ctx.anonymous:
|
||||
try:
|
||||
member_info = await bot.get_group_member_info(
|
||||
self_id=min_ctx.self_id,
|
||||
group_id=min_ctx.group_id,
|
||||
user_id=min_ctx.user_id,
|
||||
no_cache=True
|
||||
)
|
||||
if member_info:
|
||||
if member_info['role'] == 'owner':
|
||||
permission |= IS_GROUP_OWNER
|
||||
elif member_info['role'] == 'admin':
|
||||
permission |= IS_GROUP_ADMIN
|
||||
except CQHttpError:
|
||||
pass
|
||||
elif min_ctx.message_type == 'discuss':
|
||||
permission |= IS_DISCUSS
|
||||
|
||||
return bool(permission & permission_required)
|
0
nonebot/plugins/__init__.py
Normal file
0
nonebot/plugins/__init__.py
Normal file
13
nonebot/plugins/base.py
Normal file
13
nonebot/plugins/base.py
Normal file
@ -0,0 +1,13 @@
|
||||
from nonebot import on_command, CommandSession, permission as perm
|
||||
from nonebot.message import unescape
|
||||
|
||||
|
||||
@on_command('echo')
|
||||
async def echo(session: CommandSession):
|
||||
await session.send(session.get_optional('message') or session.current_arg)
|
||||
|
||||
|
||||
@on_command('say', permission=perm.SUPERUSER)
|
||||
async def say(session: CommandSession):
|
||||
await session.send(
|
||||
unescape(session.get_optional('message') or session.current_arg))
|
11
nonebot/sched.py
Normal file
11
nonebot/sched.py
Normal file
@ -0,0 +1,11 @@
|
||||
try:
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
except ImportError:
|
||||
# APScheduler is not installed
|
||||
AsyncIOScheduler = None
|
||||
|
||||
if AsyncIOScheduler:
|
||||
class Scheduler(AsyncIOScheduler):
|
||||
pass
|
||||
else:
|
||||
Scheduler = None
|
30
nonebot/session.py
Normal file
30
nonebot/session.py
Normal file
@ -0,0 +1,30 @@
|
||||
from . import NoneBot
|
||||
from .helpers import send
|
||||
from .typing import Context_T, Message_T
|
||||
|
||||
|
||||
class BaseSession:
|
||||
__slots__ = ('bot', 'ctx')
|
||||
|
||||
def __init__(self, bot: NoneBot, ctx: Context_T):
|
||||
self.bot = bot
|
||||
self.ctx = ctx
|
||||
|
||||
async def send(self, message: Message_T, *,
|
||||
at_sender: bool = False,
|
||||
ensure_private: bool = False,
|
||||
ignore_failure: bool = True,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Send a message ignoring failure by default.
|
||||
|
||||
:param message: message to send
|
||||
:param at_sender: @ the sender if in group or discuss chat
|
||||
:param ensure_private: ensure the message is sent to private chat
|
||||
:param ignore_failure: if any CQHttpError raised, ignore it
|
||||
:return: the result returned by CQHTTP
|
||||
"""
|
||||
return await send(self.bot, self.ctx, message,
|
||||
at_sender=at_sender,
|
||||
ensure_private=ensure_private,
|
||||
ignore_failure=ignore_failure, **kwargs)
|
7
nonebot/typing.py
Normal file
7
nonebot/typing.py
Normal file
@ -0,0 +1,7 @@
|
||||
from typing import Union, List, Dict, Any, Sequence, Callable, Tuple
|
||||
|
||||
Context_T = Dict[str, Any]
|
||||
Message_T = Union[str, Dict[str, Any], List[Dict[str, Any]]]
|
||||
Expression_T = Union[str, Sequence[str], Callable]
|
||||
CommandName_T = Tuple[str, ...]
|
||||
CommandArgs_T = Dict[str, Any]
|
Reference in New Issue
Block a user