diff --git a/maubot/config.py b/maubot/config.py index 57e6552..9b18b6b 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -26,7 +26,7 @@ bcrypt_regex = re.compile(r"^\$2[ayb]\$.{56}$") class Config(BaseFileConfig): @staticmethod def _new_token() -> str: - return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(64)) + return "".join(random.choices(string.ascii_lowercase + string.digits, k=64)) def do_update(self, helper: ConfigUpdateHelper) -> None: base = helper.base diff --git a/maubot/handlers/__init__.py b/maubot/handlers/__init__.py index be2d03e..1d9da7e 100644 --- a/maubot/handlers/__init__.py +++ b/maubot/handlers/__init__.py @@ -1 +1 @@ -from . import event, command +from . import event, command, web diff --git a/maubot/handlers/command.py b/maubot/handlers/command.py index 20de000..cd68ea0 100644 --- a/maubot/handlers/command.py +++ b/maubot/handlers/command.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import (Union, Callable, Sequence, Pattern, Awaitable, NewType, Optional, Any, List, - Dict, Tuple, Set) + Dict, Tuple, Set, Iterable) from abc import ABC, abstractmethod import asyncio import functools @@ -44,17 +44,17 @@ def _split_in_two(val: str, split_by: str) -> List[str]: class CommandHandler: def __init__(self, func: CommandHandlerFunc) -> None: self.__mb_func__: CommandHandlerFunc = func - self.__mb_parent__: CommandHandler = None + self.__mb_parent__: Optional[CommandHandler] = None self.__mb_subcommands__: List[CommandHandler] = [] self.__mb_arguments__: List[Argument] = [] - self.__mb_help__: str = None - self.__mb_get_name__: Callable[[], str] = None + self.__mb_help__: Optional[str] = None + self.__mb_get_name__: Callable[[Any], str] = lambda s: "noname" self.__mb_is_command_match__: Callable[[Any, str], bool] = self.__command_match_unset self.__mb_require_subcommand__: bool = True self.__mb_arg_fallthrough__: bool = True self.__mb_event_handler__: bool = True self.__mb_event_type__: EventType = EventType.ROOM_MESSAGE - self.__mb_msgtypes__: List[MessageType] = (MessageType.TEXT,) + self.__mb_msgtypes__: Iterable[MessageType] = (MessageType.TEXT,) self.__bound_copies__: Dict[Any, CommandHandler] = {} self.__bound_instance__: Any = None @@ -78,7 +78,7 @@ class CommandHandler: return new_ch @staticmethod - def __command_match_unset(self, val: str) -> str: + def __command_match_unset(self, val: str) -> bool: raise NotImplementedError("Hmm") async def __call__(self, evt: MaubotMessageEvent, *, _existing_args: Dict[str, Any] = None, @@ -132,7 +132,7 @@ class CommandHandler: except ArgumentSyntaxError as e: await evt.reply(e.message + (f"\n{self.__mb_usage__}" if e.show_usage else "")) return False, remaining_val - except ValueError as e: + except ValueError: await evt.reply(self.__mb_usage__) return False, remaining_val return True, remaining_val @@ -206,7 +206,7 @@ class CommandHandler: def new(name: PrefixType = None, *, help: str = None, aliases: AliasesType = None, - event_type: EventType = EventType.ROOM_MESSAGE, msgtypes: List[MessageType] = None, + event_type: EventType = EventType.ROOM_MESSAGE, msgtypes: Iterable[MessageType] = None, require_subcommand: bool = True, arg_fallthrough: bool = True) -> CommandHandlerDecorator: def decorator(func: Union[CommandHandler, CommandHandlerFunc]) -> CommandHandler: if not isinstance(func, CommandHandler): diff --git a/maubot/handlers/web.py b/maubot/handlers/web.py new file mode 100644 index 0000000..cf53d68 --- /dev/null +++ b/maubot/handlers/web.py @@ -0,0 +1,66 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2019 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Callable, Any, Awaitable + +from aiohttp import web, hdrs + +WebHandler = Callable[[web.Request], Awaitable[web.StreamResponse]] +WebHandlerDecorator = Callable[[WebHandler], WebHandler] + + +def head(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_HEAD, path, **kwargs) + + +def options(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_OPTIONS, path, **kwargs) + + +def get(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_GET, path, **kwargs) + + +def post(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_POST, path, **kwargs) + + +def put(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_PUT, path, **kwargs) + + +def patch(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_PATCH, path, **kwargs) + + +def delete(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_DELETE, path, **kwargs) + + +def view(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_ANY, path, **kwargs) + + +def handle(method: str, path: str, **kwargs) -> WebHandlerDecorator: + def decorator(handler: WebHandler) -> WebHandler: + try: + handlers = getattr(handler, "__mb_web_handler__") + except AttributeError: + handlers = [] + setattr(handler, "__mb_web_handler__", handlers) + handlers.append((method, path, kwargs)) + return handler + + return decorator diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index 8e1cfbd..7fd63cf 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -215,7 +215,7 @@ class ZippedPluginLoader(PluginLoader): async def unload(self) -> None: for name, mod in list(sys.modules.items()): - if getattr(mod, "__file__", "").startswith(self.path): + if (getattr(mod, "__file__", "") or "").startswith(self.path): del sys.modules[name] self._loaded = None self.log.debug(f"Unloaded plugin {self.meta.id} at {self.path}") diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index eb49818..a0b28ae 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -55,14 +55,23 @@ class Plugin(ABC): async def start(self) -> None: for key in dir(self): val = getattr(self, key) - if hasattr(val, "__mb_event_handler__") and val.__mb_event_handler__: - self._handlers_at_startup.append((val, val.__mb_event_type__)) - self.client.add_event_handler(val.__mb_event_type__, val) + try: + if val.__mb_event_handler__: + self._handlers_at_startup.append((val, val.__mb_event_type__)) + self.client.add_event_handler(val.__mb_event_type__, val) + except AttributeError: + pass + try: + web_handlers = val.__mb_web_handler__ + for method, path, kwargs in web_handlers: + self.webapp.add_route(method=method, path=path, handler=val, **kwargs) + except AttributeError: + pass async def stop(self) -> None: for func, event_type in self._handlers_at_startup: self.client.remove_event_handler(event_type, func) - if self.webapp: + if self.webapp is not None: self.webapp.clear() @classmethod diff --git a/maubot/plugin_server.py b/maubot/plugin_server.py index a5dd49a..bd6c8b3 100644 --- a/maubot/plugin_server.py +++ b/maubot/plugin_server.py @@ -39,7 +39,7 @@ class PluginWebApp(web.UrlDispatcher): self._named_resources = {} self._middleware = [] - async def handle(self, request: web.Request) -> web.Response: + async def handle(self, request: web.Request) -> web.StreamResponse: match_info = await self.resolve(request) match_info.freeze() resp = None