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

View file

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

View file

@ -13,12 +13,53 @@
#
# 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(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)

View file

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

View file

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

View file

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