Implement uploading plugins

This commit is contained in:
Tulir Asokan 2018-12-13 20:48:52 +02:00
parent c334afd38b
commit cb3993d79f
6 changed files with 88 additions and 21 deletions

View File

@ -13,20 +13,22 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional from typing import Optional, Union, IO
from io import BytesIO from io import BytesIO
import zipfile import zipfile
import os import os
from mautrix.client.api.types.util import SerializerError from mautrix.client.api.types.util import SerializerError
from ruamel.yaml import YAML, YAMLError from ruamel.yaml import YAML, YAMLError
from colorama import Fore, Style from colorama import Fore
from PyInquirer import prompt from PyInquirer import prompt
import click import click
from ...loader import PluginMeta from ...loader import PluginMeta
from ..base import app
from ..cliq.validators import PathValidator from ..cliq.validators import PathValidator
from ..base import app
from ..config import config
from .upload import upload_file, UploadError
yaml = YAML() yaml = YAML()
@ -44,16 +46,16 @@ def read_meta(path: str) -> Optional[PluginMeta]:
meta_dict = yaml.load(meta_file) meta_dict = yaml.load(meta_file)
except YAMLError as e: except YAMLError as e:
print(Fore.RED + "Failed to build plugin: Metadata file is not YAML") 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 return None
except FileNotFoundError: 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 return None
try: try:
meta = PluginMeta.deserialize(meta_dict) meta = PluginMeta.deserialize(meta_dict)
except SerializerError as e: except SerializerError as e:
print(Fore.RED + "Failed to build plugin: Metadata file is not valid") 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 None
return meta return meta
@ -77,7 +79,7 @@ def read_output_path(output: str, meta: PluginMeta) -> Optional[str]:
return os.path.abspath(output) 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: with zipfile.ZipFile(output, "w") as zip:
meta_dump = BytesIO() meta_dump = BytesIO()
yaml.dump(meta.serialize(), meta_dump) yaml.dump(meta.serialize(), meta_dump)
@ -89,12 +91,26 @@ def write_plugin(meta: PluginMeta, output: str) -> None:
elif os.path.isdir(module): elif os.path.isdir(module):
zipdir(zip, module) zipdir(zip, module)
else: 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: for file in meta.extra_files:
zip.write(file) 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", @app.command(short_help="Build a maubot plugin",
help="Build a maubot plugin. First parameter is the path to root of the 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.") "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) default=False)
def build(path: str, output: str, upload: bool) -> None: def build(path: str, output: str, upload: bool) -> None:
meta = read_meta(path) meta = read_meta(path)
output = read_output_path(output, meta) if output or not upload:
if not output: output = read_output_path(output, meta)
return if not output:
return
else:
output = BytesIO()
os.chdir(path) os.chdir(path)
write_plugin(meta, output) 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)

View File

@ -18,7 +18,7 @@ from urllib.error import HTTPError
import json import json
import os import os
from colorama import Fore, Style from colorama import Fore
from ..config import save_config, config from ..config import save_config, config
from ..cliq import cliq from ..cliq import cliq
@ -38,8 +38,9 @@ def login(server, username, password) -> None:
data=json.dumps(data).encode("utf-8")) as resp_data: data=json.dumps(data).encode("utf-8")) as resp_data:
resp = json.load(resp_data) resp = json.load(resp_data)
config["servers"][server] = resp["token"] config["servers"][server] = resp["token"]
config["default_server"] = server
save_config() save_config()
print(Fore.GREEN + "Logged in successfully") print(Fore.GREEN + "Logged in successfully")
except HTTPError as e: except HTTPError as e:
if e.code == 401: if e.code == 401:
print(Fore.RED + "Invalid username or password" + Style.RESET_ALL) print(Fore.RED + "Invalid username or password" + Fore.RESET)

View File

@ -13,12 +13,53 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # 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 import click
from ..base import app from ..base import app
from ..config import config
class UploadError(Exception):
pass
@app.command(help="Upload a maubot plugin") @app.command(help="Upload a maubot plugin")
@click.argument("path")
@click.option("-s", "--server", help="The maubot instance to upload the plugin to") @click.option("-s", "--server", help="The maubot instance to upload the plugin to")
def upload(server: str) -> None: def upload(path: str, server: str) -> None:
pass 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)

View File

@ -17,7 +17,8 @@ import json
import os import os
config = { config = {
"servers": {} "servers": {},
"default_server": None,
} }
configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config")) 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: with open(f"{configdir}/maubot-cli.json") as file:
loaded = json.load(file) loaded = json.load(file)
config["servers"] = loaded["servers"] config["servers"] = loaded["servers"]
config["default_server"] = loaded["default_server"]
except FileNotFoundError: except FileNotFoundError:
pass pass

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")