Merge pull request #36 from maubot/cli

Add plugin build tool
This commit is contained in:
Tulir Asokan 2018-12-13 22:40:32 +02:00 committed by GitHub
commit 6d92979dff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1047 additions and 5 deletions

View File

@ -17,6 +17,8 @@ RUN apk add --no-cache \
py3-attrs \ py3-attrs \
py3-bcrypt \ py3-bcrypt \
py3-cffi \ py3-cffi \
build-base \
python3-dev \
ca-certificates \ ca-certificates \
su-exec \ su-exec \
&& pip3 install -r requirements.txt && pip3 install -r requirements.txt

2
maubot/cli/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from . import commands
from .base import app

23
maubot/cli/base.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
import click
from .config import load_config
@click.group()
def app() -> None:
load_config()

View File

@ -0,0 +1,2 @@
from .cliq import command, option
from .validators import SPDXValidator, VersionValidator, PathValidator

91
maubot/cli/cliq/cliq.py Normal file
View File

@ -0,0 +1,91 @@
# 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 <https://www.gnu.org/licenses/>.
from typing import Any, Callable, Union, Optional
import functools
from prompt_toolkit.validation import Validator
from PyInquirer import prompt
import click
from ..base import app
from .validators import Required, ClickValidator
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 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=click_type)(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

View File

@ -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 <https://www.gnu.org/licenses/>.
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

View File

@ -0,0 +1 @@
from . import upload, build, login, init

View File

@ -0,0 +1,136 @@
# 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 <https://www.gnu.org/licenses/>.
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
from PyInquirer import prompt
import click
from ...loader import PluginMeta
from ..cliq.validators import PathValidator
from ..base import app
from ..config import config
from .upload import upload_file, UploadError
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) + Fore.RESET)
return None
except FileNotFoundError:
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) + Fore.RESET)
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: Union[str, IO]) -> 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" + 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.")
@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:
meta = read_meta(path)
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)
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)

View File

@ -0,0 +1,66 @@
# 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 <https://www.gnu.org/licenses/>.
from pkg_resources import resource_string
import os
from packaging.version import Version
from jinja2 import Template
from .. import cliq
from ..cliq import SPDXValidator, VersionValidator
loaded: bool = False
meta_template: Template
mod_template: Template
base_config: str
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
@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:]
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)

View File

@ -0,0 +1,49 @@
# 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 <https://www.gnu.org/licenses/>.
from urllib.request import urlopen
from urllib.error import HTTPError
import json
import os
from colorama import Fore
from ..config import save_config, config
from ..cliq import cliq
@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,
"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"]
config["default_server"] = server
save_config()
print(Fore.GREEN + "Logged in successfully")
except HTTPError as e:
try:
err = json.load(e)
except json.JSONDecodeError:
err = {}
print(Fore.RED + err.get("error", str(e)) + Fore.RESET)

View File

@ -0,0 +1,65 @@
# 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 <https://www.gnu.org/licenses/>.
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(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)

38
maubot/cli/config.py Normal file
View File

@ -0,0 +1,38 @@
# 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 <https://www.gnu.org/licenses/>.
import json
import os
config = {
"servers": {},
"default_server": None,
}
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"]
config["default_server"] = loaded["default_server"]
except FileNotFoundError:
pass

View File

@ -0,0 +1,6 @@
example_1: Example value 1
example_2:
list:
- foo
- bar
value: asd

View File

@ -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

View File

@ -0,0 +1,28 @@
from maubot import Plugin
{% if config %}
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")
{% 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 %}

View File

@ -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"
]

View File

@ -1,2 +1,2 @@
from .abc import PluginLoader, PluginClass, IDConflictError from .abc import PluginLoader, PluginClass, IDConflictError, PluginMeta
from .zip import ZippedPluginLoader, MaubotZipImportError from .zip import ZippedPluginLoader, MaubotZipImportError

View File

@ -185,7 +185,7 @@ class ZippedPluginLoader(PluginLoader):
importer = self._get_importer(reset_cache=reset_cache) importer = self._get_importer(reset_cache=reset_cache)
self._run_preload_checks(importer) self._run_preload_checks(importer)
if reset_cache: 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: for module in self.meta.modules:
try: try:
importer.load_module(module) importer.load_module(module)

View File

@ -124,10 +124,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes,
new_version: Version) -> web.Response: new_version: Version) -> web.Response:
dirname = os.path.dirname(plugin.path) dirname = os.path.dirname(plugin.path)
old_filename = os.path.basename(plugin.path) old_filename = os.path.basename(plugin.path)
if plugin.version in old_filename: if str(plugin.meta.version) in old_filename:
replacement = (new_version if plugin.version != new_version replacement = (new_version if plugin.meta.version != new_version
else f"{new_version}-ts{int(time())}") 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) replacement, old_filename)
else: else:
filename = old_filename.rstrip(".mbp") filename = old_filename.rstrip(".mbp")

View File

@ -7,3 +7,8 @@ ruamel.yaml
attrs attrs
bcrypt bcrypt
packaging packaging
click
colorama
PyInquirer
jinja2

View File

@ -30,6 +30,11 @@ setuptools.setup(
"attrs>=18.1.0,<19", "attrs>=18.1.0,<19",
"bcrypt>=3.1.4,<4", "bcrypt>=3.1.4,<4",
"packaging>=10", "packaging>=10",
"click>=7,<8",
"colorama>=0.4,<0.5",
"PyInquirer>=1,<2",
"jinja2>=2,<3",
], ],
classifiers=[ classifiers=[
@ -45,6 +50,7 @@ setuptools.setup(
entry_points=""" entry_points="""
[console_scripts] [console_scripts]
maubot=maubot.__main__:main maubot=maubot.__main__:main
mbc=maubot.cli:app
""", """,
data_files=[ data_files=[
(".", ["example-config.yaml"]), (".", ["example-config.yaml"]),
@ -52,5 +58,6 @@ setuptools.setup(
package_data={ package_data={
"maubot": ["management/frontend/build/*", "management/frontend/build/static/css/*", "maubot": ["management/frontend/build/*", "management/frontend/build/static/css/*",
"management/frontend/build/static/js/*"], "management/frontend/build/static/js/*"],
"maubot.cli": ["res/*"],
}, },
) )