diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 4fbb209..3113124 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -69,6 +69,45 @@ async def reload_plugin(request: web.Request) -> web.Response: 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: str) -> web.Response: path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp") with open(path, "wb") as p: @@ -86,10 +125,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, dirname = os.path.dirname(plugin.path) old_filename = os.path.basename(plugin.path) if plugin.version in old_filename: - filename = old_filename.replace(plugin.version, new_version) - if filename == old_filename: - filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", - f"{new_version}-ts{int(time())}", old_filename) + replacement = (new_version if plugin.version != new_version + else f"{new_version}-ts{int(time())}") + filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", + replacement, old_filename) else: filename = old_filename.rstrip(".mbp") filename = f"{filename}-v{new_version}.mbp" @@ -110,20 +149,3 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, await plugin.start_instances() ZippedPluginLoader.trash(old_path, reason="update") return resp.updated(plugin.to_dict()) - - -@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 isinstance(plugin, ZippedPluginLoader): - return await upload_replacement_plugin(plugin, content, version) - else: - return resp.unsupported_plugin_loader diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 9c815c2..be9b3b0 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -61,6 +61,13 @@ class _Response: "errcode": "mxid_mismatch", }, status=HTTPStatus.BAD_REQUEST) + @property + def pid_mismatch(self) -> web.Response: + return web.json_response({ + "error": "The ID in the path does not match the ID of the uploaded plugin", + "errcode": "pid_mismatch", + }, status=HTTPStatus.BAD_REQUEST) + @property def bad_auth(self) -> web.Response: return web.json_response({ @@ -138,6 +145,13 @@ class _Response: "errcode": "user_exists", }, status=HTTPStatus.CONFLICT) + @property + def plugin_exists(self) -> web.Response: + return web.json_response({ + "error": "A plugin with the same ID as the uploaded plugin already exists", + "errcode": "plugin_exists" + }, status=HTTPStatus.CONFLICT) + @property def plugin_in_use(self) -> web.Response: return web.json_response({ diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 75ec865..efe759e 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -85,6 +85,14 @@ paths: summary: Upload a new plugin description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. tags: [Plugins] + parameters: + - name: allow_override + in: query + description: Set to allow overriding existing plugins + required: false + schema: + type: boolean + default: false responses: 200: description: Plugin uploaded and replaced current version successfully @@ -102,6 +110,8 @@ paths: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' + 409: + description: Plugin requestBody: content: application/zip: @@ -150,6 +160,39 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + put: + operationId: put_plugin + summary: Upload a new or replacement plugin + description: | + Upload a new or replacement plugin with the specified ID. + A HTTP 400 will be returned if the ID of the uploaded plugin + doesn't match the ID in the path. If the plugin already + exists, enabled instances will be restarted. + tags: [Plugins] + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) /plugin/{id}/reload: parameters: - name: id