🔀 Merge pull request #8 from nonebot/dev

This commit is contained in:
Ju4tCode 2020-08-30 22:21:14 +08:00 committed by GitHub
commit 0a8a53a764
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 317 additions and 104 deletions

View File

@ -7,9 +7,9 @@ sidebarDepth: 0
## 日志 ## 日志
NoneBot 使用标准库 [logging](https://docs.python.org/3/library/logging.html) 来记录日志信息。 NoneBot 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。
自定义 logger 请参考 [logging](https://docs.python.org/3/library/logging.html) 文档。 自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。
## `logger` ## `logger`

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging
import importlib import importlib
from nonebot.typing import Bot, Dict, Type, Union, Driver, Optional, NoReturn from nonebot.typing import Bot, Dict, Type, Union, Driver, Optional, NoReturn
@ -84,7 +83,7 @@ def get_asgi():
return driver.asgi return driver.asgi
def get_bots() -> Dict[str, Bot]: def get_bots() -> Union[NoReturn, Dict[str, Bot]]:
""" """
:说明: :说明:
@ -109,8 +108,8 @@ def get_bots() -> Dict[str, Bot]:
return driver.bots return driver.bots
from nonebot.log import logger
from nonebot.config import Env, Config from nonebot.config import Env, Config
from nonebot.log import logger, default_filter
from nonebot.adapters.cqhttp import Bot as CQBot from nonebot.adapters.cqhttp import Bot as CQBot
try: try:
@ -146,15 +145,20 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
""" """
global _driver global _driver
if not _driver:
logger.debug("NoneBot is initializing...")
env = Env() env = Env()
logger.debug(f"Current Env: {env.environment}") logger.opt(
config = Config(**kwargs, _env_file=_env_file or f".env.{env.environment}") colors=True).debug(f"Current <y><b>Env: {env.environment}</b></y>")
config = Config(**kwargs,
_env_file=_env_file or f".env.{env.environment}")
logger.setLevel(logging.DEBUG if config.debug else logging.INFO) default_filter.level = "DEBUG" if config.debug else "INFO"
logger.debug(f"Loaded config: {config.dict()}") logger.opt(
colors=True).debug(f"Loaded <y><b>Config</b></y>: {config.dict()}")
DriverClass: Type[Driver] = getattr(importlib.import_module(config.driver), DriverClass: Type[Driver] = getattr(
"Driver") importlib.import_module(config.driver), "Driver")
_driver = DriverClass(env, config) _driver = DriverClass(env, config)
# register build-in adapters # register build-in adapters
@ -193,7 +197,10 @@ def run(host: Optional[str] = None,
nonebot.run(host="127.0.0.1", port=8080) nonebot.run(host="127.0.0.1", port=8080)
""" """
logger.info("Running NoneBot...")
get_driver().run(host, port, *args, **kwargs) get_driver().run(host, port, *args, **kwargs)
from nonebot.plugin import load_plugins, get_loaded_plugins from nonebot.plugin import on_message, on_notice, on_request, on_metaevent
from nonebot.plugin import on_startswith, on_endswith, on_command, on_regex
from nonebot.plugin import load_plugin, load_plugins, load_builtin_plugins, get_loaded_plugins

View File

@ -150,6 +150,16 @@ class BaseEvent(abc.ABC):
def message(self, value) -> None: def message(self, value) -> None:
raise NotImplementedError raise NotImplementedError
@property
@abc.abstractmethod
def reply(self) -> Optional[dict]:
raise NotImplementedError
@reply.setter
@abc.abstractmethod
def reply(self, value) -> None:
raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def raw_message(self) -> Optional[str]: def raw_message(self) -> Optional[str]:

View File

@ -27,6 +27,10 @@ from nonebot.typing import overrides, Driver, WebSocket, NoReturn
from nonebot.adapters import BaseBot, BaseEvent, BaseMessage, BaseMessageSegment from nonebot.adapters import BaseBot, BaseEvent, BaseMessage, BaseMessageSegment
def log(level: str, message: str):
return logger.opt(colors=True).log(level, "<m>CQHTTP</m> | " + message)
def escape(s: str, *, escape_comma: bool = True) -> str: def escape(s: str, *, escape_comma: bool = True) -> str:
""" """
对字符串进行 CQ 码转义 对字符串进行 CQ 码转义
@ -53,6 +57,19 @@ def _b2s(b: Optional[bool]) -> Optional[str]:
return b if b is None else str(b).lower() return b if b is None else str(b).lower()
async def _check_reply(bot: "Bot", event: "Event"):
if event.type != "message":
return
first_msg_seg = event.message[0]
if first_msg_seg.type == "reply":
msg_id = first_msg_seg.data["id"]
event.reply = await bot.get_msg(message_id=msg_id)
if event.reply["sender"]["user_id"] == event.self_id:
event.to_me = True
del event.message[0]
def _check_at_me(bot: "Bot", event: "Event"): def _check_at_me(bot: "Bot", event: "Event"):
if event.type != "message": if event.type != "message":
return return
@ -60,7 +77,6 @@ def _check_at_me(bot: "Bot", event: "Event"):
if event.detail_type == "private": if event.detail_type == "private":
event.to_me = True event.to_me = True
else: else:
event.to_me = False
at_me_seg = MessageSegment.at(event.self_id) at_me_seg = MessageSegment.at(event.self_id)
# check the first segment # check the first segment
@ -109,7 +125,7 @@ def _check_nickname(bot: "Bot", event: "Event"):
re.IGNORECASE) re.IGNORECASE)
if m: if m:
nickname = m.group(1) nickname = m.group(1)
logger.debug(f"User is calling me {nickname}") log("DEBUG", f"User is calling me {nickname}")
event.to_me = True event.to_me = True
first_msg_seg.data["text"] = first_text[m.end():] first_msg_seg.data["text"] = first_text[m.end():]
@ -146,7 +162,7 @@ class ResultStore:
try: try:
return await asyncio.wait_for(future, timeout) return await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise NetworkError("WebSocket API call timeout") raise NetworkError("WebSocket API call timeout") from None
finally: finally:
del cls._futures[seq] del cls._futures[seq]
@ -186,7 +202,7 @@ class Bot(BaseBot):
event = Event(message) event = Event(message)
# Check whether user is calling me # Check whether user is calling me
# TODO: Check reply await _check_reply(self, event)
_check_at_me(self, event) _check_at_me(self, event)
_check_nickname(self, event) _check_nickname(self, event)
@ -200,7 +216,8 @@ class Bot(BaseBot):
bot = self.driver.bots[str(self_id)] bot = self.driver.bots[str(self_id)]
return await bot.call_api(api, **data) return await bot.call_api(api, **data)
if self.type == "websocket": log("DEBUG", f"Calling API <y>{api}</y>")
if self.connection_type == "websocket":
seq = ResultStore.get_seq() seq = ResultStore.get_seq()
await self.websocket.send({ await self.websocket.send({
"action": api, "action": api,
@ -212,7 +229,7 @@ class Bot(BaseBot):
return _handle_api_result(await ResultStore.fetch( return _handle_api_result(await ResultStore.fetch(
seq, self.config.api_timeout)) seq, self.config.api_timeout))
elif self.type == "http": elif self.connection_type == "http":
api_root = self.config.api_root.get(self.self_id) api_root = self.config.api_root.get(self.self_id)
if not api_root: if not api_root:
raise ApiNotAvailable raise ApiNotAvailable
@ -372,6 +389,16 @@ class Event(BaseEvent):
def message(self, value) -> None: def message(self, value) -> None:
self._raw_event["message"] = value self._raw_event["message"] = value
@property
@overrides(BaseEvent)
def reply(self) -> Optional[dict]:
return self._raw_event.get("reply")
@reply.setter
@overrides(BaseEvent)
def reply(self, value) -> None:
self._raw_event["reply"] = value
@property @property
@overrides(BaseEvent) @overrides(BaseEvent)
def raw_message(self) -> Optional[str]: def raw_message(self) -> Optional[str]:
@ -403,7 +430,7 @@ class MessageSegment(BaseMessageSegment):
@overrides(BaseMessageSegment) @overrides(BaseMessageSegment)
def __init__(self, type: str, data: Dict[str, Union[str, list]]) -> None: def __init__(self, type: str, data: Dict[str, Union[str, list]]) -> None:
if type == "text": if type == "text":
data["text"] = unescape(data["text"]) data["text"] = unescape(data["text"]) # type: ignore
super().__init__(type=type, data=data) super().__init__(type=type, data=data)
@overrides(BaseMessageSegment) @overrides(BaseMessageSegment)
@ -413,7 +440,9 @@ class MessageSegment(BaseMessageSegment):
# process special types # process special types
if type_ == "text": if type_ == "text":
return escape(data.get("text", ""), escape_comma=False) return escape(
data.get("text", ""), # type: ignore
escape_comma=False)
params = ",".join( params = ",".join(
[f"{k}={escape(str(v))}" for k, v in data.items() if v is not None]) [f"{k}={escape(str(v))}" for k, v in data.items() if v is not None])
@ -449,7 +478,7 @@ class MessageSegment(BaseMessageSegment):
@staticmethod @staticmethod
def forward(id_: str) -> "MessageSegment": def forward(id_: str) -> "MessageSegment":
logger.warning("Forward Message only can be received!") log("WARNING", "Forward Message only can be received!")
return MessageSegment("forward", {"id": id_}) return MessageSegment("forward", {"id": id_})
@staticmethod @staticmethod

View File

@ -174,10 +174,10 @@ class Config(BaseConfig):
API_ROOT={"123456": "http://127.0.0.1:5700"} API_ROOT={"123456": "http://127.0.0.1:5700"}
""" """
api_timeout: Optional[float] = 60. api_timeout: Optional[float] = 30.
""" """
- 类型: ``Optional[float]`` - 类型: ``Optional[float]``
- 默认值: ``60.`` - 默认值: ``30.``
- 说明: - 说明:
API 请求超时时间单位: API 请求超时时间单位:
""" """

View File

@ -20,7 +20,8 @@ class BaseDriver(abc.ABC):
@classmethod @classmethod
def register_adapter(cls, name: str, adapter: Type[Bot]): def register_adapter(cls, name: str, adapter: Type[Bot]):
cls._adapters[name] = adapter cls._adapters[name] = adapter
logger.debug(f'Succeeded to load adapter "{name}"') logger.opt(
colors=True).debug(f'Succeeded to load adapter "<y>{name}</y>"')
@property @property
@abc.abstractmethod @abc.abstractmethod

View File

@ -84,17 +84,9 @@ class Driver(BaseDriver):
LOGGING_CONFIG = { LOGGING_CONFIG = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
"formatters": {
"default": {
"()": "logging.Formatter",
"fmt": "[%(asctime)s %(name)s] %(levelname)s: %(message)s",
},
},
"handlers": { "handlers": {
"default": { "default": {
"formatter": "default", "class": "nonebot.log.LoguruHandler",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
}, },
}, },
"loggers": { "loggers": {
@ -200,6 +192,9 @@ class Driver(BaseDriver):
await ws.accept() await ws.accept()
self._clients[x_self_id] = bot self._clients[x_self_id] = bot
logger.opt(colors=True).info(
f"WebSocket Connection from <y>{adapter.upper()} "
f"Bot {x_self_id}</y> Accepted!")
try: try:
while not ws.closed: while not ws.closed:

View File

@ -4,18 +4,21 @@
日志 日志
==== ====
NoneBot 使用标准库 `logging`_ 来记录日志信息 NoneBot 使用 `loguru`_ 来记录日志信息
自定义 logger 请参考 `logging`_ 文档 自定义 logger 请参考 `loguru`_ 文档
.. _logging: .. _loguru:
https://docs.python.org/3/library/logging.html https://github.com/Delgan/loguru
""" """
import sys import sys
import logging import logging
logger = logging.getLogger("nonebot") from loguru import logger as logger_
# logger = logging.getLogger("nonebot")
logger = logger_
""" """
:说明: :说明:
@ -32,13 +35,52 @@ logger = logging.getLogger("nonebot")
.. code-block:: python .. code-block:: python
from nonebot.log import logger from nonebot.log import logger
# 也可以这样
import logging
logger = logging.getLogger("nonebot")
""" """
default_handler = logging.StreamHandler(sys.stdout) # default_handler = logging.StreamHandler(sys.stdout)
default_handler.setFormatter( # default_handler.setFormatter(
logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) # logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s"))
logger.addHandler(default_handler) # logger.addHandler(default_handler)
class Filter:
def __init__(self) -> None:
self.level = "DEBUG"
def __call__(self, record):
record["name"] = record["name"].split(".")[0]
levelno = logger.level(self.level).no
return record["level"].no >= levelno
class LoguruHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth,
exception=record.exc_info).log(level, record.getMessage())
logger.remove()
default_filter = Filter()
default_format = (
"<g>{time:MM-DD HH:mm:ss}</g> "
"[<lvl>{level}</lvl>] "
"<c><u>{name}</u></c> | "
# "<c>{function}:{line}</c>| "
"{message}")
logger.add(sys.stdout,
colorize=True,
diagnose=False,
filter=default_filter,
format=default_format)

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nonebot.log import logger
import typing import typing
import inspect import inspect
from functools import wraps from functools import wraps
@ -19,9 +20,21 @@ current_bot: ContextVar = ContextVar("current_bot")
current_event: ContextVar = ContextVar("current_event") current_event: ContextVar = ContextVar("current_event")
class Matcher: class MatcherMeta(type):
def __repr__(self) -> str:
return (f"<Matcher from {self.module or 'unknow'}, " # type: ignore
f"type={self.type}, priority={self.priority}, " # type: ignore
f"temp={self.temp}>") # type: ignore
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""`Matcher`类 """`Matcher`类
""" """
module: Optional[str] = None
type: str = "" type: str = ""
rule: Rule = Rule() rule: Rule = Rule()
@ -43,8 +56,8 @@ class Matcher:
self.state = self._default_state.copy() self.state = self._default_state.copy()
def __repr__(self) -> str: def __repr__(self) -> str:
return (f"<Matcher {self.type}, priority={self.priority}," return (f"<Matcher from {self.module or 'unknow'}, type={self.type}, "
f" temp={self.temp}, expire={self.expire_time}>") f"priority={self.priority}, temp={self.temp}>")
def __str__(self) -> str: def __str__(self) -> str:
return self.__repr__() return self.__repr__()
@ -59,6 +72,7 @@ class Matcher:
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
*, *,
module: Optional[str] = None,
default_state: Optional[dict] = None, default_state: Optional[dict] = None,
expire_time: Optional[datetime] = None) -> Type["Matcher"]: expire_time: Optional[datetime] = None) -> Type["Matcher"]:
"""创建新的 Matcher """创建新的 Matcher
@ -69,6 +83,7 @@ class Matcher:
NewMatcher = type( NewMatcher = type(
"Matcher", (Matcher,), { "Matcher", (Matcher,), {
"module": module,
"type": type_, "type": type_,
"rule": rule, "rule": rule,
"permission": permission, "permission": permission,
@ -253,5 +268,6 @@ class Matcher:
except FinishedException: except FinishedException:
pass pass
finally: finally:
logger.info(f"Matcher {self} running complete")
current_bot.reset(b_t) current_bot.reset(b_t)
current_event.reset(e_t) current_event.reset(e_t)

View File

@ -30,11 +30,10 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event,
bot, event) or not await Matcher.check_rule(bot, event, state): bot, event) or not await Matcher.check_rule(bot, event, state):
return return
except Exception as e: except Exception as e:
logger.error(f"Rule check failed for matcher {Matcher}. Ignored.") logger.opt(colors=True, exception=e).error(
logger.exception(e) f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>")
return return
# TODO: log matcher
logger.info(f"Event will be handled by {Matcher}") logger.info(f"Event will be handled by {Matcher}")
matcher = Matcher() matcher = Matcher()
@ -43,8 +42,9 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event,
logger.debug(f"Running matcher {matcher}") logger.debug(f"Running matcher {matcher}")
await matcher.run(bot, event, state) await matcher.run(bot, event, state)
except Exception as e: except Exception as e:
logger.error(f"Running matcher {matcher} failed.") logger.opt(colors=True, exception=e).error(
logger.exception(e) f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
)
exceptions = [] exceptions = []
if Matcher.temp: if Matcher.temp:
@ -56,20 +56,23 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event,
async def handle_event(bot: Bot, event: Event): async def handle_event(bot: Bot, event: Event):
log_msg = f"{bot.type.upper()} Bot {event.self_id} [{event.name}]: " log_msg = f"<m>{bot.type.upper()} </m>| {event.self_id} [{event.name}]: "
if event.type == "message": if event.type == "message":
log_msg += f"Message {event.id} from " log_msg += f"Message {event.id} from "
log_msg += str(event.user_id) log_msg += str(event.user_id)
if event.detail_type == "group": if event.detail_type == "group":
log_msg += f"@[群:{event.group_id}]: " log_msg += f"@[群:{event.group_id}]:"
log_msg += repr(str(event.message))
log_msg += ' "' + "".join(
map(lambda x: str(x) if x.type == "text" else f"<le>{x!s}</le>",
event.message)) + '"' # type: ignore
elif event.type == "notice": elif event.type == "notice":
log_msg += f"Notice {event.raw_event}" log_msg += f"Notice {event.raw_event}"
elif event.type == "request": elif event.type == "request":
log_msg += f"Request {event.raw_event}" log_msg += f"Request {event.raw_event}"
elif event.type == "meta_event": elif event.type == "meta_event":
log_msg += f"MetaEvent {event.raw_event}" log_msg += f"MetaEvent {event.raw_event}"
logger.info(log_msg) logger.opt(colors=True).info(log_msg)
coros = [] coros = []
state = {} state = {}
@ -80,7 +83,8 @@ async def handle_event(bot: Bot, event: Event):
logger.debug("Running PreProcessors...") logger.debug("Running PreProcessors...")
await asyncio.gather(*coros) await asyncio.gather(*coros)
except IgnoredException: except IgnoredException:
logger.info(f"Event {event.name} is ignored") logger.opt(
colors=True).info(f"Event {event.name} is <b>ignored</b>")
return return
# Trie Match # Trie Match
@ -96,7 +100,7 @@ async def handle_event(bot: Bot, event: Event):
for matcher in matchers[priority] for matcher in matchers[priority]
] ]
logger.debug(f"Checking for all matchers in priority {priority}...") logger.debug(f"Checking for matchers in priority {priority}...")
results = await asyncio.gather(*pending_tasks, return_exceptions=True) results = await asyncio.gather(*pending_tasks, return_exceptions=True)
i = 0 i = 0
@ -104,8 +108,12 @@ async def handle_event(bot: Bot, event: Event):
if isinstance(result, _ExceptionContainer): if isinstance(result, _ExceptionContainer):
e_list = result.exceptions e_list = result.exceptions
if StopPropagation in e_list: if StopPropagation in e_list:
if not break_flag:
break_flag = True break_flag = True
logger.debug("Stop event propagation") logger.debug("Stop event propagation")
if ExpiredException in e_list: if ExpiredException in e_list:
logger.debug(
f"Matcher {matchers[priority][index - i]} will be removed."
)
del matchers[priority][index - i] del matchers[priority][index - i]
i += 1 i += 1

View File

@ -5,33 +5,32 @@ import re
import sys import sys
import pkgutil import pkgutil
import importlib import importlib
from dataclasses import dataclass
from importlib._bootstrap import _load from importlib._bootstrap import _load
from nonebot.log import logger from nonebot.log import logger
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.permission import Permission from nonebot.permission import Permission
from nonebot.typing import Handler, RuleChecker
from nonebot.rule import Rule, startswith, endswith, command, regex from nonebot.rule import Rule, startswith, endswith, command, regex
from nonebot.typing import Set, Dict, Type, Tuple, Union, Optional, ModuleType, RuleChecker from nonebot.typing import Set, List, Dict, Type, Tuple, Union, Optional, ModuleType
plugins: Dict[str, "Plugin"] = {} plugins: Dict[str, "Plugin"] = {}
_tmp_matchers: Set[Type[Matcher]] = set() _tmp_matchers: Set[Type[Matcher]] = set()
@dataclass(eq=False)
class Plugin(object): class Plugin(object):
name: str
# TODO: store plugin informations module: ModuleType
def __init__(self, module_path: str, module: ModuleType, matcher: Set[Type[Matcher]]
matchers: Set[Type[Matcher]]):
self.module_path = module_path
self.module = module
self.matchers = matchers
def on(rule: Union[Rule, RuleChecker] = Rule(), def on(rule: Union[Rule, RuleChecker] = Rule(),
permission: Permission = Permission(), permission: Permission = Permission(),
*, *,
handlers: Optional[list] = None, handlers: Optional[List[Handler]] = None,
temp: bool = False, temp: bool = False,
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
@ -50,7 +49,7 @@ def on(rule: Union[Rule, RuleChecker] = Rule(),
def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(), def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(),
*, *,
handlers: Optional[list] = None, handlers: Optional[List[Handler]] = None,
temp: bool = False, temp: bool = False,
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
@ -70,7 +69,7 @@ def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(),
def on_message(rule: Union[Rule, RuleChecker] = Rule(), def on_message(rule: Union[Rule, RuleChecker] = Rule(),
permission: Permission = Permission(), permission: Permission = Permission(),
*, *,
handlers: Optional[list] = None, handlers: Optional[List[Handler]] = None,
temp: bool = False, temp: bool = False,
priority: int = 1, priority: int = 1,
block: bool = True, block: bool = True,
@ -89,7 +88,7 @@ def on_message(rule: Union[Rule, RuleChecker] = Rule(),
def on_notice(rule: Union[Rule, RuleChecker] = Rule(), def on_notice(rule: Union[Rule, RuleChecker] = Rule(),
*, *,
handlers: Optional[list] = None, handlers: Optional[List[Handler]] = None,
temp: bool = False, temp: bool = False,
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
@ -108,7 +107,7 @@ def on_notice(rule: Union[Rule, RuleChecker] = Rule(),
def on_request(rule: Union[Rule, RuleChecker] = Rule(), def on_request(rule: Union[Rule, RuleChecker] = Rule(),
*, *,
handlers: Optional[list] = None, handlers: Optional[List[Handler]] = None,
temp: bool = False, temp: bool = False,
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
@ -149,9 +148,19 @@ def on_command(cmd: Union[str, Tuple[str, ...]],
**kwargs) -> Type[Matcher]: **kwargs) -> Type[Matcher]:
if isinstance(cmd, str): if isinstance(cmd, str):
cmd = (cmd,) cmd = (cmd,)
return on_message(command(cmd) &
rule, permission, **kwargs) if rule else on_message( async def _strip_cmd(bot, event, state: dict):
command(cmd), permission, **kwargs) message = event.message
event.message = message.__class__(
str(message)[len(state["_prefix"]["raw_command"]):].strip())
handlers = kwargs.pop("handlers", [])
handlers.insert(0, _strip_cmd)
return on_message(
command(cmd) &
rule, permission, handlers=handlers, **kwargs) if rule else on_message(
command(cmd), permission, handlers=handlers, **kwargs)
def on_regex(pattern: str, def on_regex(pattern: str,
@ -167,14 +176,24 @@ def on_regex(pattern: str,
def load_plugin(module_path: str) -> Optional[Plugin]: def load_plugin(module_path: str) -> Optional[Plugin]:
try: try:
_tmp_matchers.clear() _tmp_matchers.clear()
if module_path in plugins:
return plugins[module_path]
elif module_path in sys.modules:
logger.warning(
f"Module {module_path} has been loaded by other plugins! Ignored"
)
return
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
for m in _tmp_matchers:
m.module = module_path
plugin = Plugin(module_path, module, _tmp_matchers.copy()) plugin = Plugin(module_path, module, _tmp_matchers.copy())
plugins[module_path] = plugin plugins[module_path] = plugin
logger.info(f"Succeeded to import \"{module_path}\"") logger.opt(
colors=True).info(f'Succeeded to import "<y>{module_path}</y>"')
return plugin return plugin
except Exception as e: except Exception as e:
logger.error(f"Failed to import \"{module_path}\", error: {e}") logger.opt(colors=True, exception=e).error(
logger.exception(e) f'<r><bg #f8bbd0>Failed to import "{module_path}"</bg #f8bbd0></r>')
return None return None
@ -187,21 +206,31 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]:
continue continue
spec = module_info.module_finder.find_spec(name) spec = module_info.module_finder.find_spec(name)
if spec.name in sys.modules: if spec.name in plugins:
continue
elif spec.name in sys.modules:
logger.warning(
f"Module {spec.name} has been loaded by other plugin! Ignored")
continue continue
try: try:
module = _load(spec) module = _load(spec)
for m in _tmp_matchers:
m.module = name
plugin = Plugin(name, module, _tmp_matchers.copy()) plugin = Plugin(name, module, _tmp_matchers.copy())
plugins[name] = plugin plugins[name] = plugin
loaded_plugins.add(plugin) loaded_plugins.add(plugin)
logger.info(f"Succeeded to import \"{name}\"") logger.opt(colors=True).info(f'Succeeded to import "<y>{name}</y>"')
except Exception as e: except Exception as e:
logger.error(f"Failed to import \"{name}\", error: {e}") logger.opt(colors=True, exception=e).error(
logger.exception(e) f'<r><bg #f8bbd0>Failed to import "{name}"</bg #f8bbd0></r>')
return loaded_plugins return loaded_plugins
def load_builtin_plugins():
return load_plugin("nonebot.plugins.base")
def get_loaded_plugins() -> Set[Plugin]: def get_loaded_plugins() -> Set[Plugin]:
return set(plugins.values()) return set(plugins.values())

10
nonebot/plugins/base.py Normal file
View File

@ -0,0 +1,10 @@
from nonebot.rule import to_me
from nonebot.plugin import on_command
from nonebot.typing import Bot, Event
say = on_command("say", to_me())
@say.handle()
async def repeat(bot: Bot, event: Event, state: dict):
await bot.send(message=event.message, event=event)

View File

@ -74,13 +74,21 @@ class TrieRule:
suffix = cls.suffix.longest_prefix( suffix = cls.suffix.longest_prefix(
message_r.data["text"].rstrip()[::-1]) message_r.data["text"].rstrip()[::-1])
state["_prefix"] = {prefix.key: prefix.value} if prefix else {} state["_prefix"] = {
state["_suffix"] = {suffix.key: suffix.value} if suffix else {} "raw_command": prefix.key,
"command": prefix.value
} if prefix else {}
state["_suffix"] = {
"raw_command": suffix.key,
"command": suffix.value
} if suffix else {}
return ({ return ({
prefix.key: prefix.value "raw_command": prefix.key,
"command": prefix.value
} if prefix else {}, { } if prefix else {}, {
suffix.key: suffix.value "raw_command": suffix.key,
"command": suffix.value
} if suffix else {}) } if suffix else {})
@ -122,7 +130,7 @@ def command(command: Tuple[str, ...]) -> Rule:
TrieRule.add_prefix(f"{start}{sep.join(command)}", command) TrieRule.add_prefix(f"{start}{sep.join(command)}", command)
async def _command(bot: Bot, event: Event, state: dict) -> bool: async def _command(bot: Bot, event: Event, state: dict) -> bool:
return command in state["_prefix"].values() return command == state["_prefix"]["command"]
return Rule(_command) return Rule(_command)

51
poetry.lock generated
View File

@ -99,7 +99,7 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
[[package]] [[package]]
category = "dev" category = "main"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\"" marker = "sys_platform == \"win32\""
name = "colorama" name = "colorama"
@ -332,6 +332,26 @@ reference = "aliyun"
type = "legacy" type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
[[package]]
category = "main"
description = "Python logging made (stupidly) simple"
name = "loguru"
optional = false
python-versions = ">=3.5"
version = "0.5.1"
[package.dependencies]
colorama = ">=0.3.4"
win32-setctime = ">=1.0.0"
[package.extras]
dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.3b0)"]
[package.source]
reference = "aliyun"
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
@ -603,6 +623,7 @@ yapf = "*"
reference = "1438d33cbeaab0230c9f7e33bd059eb9f57c86d6" reference = "1438d33cbeaab0230c9f7e33bd059eb9f57c86d6"
type = "git" type = "git"
url = "https://github.com/nonebot/sphinx-markdown-builder.git" url = "https://github.com/nonebot/sphinx-markdown-builder.git"
[[package]] [[package]]
category = "dev" category = "dev"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
@ -833,6 +854,23 @@ reference = "aliyun"
type = "legacy" type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
[[package]]
category = "main"
description = "A small Python utility to set file creation time on Windows"
marker = "sys_platform == \"win32\""
name = "win32-setctime"
optional = false
python-versions = ">=3.5"
version = "1.0.1"
[package.extras]
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[package.source]
reference = "aliyun"
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
[[package]] [[package]]
category = "dev" category = "dev"
description = "A formatter for Python code." description = "A formatter for Python code."
@ -848,10 +886,9 @@ url = "https://mirrors.aliyun.com/pypi/simple"
[extras] [extras]
scheduler = ["apscheduler"] scheduler = ["apscheduler"]
test = []
[metadata] [metadata]
content-hash = "4d16d7ad0930bc9851802bc149f843c4e990a987e89414d765579ea8dccc8d6e" content-hash = "2e8f1fc9fcb89a528ecbebbf0f2315abf39e3de8eb40c133b91085a784e49173"
python-versions = "^3.7" python-versions = "^3.7"
[metadata.files] [metadata.files]
@ -949,6 +986,10 @@ jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
] ]
loguru = [
{file = "loguru-0.5.1-py3-none-any.whl", hash = "sha256:e5d362a43cd2fc2da63551d79a6830619c4d5b3a8b976515748026f92f351b61"},
{file = "loguru-0.5.1.tar.gz", hash = "sha256:70201d5fce26da89b7a5f168caa2bb674e06b969829f56737db1d6472e53e7c3"},
]
markupsafe = [ markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
@ -1136,6 +1177,10 @@ websockets = [
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
] ]
win32-setctime = [
{file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"},
{file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"},
]
yapf = [ yapf = [
{file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"}, {file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"},
{file = "yapf-0.30.0.tar.gz", hash = "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427"}, {file = "yapf-0.30.0.tar.gz", hash = "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427"},

View File

@ -27,15 +27,16 @@ fastapi = "^0.58.1"
uvicorn = "^0.11.5" uvicorn = "^0.11.5"
pydantic = { extras = ["dotenv"], version = "^1.6.1" } pydantic = { extras = ["dotenv"], version = "^1.6.1" }
apscheduler = { version = "^3.6.3", optional = true } apscheduler = { version = "^3.6.3", optional = true }
nonebot-test = { version = "^0.1.0", optional = true } # nonebot-test = { version = "^0.1.0", optional = true }
loguru = "^0.5.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
yapf = "^0.30.0" yapf = "^0.30.0"
sphinx = "^3.1.1" sphinx = "^3.1.1"
sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" } sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" }
[tool.poetry.extras] [tool.poetry.extras]
test = ["nonebot-test"] # test = ["nonebot-test"]
scheduler = ["apscheduler"] scheduler = ["apscheduler"]
[[tool.poetry.source]] [[tool.poetry.source]]

View File

@ -7,10 +7,22 @@ import sys
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".."))
import nonebot import nonebot
from nonebot.log import logger, default_format
# test custom log
logger.add("error.log",
rotation="00:00",
diagnose=False,
level="ERROR",
format=default_format)
nonebot.init() nonebot.init()
app = nonebot.get_asgi() app = nonebot.get_asgi()
# load builtin plugin
nonebot.load_plugin("nonebot.plugins.base")
# load local plugins
nonebot.load_plugins("test_plugins") nonebot.load_plugins("test_plugins")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -11,7 +11,7 @@ test_command = on_command("帮助", to_me())
@test_command.handle() @test_command.handle()
async def test_handler(bot: Bot, event: Event, state: dict): async def test_handler(bot: Bot, event: Event, state: dict):
args = str(event.message)[len(list(state["_prefix"].keys())[0]):].strip() args = str(event.message).strip()
print("[!] Command:", state["_prefix"], "Args:", args) print("[!] Command:", state["_prefix"], "Args:", args)
if args: if args:
state["help"] = args state["help"] = args