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: