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)