From cb3993d79facfef4c36f9bb96cc6467a5488a257 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 20:48:52 +0200 Subject: [PATCH] Implement uploading plugins --- maubot/cli/commands/build.py | 47 ++++++++++++++++++++++++--------- maubot/cli/commands/login.py | 5 ++-- maubot/cli/commands/upload.py | 45 +++++++++++++++++++++++++++++-- maubot/cli/config.py | 4 ++- maubot/loader/zip.py | 2 +- maubot/management/api/plugin.py | 6 ++--- 6 files changed, 88 insertions(+), 21 deletions(-) diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py index 7d1a687..cd359e0 100644 --- a/maubot/cli/commands/build.py +++ b/maubot/cli/commands/build.py @@ -13,20 +13,22 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional +from typing import Optional, Union, IO from io import BytesIO import zipfile import os from mautrix.client.api.types.util import SerializerError from ruamel.yaml import YAML, YAMLError -from colorama import Fore, Style +from colorama import Fore from PyInquirer import prompt import click from ...loader import PluginMeta -from ..base import app from ..cliq.validators import PathValidator +from ..base import app +from ..config import config +from .upload import upload_file, UploadError yaml = YAML() @@ -44,16 +46,16 @@ def read_meta(path: str) -> Optional[PluginMeta]: meta_dict = yaml.load(meta_file) except YAMLError as e: print(Fore.RED + "Failed to build plugin: Metadata file is not YAML") - print(Fore.RED + str(e) + Style.RESET_ALL) + print(Fore.RED + str(e) + Fore.RESET) return None except FileNotFoundError: - print(Fore.RED + "Failed to build plugin: Metadata file not found" + Style.RESET_ALL) + print(Fore.RED + "Failed to build plugin: Metadata file not found" + Fore.RESET) return None try: meta = PluginMeta.deserialize(meta_dict) except SerializerError as e: print(Fore.RED + "Failed to build plugin: Metadata file is not valid") - print(Fore.RED + str(e) + Style.RESET_ALL) + print(Fore.RED + str(e) + Fore.RESET) return None return meta @@ -77,7 +79,7 @@ def read_output_path(output: str, meta: PluginMeta) -> Optional[str]: return os.path.abspath(output) -def write_plugin(meta: PluginMeta, output: str) -> None: +def write_plugin(meta: PluginMeta, output: Union[str, IO]) -> None: with zipfile.ZipFile(output, "w") as zip: meta_dump = BytesIO() yaml.dump(meta.serialize(), meta_dump) @@ -89,12 +91,26 @@ def write_plugin(meta: PluginMeta, output: str) -> None: elif os.path.isdir(module): zipdir(zip, module) else: - print(Fore.YELLOW + f"Module {module} not found, skipping") + print(Fore.YELLOW + f"Module {module} not found, skipping" + Fore.RESET) for file in meta.extra_files: zip.write(file) +def upload_plugin(output: Union[str, IO]) -> None: + try: + server = config["default_server"] + token = config["servers"][server] + except KeyError: + print(Fore.RED + "Default server not configured." + Fore.RESET) + return + if isinstance(output, str): + with open(output, "rb") as file: + upload_file(file, server, token) + else: + upload_file(output, server, token) + + @app.command(short_help="Build a maubot plugin", help="Build a maubot plugin. First parameter is the path to root of the plugin " "to build. You can also use --output to specify output file.") @@ -105,9 +121,16 @@ def write_plugin(meta: PluginMeta, output: str) -> None: default=False) def build(path: str, output: str, upload: bool) -> None: meta = read_meta(path) - output = read_output_path(output, meta) - if not output: - return + if output or not upload: + output = read_output_path(output, meta) + if not output: + return + else: + output = BytesIO() os.chdir(path) write_plugin(meta, output) - print(Fore.GREEN + "Plugin build complete.") + output.seek(0) + if isinstance(output, str): + print(f"{Fore.GREEN}Plugin built to {Fore.CYAN}{path}{Fore.GREEN}.{Fore.RESET}") + if upload: + upload_plugin(output) diff --git a/maubot/cli/commands/login.py b/maubot/cli/commands/login.py index 7cc85f1..aa138a9 100644 --- a/maubot/cli/commands/login.py +++ b/maubot/cli/commands/login.py @@ -18,7 +18,7 @@ from urllib.error import HTTPError import json import os -from colorama import Fore, Style +from colorama import Fore from ..config import save_config, config from ..cliq import cliq @@ -38,8 +38,9 @@ def login(server, username, password) -> None: data=json.dumps(data).encode("utf-8")) as resp_data: resp = json.load(resp_data) config["servers"][server] = resp["token"] + config["default_server"] = server save_config() print(Fore.GREEN + "Logged in successfully") except HTTPError as e: if e.code == 401: - print(Fore.RED + "Invalid username or password" + Style.RESET_ALL) + print(Fore.RED + "Invalid username or password" + Fore.RESET) diff --git a/maubot/cli/commands/upload.py b/maubot/cli/commands/upload.py index 43a9850..f307f6f 100644 --- a/maubot/cli/commands/upload.py +++ b/maubot/cli/commands/upload.py @@ -13,12 +13,53 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from urllib.request import urlopen, Request +from urllib.error import HTTPError +from typing import IO, Tuple +import json + +from colorama import Fore import click from ..base import app +from ..config import config + + +class UploadError(Exception): + pass @app.command(help="Upload a maubot plugin") +@click.argument("path") @click.option("-s", "--server", help="The maubot instance to upload the plugin to") -def upload(server: str) -> None: - pass +def upload(path: str, server: str) -> None: + if not server: + try: + server = config["default_server"] + except KeyError: + print(Fore.RED + "Default server not configured" + Fore.RESET) + return + try: + token = config["servers"][server] + except KeyError: + print(Fore.RED + "Server not found" + Fore.RESET) + return + with open(path, "rb") as file: + upload_file(file, server, token) + + +def upload_file(file: IO, server: str, token: str) -> None: + req = Request(f"{server}/_matrix/maubot/v1/plugins/upload?allow_override=true", data=file, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/zip"}) + try: + with urlopen(req) as resp_data: + resp = json.load(resp_data) + print(f"{Fore.GREEN}Plugin {Fore.CYAN}{resp['id']} v{resp['version']}{Fore.GREEN} " + f"uploaded to {Fore.CYAN}{server}{Fore.GREEN} successfully.{Fore.RESET}") + except HTTPError as e: + try: + err = json.load(e) + except json.JSONDecodeError: + err = {} + print(err.get("stacktrace", "")) + print(Fore.RED + "Failed to upload plugin: " + err.get("error", str(e)) + Fore.RESET) diff --git a/maubot/cli/config.py b/maubot/cli/config.py index fba278a..653ffef 100644 --- a/maubot/cli/config.py +++ b/maubot/cli/config.py @@ -17,7 +17,8 @@ import json import os config = { - "servers": {} + "servers": {}, + "default_server": None, } configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config")) @@ -32,5 +33,6 @@ def load_config() -> None: with open(f"{configdir}/maubot-cli.json") as file: loaded = json.load(file) config["servers"] = loaded["servers"] + config["default_server"] = loaded["default_server"] except FileNotFoundError: pass diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index e341cff..8e1cfbd 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -185,7 +185,7 @@ class ZippedPluginLoader(PluginLoader): importer = self._get_importer(reset_cache=reset_cache) self._run_preload_checks(importer) if reset_cache: - self.log.debug(f"Re-preloaded plugin {self.meta.id} from {self.meta.path}") + self.log.debug(f"Re-preloaded plugin {self.meta.id} from {self.path}") for module in self.meta.modules: try: importer.load_module(module) diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 421584d..d0c79f8 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -124,10 +124,10 @@ 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 plugin.version in old_filename: - replacement = (new_version if plugin.version != new_version + 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(plugin.version)}(-ts[0-9]+)?", + filename = re.sub(f"{re.escape(str(plugin.meta.version))}(-ts[0-9]+)?", replacement, old_filename) else: filename = old_filename.rstrip(".mbp")