diff --git a/maubot/client.py b/maubot/client.py index ea7db0d..5b91d8c 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -177,13 +177,13 @@ class Client: await self.stop() async def update_displayname(self, displayname: str) -> None: - if not displayname or displayname == self.displayname: + if displayname is None or displayname == self.displayname: return self.db_instance.displayname = displayname await self.client.set_displayname(self.displayname) async def update_avatar_url(self, avatar_url: ContentURI) -> None: - if not avatar_url or avatar_url == self.avatar_url: + if avatar_url is None or avatar_url == self.avatar_url: return self.db_instance.avatar_url = avatar_url await self.client.set_avatar_url(self.avatar_url) @@ -198,7 +198,7 @@ class Client: client_session=self.http_client, log=self.log) mxid = await new_client.whoami() if mxid != self.id: - raise ValueError("MXID mismatch") + raise ValueError(f"MXID mismatch: {mxid}") new_client.store = self.db_instance self.stop_sync() self.client = new_client diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 051f9be..5bee0af 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -20,7 +20,7 @@ from http import HTTPStatus from aiohttp import web from mautrix.types import UserID, SyncToken, FilterID -from mautrix.errors import MatrixRequestError, MatrixInvalidToken +from mautrix.errors import MatrixRequestError, MatrixConnectionError, MatrixInvalidToken from mautrix.client import Client as MatrixClient from ...db import DBClient @@ -54,12 +54,14 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: return resp.bad_client_access_token except MatrixRequestError: return resp.bad_client_access_details + except MatrixConnectionError: + return resp.bad_client_connection_details if user_id is None: existing_client = Client.get(mxid, None) if existing_client is not None: return resp.user_exists elif mxid != user_id: - return resp.mxid_mismatch + return resp.mxid_mismatch(mxid) db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token, enabled=data.get("enabled", True), next_batch=SyncToken(""), filter_id=FilterID(""), sync=data.get("sync", True), @@ -81,8 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response: return resp.bad_client_access_token except MatrixRequestError: return resp.bad_client_access_details - except ValueError: - return resp.mxid_mismatch + except ValueError as e: + return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):]) await client.update_avatar_url(data.get("avatar_url", None)) await client.update_displayname(data.get("displayname", None)) await client.update_started(data.get("started", None)) diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index be9b3b0..5204927 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -55,9 +55,16 @@ class _Response: }, status=HTTPStatus.BAD_REQUEST) @property - def mxid_mismatch(self) -> web.Response: + def bad_client_connection_details(self) -> web.Response: return web.json_response({ - "error": "The Matrix user ID of the client and the user ID of the access token don't match", + "error": "Could not connect to homeserver", + "errcode": "bad_client_connection_details" + }, status=HTTPStatus.BAD_REQUEST) + + def mxid_mismatch(self, found: str) -> web.Response: + return web.json_response({ + "error": "The Matrix user ID of the client and the user ID of the access token don't " + f"match. Access token is for user {found}", "errcode": "mxid_mismatch", }, status=HTTPStatus.BAD_REQUEST) diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 8721685..e9fc926 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -114,9 +114,22 @@ export async function putClient(client) { return await resp.json() } +export async function deleteClient(id) { + const resp = await fetch(`${BASE_PATH}/client/${id}`, { + headers: getHeaders(), + method: "DELETE", + }) + if (resp.status === 204) { + return { + "success": true, + } + } + return await resp.json() +} + export default { login, ping, getInstances, getInstance, getPlugins, getPlugin, uploadPlugin, - getClients, getClient, uploadAvatar, putClient, + getClients, getClient, uploadAvatar, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 2019444..9b7dbd1 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.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 { Link } from "react-router-dom" +import { Link, withRouter } from "react-router-dom" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable" @@ -67,10 +67,24 @@ class Client extends Component { uploadingAvatar: false, saving: false, + deleting: false, startingOrStopping: false, + deleted: false, + error: "", } } + get clientInState() { + const client = Object.assign({}, this.state) + delete client.uploadingAvatar + delete client.saving + delete client.deleting + delete client.startingOrStopping + delete client.deleted + delete client.error + return client + } + componentWillReceiveProps(nextProps) { this.setState(Object.assign(this.initialState, nextProps.client)) } @@ -106,12 +120,30 @@ class Client extends Component { save = async () => { this.setState({ saving: true }) - const resp = await api.putClient(this.state) + const resp = await api.putClient(this.clientInState) if (resp.id) { - resp.saving = false - this.setState(resp) + this.props.onChange(resp) + if (this.isNew) { + this.props.history.push(`/client/${resp.id}`) + } else { + this.setState({ saving: false, error: "" }) + } } else { - console.error(resp) + this.setState({ saving: false, error: resp.error }) + } + } + + delete = async () => { + if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { + return + } + this.setState({ deleting: true }) + const resp = await api.deleteClient(this.state.id) + if (resp.success) { + this.props.onDelete() + this.props.history.push("/") + } else { + this.setState({ deleting: false, error: resp.error }) } } @@ -122,42 +154,54 @@ class Client extends Component { started: !this.state.started, }) if (resp.id) { - resp.startingOrStopping = false - this.setState(resp) + this.props.onChange(resp) + this.setState({ startingOrStopping: false, error: "" }) } else { - console.error(resp) + this.setState({ startingOrStopping: false, error: resp.error }) } } + get loading() { + return this.state.saving || this.state.startingOrStopping || this.state.deleting + } + + get isNew() { + return !Boolean(this.props.client) + } + render() { return
-
+ {!this.isNew &&
Avatar + onChange={this.avatarUpload} disabled={this.state.uploadingAvatar} + onDragEnter={evt => evt.target.parentElement.classList.add("drag")} + onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/> {this.state.uploadingAvatar && }
- {this.props.client && (<> -
- - {this.state.started ? "Started" : "Stopped"} -
- - )} -
+ )} +
}
- + @@ -167,21 +211,33 @@ class Client extends Component { + this.setState({ sync })}/> this.setState({ autojoin })}/> this.setState({ enabled })}/> + onToggle={enabled => this.setState({ + enabled, + started: enabled && this.state.started, + })}/> - - +
+ {!this.isNew && ( + + )} + +
+
{this.state.error}
} } -export default Client +export default withRouter(Client) diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 5d9a38d..2ca434f 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -57,6 +57,18 @@ class Dashboard extends Component { React.createElement(type, { key: entry.id, [field]: entry })) } + delete(stateField, id) { + const data = Object.assign({}, this.state[stateField]) + delete data[id] + this.setState({ [stateField]: data }) + } + + add(stateField, entry) { + const data = Object.assign({}, this.state[stateField]) + data[entry.id] = entry + this.setState({ [stateField]: data }) + } + renderView(field, type, id) { const stateField = field + "s" const entry = this.state[stateField][id] @@ -65,11 +77,8 @@ class Dashboard extends Component { } return React.createElement(type, { [field]: entry, - onChange: newEntry => this.setState({ - [stateField]: Object.assign({}, this.state[stateField], { - [id]: newEntry, - }), - }), + onDelete: () => this.delete(stateField, id), + onChange: newEntry => this.add(stateField, newEntry), }) } @@ -111,7 +120,8 @@ class Dashboard extends Component { "Hello, World!"}/> }/> - }/> + this.add("clients", newEntry)}/>}/> }/> this.renderView("instance", InstanceView, match.params.id)}/> diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index 59e6a4a..28ea689 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -48,10 +48,13 @@ font-size: 1rem - border-bottom: 1px dotted $primary + border-bottom: 1px solid $background - &:hover:not(:disabled) - border-bottom: 1px solid $primary + &:not(:disabled) + border-bottom: 1px dotted $primary - &:focus:not(:disabled) - border-bottom: 2px solid $primary + &:hover + border-bottom: 1px solid $primary + + &:focus + border-bottom: 2px solid $primary diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass index bfb41bc..e0ffb66 100644 --- a/maubot/management/frontend/src/style/pages/client.sass +++ b/maubot/management/frontend/src/style/pages/client.sass @@ -21,6 +21,7 @@ > div.sidebar vertical-align: top text-align: center + width: 8rem > div margin-bottom: 1rem @@ -68,7 +69,7 @@ > input.file-selector cursor: pointer - &:hover + &:hover, &.drag > img.avatar opacity: .25 @@ -105,6 +106,10 @@ background-color: $error-light box-shadow: 0 0 .75rem .75rem $error-light + &.disabled + background-color: $border-color + box-shadow: 0 0 .75rem .75rem $border-color + > span.text display: inline-block margin-left: 1rem @@ -115,7 +120,19 @@ margin: 0 1rem flex: 1 - button.save + > .buttons + display: flex + + +button-group + + > .error + margin-top: 1rem + +notification($error) + + &:empty + display: none + + button.save, button.delete +button +main-color-button width: 100% @@ -126,15 +143,3 @@ +thick-spinner +white-spinner width: 2rem - -//> .client - display: table - - > .field - display: table-row - width: 100% - - > .name, > .value - display: table-cell - width: 50% - text-align: center