From d5e78db5cf71c90f92a0406734f4335f5c34dde1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Jun 2019 17:02:44 +0300 Subject: [PATCH] Add Matrix client login/register commands --- maubot/cli/cliq/cliq.py | 8 +++- maubot/cli/commands/__init__.py | 2 +- maubot/cli/commands/auth.py | 65 +++++++++++++++++++++++++++++++++ maubot/cli/commands/build.py | 5 +-- maubot/cli/commands/logs.py | 7 +--- maubot/cli/commands/upload.py | 5 +-- maubot/cli/config.py | 16 +++++--- 7 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 maubot/cli/commands/auth.py diff --git a/maubot/cli/cliq/cliq.py b/maubot/cli/cliq/cliq.py index 69cf716..9d78325 100644 --- a/maubot/cli/cliq/cliq.py +++ b/maubot/cli/cliq/cliq.py @@ -31,6 +31,8 @@ def command(help: str) -> Callable[[Callable], Callable]: @functools.wraps(func) def wrapper(*args, **kwargs): for key, value in kwargs.items(): + if key not in questions: + continue if value is not None and (questions[key]["type"] != "confirm" or value != "null"): questions.pop(key, None) question_list = list(questions.values()) @@ -49,6 +51,8 @@ def command(help: str) -> Callable[[Callable], Callable]: def yesno(val: str) -> Optional[bool]: if not val: return None + elif isinstance(val, bool): + return val elif val.lower() in ("true", "t", "yes", "y"): return True elif val.lower() in ("false", "f", "no", "n"): @@ -61,7 +65,7 @@ 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]: + is_flag: bool = False, prompt: bool = True) -> Callable[[Callable], Callable]: if not message: message = long[2].upper() + long[3:] click_type = validator.click_type if isinstance(validator, ClickValidator) else click_type @@ -70,6 +74,8 @@ def option(short: str, long: str, message: str = None, help: str = None, def decorator(func) -> Callable: click.option(short, long, help=help, type=click_type)(func) + if not prompt: + return func if not hasattr(func, "__inquirer_questions__"): func.__inquirer_questions__ = {} q = { diff --git a/maubot/cli/commands/__init__.py b/maubot/cli/commands/__init__.py index 4eba273..c535234 100644 --- a/maubot/cli/commands/__init__.py +++ b/maubot/cli/commands/__init__.py @@ -1 +1 @@ -from . import upload, build, login, init, logs +from . import upload, build, login, init, logs, auth diff --git a/maubot/cli/commands/auth.py b/maubot/cli/commands/auth.py new file mode 100644 index 0000000..0f47c94 --- /dev/null +++ b/maubot/cli/commands/auth.py @@ -0,0 +1,65 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2019 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 . +from urllib.request import urlopen, Request +from urllib.error import HTTPError +import json + +from colorama import Fore +import click + +from ..config import get_token +from ..cliq import cliq + +history_count: int = 10 + + +@cliq.command(help="Log into a Matrix account via the Maubot server") +@cliq.option("-h", "--homeserver", help="The homeserver to log into", required=True) +@cliq.option("-u", "--username", help="The username to log in with", required=True) +@cliq.option("-p", "--password", help="The password to log in with", inq_type="password", + required=True) +@cliq.option("-s", "--server", help="The maubot instance to log in through", default="", + required=False, prompt=False) +@click.option("-r", "--register", help="Register instead of logging in", is_flag=True, + default=False) +def auth(homeserver: str, username: str, password: str, server: str, register: bool) -> None: + server, token = get_token(server) + if not token: + return + endpoint = "register" if register else "login" + req = Request(f"{server}/_matrix/maubot/v1/client/auth/{homeserver}/{endpoint}", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + }, + data=json.dumps({ + "username": username, + "password": password, + }).encode("utf-8")) + try: + with urlopen(req) as resp_data: + resp = json.load(resp_data) + action = "registered" if register else "logged in as" + print(f"{Fore.GREEN}Successfully {action} " + f"{Fore.CYAN}{resp['user_id']}{Fore.GREEN}.") + print(f"{Fore.GREEN}Access token: {Fore.CYAN}{resp['access_token']}{Fore.RESET}") + except HTTPError as e: + try: + err = json.load(e) + except json.JSONDecodeError: + err = {} + action = "register" if register else "log in" + print(f"{Fore.RED}Failed to {action}: {err.get('error', str(e))}{Fore.RESET}") diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py index 81c7591..a5e6148 100644 --- a/maubot/cli/commands/build.py +++ b/maubot/cli/commands/build.py @@ -98,10 +98,7 @@ def write_plugin(meta: PluginMeta, output: Union[str, IO]) -> None: def upload_plugin(output: Union[str, IO], server: str) -> None: - if not server: - server, token = get_default_server() - else: - token = get_token(server) + server, token = get_token(server) if not token: return if isinstance(output, str): diff --git a/maubot/cli/commands/logs.py b/maubot/cli/commands/logs.py index e931f34..52018e1 100644 --- a/maubot/cli/commands/logs.py +++ b/maubot/cli/commands/logs.py @@ -21,7 +21,7 @@ from aiohttp import WSMsgType, WSMessage, ClientSession from mautrix.client.api.types.util import Obj import click -from ..config import get_token, get_default_server +from ..config import get_token from ..base import app history_count: int = 10 @@ -31,10 +31,7 @@ history_count: int = 10 @click.argument("server", required=False) @click.option("-t", "--tail", default=10, help="Maximum number of old log lines to display") def logs(server: str, tail: int) -> None: - if not server: - server, token = get_default_server() - else: - token = get_token(server) + server, token = get_token(server) if not token: return global history_count diff --git a/maubot/cli/commands/upload.py b/maubot/cli/commands/upload.py index 428c1a4..2111d8a 100644 --- a/maubot/cli/commands/upload.py +++ b/maubot/cli/commands/upload.py @@ -33,10 +33,7 @@ class UploadError(Exception): @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: - server, token = get_default_server() - else: - token = get_token(server) + server, token = get_token(server) if not token: return with open(path, "rb") as file: diff --git a/maubot/cli/config.py b/maubot/cli/config.py index 70336f8..08995f6 100644 --- a/maubot/cli/config.py +++ b/maubot/cli/config.py @@ -13,13 +13,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Tuple, Optional +from typing import Tuple, Optional, Dict, Any import json import os from colorama import Fore -config = { +config: Dict[str, Any] = { "servers": {}, "default_server": None, } @@ -28,16 +28,22 @@ configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME" def get_default_server() -> Tuple[Optional[str], Optional[str]]: try: - server: str = config["default_server"] + server: Optional[str] = config["default_server"] except KeyError: server = None if server is None: print(f"{Fore.RED}Default server not configured.{Fore.RESET}") return None, None - return server, get_token(server) + return server, _get_token(server) -def get_token(server: str) -> Optional[str]: +def get_token(server: str) -> Tuple[Optional[str], Optional[str]]: + if not server: + return get_default_server() + return server, _get_token(server) + + +def _get_token(server: str) -> Optional[str]: try: return config["servers"][server] except KeyError: