Add Cloudron packaging for Maubot
This commit is contained in:
217
maubot-src/maubot/management/api/client.py
Normal file
217
maubot-src/maubot/management/api/client.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 __future__ import annotations
|
||||
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from mautrix.client import Client as MatrixClient
|
||||
from mautrix.errors import MatrixConnectionError, MatrixInvalidToken, MatrixRequestError
|
||||
from mautrix.types import FilterID, SyncToken, UserID
|
||||
|
||||
from ...client import Client
|
||||
from .base import routes
|
||||
from .responses import resp
|
||||
|
||||
log = logging.getLogger("maubot.server.client")
|
||||
|
||||
|
||||
@routes.get("/clients")
|
||||
async def get_clients(_: web.Request) -> web.Response:
|
||||
return resp.found([client.to_dict() for client in Client.cache.values()])
|
||||
|
||||
|
||||
@routes.get("/client/{id}")
|
||||
async def get_client(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info.get("id", None)
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return resp.client_not_found
|
||||
return resp.found(client.to_dict())
|
||||
|
||||
|
||||
async def _create_client(user_id: UserID | None, data: dict) -> web.Response:
|
||||
homeserver = data.get("homeserver", None)
|
||||
access_token = data.get("access_token", None)
|
||||
device_id = data.get("device_id", None)
|
||||
new_client = MatrixClient(
|
||||
mxid="@not:a.mxid",
|
||||
base_url=homeserver,
|
||||
token=access_token,
|
||||
client_session=Client.http_client,
|
||||
)
|
||||
try:
|
||||
whoami = await new_client.whoami()
|
||||
except MatrixInvalidToken as e:
|
||||
return resp.bad_client_access_token
|
||||
except MatrixRequestError:
|
||||
log.warning(f"Failed to get whoami from {homeserver} for new client", exc_info=True)
|
||||
return resp.bad_client_access_details
|
||||
except MatrixConnectionError:
|
||||
log.warning(f"Failed to connect to {homeserver} for new client", exc_info=True)
|
||||
return resp.bad_client_connection_details
|
||||
if user_id is None:
|
||||
existing_client = await Client.get(whoami.user_id)
|
||||
if existing_client is not None:
|
||||
return resp.user_exists
|
||||
elif whoami.user_id != user_id:
|
||||
return resp.mxid_mismatch(whoami.user_id)
|
||||
elif whoami.device_id and device_id and whoami.device_id != device_id:
|
||||
return resp.device_id_mismatch(whoami.device_id)
|
||||
client = await Client.get(
|
||||
whoami.user_id, homeserver=homeserver, access_token=access_token, device_id=device_id
|
||||
)
|
||||
client.enabled = data.get("enabled", True)
|
||||
client.sync = data.get("sync", True)
|
||||
await client.update_autojoin(data.get("autojoin", True), save=False)
|
||||
await client.update_online(data.get("online", True), save=False)
|
||||
client.displayname = data.get("displayname", "disable")
|
||||
client.avatar_url = data.get("avatar_url", "disable")
|
||||
await client.update()
|
||||
await client.start()
|
||||
return resp.created(client.to_dict())
|
||||
|
||||
|
||||
async def _update_client(client: Client, data: dict, is_login: bool = False) -> web.Response:
|
||||
try:
|
||||
await client.update_access_details(
|
||||
data.get("access_token"), data.get("homeserver"), data.get("device_id")
|
||||
)
|
||||
except MatrixInvalidToken:
|
||||
return resp.bad_client_access_token
|
||||
except MatrixRequestError:
|
||||
log.warning(
|
||||
f"Failed to get whoami from homeserver to update client details", exc_info=True
|
||||
)
|
||||
return resp.bad_client_access_details
|
||||
except MatrixConnectionError:
|
||||
log.warning(f"Failed to connect to homeserver to update client details", exc_info=True)
|
||||
return resp.bad_client_connection_details
|
||||
except ValueError as e:
|
||||
str_err = str(e)
|
||||
if str_err.startswith("MXID mismatch"):
|
||||
return resp.mxid_mismatch(str(e)[len("MXID mismatch: ") :])
|
||||
elif str_err.startswith("Device ID mismatch"):
|
||||
return resp.device_id_mismatch(str(e)[len("Device ID mismatch: ") :])
|
||||
await client.update_avatar_url(data.get("avatar_url"), save=False)
|
||||
await client.update_displayname(data.get("displayname"), save=False)
|
||||
await client.update_started(data.get("started"))
|
||||
await client.update_enabled(data.get("enabled"), save=False)
|
||||
await client.update_autojoin(data.get("autojoin"), save=False)
|
||||
await client.update_online(data.get("online"), save=False)
|
||||
await client.update_sync(data.get("sync"), save=False)
|
||||
await client.update()
|
||||
return resp.updated(client.to_dict(), is_login=is_login)
|
||||
|
||||
|
||||
async def _create_or_update_client(
|
||||
user_id: UserID, data: dict, is_login: bool = False
|
||||
) -> web.Response:
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return await _create_client(user_id, data)
|
||||
else:
|
||||
return await _update_client(client, data, is_login=is_login)
|
||||
|
||||
|
||||
@routes.post("/client/new")
|
||||
async def create_client(request: web.Request) -> web.Response:
|
||||
try:
|
||||
data = await request.json()
|
||||
except JSONDecodeError:
|
||||
return resp.body_not_json
|
||||
return await _create_client(None, data)
|
||||
|
||||
|
||||
@routes.put("/client/{id}")
|
||||
async def update_client(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info["id"]
|
||||
try:
|
||||
data = await request.json()
|
||||
except JSONDecodeError:
|
||||
return resp.body_not_json
|
||||
return await _create_or_update_client(user_id, data)
|
||||
|
||||
|
||||
@routes.delete("/client/{id}")
|
||||
async def delete_client(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info["id"]
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return resp.client_not_found
|
||||
if len(client.references) > 0:
|
||||
return resp.client_in_use
|
||||
if client.started:
|
||||
await client.stop()
|
||||
await client.delete()
|
||||
return resp.deleted
|
||||
|
||||
|
||||
@routes.post("/client/{id}/clearcache")
|
||||
async def clear_client_cache(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info["id"]
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return resp.client_not_found
|
||||
await client.clear_cache()
|
||||
return resp.ok
|
||||
|
||||
|
||||
@routes.post("/client/{id}/verify")
|
||||
async def verify_client(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info["id"]
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return resp.client_not_found
|
||||
try:
|
||||
req = await request.json()
|
||||
except Exception:
|
||||
return resp.body_not_json
|
||||
try:
|
||||
await client.crypto.verify_with_recovery_key(req["recovery_key"])
|
||||
client.trust_state = await client.crypto.resolve_trust(
|
||||
client.crypto.own_identity,
|
||||
allow_fetch=False,
|
||||
)
|
||||
log.debug(f"Trust state after verifying {client.id}: {client.trust_state}")
|
||||
except Exception as e:
|
||||
log.exception("Failed to verify client with recovery key")
|
||||
return resp.internal_crypto_error(str(e))
|
||||
return resp.ok
|
||||
|
||||
|
||||
@routes.post("/client/{id}/generate_recovery_key")
|
||||
async def generate_recovery_key(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info["id"]
|
||||
client = await Client.get(user_id)
|
||||
if not client:
|
||||
return resp.client_not_found
|
||||
try:
|
||||
keys = await client.crypto.get_own_cross_signing_public_keys()
|
||||
if keys:
|
||||
return resp.client_has_keys
|
||||
key = await client.crypto.generate_recovery_key()
|
||||
client.trust_state = await client.crypto.resolve_trust(
|
||||
client.crypto.own_identity,
|
||||
allow_fetch=False,
|
||||
)
|
||||
log.debug(f"Trust state generating recovery key for {client.id}: {client.trust_state}")
|
||||
except Exception as e:
|
||||
log.exception("Failed to generate recovery key for client")
|
||||
return resp.internal_crypto_error(str(e))
|
||||
return resp.created({"success": True, "recovery_key": key})
|
||||
Reference in New Issue
Block a user