commit
6d92979dff
21 changed files with 1047 additions and 5 deletions
|
@ -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
|
||||
|
|
2
maubot/cli/__init__.py
Normal file
2
maubot/cli/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from . import commands
|
||||
from .base import app
|
23
maubot/cli/base.py
Normal file
23
maubot/cli/base.py
Normal 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()
|
2
maubot/cli/cliq/__init__.py
Normal file
2
maubot/cli/cliq/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .cliq import command, option
|
||||
from .validators import SPDXValidator, VersionValidator, PathValidator
|
91
maubot/cli/cliq/cliq.py
Normal file
91
maubot/cli/cliq/cliq.py
Normal 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
|
96
maubot/cli/cliq/validators.py
Normal file
96
maubot/cli/cliq/validators.py
Normal 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
|
1
maubot/cli/commands/__init__.py
Normal file
1
maubot/cli/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from . import upload, build, login, init
|
136
maubot/cli/commands/build.py
Normal file
136
maubot/cli/commands/build.py
Normal 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)
|
66
maubot/cli/commands/init.py
Normal file
66
maubot/cli/commands/init.py
Normal 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)
|
49
maubot/cli/commands/login.py
Normal file
49
maubot/cli/commands/login.py
Normal 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)
|
65
maubot/cli/commands/upload.py
Normal file
65
maubot/cli/commands/upload.py
Normal 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
38
maubot/cli/config.py
Normal 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
|
6
maubot/cli/res/config.yaml
Normal file
6
maubot/cli/res/config.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
example_1: Example value 1
|
||||
example_2:
|
||||
list:
|
||||
- foo
|
||||
- bar
|
||||
value: asd
|
42
maubot/cli/res/maubot.yaml.j2
Normal file
42
maubot/cli/res/maubot.yaml.j2
Normal 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
|
28
maubot/cli/res/plugin.py.j2
Normal file
28
maubot/cli/res/plugin.py.j2
Normal 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 %}
|
383
maubot/cli/res/spdx-simple.json
Normal file
383
maubot/cli/res/spdx-simple.json
Normal 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"
|
||||
]
|
|
@ -1,2 +1,2 @@
|
|||
from .abc import PluginLoader, PluginClass, IDConflictError
|
||||
from .abc import PluginLoader, PluginClass, IDConflictError, PluginMeta
|
||||
from .zip import ZippedPluginLoader, MaubotZipImportError
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -7,3 +7,8 @@ ruamel.yaml
|
|||
attrs
|
||||
bcrypt
|
||||
packaging
|
||||
|
||||
click
|
||||
colorama
|
||||
PyInquirer
|
||||
jinja2
|
||||
|
|
7
setup.py
7
setup.py
|
@ -30,6 +30,11 @@ setuptools.setup(
|
|||
"attrs>=18.1.0,<19",
|
||||
"bcrypt>=3.1.4,<4",
|
||||
"packaging>=10",
|
||||
|
||||
"click>=7,<8",
|
||||
"colorama>=0.4,<0.5",
|
||||
"PyInquirer>=1,<2",
|
||||
"jinja2>=2,<3",
|
||||
],
|
||||
|
||||
classifiers=[
|
||||
|
@ -45,6 +50,7 @@ setuptools.setup(
|
|||
entry_points="""
|
||||
[console_scripts]
|
||||
maubot=maubot.__main__:main
|
||||
mbc=maubot.cli:app
|
||||
""",
|
||||
data_files=[
|
||||
(".", ["example-config.yaml"]),
|
||||
|
@ -52,5 +58,6 @@ setuptools.setup(
|
|||
package_data={
|
||||
"maubot": ["management/frontend/build/*", "management/frontend/build/static/css/*",
|
||||
"management/frontend/build/static/js/*"],
|
||||
"maubot.cli": ["res/*"],
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue