From 44aba396b7d6902af9811fe053f66f4a1759ef03 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 29 Oct 2018 17:47:49 +0200 Subject: [PATCH 01/10] Random changes slightly related to a maubot plugin packager cli program --- example-maubot.ini | 5 +++++ maubot/cli/__main__.py | 0 setup.py | 1 + 3 files changed, 6 insertions(+) create mode 100644 maubot/cli/__main__.py diff --git a/example-maubot.ini b/example-maubot.ini index f5ffb5a..000ea4a 100644 --- a/example-maubot.ini +++ b/example-maubot.ini @@ -5,6 +5,9 @@ ID = xyz.maubot.plugin # A PEP 440 compliant version string. Version = 1.0.0 +# The SPDX license identifier of the license of your project (see https://spdx.org/licenses/) +# Alternatively, you may enter the name of a license file. The file must be listed under ExtraFiles. +License = AGPL-3.0-or-later # The comma-separated list of modules to load from the plugin archive. # Submodules that are imported by modules listed here don't need to be listed separately. # However, top-level modules must always be listed even if they're imported by other modules. @@ -14,3 +17,5 @@ Modules = plugin # Even if `module` is not omitted here, it must be included in the modules list. # The main class must extend maubot.Plugin MainClass = PluginClass +# The comma-separated list of additional files to include in the plugin archive. +ExtraFiles = LICENSE diff --git a/maubot/cli/__main__.py b/maubot/cli/__main__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index a546b06..9107354 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ setuptools.setup( entry_points=""" [console_scripts] maubot=maubot.__main__:main + mbp=maubot.cli.__main__:main """, data_files=[ (".", ["example-config.yaml"]), From 8e2f2908a640031c537cff0e39fe09ef253be20e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 01:28:23 +0200 Subject: [PATCH 02/10] Add stuff --- maubot/cli/__init__.py | 2 + maubot/cli/__main__.py | 0 maubot/cli/base.py | 23 ++++++++++ maubot/cli/commands/__init__.py | 1 + maubot/cli/commands/build.py | 31 +++++++++++++ maubot/cli/commands/init.py | 34 ++++++++++++++ maubot/cli/commands/login.py | 48 ++++++++++++++++++++ maubot/cli/commands/upload.py | 25 ++++++++++ maubot/cli/config.py | 36 +++++++++++++++ maubot/cli/template/config.yaml | 6 +++ maubot/cli/template/plugin-with-config.py.j2 | 20 ++++++++ maubot/cli/template/plugin.py.j2 | 8 ++++ maubot/cli/util/__init__.py | 1 + maubot/cli/util/path.py | 14 ++++++ requirements.txt | 4 ++ setup.py | 6 ++- 16 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 maubot/cli/__init__.py delete mode 100644 maubot/cli/__main__.py create mode 100644 maubot/cli/base.py create mode 100644 maubot/cli/commands/__init__.py create mode 100644 maubot/cli/commands/build.py create mode 100644 maubot/cli/commands/init.py create mode 100644 maubot/cli/commands/login.py create mode 100644 maubot/cli/commands/upload.py create mode 100644 maubot/cli/config.py create mode 100644 maubot/cli/template/config.yaml create mode 100644 maubot/cli/template/plugin-with-config.py.j2 create mode 100644 maubot/cli/template/plugin.py.j2 create mode 100644 maubot/cli/util/__init__.py create mode 100644 maubot/cli/util/path.py diff --git a/maubot/cli/__init__.py b/maubot/cli/__init__.py new file mode 100644 index 0000000..d25736b --- /dev/null +++ b/maubot/cli/__init__.py @@ -0,0 +1,2 @@ +from . import commands +from .base import app diff --git a/maubot/cli/__main__.py b/maubot/cli/__main__.py deleted file mode 100644 index e69de29..0000000 diff --git a/maubot/cli/base.py b/maubot/cli/base.py new file mode 100644 index 0000000..2201c30 --- /dev/null +++ b/maubot/cli/base.py @@ -0,0 +1,23 @@ +# 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 . +import click + +from .config import load_config + + +@click.group() +def app() -> None: + load_config() diff --git a/maubot/cli/commands/__init__.py b/maubot/cli/commands/__init__.py new file mode 100644 index 0000000..e94091c --- /dev/null +++ b/maubot/cli/commands/__init__.py @@ -0,0 +1 @@ +from . import upload, build, login, init diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py new file mode 100644 index 0000000..b820e2c --- /dev/null +++ b/maubot/cli/commands/build.py @@ -0,0 +1,31 @@ +# 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 . +import click +import os + +from ..base import app +from ..util import type_path + + +@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.") +@click.argument("path", default=".") +@click.option("-o", "--output", help="Path to output built plugin to", type=type_path) +@click.option("-u", "--upload", help="Upload plugin to main server after building", is_flag=True, + default=False) +def build(path: str, output: str, upload: bool) -> None: + pass diff --git a/maubot/cli/commands/init.py b/maubot/cli/commands/init.py new file mode 100644 index 0000000..d2bb633 --- /dev/null +++ b/maubot/cli/commands/init.py @@ -0,0 +1,34 @@ +# 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 . +import click +import os + +from ..base import app +from ..util import type_path + + +@app.command(help="Initialize a new maubot plugin") +@click.option("-n", "--name", help="The name of the project", default=os.path.basename(os.getcwd()), + prompt=True, show_default="directory name") +@click.option("-i", "--id", help="The maubot plugin ID (Java package name format)", prompt=True) +@click.option("-v", "--version", help="Initial version for project", default="0.1.0", + show_default=True) +@click.option("-l", "--license", help="The SPDX license identifier of the license for the project", + prompt=True, default="AGPL-3.0-or-later") +@click.option("-c", "--config", help="Include a config in the plugin stub", is_flag=True, + default=False) +def init(name: str, id: str, version: str, license: str, config: bool) -> None: + pass diff --git a/maubot/cli/commands/login.py b/maubot/cli/commands/login.py new file mode 100644 index 0000000..9f91bc1 --- /dev/null +++ b/maubot/cli/commands/login.py @@ -0,0 +1,48 @@ +# 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 . +from urllib.request import urlopen +from urllib.error import HTTPError +import click +import json +import os + +from colorama import Fore, Style + +from maubot.cli.base import app +from maubot.cli.config import save_config, config + + +@app.command(help="Log in to a Maubot instance") +@click.argument("server", required=True, default="http://localhost:29316") +@click.option("-u", "--username", help="The username of your account", prompt=True, + default=lambda: os.environ.get('USER', ''), show_default="current user") +@click.password_option("-p", "--password", help="The password to your account", required=True, + confirmation_prompt=False) +def login(server, username, password) -> None: + data = { + "username": username, + "password": password, + } + try: + with urlopen(f"{server}/_matrix/maubot/v1/auth/login", + data=json.dumps(data).encode("utf-8")) as resp_data: + resp = json.load(resp_data) + config["servers"][server] = resp["token"] + 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) diff --git a/maubot/cli/commands/upload.py b/maubot/cli/commands/upload.py new file mode 100644 index 0000000..c30a7a2 --- /dev/null +++ b/maubot/cli/commands/upload.py @@ -0,0 +1,25 @@ +# 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 . +import click +import os + +from maubot.cli.base import app + + +@app.command(help="Upload a maubot plugin") +@click.option("-s", "--server", help="The maubot instance to upload the plugin to") +def upload(server: str) -> None: + pass diff --git a/maubot/cli/config.py b/maubot/cli/config.py new file mode 100644 index 0000000..fba278a --- /dev/null +++ b/maubot/cli/config.py @@ -0,0 +1,36 @@ +# 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 . +import json +import os + +config = { + "servers": {} +} +configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config")) + + +def save_config() -> None: + with open(f"{configdir}/maubot-cli.json", "w") as file: + json.dump(config, file) + + +def load_config() -> None: + try: + with open(f"{configdir}/maubot-cli.json") as file: + loaded = json.load(file) + config["servers"] = loaded["servers"] + except FileNotFoundError: + pass diff --git a/maubot/cli/template/config.yaml b/maubot/cli/template/config.yaml new file mode 100644 index 0000000..bbeb6da --- /dev/null +++ b/maubot/cli/template/config.yaml @@ -0,0 +1,6 @@ +example_1: Example value 1 +example_2: + list: + - foo + - bar + value: asd diff --git a/maubot/cli/template/plugin-with-config.py.j2 b/maubot/cli/template/plugin-with-config.py.j2 new file mode 100644 index 0000000..18ca827 --- /dev/null +++ b/maubot/cli/template/plugin-with-config.py.j2 @@ -0,0 +1,20 @@ +from maubot import Plugin +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper + +class Config(BaseProxyConfig): + def do_update(self, helper: ConfigUpdateHelper) -> None: + helper.copy("example_1") + helper.copy("example_2.list") + helper.copy("example_2.value") + +class {{ name }}: + async def start() -> None: + self.config.load_and_update() + self.log.debug("Loaded %s from config example 2", self.config["example_2.value"]) + + async def stop() -> None: + pass + + @classmethod + def get_config_class(cls) -> Type[BaseProxyConfig]: + return Config diff --git a/maubot/cli/template/plugin.py.j2 b/maubot/cli/template/plugin.py.j2 new file mode 100644 index 0000000..fc056ff --- /dev/null +++ b/maubot/cli/template/plugin.py.j2 @@ -0,0 +1,8 @@ +from maubot import Plugin + +class {{ name }}: + async def start() -> None: + pass + + async def stop() -> None: + pass diff --git a/maubot/cli/util/__init__.py b/maubot/cli/util/__init__.py new file mode 100644 index 0000000..a5cbafb --- /dev/null +++ b/maubot/cli/util/__init__.py @@ -0,0 +1 @@ +from .path import type_path diff --git a/maubot/cli/util/path.py b/maubot/cli/util/path.py new file mode 100644 index 0000000..75cae57 --- /dev/null +++ b/maubot/cli/util/path.py @@ -0,0 +1,14 @@ +import click +import os + + +def type_path(val: str) -> str: + val = os.path.abspath(val) + if os.path.exists(val): + return val + directory = os.path.dirname(val) + if not os.path.isdir(directory): + if os.path.exists(directory): + raise click.BadParameter(f"{directory} is not a directory") + raise click.BadParameter(f"{directory} does not exist") + return val diff --git a/requirements.txt b/requirements.txt index 4067af6..79baba1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,7 @@ ruamel.yaml attrs bcrypt packaging + +click +colorama +jinja2 diff --git a/setup.py b/setup.py index a8c79db..2dc5ae3 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,10 @@ setuptools.setup( "attrs>=18.1.0,<19", "bcrypt>=3.1.4,<4", "packaging>=10", + + "click>=7,<8", + "colorama>=0.4,<0.5", + "jinja2>=2,<3", ], classifiers=[ @@ -45,7 +49,7 @@ setuptools.setup( entry_points=""" [console_scripts] maubot=maubot.__main__:main - mbp=maubot.cli.__main__:main + mbc=maubot.cli:app """, data_files=[ (".", ["example-config.yaml"]), From 7816212190ea9a065fa9d5ac8e675b773e7938ca Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 18:15:24 +0200 Subject: [PATCH 03/10] Add more stuff --- maubot/__main__.py | 20 +- maubot/cli/commands/build.py | 6 +- maubot/cli/commands/init.py | 62 ++- maubot/cli/commands/login.py | 17 +- maubot/cli/commands/upload.py | 3 +- maubot/cli/{template => res}/config.yaml | 0 maubot/cli/res/maubot.yaml.j2 | 42 ++ .../plugin.py.j2} | 8 + maubot/cli/res/spdx-simple.json | 383 ++++++++++++++++++ maubot/cli/template/plugin.py.j2 | 8 - maubot/cli/util/__init__.py | 1 - maubot/cli/util/clickquiry.py | 77 ++++ maubot/cli/util/path.py | 14 - maubot/cli/util/validators.py | 96 +++++ requirements.txt | 1 + setup.py | 2 + 16 files changed, 677 insertions(+), 63 deletions(-) rename maubot/cli/{template => res}/config.yaml (100%) create mode 100644 maubot/cli/res/maubot.yaml.j2 rename maubot/cli/{template/plugin-with-config.py.j2 => res/plugin.py.j2} (85%) create mode 100644 maubot/cli/res/spdx-simple.json delete mode 100644 maubot/cli/template/plugin.py.j2 create mode 100644 maubot/cli/util/clickquiry.py delete mode 100644 maubot/cli/util/path.py create mode 100644 maubot/cli/util/validators.py diff --git a/maubot/__main__.py b/maubot/__main__.py index 4d6d525..e0f0394 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -38,24 +38,24 @@ parser.add_argument("-b", "--base-config", type=str, default="example-config.yam "(for automatic config updates)") args = parser.parse_args() -config = Config(args.config, args.base_config) -config.load() -config.update() +base_config = Config(args.config, args.base_config) +base_config.load() +base_config.update() -logging.config.dictConfig(copy.deepcopy(config["logging"])) +logging.config.dictConfig(copy.deepcopy(base_config["logging"])) init_log_listener() log = logging.getLogger("maubot.init") log.info(f"Initializing maubot {__version__}") loop = asyncio.get_event_loop() -init_zip_loader(config) -db_session = init_db(config) +init_zip_loader(base_config) +db_session = init_db(base_config) clients = init_client_class(db_session, loop) -plugins = init_plugin_instance_class(db_session, config, loop) -management_api = init_mgmt_api(config, loop) -server = MaubotServer(config, loop) -server.app.add_subapp(config["server.base_path"], management_api) +plugins = init_plugin_instance_class(db_session, base_config, loop) +management_api = init_mgmt_api(base_config, loop) +server = MaubotServer(base_config, loop) +server.app.add_subapp(base_config["server.base_path"], management_api) for plugin in plugins: plugin.load() diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py index b820e2c..295e938 100644 --- a/maubot/cli/commands/build.py +++ b/maubot/cli/commands/build.py @@ -14,17 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import click -import os from ..base import app -from ..util import type_path +from ..util.validators import PathValidator @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.") @click.argument("path", default=".") -@click.option("-o", "--output", help="Path to output built plugin to", type=type_path) +@click.option("-o", "--output", help="Path to output built plugin to", + type=PathValidator.click_type) @click.option("-u", "--upload", help="Upload plugin to main server after building", is_flag=True, default=False) def build(path: str, output: str, upload: bool) -> None: diff --git a/maubot/cli/commands/init.py b/maubot/cli/commands/init.py index d2bb633..4d82825 100644 --- a/maubot/cli/commands/init.py +++ b/maubot/cli/commands/init.py @@ -13,22 +13,54 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import click +from pkg_resources import resource_string import os -from ..base import app -from ..util import type_path +from packaging.version import Version +from jinja2 import Template + +from ..util.validators import SPDXValidator, VersionValidator +from ..util import clickquiry + +loaded: bool = False +meta_template: Template +mod_template: Template +base_config: str -@app.command(help="Initialize a new maubot plugin") -@click.option("-n", "--name", help="The name of the project", default=os.path.basename(os.getcwd()), - prompt=True, show_default="directory name") -@click.option("-i", "--id", help="The maubot plugin ID (Java package name format)", prompt=True) -@click.option("-v", "--version", help="Initial version for project", default="0.1.0", - show_default=True) -@click.option("-l", "--license", help="The SPDX license identifier of the license for the project", - prompt=True, default="AGPL-3.0-or-later") -@click.option("-c", "--config", help="Include a config in the plugin stub", is_flag=True, - default=False) -def init(name: str, id: str, version: str, license: str, config: bool) -> None: - pass +def load_templates(): + global mod_template, meta_template, base_config, loaded + if loaded: + return + meta_template = Template(resource_string("maubot.cli", "res/maubot.yaml.j2").decode("utf-8")) + mod_template = Template(resource_string("maubot.cli", "res/plugin.py.j2").decode("utf-8")) + base_config = resource_string("maubot.cli", "res/config.yaml").decode("utf-8") + loaded = True + + +@clickquiry.command(help="Initialize a new maubot plugin") +@clickquiry.option("-n", "--name", help="The name of the project", required=True, + default=os.path.basename(os.getcwd())) +@clickquiry.option("-i", "--id", message="ID", required=True, + help="The maubot plugin ID (Java package name format)") +@clickquiry.option("-v", "--version", help="Initial version for project (PEP-440 format)", + default="0.1.0", validator=VersionValidator, required=True) +@clickquiry.option("-l", "--license", validator=SPDXValidator, default="AGPL-3.0-or-later", + help="The license for the project (SPDX identifier)", required=False) +@clickquiry.option("-c", "--config", message="Should the plugin include a config?", + help="Include a config in the plugin stub", is_flag=True, default="null") +def init(name: str, id: str, version: Version, license: str, config: bool) -> None: + load_templates() + main_class = name[0].upper() + name[1:] + meta = meta_template.render(id=id, version=str(version), license=license, config=config, + main_class=main_class) + with open("maubot.yaml", "w") as file: + file.write(meta) + if not os.path.isdir(name): + os.mkdir(name) + mod = mod_template.render(config=config, name=main_class) + with open(f"{name}/__init__.py", "w") as file: + file.write(mod) + if config: + with open("base-config.yaml", "w") as file: + file.write(base_config) diff --git a/maubot/cli/commands/login.py b/maubot/cli/commands/login.py index 9f91bc1..2bbfe4c 100644 --- a/maubot/cli/commands/login.py +++ b/maubot/cli/commands/login.py @@ -15,22 +15,19 @@ # along with this program. If not, see . from urllib.request import urlopen from urllib.error import HTTPError -import click import json import os from colorama import Fore, Style -from maubot.cli.base import app -from maubot.cli.config import save_config, config +from ..config import save_config, config +from ..util import clickquiry -@app.command(help="Log in to a Maubot instance") -@click.argument("server", required=True, default="http://localhost:29316") -@click.option("-u", "--username", help="The username of your account", prompt=True, - default=lambda: os.environ.get('USER', ''), show_default="current user") -@click.password_option("-p", "--password", help="The password to your account", required=True, - confirmation_prompt=False) +@clickquiry.command(help="Log in to a Maubot instance") +@clickquiry.option("-u", "--username", help="The username of your account", default=os.environ.get("USER", None), required=True) +@clickquiry.option("-p", "--password", help="The password to your account", inq_type="password", required=True) +@clickquiry.option("-s", "--server", help="The server to log in to", default="http://localhost:29316", required=True) def login(server, username, password) -> None: data = { "username": username, @@ -42,7 +39,7 @@ def login(server, username, password) -> None: resp = json.load(resp_data) config["servers"][server] = resp["token"] save_config() - print(Fore.GREEN, "Logged in successfully") + print(Fore.GREEN + "Logged in successfully") except HTTPError as e: if e.code == 401: print(Fore.RED + "Invalid username or password" + Style.RESET_ALL) diff --git a/maubot/cli/commands/upload.py b/maubot/cli/commands/upload.py index c30a7a2..43a9850 100644 --- a/maubot/cli/commands/upload.py +++ b/maubot/cli/commands/upload.py @@ -14,9 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import click -import os -from maubot.cli.base import app +from ..base import app @app.command(help="Upload a maubot plugin") diff --git a/maubot/cli/template/config.yaml b/maubot/cli/res/config.yaml similarity index 100% rename from maubot/cli/template/config.yaml rename to maubot/cli/res/config.yaml diff --git a/maubot/cli/res/maubot.yaml.j2 b/maubot/cli/res/maubot.yaml.j2 new file mode 100644 index 0000000..2d91ba2 --- /dev/null +++ b/maubot/cli/res/maubot.yaml.j2 @@ -0,0 +1,42 @@ +# The unique ID for the plugin. Java package naming style. (i.e. use your own domain, not xyz.maubot) +id: {{ id }} + +# A PEP 440 compliant version string. +version: {{ version }} + +# The SPDX license identifier for the plugin. https://spdx.org/licenses/ +# Optional, assumes all rights reserved if omitted. +{% if license %} +license: {{ license }} +{% else %} +#license: null +{% endif %} + +# The list of modules to load from the plugin archive. +# Modules can be directories with an __init__.py file or simply python files. +# Submodules that are imported by modules listed here don't need to be listed separately. +# However, top-level modules must always be listed even if they're imported by other modules. +modules: +- {{ name }} + +# The main class of the plugin. Format: module/Class +# If `module` is omitted, will default to last module specified in the module list. +# Even if `module` is not omitted here, it must be included in the modules list. +# The main class must extend maubot.Plugin +main_class: {{ main_class }} + +# Extra files that the upcoming build tool should include in the mbp file. +{% if config %} +extra_files: +- base-config.yaml +{% else %} +#extra_files: +#- base-config.yaml +{% endif %} + +# List of dependencies +#dependencies: +#- foo + +#soft_dependencies: +#- bar>=0.1 diff --git a/maubot/cli/template/plugin-with-config.py.j2 b/maubot/cli/res/plugin.py.j2 similarity index 85% rename from maubot/cli/template/plugin-with-config.py.j2 rename to maubot/cli/res/plugin.py.j2 index 18ca827..7e3ccc9 100644 --- a/maubot/cli/template/plugin-with-config.py.j2 +++ b/maubot/cli/res/plugin.py.j2 @@ -1,4 +1,5 @@ from maubot import Plugin +{% if config %} from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper class Config(BaseProxyConfig): @@ -6,15 +7,22 @@ class Config(BaseProxyConfig): helper.copy("example_1") helper.copy("example_2.list") helper.copy("example_2.value") +{% endif %} class {{ name }}: async def start() -> None: +{% if config %} self.config.load_and_update() self.log.debug("Loaded %s from config example 2", self.config["example_2.value"]) +{% else %} + pass +{% endif %} async def stop() -> None: pass +{% if config %} @classmethod def get_config_class(cls) -> Type[BaseProxyConfig]: return Config +{% endif %} diff --git a/maubot/cli/res/spdx-simple.json b/maubot/cli/res/spdx-simple.json new file mode 100644 index 0000000..39f8a95 --- /dev/null +++ b/maubot/cli/res/spdx-simple.json @@ -0,0 +1,383 @@ +[ + "0BSD", + "AAL", + "Abstyles", + "Adobe-2006", + "Adobe-Glyph", + "ADSL", + "AFL-1.1", + "AFL-1.2", + "AFL-2.0", + "AFL-2.1", + "AFL-3.0", + "Afmparse", + "AGPL-1.0-only", + "AGPL-1.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", + "Aladdin", + "AMDPLPA", + "AML", + "AMPAS", + "ANTLR-PD", + "Apache-1.0", + "Apache-1.1", + "Apache-2.0", + "APAFML", + "APL-1.0", + "APSL-1.0", + "APSL-1.1", + "APSL-1.2", + "APSL-2.0", + "Artistic-1.0-cl8", + "Artistic-1.0-Perl", + "Artistic-1.0", + "Artistic-2.0", + "Bahyph", + "Barr", + "Beerware", + "BitTorrent-1.0", + "BitTorrent-1.1", + "Borceux", + "BSD-1-Clause", + "BSD-2-Clause-FreeBSD", + "BSD-2-Clause-NetBSD", + "BSD-2-Clause-Patent", + "BSD-2-Clause", + "BSD-3-Clause-Attribution", + "BSD-3-Clause-Clear", + "BSD-3-Clause-LBNL", + "BSD-3-Clause-No-Nuclear-License-2014", + "BSD-3-Clause-No-Nuclear-License", + "BSD-3-Clause-No-Nuclear-Warranty", + "BSD-3-Clause", + "BSD-4-Clause-UC", + "BSD-4-Clause", + "BSD-Protection", + "BSD-Source-Code", + "BSL-1.0", + "bzip2-1.0.5", + "bzip2-1.0.6", + "Caldera", + "CATOSL-1.1", + "CC-BY-1.0", + "CC-BY-2.0", + "CC-BY-2.5", + "CC-BY-3.0", + "CC-BY-4.0", + "CC-BY-NC-1.0", + "CC-BY-NC-2.0", + "CC-BY-NC-2.5", + "CC-BY-NC-3.0", + "CC-BY-NC-4.0", + "CC-BY-NC-ND-1.0", + "CC-BY-NC-ND-2.0", + "CC-BY-NC-ND-2.5", + "CC-BY-NC-ND-3.0", + "CC-BY-NC-ND-4.0", + "CC-BY-NC-SA-1.0", + "CC-BY-NC-SA-2.0", + "CC-BY-NC-SA-2.5", + "CC-BY-NC-SA-3.0", + "CC-BY-NC-SA-4.0", + "CC-BY-ND-1.0", + "CC-BY-ND-2.0", + "CC-BY-ND-2.5", + "CC-BY-ND-3.0", + "CC-BY-ND-4.0", + "CC-BY-SA-1.0", + "CC-BY-SA-2.0", + "CC-BY-SA-2.5", + "CC-BY-SA-3.0", + "CC-BY-SA-4.0", + "CC0-1.0", + "CDDL-1.0", + "CDDL-1.1", + "CDLA-Permissive-1.0", + "CDLA-Sharing-1.0", + "CECILL-1.0", + "CECILL-1.1", + "CECILL-2.0", + "CECILL-2.1", + "CECILL-B", + "CECILL-C", + "ClArtistic", + "CNRI-Jython", + "CNRI-Python-GPL-Compatible", + "CNRI-Python", + "Condor-1.1", + "copyleft-next-0.3.1", + "CPAL-1.0", + "CPL-1.0", + "CPOL-1.02", + "Crossword", + "CrystalStacker", + "CUA-OPL-1.0", + "Cube", + "curl", + "D-FSL-1.0", + "diffmark", + "DOC", + "Dotseqn", + "DSDP", + "dvipdfm", + "ECL-1.0", + "ECL-2.0", + "EFL-1.0", + "EFL-2.0", + "eGenix", + "Entessa", + "EPL-1.0", + "EPL-2.0", + "ErlPL-1.1", + "EUDatagrid", + "EUPL-1.0", + "EUPL-1.1", + "EUPL-1.2", + "Eurosym", + "Fair", + "Frameworx-1.0", + "FreeImage", + "FSFAP", + "FSFUL", + "FSFULLR", + "FTL", + "GFDL-1.1-only", + "GFDL-1.1-or-later", + "GFDL-1.2-only", + "GFDL-1.2-or-later", + "GFDL-1.3-only", + "GFDL-1.3-or-later", + "Giftware", + "GL2PS", + "Glide", + "Glulxe", + "gnuplot", + "GPL-1.0-only", + "GPL-1.0-or-later", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0-only", + "GPL-3.0-or-later", + "gSOAP-1.3b", + "HaskellReport", + "HPND", + "IBM-pibs", + "ICU", + "IJG", + "ImageMagick", + "iMatix", + "Imlib2", + "Info-ZIP", + "Intel-ACPI", + "Intel", + "Interbase-1.0", + "IPA", + "IPL-1.0", + "ISC", + "JasPer-2.0", + "JSON", + "LAL-1.2", + "LAL-1.3", + "Latex2e", + "Leptonica", + "LGPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only", + "LGPL-2.1-or-later", + "LGPL-3.0-only", + "LGPL-3.0-or-later", + "LGPLLR", + "Libpng", + "libtiff", + "LiLiQ-P-1.1", + "LiLiQ-R-1.1", + "LiLiQ-Rplus-1.1", + "Linux-OpenIB", + "LPL-1.0", + "LPL-1.02", + "LPPL-1.0", + "LPPL-1.1", + "LPPL-1.2", + "LPPL-1.3a", + "LPPL-1.3c", + "MakeIndex", + "MirOS", + "MIT-0", + "MIT-advertising", + "MIT-CMU", + "MIT-enna", + "MIT-feh", + "MIT", + "MITNFA", + "Motosoto", + "mpich2", + "MPL-1.0", + "MPL-1.1", + "MPL-2.0-no-copyleft-exception", + "MPL-2.0", + "MS-PL", + "MS-RL", + "MTLL", + "Multics", + "Mup", + "NASA-1.3", + "Naumen", + "NBPL-1.0", + "NCSA", + "Net-SNMP", + "NetCDF", + "Newsletr", + "NGPL", + "NLOD-1.0", + "NLPL", + "Nokia", + "NOSL", + "Noweb", + "NPL-1.0", + "NPL-1.1", + "NPOSL-3.0", + "NRL", + "NTP", + "OCCT-PL", + "OCLC-2.0", + "ODbL-1.0", + "ODC-By-1.0", + "OFL-1.0", + "OFL-1.1", + "OGL-UK-1.0", + "OGL-UK-2.0", + "OGL-UK-3.0", + "OGTSL", + "OLDAP-1.1", + "OLDAP-1.2", + "OLDAP-1.3", + "OLDAP-1.4", + "OLDAP-2.0.1", + "OLDAP-2.0", + "OLDAP-2.1", + "OLDAP-2.2.1", + "OLDAP-2.2.2", + "OLDAP-2.2", + "OLDAP-2.3", + "OLDAP-2.4", + "OLDAP-2.5", + "OLDAP-2.6", + "OLDAP-2.7", + "OLDAP-2.8", + "OML", + "OpenSSL", + "OPL-1.0", + "OSET-PL-2.1", + "OSL-1.0", + "OSL-1.1", + "OSL-2.0", + "OSL-2.1", + "OSL-3.0", + "PDDL-1.0", + "PHP-3.0", + "PHP-3.01", + "Plexus", + "PostgreSQL", + "psfrag", + "psutils", + "Python-2.0", + "Qhull", + "QPL-1.0", + "Rdisc", + "RHeCos-1.1", + "RPL-1.1", + "RPL-1.5", + "RPSL-1.0", + "RSA-MD", + "RSCPL", + "Ruby", + "SAX-PD", + "Saxpath", + "SCEA", + "Sendmail-8.23", + "Sendmail", + "SGI-B-1.0", + "SGI-B-1.1", + "SGI-B-2.0", + "SimPL-2.0", + "SISSL-1.2", + "SISSL", + "Sleepycat", + "SMLNJ", + "SMPPL", + "SNIA", + "Spencer-86", + "Spencer-94", + "Spencer-99", + "SPL-1.0", + "SugarCRM-1.1.3", + "SWL", + "TCL", + "TCP-wrappers", + "TMate", + "TORQUE-1.1", + "TOSL", + "TU-Berlin-1.0", + "TU-Berlin-2.0", + "Unicode-DFS-2015", + "Unicode-DFS-2016", + "Unicode-TOU", + "Unlicense", + "UPL-1.0", + "Vim", + "VOSTROM", + "VSL-1.0", + "W3C-19980720", + "W3C-20150513", + "W3C", + "Watcom-1.0", + "Wsuipa", + "WTFPL", + "X11", + "Xerox", + "XFree86-1.1", + "xinetd", + "Xnet", + "xpp", + "XSkat", + "YPL-1.0", + "YPL-1.1", + "Zed", + "Zend-2.0", + "Zimbra-1.3", + "Zimbra-1.4", + "zlib-acknowledgement", + "Zlib", + "ZPL-1.1", + "ZPL-2.0", + "ZPL-2.1", + "AGPL-1.0", + "AGPL-3.0", + "eCos-2.0", + "GFDL-1.1", + "GFDL-1.2", + "GFDL-1.3", + "GPL-1.0+", + "GPL-1.0", + "GPL-2.0+", + "GPL-2.0-with-autoconf-exception", + "GPL-2.0-with-bison-exception", + "GPL-2.0-with-classpath-exception", + "GPL-2.0-with-font-exception", + "GPL-2.0-with-GCC-exception", + "GPL-2.0", + "GPL-3.0+", + "GPL-3.0-with-autoconf-exception", + "GPL-3.0-with-GCC-exception", + "GPL-3.0", + "LGPL-2.0+", + "LGPL-2.0", + "LGPL-2.1+", + "LGPL-2.1", + "LGPL-3.0+", + "LGPL-3.0", + "Nunit", + "StandardML-NJ", + "wxWindows" +] \ No newline at end of file diff --git a/maubot/cli/template/plugin.py.j2 b/maubot/cli/template/plugin.py.j2 deleted file mode 100644 index fc056ff..0000000 --- a/maubot/cli/template/plugin.py.j2 +++ /dev/null @@ -1,8 +0,0 @@ -from maubot import Plugin - -class {{ name }}: - async def start() -> None: - pass - - async def stop() -> None: - pass diff --git a/maubot/cli/util/__init__.py b/maubot/cli/util/__init__.py index a5cbafb..e69de29 100644 --- a/maubot/cli/util/__init__.py +++ b/maubot/cli/util/__init__.py @@ -1 +0,0 @@ -from .path import type_path diff --git a/maubot/cli/util/clickquiry.py b/maubot/cli/util/clickquiry.py new file mode 100644 index 0000000..c0a4bc3 --- /dev/null +++ b/maubot/cli/util/clickquiry.py @@ -0,0 +1,77 @@ +# 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 . +from typing import Any, Callable, Union +import functools + +from prompt_toolkit.validation import Validator +from PyInquirer import prompt +import click + +from ..base import app +from .validators import Required + + +def command(help: str) -> Callable[[Callable], Callable]: + def decorator(func) -> Callable: + questions = func.__inquirer_questions__.copy() + + @functools.wraps(func) + def wrapper(*args, **kwargs): + for key, value in kwargs.items(): + if value is not None and (questions[key]["type"] != "confirm" or value != "null"): + questions.pop(key, None) + question_list = list(questions.values()) + question_list.reverse() + resp = prompt(question_list, keyboard_interrupt_msg="Aborted!") + if not resp and question_list: + return + kwargs = {**kwargs, **resp} + func(*args, **kwargs) + + return app.command(help=help)(wrapper) + + return decorator + + +def option(short: str, long: str, message: str = None, help: str = None, + click_type: Union[str, Callable[[str], Any]] = None, inq_type: str = None, + validator: Validator = None, required: bool = False, default: str = None, + is_flag: bool = False) -> Callable[[Callable], Callable]: + if not message: + message = long[2].upper() + long[3:] + + def decorator(func) -> Callable: + click.option(short, long, help=help, type=validator.click_type if validator else click_type, + is_flag=is_flag)(func) + if not hasattr(func, "__inquirer_questions__"): + func.__inquirer_questions__ = {} + q = { + "type": (inq_type if isinstance(inq_type, str) + else ("input" if not is_flag + else "confirm")), + "name": long[2:], + "message": message, + } + if default is not None: + q["default"] = default + if required: + q["validator"] = Required(validator) + elif validator: + q["validator"] = validator + func.__inquirer_questions__[long[2:]] = q + return func + + return decorator diff --git a/maubot/cli/util/path.py b/maubot/cli/util/path.py deleted file mode 100644 index 75cae57..0000000 --- a/maubot/cli/util/path.py +++ /dev/null @@ -1,14 +0,0 @@ -import click -import os - - -def type_path(val: str) -> str: - val = os.path.abspath(val) - if os.path.exists(val): - return val - directory = os.path.dirname(val) - if not os.path.isdir(directory): - if os.path.exists(directory): - raise click.BadParameter(f"{directory} is not a directory") - raise click.BadParameter(f"{directory} does not exist") - return val diff --git a/maubot/cli/util/validators.py b/maubot/cli/util/validators.py new file mode 100644 index 0000000..d267658 --- /dev/null +++ b/maubot/cli/util/validators.py @@ -0,0 +1,96 @@ +# 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 . +from typing import Callable +import pkg_resources +import json +import os + +from packaging.version import Version, InvalidVersion +from prompt_toolkit.validation import Validator, ValidationError +from prompt_toolkit.document import Document +import click + + +class Required(Validator): + proxy: Validator + + def __init__(self, proxy: Validator = None) -> None: + self.proxy = proxy + + def validate(self, document: Document) -> None: + if len(document.text) == 0: + raise ValidationError(message="This field is required") + if self.proxy: + return self.proxy.validate(document) + + +class ClickValidator(Validator): + click_type: Callable[[str], str] = None + + @classmethod + def validate(cls, document: Document) -> None: + try: + cls.click_type(document.text) + except click.BadParameter as e: + raise ValidationError(message=e.message, cursor_position=len(document.text)) + + +def path(val: str) -> str: + val = os.path.abspath(val) + if os.path.exists(val): + return val + directory = os.path.dirname(val) + if not os.path.isdir(directory): + if os.path.exists(directory): + raise click.BadParameter(f"{directory} is not a directory") + raise click.BadParameter(f"{directory} does not exist") + return val + + +class PathValidator(ClickValidator): + click_type = path + + +def version(val: str) -> Version: + try: + return Version(val) + except InvalidVersion as e: + raise click.BadParameter(f"{val} is not a valid PEP-440 version") from e + + +class VersionValidator(ClickValidator): + click_type = version + + +spdx_list = None + + +def load_spdx(): + global spdx_list + spdx_data = pkg_resources.resource_stream("maubot.cli", "res/spdx-simple.json") + spdx_list = json.load(spdx_data) + + +def spdx(val: str) -> str: + if not spdx_list: + load_spdx() + if val not in spdx_list: + raise click.BadParameter(f"{val} is not a valid SPDX license identifier") + return val + + +class SPDXValidator(ClickValidator): + click_type = spdx diff --git a/requirements.txt b/requirements.txt index 79baba1..dd719a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ packaging click colorama +PyInquirer jinja2 diff --git a/setup.py b/setup.py index 2dc5ae3..a25d1a8 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ setuptools.setup( "click>=7,<8", "colorama>=0.4,<0.5", + "PyInquirer>=1,<2", "jinja2>=2,<3", ], @@ -57,5 +58,6 @@ setuptools.setup( package_data={ "maubot": ["management/frontend/build/*", "management/frontend/build/static/css/*", "management/frontend/build/static/js/*"], + "maubot.cli": ["res/*"], }, ) From 1bc51d2de566aa96ba7d6232b9278b963c8a134e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 19:09:02 +0200 Subject: [PATCH 04/10] Fix inquiring flags --- maubot/cli/commands/init.py | 2 +- maubot/cli/util/clickquiry.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/maubot/cli/commands/init.py b/maubot/cli/commands/init.py index 4d82825..d9e3368 100644 --- a/maubot/cli/commands/init.py +++ b/maubot/cli/commands/init.py @@ -48,7 +48,7 @@ def load_templates(): @clickquiry.option("-l", "--license", validator=SPDXValidator, default="AGPL-3.0-or-later", help="The license for the project (SPDX identifier)", required=False) @clickquiry.option("-c", "--config", message="Should the plugin include a config?", - help="Include a config in the plugin stub", is_flag=True, default="null") + help="Include a config in the plugin stub", default=False, is_flag=True) def init(name: str, id: str, version: Version, license: str, config: bool) -> None: load_templates() main_class = name[0].upper() + name[1:] diff --git a/maubot/cli/util/clickquiry.py b/maubot/cli/util/clickquiry.py index c0a4bc3..69cf716 100644 --- a/maubot/cli/util/clickquiry.py +++ b/maubot/cli/util/clickquiry.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Any, Callable, Union +from typing import Any, Callable, Union, Optional import functools from prompt_toolkit.validation import Validator @@ -21,7 +21,7 @@ from PyInquirer import prompt import click from ..base import app -from .validators import Required +from .validators import Required, ClickValidator def command(help: str) -> Callable[[Callable], Callable]: @@ -46,16 +46,30 @@ def command(help: str) -> Callable[[Callable], Callable]: return decorator +def yesno(val: str) -> Optional[bool]: + if not val: + return None + elif val.lower() in ("true", "t", "yes", "y"): + return True + elif val.lower() in ("false", "f", "no", "n"): + return False + + +yesno.__name__ = "yes/no" + + def option(short: str, long: str, message: str = None, help: str = None, click_type: Union[str, Callable[[str], Any]] = None, inq_type: str = None, validator: Validator = None, required: bool = False, default: str = None, is_flag: bool = False) -> Callable[[Callable], Callable]: if not message: message = long[2].upper() + long[3:] + click_type = validator.click_type if isinstance(validator, ClickValidator) else click_type + if is_flag: + click_type = yesno def decorator(func) -> Callable: - click.option(short, long, help=help, type=validator.click_type if validator else click_type, - is_flag=is_flag)(func) + click.option(short, long, help=help, type=click_type)(func) if not hasattr(func, "__inquirer_questions__"): func.__inquirer_questions__ = {} q = { From 54e117c9e7bd47658f19675b4d1757e305135a9b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 19:12:38 +0200 Subject: [PATCH 05/10] Rename clickquiry to cliq and remove util package --- maubot/cli/cliq/__init__.py | 2 ++ .../cli/{util/clickquiry.py => cliq/cliq.py} | 0 maubot/cli/{util => cliq}/validators.py | 0 maubot/cli/commands/build.py | 2 +- maubot/cli/commands/init.py | 26 +++++++++---------- maubot/cli/commands/login.py | 10 +++---- maubot/cli/util/__init__.py | 0 7 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 maubot/cli/cliq/__init__.py rename maubot/cli/{util/clickquiry.py => cliq/cliq.py} (100%) rename maubot/cli/{util => cliq}/validators.py (100%) delete mode 100644 maubot/cli/util/__init__.py diff --git a/maubot/cli/cliq/__init__.py b/maubot/cli/cliq/__init__.py new file mode 100644 index 0000000..cba14a4 --- /dev/null +++ b/maubot/cli/cliq/__init__.py @@ -0,0 +1,2 @@ +from .cliq import command, option +from .validators import SPDXValidator, VersionValidator, PathValidator diff --git a/maubot/cli/util/clickquiry.py b/maubot/cli/cliq/cliq.py similarity index 100% rename from maubot/cli/util/clickquiry.py rename to maubot/cli/cliq/cliq.py diff --git a/maubot/cli/util/validators.py b/maubot/cli/cliq/validators.py similarity index 100% rename from maubot/cli/util/validators.py rename to maubot/cli/cliq/validators.py diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py index 295e938..bd4416c 100644 --- a/maubot/cli/commands/build.py +++ b/maubot/cli/commands/build.py @@ -16,7 +16,7 @@ import click from ..base import app -from ..util.validators import PathValidator +from ..cliq.validators import PathValidator @app.command(short_help="Build a maubot plugin", diff --git a/maubot/cli/commands/init.py b/maubot/cli/commands/init.py index d9e3368..a3fb278 100644 --- a/maubot/cli/commands/init.py +++ b/maubot/cli/commands/init.py @@ -19,8 +19,8 @@ import os from packaging.version import Version from jinja2 import Template -from ..util.validators import SPDXValidator, VersionValidator -from ..util import clickquiry +from .. import cliq +from ..cliq import SPDXValidator, VersionValidator loaded: bool = False meta_template: Template @@ -38,17 +38,17 @@ def load_templates(): loaded = True -@clickquiry.command(help="Initialize a new maubot plugin") -@clickquiry.option("-n", "--name", help="The name of the project", required=True, - default=os.path.basename(os.getcwd())) -@clickquiry.option("-i", "--id", message="ID", required=True, - help="The maubot plugin ID (Java package name format)") -@clickquiry.option("-v", "--version", help="Initial version for project (PEP-440 format)", - default="0.1.0", validator=VersionValidator, required=True) -@clickquiry.option("-l", "--license", validator=SPDXValidator, default="AGPL-3.0-or-later", - help="The license for the project (SPDX identifier)", required=False) -@clickquiry.option("-c", "--config", message="Should the plugin include a config?", - help="Include a config in the plugin stub", default=False, is_flag=True) +@cliq.command(help="Initialize a new maubot plugin") +@cliq.option("-n", "--name", help="The name of the project", required=True, + default=os.path.basename(os.getcwd())) +@cliq.option("-i", "--id", message="ID", required=True, + help="The maubot plugin ID (Java package name format)") +@cliq.option("-v", "--version", help="Initial version for project (PEP-440 format)", + default="0.1.0", validator=VersionValidator, required=True) +@cliq.option("-l", "--license", validator=SPDXValidator, default="AGPL-3.0-or-later", + help="The license for the project (SPDX identifier)", required=False) +@cliq.option("-c", "--config", message="Should the plugin include a config?", + help="Include a config in the plugin stub", default=False, is_flag=True) def init(name: str, id: str, version: Version, license: str, config: bool) -> None: load_templates() main_class = name[0].upper() + name[1:] diff --git a/maubot/cli/commands/login.py b/maubot/cli/commands/login.py index 2bbfe4c..7cc85f1 100644 --- a/maubot/cli/commands/login.py +++ b/maubot/cli/commands/login.py @@ -21,13 +21,13 @@ import os from colorama import Fore, Style from ..config import save_config, config -from ..util import clickquiry +from ..cliq import cliq -@clickquiry.command(help="Log in to a Maubot instance") -@clickquiry.option("-u", "--username", help="The username of your account", default=os.environ.get("USER", None), required=True) -@clickquiry.option("-p", "--password", help="The password to your account", inq_type="password", required=True) -@clickquiry.option("-s", "--server", help="The server to log in to", default="http://localhost:29316", required=True) +@cliq.command(help="Log in to a Maubot instance") +@cliq.option("-u", "--username", help="The username of your account", default=os.environ.get("USER", None), required=True) +@cliq.option("-p", "--password", help="The password to your account", inq_type="password", required=True) +@cliq.option("-s", "--server", help="The server to log in to", default="http://localhost:29316", required=True) def login(server, username, password) -> None: data = { "username": username, diff --git a/maubot/cli/util/__init__.py b/maubot/cli/util/__init__.py deleted file mode 100644 index e69de29..0000000 From c334afd38b73c16273485aed4057b3eb7c9b1879 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 19:47:41 +0200 Subject: [PATCH 06/10] Implement building plugins --- maubot/cli/commands/build.py | 86 +++++++++++++++++++++++++++++++++++- maubot/loader/__init__.py | 2 +- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py index bd4416c..7d1a687 100644 --- a/maubot/cli/commands/build.py +++ b/maubot/cli/commands/build.py @@ -13,19 +13,101 @@ # # 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 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 PyInquirer import prompt import click +from ...loader import PluginMeta from ..base import app from ..cliq.validators import PathValidator +yaml = YAML() + + +def zipdir(zip, dir): + for root, dirs, files in os.walk(dir): + for file in files: + zip.write(os.path.join(root, file)) + + +def read_meta(path: str) -> Optional[PluginMeta]: + try: + with open(os.path.join(path, "maubot.yaml")) as meta_file: + try: + 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) + return None + except FileNotFoundError: + print(Fore.RED + "Failed to build plugin: Metadata file not found" + Style.RESET_ALL) + 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) + return None + return meta + + +def read_output_path(output: str, meta: PluginMeta) -> Optional[str]: + directory = os.getcwd() + filename = f"{meta.id}-v{meta.version}.mbp" + if not output: + output = os.path.join(directory, filename) + elif os.path.isdir(output): + output = os.path.join(output, filename) + elif os.path.exists(output): + override = prompt({ + "type": "confirm", + "name": "override", + "message": f"{output} exists, override?" + })["override"] + if not override: + return None + os.remove(output) + return os.path.abspath(output) + + +def write_plugin(meta: PluginMeta, output: str) -> None: + with zipfile.ZipFile(output, "w") as zip: + meta_dump = BytesIO() + yaml.dump(meta.serialize(), meta_dump) + zip.writestr("maubot.yaml", meta_dump.getvalue()) + + for module in meta.modules: + if os.path.isfile(f"{module}.py"): + zip.write(f"{module}.py") + elif os.path.isdir(module): + zipdir(zip, module) + else: + print(Fore.YELLOW + f"Module {module} not found, skipping") + + for file in meta.extra_files: + zip.write(file) + @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.") -@click.argument("path", default=".") +@click.argument("path", default=os.getcwd()) @click.option("-o", "--output", help="Path to output built plugin to", type=PathValidator.click_type) @click.option("-u", "--upload", help="Upload plugin to main server after building", is_flag=True, default=False) def build(path: str, output: str, upload: bool) -> None: - pass + meta = read_meta(path) + output = read_output_path(output, meta) + if not output: + return + os.chdir(path) + write_plugin(meta, output) + print(Fore.GREEN + "Plugin build complete.") diff --git a/maubot/loader/__init__.py b/maubot/loader/__init__.py index e2a356e..ec298ad 100644 --- a/maubot/loader/__init__.py +++ b/maubot/loader/__init__.py @@ -1,2 +1,2 @@ -from .abc import PluginLoader, PluginClass, IDConflictError +from .abc import PluginLoader, PluginClass, IDConflictError, PluginMeta from .zip import ZippedPluginLoader, MaubotZipImportError From cb3993d79facfef4c36f9bb96cc6467a5488a257 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 20:48:52 +0200 Subject: [PATCH 07/10] 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") From 66aef1978ca4d2ffc9694ea2f12e63c8771d5740 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 22:14:06 +0200 Subject: [PATCH 08/10] Fix accidentally changed variable name --- maubot/__main__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index e0f0394..4d6d525 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -38,24 +38,24 @@ parser.add_argument("-b", "--base-config", type=str, default="example-config.yam "(for automatic config updates)") args = parser.parse_args() -base_config = Config(args.config, args.base_config) -base_config.load() -base_config.update() +config = Config(args.config, args.base_config) +config.load() +config.update() -logging.config.dictConfig(copy.deepcopy(base_config["logging"])) +logging.config.dictConfig(copy.deepcopy(config["logging"])) init_log_listener() log = logging.getLogger("maubot.init") log.info(f"Initializing maubot {__version__}") loop = asyncio.get_event_loop() -init_zip_loader(base_config) -db_session = init_db(base_config) +init_zip_loader(config) +db_session = init_db(config) clients = init_client_class(db_session, loop) -plugins = init_plugin_instance_class(db_session, base_config, loop) -management_api = init_mgmt_api(base_config, loop) -server = MaubotServer(base_config, loop) -server.app.add_subapp(base_config["server.base_path"], management_api) +plugins = init_plugin_instance_class(db_session, config, loop) +management_api = init_mgmt_api(config, loop) +server = MaubotServer(config, loop) +server.app.add_subapp(config["server.base_path"], management_api) for plugin in plugins: plugin.load() From adc762b9ddcf2ba3a45c54d853f7a9db2669758e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 22:36:47 +0200 Subject: [PATCH 09/10] Show any errors when logging in --- maubot/cli/commands/login.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/maubot/cli/commands/login.py b/maubot/cli/commands/login.py index aa138a9..7171a6e 100644 --- a/maubot/cli/commands/login.py +++ b/maubot/cli/commands/login.py @@ -42,5 +42,8 @@ def login(server, username, password) -> None: save_config() print(Fore.GREEN + "Logged in successfully") except HTTPError as e: - if e.code == 401: - print(Fore.RED + "Invalid username or password" + Fore.RESET) + try: + err = json.load(e) + except json.JSONDecodeError: + err = {} + print(Fore.RED + err.get("error", str(e)) + Fore.RESET) From d41f4ad0c2226c71994d905995f6eb6d7230a585 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 13 Dec 2018 22:37:17 +0200 Subject: [PATCH 10/10] Add build-base and python3-dev to docker image --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index ae90a34..b8498e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ RUN apk add --no-cache \ py3-attrs \ py3-bcrypt \ py3-cffi \ + build-base \ + python3-dev \ ca-certificates \ su-exec \ && pip3 install -r requirements.txt