Add config option to disable parts of management API
This commit is contained in:
parent
147081c0db
commit
75b5ac8ebd
17 changed files with 287 additions and 149 deletions
|
@ -48,6 +48,19 @@ registration_secrets:
|
|||
admins:
|
||||
root: ""
|
||||
|
||||
# API feature switches.
|
||||
api_features:
|
||||
login: true
|
||||
plugin: true
|
||||
plugin_upload: true
|
||||
instance: true
|
||||
instance_database: true
|
||||
client: true
|
||||
client_proxy: true
|
||||
client_auth: true
|
||||
dev_open: true
|
||||
log: true
|
||||
|
||||
# Python logging configuration.
|
||||
#
|
||||
# See section 16.7.2 of the Python documentation for more info:
|
||||
|
|
|
@ -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_mgmt_api, stop as stop_mgmt_api, init_log_listener
|
||||
from .management.api import init as init_mgmt_api
|
||||
from .__meta__ import __version__
|
||||
|
||||
parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.",
|
||||
|
@ -43,7 +43,13 @@ config.load()
|
|||
config.update()
|
||||
|
||||
logging.config.dictConfig(copy.deepcopy(config["logging"]))
|
||||
init_log_listener()
|
||||
|
||||
stop_log_listener = None
|
||||
if config["api_features.log"]:
|
||||
from .management.api.log import init as init_log_listener, stop_all as stop_log_listener
|
||||
|
||||
init_log_listener()
|
||||
|
||||
log = logging.getLogger("maubot.init")
|
||||
log.info(f"Initializing maubot {__version__}")
|
||||
|
||||
|
@ -88,8 +94,9 @@ except KeyboardInterrupt:
|
|||
loop.run_until_complete(asyncio.gather(*[client.stop() for client in Client.cache.values()],
|
||||
loop=loop))
|
||||
db_session.commit()
|
||||
log.debug("Closing websockets")
|
||||
loop.run_until_complete(stop_mgmt_api())
|
||||
if stop_log_listener is not None:
|
||||
log.debug("Closing websockets")
|
||||
loop.run_until_complete(stop_log_listener())
|
||||
log.debug("Stopping server")
|
||||
try:
|
||||
loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop))
|
||||
|
|
|
@ -56,6 +56,16 @@ class Config(BaseFileConfig):
|
|||
password = self._new_token()
|
||||
base["admins"][username] = bcrypt.hashpw(password.encode("utf-8"),
|
||||
bcrypt.gensalt()).decode("utf-8")
|
||||
copy("api_features.login")
|
||||
copy("api_features.plugin")
|
||||
copy("api_features.plugin_upload")
|
||||
copy("api_features.instance")
|
||||
copy("api_features.instance_database")
|
||||
copy("api_features.client")
|
||||
copy("api_features.client_proxy")
|
||||
copy("api_features.client_auth")
|
||||
copy("api_features.dev_open")
|
||||
copy("api_features.log")
|
||||
copy("logging")
|
||||
|
||||
def is_admin(self, user: str) -> bool:
|
||||
|
|
|
@ -77,7 +77,8 @@ class PluginInstance:
|
|||
"started": self.started,
|
||||
"primary_user": self.primary_user,
|
||||
"config": self.db_instance.config,
|
||||
"database": self.inst_db is not None,
|
||||
"database": (self.inst_db is not None
|
||||
and self.mb_config["api_features.instance_database"]),
|
||||
}
|
||||
|
||||
def get_db_tables(self) -> Dict[str, sql.Table]:
|
||||
|
|
|
@ -15,21 +15,24 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from aiohttp import web
|
||||
from asyncio import AbstractEventLoop
|
||||
import importlib
|
||||
|
||||
from ...config import Config
|
||||
from .base import routes, set_config, set_loop
|
||||
from .base import routes, get_config, set_config, set_loop
|
||||
from .middleware import auth, error
|
||||
from . import auth, plugin, instance, instance_database, client, client_proxy, client_auth, dev_open
|
||||
from .log import stop_all as stop_log_sockets, init as init_log_listener
|
||||
|
||||
|
||||
@routes.get("/features")
|
||||
def features(_: web.Request) -> web.Response:
|
||||
return web.json_response(get_config()["api_features"])
|
||||
|
||||
|
||||
def init(cfg: Config, loop: AbstractEventLoop) -> web.Application:
|
||||
set_config(cfg)
|
||||
set_loop(loop)
|
||||
app = web.Application(loop=loop, middlewares=[auth, error], client_max_size=100*1024*1024)
|
||||
for pkg, enabled in cfg["api_features"].items():
|
||||
if enabled:
|
||||
importlib.import_module(f"maubot.management.api.{pkg}")
|
||||
app = web.Application(loop=loop, middlewares=[auth, error], client_max_size=100 * 1024 * 1024)
|
||||
app.add_routes(routes)
|
||||
return app
|
||||
|
||||
|
||||
async def stop() -> None:
|
||||
await stop_log_sockets()
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import Optional
|
||||
from time import time
|
||||
import json
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
@ -71,22 +70,3 @@ async def ping(request: web.Request) -> web.Response:
|
|||
if not get_config().is_admin(user):
|
||||
return resp.invalid_token
|
||||
return resp.pong(user)
|
||||
|
||||
|
||||
@routes.post("/auth/login")
|
||||
async def login(request: web.Request) -> web.Response:
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return resp.body_not_json
|
||||
secret = data.get("secret")
|
||||
if secret and get_config()["server.unshared_secret"] == secret:
|
||||
user = data.get("user") or "root"
|
||||
return resp.logged_in(create_token(user))
|
||||
|
||||
username = data.get("username")
|
||||
password = data.get("password")
|
||||
if get_config().check_password(username, password):
|
||||
return resp.logged_in(create_token(username))
|
||||
|
||||
return resp.bad_auth
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from aiohttp import web, client as http
|
||||
|
||||
from ...client import Client
|
||||
from .base import routes
|
||||
from .responses import resp
|
||||
|
|
40
maubot/management/api/login.py
Normal file
40
maubot/management/api/login.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2018 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 <https://www.gnu.org/licenses/>.
|
||||
import json
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .base import routes, get_config
|
||||
from .responses import resp
|
||||
from .auth import create_token
|
||||
|
||||
@routes.post("/auth/login")
|
||||
async def login(request: web.Request) -> web.Response:
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return resp.body_not_json
|
||||
secret = data.get("secret")
|
||||
if secret and get_config()["server.unshared_secret"] == secret:
|
||||
user = data.get("user") or "root"
|
||||
return resp.logged_in(create_token(user))
|
||||
|
||||
username = data.get("username")
|
||||
password = data.get("password")
|
||||
if get_config().check_password(username, password):
|
||||
return resp.logged_in(create_token(username))
|
||||
|
||||
return resp.bad_auth
|
|
@ -13,18 +13,13 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from io import BytesIO
|
||||
from time import time
|
||||
import traceback
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from aiohttp import web
|
||||
from packaging.version import Version
|
||||
|
||||
from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError
|
||||
from ...loader import PluginLoader, MaubotZipImportError
|
||||
from .responses import resp
|
||||
from .base import routes, get_config
|
||||
from .base import routes
|
||||
|
||||
|
||||
@routes.get("/plugins")
|
||||
|
@ -67,85 +62,3 @@ async def reload_plugin(request: web.Request) -> web.Response:
|
|||
return resp.plugin_reload_error(str(e), traceback.format_exc())
|
||||
await plugin.start_instances()
|
||||
return resp.ok
|
||||
|
||||
|
||||
@routes.put("/plugin/{id}")
|
||||
async def put_plugin(request: web.Request) -> web.Response:
|
||||
plugin_id = request.match_info.get("id", None)
|
||||
content = await request.read()
|
||||
file = BytesIO(content)
|
||||
try:
|
||||
pid, version = ZippedPluginLoader.verify_meta(file)
|
||||
except MaubotZipImportError as e:
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
if pid != plugin_id:
|
||||
return resp.pid_mismatch
|
||||
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
||||
if not plugin:
|
||||
return await upload_new_plugin(content, pid, version)
|
||||
elif isinstance(plugin, ZippedPluginLoader):
|
||||
return await upload_replacement_plugin(plugin, content, version)
|
||||
else:
|
||||
return resp.unsupported_plugin_loader
|
||||
|
||||
|
||||
@routes.post("/plugins/upload")
|
||||
async def upload_plugin(request: web.Request) -> web.Response:
|
||||
content = await request.read()
|
||||
file = BytesIO(content)
|
||||
try:
|
||||
pid, version = ZippedPluginLoader.verify_meta(file)
|
||||
except MaubotZipImportError as e:
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
plugin = PluginLoader.id_cache.get(pid, None)
|
||||
if not plugin:
|
||||
return await upload_new_plugin(content, pid, version)
|
||||
elif not request.query.get("allow_override"):
|
||||
return resp.plugin_exists
|
||||
elif isinstance(plugin, ZippedPluginLoader):
|
||||
return await upload_replacement_plugin(plugin, content, version)
|
||||
else:
|
||||
return resp.unsupported_plugin_loader
|
||||
|
||||
|
||||
async def upload_new_plugin(content: bytes, pid: str, version: Version) -> web.Response:
|
||||
path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp")
|
||||
with open(path, "wb") as p:
|
||||
p.write(content)
|
||||
try:
|
||||
plugin = ZippedPluginLoader.get(path)
|
||||
except MaubotZipImportError as e:
|
||||
ZippedPluginLoader.trash(path)
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
return resp.created(plugin.to_dict())
|
||||
|
||||
|
||||
async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes,
|
||||
new_version: Version) -> web.Response:
|
||||
dirname = os.path.dirname(plugin.path)
|
||||
old_filename = os.path.basename(plugin.path)
|
||||
if str(plugin.meta.version) in old_filename:
|
||||
replacement = (new_version if plugin.meta.version != new_version
|
||||
else f"{new_version}-ts{int(time())}")
|
||||
filename = re.sub(f"{re.escape(str(plugin.meta.version))}(-ts[0-9]+)?",
|
||||
replacement, old_filename)
|
||||
else:
|
||||
filename = old_filename.rstrip(".mbp")
|
||||
filename = f"{filename}-v{new_version}.mbp"
|
||||
path = os.path.join(dirname, filename)
|
||||
with open(path, "wb") as p:
|
||||
p.write(content)
|
||||
old_path = plugin.path
|
||||
await plugin.stop_instances()
|
||||
try:
|
||||
await plugin.reload(new_path=path)
|
||||
except MaubotZipImportError as e:
|
||||
try:
|
||||
await plugin.reload(new_path=old_path)
|
||||
await plugin.start_instances()
|
||||
except MaubotZipImportError:
|
||||
pass
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
await plugin.start_instances()
|
||||
ZippedPluginLoader.trash(old_path, reason="update")
|
||||
return resp.updated(plugin.to_dict())
|
||||
|
|
109
maubot/management/api/plugin_upload.py
Normal file
109
maubot/management/api/plugin_upload.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2018 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 <https://www.gnu.org/licenses/>.
|
||||
from io import BytesIO
|
||||
from time import time
|
||||
import traceback
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from aiohttp import web
|
||||
from packaging.version import Version
|
||||
|
||||
from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError
|
||||
from .responses import resp
|
||||
from .base import routes, get_config
|
||||
|
||||
|
||||
@routes.put("/plugin/{id}")
|
||||
async def put_plugin(request: web.Request) -> web.Response:
|
||||
plugin_id = request.match_info.get("id", None)
|
||||
content = await request.read()
|
||||
file = BytesIO(content)
|
||||
try:
|
||||
pid, version = ZippedPluginLoader.verify_meta(file)
|
||||
except MaubotZipImportError as e:
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
if pid != plugin_id:
|
||||
return resp.pid_mismatch
|
||||
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
||||
if not plugin:
|
||||
return await upload_new_plugin(content, pid, version)
|
||||
elif isinstance(plugin, ZippedPluginLoader):
|
||||
return await upload_replacement_plugin(plugin, content, version)
|
||||
else:
|
||||
return resp.unsupported_plugin_loader
|
||||
|
||||
|
||||
@routes.post("/plugins/upload")
|
||||
async def upload_plugin(request: web.Request) -> web.Response:
|
||||
content = await request.read()
|
||||
file = BytesIO(content)
|
||||
try:
|
||||
pid, version = ZippedPluginLoader.verify_meta(file)
|
||||
except MaubotZipImportError as e:
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
plugin = PluginLoader.id_cache.get(pid, None)
|
||||
if not plugin:
|
||||
return await upload_new_plugin(content, pid, version)
|
||||
elif not request.query.get("allow_override"):
|
||||
return resp.plugin_exists
|
||||
elif isinstance(plugin, ZippedPluginLoader):
|
||||
return await upload_replacement_plugin(plugin, content, version)
|
||||
else:
|
||||
return resp.unsupported_plugin_loader
|
||||
|
||||
|
||||
async def upload_new_plugin(content: bytes, pid: str, version: Version) -> web.Response:
|
||||
path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp")
|
||||
with open(path, "wb") as p:
|
||||
p.write(content)
|
||||
try:
|
||||
plugin = ZippedPluginLoader.get(path)
|
||||
except MaubotZipImportError as e:
|
||||
ZippedPluginLoader.trash(path)
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
return resp.created(plugin.to_dict())
|
||||
|
||||
|
||||
async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes,
|
||||
new_version: Version) -> web.Response:
|
||||
dirname = os.path.dirname(plugin.path)
|
||||
old_filename = os.path.basename(plugin.path)
|
||||
if str(plugin.meta.version) in old_filename:
|
||||
replacement = (new_version if plugin.meta.version != new_version
|
||||
else f"{new_version}-ts{int(time())}")
|
||||
filename = re.sub(f"{re.escape(str(plugin.meta.version))}(-ts[0-9]+)?",
|
||||
replacement, old_filename)
|
||||
else:
|
||||
filename = old_filename.rstrip(".mbp")
|
||||
filename = f"{filename}-v{new_version}.mbp"
|
||||
path = os.path.join(dirname, filename)
|
||||
with open(path, "wb") as p:
|
||||
p.write(content)
|
||||
old_path = plugin.path
|
||||
await plugin.stop_instances()
|
||||
try:
|
||||
await plugin.reload(new_path=path)
|
||||
except MaubotZipImportError as e:
|
||||
try:
|
||||
await plugin.reload(new_path=old_path)
|
||||
await plugin.start_instances()
|
||||
except MaubotZipImportError:
|
||||
pass
|
||||
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||
await plugin.start_instances()
|
||||
ZippedPluginLoader.trash(old_path, reason="update")
|
||||
return resp.updated(plugin.to_dict())
|
|
@ -61,7 +61,12 @@ export async function login(username, password) {
|
|||
return await resp.json()
|
||||
}
|
||||
|
||||
let features = null
|
||||
|
||||
export async function ping() {
|
||||
if (!features) {
|
||||
await remoteGetFeatures()
|
||||
}
|
||||
const response = await fetch(`${BASE_PATH}/auth/ping`, {
|
||||
method: "POST",
|
||||
headers: getHeaders(),
|
||||
|
@ -75,6 +80,12 @@ export async function ping() {
|
|||
throw json
|
||||
}
|
||||
|
||||
export const remoteGetFeatures = async () => {
|
||||
features = await defaultGet("/features")
|
||||
}
|
||||
|
||||
export const getFeatures = () => features
|
||||
|
||||
export async function openLogSocket() {
|
||||
let protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
const url = `${protocol}//${window.location.host}${BASE_PATH}/logs`
|
||||
|
@ -211,7 +222,9 @@ export const deleteClient = id => defaultDelete("client", id)
|
|||
|
||||
export default {
|
||||
BASE_PATH,
|
||||
login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
|
||||
login, ping, getFeatures, remoteGetFeatures,
|
||||
openLogSocket,
|
||||
debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
|
||||
getInstances, getInstance, putInstance, deleteInstance,
|
||||
getInstanceDatabase, queryInstanceDatabase,
|
||||
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||
|
|
|
@ -44,6 +44,14 @@ class Login extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!api.getFeatures().login) {
|
||||
return <div className="login-wrapper">
|
||||
<div className="login errored">
|
||||
<h1>Maubot Manager</h1>
|
||||
<div className="error">Login has been disabled in the maubot config.</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
return <div className="login-wrapper">
|
||||
<div className={`login ${this.state.error && "errored"}`}>
|
||||
<h1>Maubot Manager</h1>
|
||||
|
|
|
@ -33,6 +33,8 @@ class Main extends Component {
|
|||
async componentWillMount() {
|
||||
if (localStorage.accessToken) {
|
||||
await this.ping()
|
||||
} else {
|
||||
await api.remoteGetFeatures()
|
||||
}
|
||||
this.setState({ pinged: true })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import api from "../../api"
|
||||
|
||||
class BaseMainView extends Component {
|
||||
constructor(props) {
|
||||
|
@ -74,7 +75,7 @@ class BaseMainView extends Component {
|
|||
</div>
|
||||
)
|
||||
|
||||
renderLogButton = (filter) => !this.isNew && <div className="buttons">
|
||||
renderLogButton = (filter) => !this.isNew && api.getFeatures().log && <div className="buttons">
|
||||
<button className="open-log" onClick={() => this.props.openLog(filter)}>View logs</button>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -157,13 +157,24 @@ class Instance extends BaseMainView {
|
|||
<PrefSwitch rowName="Running"
|
||||
active={this.state.started} origActive={this.props.entry.started}
|
||||
onToggle={started => this.setState({ started })}/>
|
||||
<PrefSelect rowName="Primary user" options={this.clientOptions}
|
||||
isSearchable={false} value={this.selectedClientEntry}
|
||||
origValue={this.props.entry.primary_user}
|
||||
onChange={({ id }) => this.setState({ primary_user: id })}/>
|
||||
<PrefSelect rowName="Type" options={this.typeOptions} isSearchable={false}
|
||||
value={this.selectedPluginEntry} origValue={this.props.entry.type}
|
||||
onChange={({ id }) => this.setState({ type: id })}/>
|
||||
{api.getFeatures().client ? (
|
||||
<PrefSelect rowName="Primary user" options={this.clientOptions}
|
||||
isSearchable={false} value={this.selectedClientEntry}
|
||||
origValue={this.props.entry.primary_user}
|
||||
onChange={({ id }) => this.setState({ primary_user: id })}/>
|
||||
) : (
|
||||
<PrefInput rowName="Primary user" type="text" name="primary_user"
|
||||
value={this.state.primary_user} placeholder="@user:example.com"
|
||||
onChange={this.inputChange}/>
|
||||
)}
|
||||
{api.getFeatures().plugin ? (
|
||||
<PrefSelect rowName="Type" options={this.typeOptions} isSearchable={false}
|
||||
value={this.selectedPluginEntry} origValue={this.props.entry.type}
|
||||
onChange={({ id }) => this.setState({ type: id })}/>
|
||||
) : (
|
||||
<PrefInput rowName="Type" type="text" name="type" value={this.state.type}
|
||||
placeholder="xyz.maubot.example" onChange={this.inputChange}/>
|
||||
)}
|
||||
</PrefTable>
|
||||
{!this.isNew &&
|
||||
<AceEditor mode="yaml" theme="github" onChange={config => this.setState({ config })}
|
||||
|
@ -190,10 +201,11 @@ class Instance extends BaseMainView {
|
|||
View database
|
||||
</Link>
|
||||
)}
|
||||
{api.getFeatures().log &&
|
||||
<button className="open-log"
|
||||
onClick={() => this.props.openLog(`instance.${this.state.id}`)}>
|
||||
View logs
|
||||
</button>
|
||||
</button>}
|
||||
</div>}
|
||||
<div className="error">{this.state.error}</div>
|
||||
</div>
|
||||
|
|
|
@ -78,6 +78,7 @@ class Plugin extends BaseMainView {
|
|||
<PrefInput rowName="Version" type="text" value={this.state.version}
|
||||
disabled={true}/>
|
||||
</PrefTable>}
|
||||
{api.getFeatures().plugin_upload &&
|
||||
<div className={`upload-box ${this.state.uploading ? "uploading" : ""}`}>
|
||||
<UploadButton className="upload"/>
|
||||
<input className="file-selector" type="file" accept="application/zip+mbp"
|
||||
|
@ -85,7 +86,7 @@ class Plugin extends BaseMainView {
|
|||
onDragEnter={evt => evt.target.parentElement.classList.add("drag")}
|
||||
onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/>
|
||||
{this.state.uploading && <Spinner/>}
|
||||
</div>
|
||||
</div>}
|
||||
{!this.isNew && <div className="buttons">
|
||||
<button className={`delete ${this.hasInstances ? "disabled-bg" : ""}`}
|
||||
onClick={this.delete} disabled={this.loading || this.hasInstances}
|
||||
|
|
|
@ -54,19 +54,33 @@ class Dashboard extends Component {
|
|||
api.getInstances(), api.getClients(), api.getPlugins(),
|
||||
api.updateDebugOpenFileEnabled()])
|
||||
const instances = {}
|
||||
for (const instance of instanceList) {
|
||||
instances[instance.id] = instance
|
||||
if (api.getFeatures().instance) {
|
||||
for (const instance of instanceList) {
|
||||
instances[instance.id] = instance
|
||||
}
|
||||
}
|
||||
const clients = {}
|
||||
for (const client of clientList) {
|
||||
clients[client.id] = client
|
||||
if (api.getFeatures().client) {
|
||||
for (const client of clientList) {
|
||||
clients[client.id] = client
|
||||
}
|
||||
}
|
||||
const plugins = {}
|
||||
for (const plugin of pluginList) {
|
||||
plugins[plugin.id] = plugin
|
||||
if (api.getFeatures().plugin) {
|
||||
for (const plugin of pluginList) {
|
||||
plugins[plugin.id] = plugin
|
||||
}
|
||||
}
|
||||
this.setState({ instances, clients, plugins })
|
||||
|
||||
await this.enableLogs()
|
||||
}
|
||||
|
||||
async enableLogs() {
|
||||
if (api.getFeatures().log) {
|
||||
return
|
||||
}
|
||||
|
||||
const logs = await api.openLogSocket()
|
||||
|
||||
const processEntry = (entry) => {
|
||||
|
@ -119,9 +133,13 @@ class Dashboard extends Component {
|
|||
}
|
||||
|
||||
renderView(field, type, id) {
|
||||
const typeName = field.slice(0, -1)
|
||||
if (!api.getFeatures()[typeName]) {
|
||||
return this.renderDisabled(typeName)
|
||||
}
|
||||
const entry = this.state[field][id]
|
||||
if (!entry) {
|
||||
return this.renderNotFound(field.slice(0, -1))
|
||||
return this.renderNotFound(typeName)
|
||||
}
|
||||
return React.createElement(type, {
|
||||
entry,
|
||||
|
@ -145,6 +163,12 @@ class Dashboard extends Component {
|
|||
</div>
|
||||
)
|
||||
|
||||
renderDisabled = (thing = "path") => (
|
||||
<div className="not-found">
|
||||
The {thing} API has been disabled in the maubot config.
|
||||
</div>
|
||||
)
|
||||
|
||||
renderMain() {
|
||||
return <div className={`dashboard ${this.state.sidebarOpen ? "sidebar-open" : ""}`}>
|
||||
<Link to="/" className="title">
|
||||
|
@ -157,31 +181,31 @@ class Dashboard extends Component {
|
|||
|
||||
<nav className="sidebar">
|
||||
<div className="buttons">
|
||||
<button className="open-log" onClick={this.openLog}>
|
||||
{api.getFeatures().log && <button className="open-log" onClick={this.openLog}>
|
||||
<span>View logs</span>
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
<div className="instances list">
|
||||
{api.getFeatures().instance && <div className="instances list">
|
||||
<div className="title">
|
||||
<h2>Instances</h2>
|
||||
<Link to="/new/instance"><Plus/></Link>
|
||||
</div>
|
||||
{this.renderList("instances", Instance.ListEntry)}
|
||||
</div>
|
||||
<div className="clients list">
|
||||
</div>}
|
||||
{api.getFeatures().client && <div className="clients list">
|
||||
<div className="title">
|
||||
<h2>Clients</h2>
|
||||
<Link to="/new/client"><Plus/></Link>
|
||||
</div>
|
||||
{this.renderList("clients", Client.ListEntry)}
|
||||
</div>
|
||||
<div className="plugins list">
|
||||
</div>}
|
||||
{api.getFeatures().plugin && <div className="plugins list">
|
||||
<div className="title">
|
||||
<h2>Plugins</h2>
|
||||
<Link to="/new/plugin"><Plus/></Link>
|
||||
{api.getFeatures().plugin_upload && <Link to="/new/plugin"><Plus/></Link>}
|
||||
</div>
|
||||
{this.renderList("plugins", Plugin.ListEntry)}
|
||||
</div>
|
||||
</div>}
|
||||
</nav>
|
||||
|
||||
<div className="topbar"
|
||||
|
|
Loading…
Reference in a new issue