diff --git a/maubot/cli/commands/__init__.py b/maubot/cli/commands/__init__.py
index e94091c..4eba273 100644
--- a/maubot/cli/commands/__init__.py
+++ b/maubot/cli/commands/__init__.py
@@ -1 +1 @@
-from . import upload, build, login, init
+from . import upload, build, login, init, logs
diff --git a/maubot/cli/commands/build.py b/maubot/cli/commands/build.py
index 3e90f6a..0f3201c 100644
--- a/maubot/cli/commands/build.py
+++ b/maubot/cli/commands/build.py
@@ -27,7 +27,7 @@ import click
from ...loader import PluginMeta
from ..cliq.validators import PathValidator
from ..base import app
-from ..config import config
+from ..config import get_default_server
from .upload import upload_file
yaml = YAML()
@@ -98,11 +98,8 @@ def write_plugin(meta: PluginMeta, output: Union[str, IO]) -> None:
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)
+ server, token = get_default_server()
+ if not token:
return
if isinstance(output, str):
with open(output, "rb") as file:
diff --git a/maubot/cli/commands/logs.py b/maubot/cli/commands/logs.py
new file mode 100644
index 0000000..3705d47
--- /dev/null
+++ b/maubot/cli/commands/logs.py
@@ -0,0 +1,110 @@
+# maubot - A plugin-based Matrix bot system.
+# Copyright (C) 2018 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 datetime import datetime
+import asyncio
+
+from colorama import Fore
+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 ..base import app
+
+history_count: int = 10
+
+
+@app.command(help="View the logs of a server")
+@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)
+ if not token:
+ return
+ global history_count
+ history_count = tail
+ loop = asyncio.get_event_loop()
+ future = asyncio.ensure_future(view_logs(server, token), loop=loop)
+ try:
+ loop.run_until_complete(future)
+ except KeyboardInterrupt:
+ future.cancel()
+ loop.run_until_complete(future)
+ loop.close()
+
+
+def parsedate(entry: Obj) -> None:
+ i = entry.time.index("+")
+ i = entry.time.index(":", i)
+ entry.time = entry.time[:i] + entry.time[i + 1:]
+ entry.time = datetime.strptime(entry.time, "%Y-%m-%dT%H:%M:%S.%f%z")
+
+
+levelcolors = {
+ "DEBUG": "",
+ "INFO": Fore.CYAN,
+ "WARNING": Fore.YELLOW,
+ "ERROR": Fore.RED,
+ "FATAL": Fore.MAGENTA,
+}
+
+
+def print_entry(entry: dict) -> None:
+ entry = Obj(**entry)
+ parsedate(entry)
+ print("{levelcolor}[{date}] [{level}@{logger}] {message}{resetcolor}"
+ .format(date=entry.time.strftime("%Y-%m-%d %H:%M:%S"),
+ level=entry.levelname,
+ levelcolor=levelcolors.get(entry.levelname, ""),
+ resetcolor=Fore.RESET,
+ logger=entry.name,
+ message=entry.msg))
+
+
+def handle_msg(data: dict) -> bool:
+ if "auth_success" in data:
+ if data["auth_success"]:
+ print(Fore.GREEN + "Connected to log websocket" + Fore.RESET)
+ else:
+ print(Fore.RED + "Failed to authenticate to log websocket" + Fore.RESET)
+ return False
+ elif "history" in data:
+ for entry in data["history"][-history_count:]:
+ print_entry(entry)
+ else:
+ print_entry(data)
+ return True
+
+
+async def view_logs(server: str, token: str) -> None:
+ async with ClientSession() as session:
+ async with session.ws_connect(f"{server}/_matrix/maubot/v1/logs") as ws:
+ await ws.send_str(token)
+ try:
+ msg: WSMessage
+ async for msg in ws:
+ if msg.type == WSMsgType.TEXT:
+ if not handle_msg(msg.json()):
+ break
+ elif msg.type == WSMsgType.ERROR:
+ print(Fore.YELLOW + "Connection error: " + msg.data + Fore.RESET)
+ elif msg.type == WSMsgType.CLOSE:
+ print(Fore.YELLOW + "Server closed connection" + Fore.RESET)
+ except asyncio.CancelledError:
+ pass
diff --git a/maubot/cli/commands/upload.py b/maubot/cli/commands/upload.py
index 8da3558..428c1a4 100644
--- a/maubot/cli/commands/upload.py
+++ b/maubot/cli/commands/upload.py
@@ -22,7 +22,7 @@ from colorama import Fore
import click
from ..base import app
-from ..config import config
+from ..config import get_default_server, get_token
class UploadError(Exception):
@@ -34,15 +34,10 @@ class UploadError(Exception):
@click.option("-s", "--server", help="The maubot instance to upload the plugin to")
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)
+ server, token = get_default_server()
+ else:
+ token = get_token(server)
+ if not token:
return
with open(path, "rb") as file:
upload_file(file, server, token)
diff --git a/maubot/cli/config.py b/maubot/cli/config.py
index 653ffef..70336f8 100644
--- a/maubot/cli/config.py
+++ b/maubot/cli/config.py
@@ -13,9 +13,12 @@
#
# 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
import json
import os
+from colorama import Fore
+
config = {
"servers": {},
"default_server": None,
@@ -23,6 +26,25 @@ config = {
configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config"))
+def get_default_server() -> Tuple[Optional[str], Optional[str]]:
+ try:
+ server: 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)
+
+
+def get_token(server: str) -> Optional[str]:
+ try:
+ return config["servers"][server]
+ except KeyError:
+ print(f"{Fore.RED}No access token saved for {server}.{Fore.RESET}")
+ return None
+
+
def save_config() -> None:
with open(f"{configdir}/maubot-cli.json", "w") as file:
json.dump(config, file)