diff --git a/example-config.yaml b/example-config.yaml index d4701b8..f4bc3d6 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -26,6 +26,8 @@ server: base_path: /_matrix/maubot/v1 # The base path for the UI. ui_base_path: /_matrix/maubot + # The base path for plugin endpoints. {id} is replaced with the ID of the instance. + plugin_base_path: /_matrix/maubot/plugin/{id} # Override path from where to load UI resources. # Set to false to using pkg_resources to find the path. override_resource_path: false diff --git a/maubot/__main__.py b/maubot/__main__.py index 2a6a3f2..b88d960 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -58,10 +58,10 @@ loop = asyncio.get_event_loop() init_zip_loader(config) db_session = init_db(config) clients = init_client_class(db_session, loop) -plugins = init_plugin_instance_class(db_session, config, loop) management_api = init_mgmt_api(config, loop) server = MaubotServer(config, loop) server.app.add_subapp(config["server.base_path"], management_api) +plugins = init_plugin_instance_class(db_session, config, server.app, loop) for plugin in plugins: plugin.load() diff --git a/maubot/config.py b/maubot/config.py index f3b56e3..b1ee67f 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -41,6 +41,7 @@ class Config(BaseFileConfig): copy("server.listen") copy("server.base_path") copy("server.ui_base_path") + copy("server.plugin_base_path") copy("server.override_resource_path") copy("server.appservice_base_path") shared_secret = self["server.unshared_secret"] diff --git a/maubot/instance.py b/maubot/instance.py index fd6fa57..10db272 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -13,8 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Optional +from typing import Dict, List, Optional, TYPE_CHECKING from asyncio import AbstractEventLoop +from aiohttp import web import os.path import logging import io @@ -33,6 +34,9 @@ from .client import Client from .loader import PluginLoader, ZippedPluginLoader from .plugin_base import Plugin +if TYPE_CHECKING: + from .server import MaubotServer + log = logging.getLogger("maubot.instance") yaml = YAML() @@ -41,6 +45,7 @@ yaml.indent(4) class PluginInstance: db: Session = None + webserver: 'MaubotServer' = None mb_config: Config = None loop: AbstractEventLoop = None cache: Dict[str, 'PluginInstance'] = {} @@ -54,6 +59,7 @@ class PluginInstance: base_cfg: RecursiveDict[CommentedMap] inst_db: sql.engine.Engine inst_db_tables: Dict[str, sql.Table] + inst_webapp: web.Application started: bool def __init__(self, db_instance: DBPlugin): @@ -66,6 +72,7 @@ class PluginInstance: self.plugin = None self.inst_db = None self.inst_db_tables = None + self.inst_webapp = None self.base_cfg = None self.cache[self.id] = self @@ -105,6 +112,8 @@ class PluginInstance: if self.loader.meta.database: db_path = os.path.join(self.mb_config["plugin_directories.db"], self.id) self.inst_db = sql.create_engine(f"sqlite:///{db_path}.db") + if self.loader.meta.webapp: + self.inst_webapp = self.webserver.get_instance_subapp(self.id) self.log.debug("Plugin instance dependencies loaded") self.loader.references.add(self) self.client.references.add(self) @@ -126,6 +135,8 @@ class PluginInstance: ZippedPluginLoader.trash( os.path.join(self.mb_config["plugin_directories.db"], f"{self.id}.db"), reason="deleted") + if self.inst_webapp: + self.webserver.remove_instance_webapp(self.id) def load_config(self) -> CommentedMap: return yaml.load(self.db_instance.config) @@ -157,7 +168,7 @@ class PluginInstance: self.config = config_class(self.load_config, lambda: self.base_cfg, self.save_config) self.plugin = cls(client=self.client.client, loop=self.loop, http=self.client.http_client, instance_id=self.id, log=self.log, config=self.config, - database=self.inst_db) + database=self.inst_db, webapp=self.inst_webapp) try: await self.plugin.start() except Exception: @@ -274,8 +285,10 @@ class PluginInstance: # endregion -def init(db: Session, config: Config, loop: AbstractEventLoop) -> List[PluginInstance]: +def init(db: Session, config: Config, webserver: 'MaubotServer', loop: AbstractEventLoop) -> List[ + PluginInstance]: PluginInstance.db = db PluginInstance.mb_config = config PluginInstance.loop = loop + PluginInstance.webserver = webserver return PluginInstance.all() diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index f4b62a7..4f0c5ed 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -57,6 +57,7 @@ class PluginMeta(SerializableAttrs['PluginMeta']): maubot: Version = Version(__version__) database: bool = False + webapp: bool = False license: str = "" extra_files: List[str] = [] dependencies: List[str] = [] diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index fbc7891..1e40bd9 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -17,7 +17,7 @@ from typing import Type, Optional, TYPE_CHECKING from abc import ABC from logging import Logger from asyncio import AbstractEventLoop -import functools +from aiohttp.web import Application from sqlalchemy.engine.base import Engine from aiohttp import ClientSession @@ -37,7 +37,7 @@ class Plugin(ABC): def __init__(self, client: 'MaubotMatrixClient', loop: AbstractEventLoop, http: ClientSession, instance_id: str, log: Logger, config: Optional['BaseProxyConfig'], - database: Optional[Engine]) -> None: + database: Optional[Engine], webapp: Optional[Application]) -> None: self.client = client self.loop = loop self.http = http @@ -45,6 +45,7 @@ class Plugin(ABC): self.log = log self.config = config self.database = database + self.webapp = webapp self._handlers_at_startup = [] async def start(self) -> None: diff --git a/maubot/server.py b/maubot/server.py index 35682ee..ed3eebf 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -38,16 +38,34 @@ class MaubotServer: def __init__(self, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() - self.app = web.Application(loop=self.loop, client_max_size=100*1024*1024) + self.app = web.Application(loop=self.loop, client_max_size=100 * 1024 * 1024) self.config = config as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) + self.subapps = {} self.setup_management_ui() self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) + def get_instance_subapp(self, instance_id: str) -> web.Application: + try: + return self.subapps[instance_id] + except KeyError: + app = web.Application(loop=self.loop) + self.app.add_subapp(self.config["server.plugin_base_path"].format(id=instance_id), app) + self.subapps[instance_id] = app + return app + + def remove_instance_webapp(self, instance_id: str) -> None: + try: + subapp: web.Application = self.subapps.pop(instance_id) + except KeyError: + return + subapp.shutdown() + subapp.cleanup() + def setup_management_ui(self) -> None: ui_base = self.config["server.ui_base_path"] if ui_base == "/":