Update rest of maubot-cli HTTP requests to asyncio

This commit is contained in:
Tulir Asokan 2021-11-19 21:45:30 +02:00
parent 14d410739c
commit e1a1f7b65e
5 changed files with 77 additions and 58 deletions

View File

@ -1,5 +1,5 @@
# maubot - A plugin-based Matrix bot system. # maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2021 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -13,8 +13,9 @@
# #
# 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 Any, Callable, Union, Optional from typing import Any, Callable, Union, Optional, Type
import functools import functools
import traceback
import inspect import inspect
import asyncio import asyncio
@ -22,6 +23,7 @@ import aiohttp
from prompt_toolkit.validation import Validator from prompt_toolkit.validation import Validator
from questionary import prompt from questionary import prompt
from colorama import Fore
import click import click
from ..base import app from ..base import app
@ -33,7 +35,10 @@ def with_http(func):
@functools.wraps(func) @functools.wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
async with aiohttp.ClientSession() as sess: async with aiohttp.ClientSession() as sess:
return await func(*args, sess=sess, **kwargs) try:
return await func(*args, sess=sess, **kwargs)
except aiohttp.ClientError as e:
print(f"{Fore.RED}Connection error: {e}{Fore.RESET}")
return wrapper return wrapper
@ -45,14 +50,17 @@ def with_authenticated_http(func):
if not token: if not token:
return return
async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {token}"}) as sess: async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {token}"}) as sess:
return await func(*args, sess=sess, server=server, **kwargs) try:
return await func(*args, sess=sess, server=server, **kwargs)
except aiohttp.ClientError as e:
print(f"{Fore.RED}Connection error: {e}{Fore.RESET}")
return wrapper return wrapper
def command(help: str) -> Callable[[Callable], Callable]: def command(help: str) -> Callable[[Callable], Callable]:
def decorator(func) -> Callable: def decorator(func) -> Callable:
questions = func.__inquirer_questions__.copy() questions = getattr(func, "__inquirer_questions__", {}).copy()
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@ -79,9 +87,13 @@ def command(help: str) -> Callable[[Callable], Callable]:
return return
kwargs = {**kwargs, **resp} kwargs = {**kwargs, **resp}
res = func(*args, **kwargs) try:
if inspect.isawaitable(res): res = func(*args, **kwargs)
asyncio.run(res) if inspect.isawaitable(res):
asyncio.run(res)
except Exception:
print(Fore.RED + "Fatal error running command" + Fore.RESET)
traceback.print_exc()
return app.command(help=help)(wrapper) return app.command(help=help)(wrapper)
@ -104,12 +116,14 @@ yesno.__name__ = "yes/no"
def option(short: str, long: str, message: str = None, help: str = None, def option(short: str, long: str, message: str = None, help: str = None,
click_type: Union[str, Callable[[str], Any]] = None, inq_type: str = None, click_type: Union[str, Callable[[str], Any]] = None, inq_type: str = None,
validator: Validator = None, required: bool = False, default: str = None, validator: Type[Validator] = None, required: bool = False,
is_flag: bool = False, prompt: bool = True, required_unless: str = None default: Union[str, bool, None] = None, is_flag: bool = False, prompt: bool = True,
) -> Callable[[Callable], Callable]: required_unless: str = None) -> Callable[[Callable], Callable]:
if not message: if not message:
message = long[2].upper() + long[3:] message = long[2].upper() + long[3:]
click_type = validator.click_type if isinstance(validator, ClickValidator) else click_type
if isinstance(validator, type) and issubclass(validator, ClickValidator):
click_type = validator.click_type
if is_flag: if is_flag:
click_type = yesno click_type = yesno

View File

@ -1,5 +1,5 @@
# maubot - A plugin-based Matrix bot system. # maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2021 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -16,12 +16,14 @@
from typing import Optional, Union, IO from typing import Optional, Union, IO
from io import BytesIO from io import BytesIO
import zipfile import zipfile
import asyncio
import glob import glob
import os import os
from ruamel.yaml import YAML, YAMLError from ruamel.yaml import YAML, YAMLError
from colorama import Fore from aiohttp import ClientSession
from questionary import prompt from questionary import prompt
from colorama import Fore
import click import click
from mautrix.types import SerializerError from mautrix.types import SerializerError
@ -30,6 +32,7 @@ from ...loader import PluginMeta
from ..cliq.validators import PathValidator from ..cliq.validators import PathValidator
from ..base import app from ..base import app
from ..config import get_token from ..config import get_token
from ..cliq import cliq
from .upload import upload_file from .upload import upload_file
yaml = YAML() yaml = YAML()
@ -100,15 +103,16 @@ def write_plugin(meta: PluginMeta, output: Union[str, IO]) -> None:
zip.write(file) zip.write(file)
def upload_plugin(output: Union[str, IO], server: str) -> None: @cliq.with_authenticated_http
async def upload_plugin(output: Union[str, IO], *, server: str, sess: ClientSession) -> None:
server, token = get_token(server) server, token = get_token(server)
if not token: if not token:
return return
if isinstance(output, str): if isinstance(output, str):
with open(output, "rb") as file: with open(output, "rb") as file:
upload_file(file, server, token) await upload_file(sess, file, server)
else: else:
upload_file(output, server, token) await upload_file(sess, output, server)
@app.command(short_help="Build a maubot plugin", @app.command(short_help="Build a maubot plugin",
@ -137,4 +141,4 @@ def build(path: str, output: str, upload: bool, server: str) -> None:
else: else:
output.seek(0) output.seek(0)
if upload: if upload:
upload_plugin(output, server) asyncio.run(upload_plugin(output, server=server))

View File

@ -1,5 +1,5 @@
# maubot - A plugin-based Matrix bot system. # maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2021 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -13,12 +13,12 @@
# #
# 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
from urllib.error import HTTPError
import json import json
import os import os
from colorama import Fore from colorama import Fore
from yarl import URL
import aiohttp
from ..config import save_config, config from ..config import save_config, config
from ..cliq import cliq from ..cliq import cliq
@ -29,16 +29,17 @@ from ..cliq import cliq
@cliq.option("-p", "--password", help="The password to your account", inq_type="password", 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) @cliq.option("-s", "--server", help="The server to log in to", default="http://localhost:29316", required=True)
@cliq.option("-a", "--alias", help="Alias to reference the server without typing the full URL", default="", required=False) @cliq.option("-a", "--alias", help="Alias to reference the server without typing the full URL", default="", required=False)
def login(server, username, password, alias) -> None: @cliq.with_http
async def login(server: str, username: str, password: str, alias: str, sess: aiohttp.ClientSession) -> None:
data = { data = {
"username": username, "username": username,
"password": password, "password": password,
} }
try: url = URL(server) / "_matrix/maubot/v1/auth/login"
with urlopen(f"{server}/_matrix/maubot/v1/auth/login", async with sess.post(url, json=data) as resp:
data=json.dumps(data).encode("utf-8")) as resp_data: if resp.status == 200:
resp = json.load(resp_data) data = await resp.json()
config["servers"][server] = resp["token"] config["servers"][server] = data["token"]
if not config["default_server"]: if not config["default_server"]:
print(Fore.CYAN, "Setting", server, "as the default server") print(Fore.CYAN, "Setting", server, "as the default server")
config["default_server"] = server config["default_server"] = server
@ -46,9 +47,9 @@ def login(server, username, password, alias) -> None:
config["aliases"][alias] = server config["aliases"][alias] = server
save_config() save_config()
print(Fore.GREEN + "Logged in successfully") print(Fore.GREEN + "Logged in successfully")
except HTTPError as e: else:
try: try:
err = json.load(e) err = (await resp.json())["error"]
except json.JSONDecodeError: except (json.JSONDecodeError, KeyError):
err = {} err = await resp.text()
print(Fore.RED + err.get("error", str(e)) + Fore.RESET) print(Fore.RED + err + Fore.RESET)

View File

@ -1,5 +1,5 @@
# maubot - A plugin-based Matrix bot system. # maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2021 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -13,45 +13,45 @@
# #
# 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 from typing import IO
import json import json
from colorama import Fore from colorama import Fore
from yarl import URL
import aiohttp
import click import click
from ..base import app from ..cliq import cliq
from ..config import get_default_server, get_token
class UploadError(Exception): class UploadError(Exception):
pass pass
@app.command(help="Upload a maubot plugin") @cliq.command(help="Upload a maubot plugin")
@click.argument("path") @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(path: str, server: str) -> None: @cliq.with_authenticated_http
server, token = get_token(server) async def upload(path: str, server: str, sess: aiohttp.ClientSession) -> None:
if not token: print("hmm")
return
with open(path, "rb") as file: with open(path, "rb") as file:
upload_file(file, server, token) await upload_file(sess, file, server)
def upload_file(file: IO, server: str, token: str) -> None: async def upload_file(sess: aiohttp.ClientSession, file: IO, server: str) -> None:
req = Request(f"{server}/_matrix/maubot/v1/plugins/upload?allow_override=true", data=file, url = (URL(server) / "_matrix/maubot/v1/plugins/upload").with_query({"allow_override": "true"})
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/zip"}) headers = {"Content-Type": "application/zip"}
try: async with sess.post(url, data=file, headers=headers) as resp:
with urlopen(req) as resp_data: if resp.status == 200:
resp = json.load(resp_data) data = await resp.json()
print(f"{Fore.GREEN}Plugin {Fore.CYAN}{resp['id']} v{resp['version']}{Fore.GREEN} " print(f"{Fore.GREEN}Plugin {Fore.CYAN}{data['id']} v{data['version']}{Fore.GREEN} "
f"uploaded to {Fore.CYAN}{server}{Fore.GREEN} successfully.{Fore.RESET}") f"uploaded to {Fore.CYAN}{server}{Fore.GREEN} successfully.{Fore.RESET}")
except HTTPError as e: else:
try: try:
err = json.load(e) err = await resp.json()
except json.JSONDecodeError: if "stacktrace" in err:
err = {} print(err["stacktrace"])
print(err.get("stacktrace", "")) err = err["error"]
print(Fore.RED + "Failed to upload plugin: " + err.get("error", str(e)) + Fore.RESET) except (aiohttp.ContentTypeError, json.JSONDecodeError, KeyError):
err = await resp.text()
print(f"{Fore.RED}Failed to upload plugin: {err}{Fore.RESET}")