diff --git a/example-config.yaml b/example-config.yaml index b3987f1..1b02d67 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -24,6 +24,11 @@ server: port: 29316 # The base management API path. base_path: /_matrix/maubot/v1 + # The base path for the UI. + ui_base_path: /_matrix/maubot + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: false # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 # The shared secret to sign API access tokens. diff --git a/maubot/__main__.py b/maubot/__main__.py index 3929095..e970c50 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management +from .management.api import init as init_management_api from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -52,8 +52,9 @@ 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_management(config, loop) -server = MaubotServer(config, management_api, loop) +management_api = init_management_api(config, loop) +server = MaubotServer(config, loop) +server.app.add_subapp(config["server.base_path"], management_api) for plugin in plugins: plugin.load() diff --git a/maubot/config.py b/maubot/config.py index cf39d00..ea8dd3c 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -38,6 +38,9 @@ class Config(BaseFileConfig): copy("server.hostname") copy("server.port") copy("server.listen") + copy("server.base_path") + copy("server.ui_base_path") + copy("server.override_resource_path") copy("server.appservice_base_path") shared_secret = self["server.unshared_secret"] if shared_secret is None or shared_secret == "generate": diff --git a/maubot/management/api/base.py b/maubot/management/api/base.py index d9c2077..4cf9636 100644 --- a/maubot/management/api/base.py +++ b/maubot/management/api/base.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from aiohttp import web +from ...__meta__ import __version__ from ...config import Config routes: web.RouteTableDef = web.RouteTableDef() @@ -28,3 +29,10 @@ def set_config(config: Config) -> None: def get_config() -> Config: return _config + + +@routes.get("/version") +async def version(_: web.Request) -> web.Response: + return web.json_response({ + "version": __version__ + }) diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index c25113f..320679b 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -26,6 +26,7 @@ "last 2 ios_saf versions" ], "proxy": "http://localhost:29316", + "homepage": ".", "devDependencies": { "sass-lint": "^1.12.1", "sass-lint-auto-fix": "^0.15.0" diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index b655085..63f419c 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { BrowserRouter as Router, Switch } from "react-router-dom" +import { HashRouter as Router, Switch } from "react-router-dom" import PrivateRoute from "../components/PrivateRoute" import Spinner from "../components/Spinner" import api from "../api" diff --git a/maubot/server.py b/maubot/server.py index 5788d8a..06f1c4d 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -18,6 +18,7 @@ import asyncio from aiohttp import web from aiohttp.abc import AbstractAccessLogger +import pkg_resources from mautrix.api import PathBuilder, Method @@ -35,21 +36,56 @@ class AccessLogger(AbstractAccessLogger): class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") - def __init__(self, config: Config, management: web.Application, - loop: asyncio.AbstractEventLoop) -> None: + def __init__(self, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() self.app = web.Application(loop=self.loop) self.config = config - path = PathBuilder(config["server.base_path"]) - self.add_route(Method.GET, path.version, self.version) - self.app.add_subapp(config["server.base_path"], management) - as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) + self.setup_management_ui() + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) + def setup_management_ui(self) -> None: + ui_base = self.config["server.ui_base_path"] + if ui_base == "/": + ui_base = "" + directory = (self.config["server.override_resource_path"] + or pkg_resources.resource_filename("maubot", "management/frontend/build")) + self.app.router.add_static(f"{ui_base}/static", f"{directory}/static") + self.setup_static_root_files(directory, ui_base) + + with open(f"{directory}/index.html", "r") as file: + index_html = file.read() + + @web.middleware + async def frontend_404_middleware(request, handler): + if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource): + try: + return await handler(request) + except web.HTTPNotFound: + return web.Response(body=index_html, content_type="text/html") + return await handler(request) + + self.app.middlewares.append(frontend_404_middleware) + self.app.router.add_get(f"{ui_base}/", lambda _: web.Response(body=index_html, + content_type="text/html")) + self.app.router.add_get(ui_base, lambda _: web.HTTPFound(f"{ui_base}/")) + + def setup_static_root_files(self, directory: str, ui_base: str) -> None: + files = { + "asset-manifest.json": "application/json", + "manifest.json": "application/json", + "favicon.png": "image/png", + } + for file, mime in files.items(): + with open(f"{directory}/{file}", "rb") as stream: + data = stream.read() + self.app.router.add_get(f"{ui_base}/{file}", lambda _: web.Response(body=data, + content_type=mime)) + def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler)