Add Cloudron packaging for Maubot
This commit is contained in:
1
maubot-src/maubot/cli/commands/__init__.py
Normal file
1
maubot-src/maubot/cli/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import auth, build, init, login, logs, upload
|
||||
166
maubot-src/maubot/cli/commands/auth.py
Normal file
166
maubot-src/maubot/cli/commands/auth.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
import json
|
||||
import webbrowser
|
||||
|
||||
from colorama import Fore
|
||||
from yarl import URL
|
||||
import aiohttp
|
||||
import click
|
||||
|
||||
from ..cliq import cliq
|
||||
|
||||
history_count: int = 10
|
||||
|
||||
friendly_errors = {
|
||||
"server_not_found": (
|
||||
"Registration target server not found.\n\n"
|
||||
"To log in or register through maubot, you must add the server to the\n"
|
||||
"homeservers section in the config. If you only want to log in,\n"
|
||||
"leave the `secret` field empty."
|
||||
),
|
||||
"registration_no_sso": (
|
||||
"The register operation is only for registering with a password.\n\n"
|
||||
"To register with SSO, simply leave out the --register flag."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def list_servers(server: str, sess: aiohttp.ClientSession) -> None:
|
||||
url = URL(server) / "_matrix/maubot/v1/client/auth/servers"
|
||||
async with sess.get(url) as resp:
|
||||
data = await resp.json()
|
||||
print(f"{Fore.GREEN}Available Matrix servers for registration and login:{Fore.RESET}")
|
||||
for server in data.keys():
|
||||
print(f"* {Fore.CYAN}{server}{Fore.RESET}")
|
||||
|
||||
|
||||
@cliq.command(help="Log into a Matrix account via the Maubot server")
|
||||
@cliq.option("-h", "--homeserver", help="The homeserver to log into", required_unless="list")
|
||||
@cliq.option(
|
||||
"-u", "--username", help="The username to log in with", required_unless=["list", "sso"]
|
||||
)
|
||||
@cliq.option(
|
||||
"-p",
|
||||
"--password",
|
||||
help="The password to log in with",
|
||||
inq_type="password",
|
||||
required_unless=["list", "sso"],
|
||||
)
|
||||
@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
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--update-client",
|
||||
help="Instead of returning the access token, create or update a client in maubot using it",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
)
|
||||
@click.option("-l", "--list", help="List available homeservers", is_flag=True, default=False)
|
||||
@click.option(
|
||||
"-o", "--sso", help="Use single sign-on instead of password login", is_flag=True, default=False
|
||||
)
|
||||
@click.option(
|
||||
"-n",
|
||||
"--device-name",
|
||||
help="The initial e2ee device displayname (only for login)",
|
||||
default="Maubot",
|
||||
required=False,
|
||||
)
|
||||
@cliq.with_authenticated_http
|
||||
async def auth(
|
||||
homeserver: str,
|
||||
username: str,
|
||||
password: str,
|
||||
server: str,
|
||||
register: bool,
|
||||
list: bool,
|
||||
update_client: bool,
|
||||
device_name: str,
|
||||
sso: bool,
|
||||
sess: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
if list:
|
||||
await list_servers(server, sess)
|
||||
return
|
||||
endpoint = "register" if register else "login"
|
||||
url = URL(server) / "_matrix/maubot/v1/client/auth" / homeserver / endpoint
|
||||
if update_client:
|
||||
url = url.update_query({"update_client": "true"})
|
||||
if sso:
|
||||
url = url.update_query({"sso": "true"})
|
||||
req_data = {"device_name": device_name}
|
||||
else:
|
||||
req_data = {"username": username, "password": password, "device_name": device_name}
|
||||
|
||||
async with sess.post(url, json=req_data) as resp:
|
||||
if not 200 <= resp.status < 300:
|
||||
await print_error(resp, is_register=register)
|
||||
elif sso:
|
||||
await wait_sso(resp, sess, server, homeserver)
|
||||
else:
|
||||
await print_response(resp, is_register=register)
|
||||
|
||||
|
||||
async def wait_sso(
|
||||
resp: aiohttp.ClientResponse, sess: aiohttp.ClientSession, server: str, homeserver: str
|
||||
) -> None:
|
||||
data = await resp.json()
|
||||
sso_url, reg_id = data["sso_url"], data["id"]
|
||||
print(f"{Fore.GREEN}Opening {Fore.CYAN}{sso_url}{Fore.RESET}")
|
||||
webbrowser.open(sso_url, autoraise=True)
|
||||
print(f"{Fore.GREEN}Waiting for login token...{Fore.RESET}")
|
||||
wait_url = URL(server) / "_matrix/maubot/v1/client/auth" / homeserver / "sso" / reg_id / "wait"
|
||||
async with sess.post(wait_url, json={}) as resp:
|
||||
await print_response(resp, is_register=False)
|
||||
|
||||
|
||||
async def print_response(resp: aiohttp.ClientResponse, is_register: bool) -> None:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
action = "registered" if is_register else "logged in as"
|
||||
print(f"{Fore.GREEN}Successfully {action} {Fore.CYAN}{data['user_id']}{Fore.GREEN}.")
|
||||
print(f"{Fore.GREEN}Access token: {Fore.CYAN}{data['access_token']}{Fore.RESET}")
|
||||
print(f"{Fore.GREEN}Device ID: {Fore.CYAN}{data['device_id']}{Fore.RESET}")
|
||||
elif resp.status in (201, 202):
|
||||
data = await resp.json()
|
||||
action = "created" if resp.status == 201 else "updated"
|
||||
print(
|
||||
f"{Fore.GREEN}Successfully {action} client for "
|
||||
f"{Fore.CYAN}{data['id']}{Fore.GREEN} / "
|
||||
f"{Fore.CYAN}{data['device_id']}{Fore.GREEN}.{Fore.RESET}"
|
||||
)
|
||||
else:
|
||||
await print_error(resp, is_register)
|
||||
|
||||
|
||||
async def print_error(resp: aiohttp.ClientResponse, is_register: bool) -> None:
|
||||
try:
|
||||
err_data = await resp.json()
|
||||
error = friendly_errors.get(err_data["errcode"], err_data["error"])
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError, KeyError):
|
||||
error = await resp.text()
|
||||
action = "register" if is_register else "log in"
|
||||
print(f"{Fore.RED}Failed to {action}: {error}{Fore.RESET}")
|
||||
155
maubot-src/maubot/cli/commands/build.py
Normal file
155
maubot-src/maubot/cli/commands/build.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import IO
|
||||
from io import BytesIO
|
||||
import asyncio
|
||||
import glob
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from colorama import Fore
|
||||
from questionary import prompt
|
||||
from ruamel.yaml import YAML, YAMLError
|
||||
import click
|
||||
|
||||
from mautrix.types import SerializerError
|
||||
|
||||
from ...loader import PluginMeta
|
||||
from ..base import app
|
||||
from ..cliq import cliq
|
||||
from ..cliq.validators import PathValidator
|
||||
from ..config import get_token
|
||||
from .upload import upload_file
|
||||
|
||||
yaml = YAML()
|
||||
|
||||
|
||||
def zipdir(zip, dir):
|
||||
for root, dirs, files in os.walk(dir):
|
||||
for file in files:
|
||||
zip.write(os.path.join(root, file))
|
||||
|
||||
|
||||
def read_meta(path: str) -> PluginMeta | None:
|
||||
try:
|
||||
with open(os.path.join(path, "maubot.yaml")) as meta_file:
|
||||
try:
|
||||
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) + Fore.RESET)
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
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) + Fore.RESET)
|
||||
return None
|
||||
return meta
|
||||
|
||||
|
||||
def read_output_path(output: str, meta: PluginMeta) -> str | None:
|
||||
directory = os.getcwd()
|
||||
filename = f"{meta.id}-v{meta.version}.mbp"
|
||||
if not output:
|
||||
output = os.path.join(directory, filename)
|
||||
elif os.path.isdir(output):
|
||||
output = os.path.join(output, filename)
|
||||
elif os.path.exists(output):
|
||||
q = [{"type": "confirm", "name": "override", "message": f"{output} exists, override?"}]
|
||||
override = prompt(q)["override"]
|
||||
if not override:
|
||||
return None
|
||||
os.remove(output)
|
||||
return os.path.abspath(output)
|
||||
|
||||
|
||||
def write_plugin(meta: PluginMeta, output: str | IO) -> None:
|
||||
with zipfile.ZipFile(output, "w") as zip:
|
||||
meta_dump = BytesIO()
|
||||
yaml.dump(meta.serialize(), meta_dump)
|
||||
zip.writestr("maubot.yaml", meta_dump.getvalue())
|
||||
|
||||
for module in meta.modules:
|
||||
if os.path.isfile(f"{module}.py"):
|
||||
zip.write(f"{module}.py")
|
||||
elif module is not None and os.path.isdir(module):
|
||||
if os.path.isfile(f"{module}/__init__.py"):
|
||||
zipdir(zip, module)
|
||||
else:
|
||||
print(
|
||||
Fore.YELLOW
|
||||
+ f"Module {module} is missing __init__.py, skipping"
|
||||
+ Fore.RESET
|
||||
)
|
||||
else:
|
||||
print(Fore.YELLOW + f"Module {module} not found, skipping" + Fore.RESET)
|
||||
for pattern in meta.extra_files:
|
||||
for file in glob.iglob(pattern):
|
||||
zip.write(file)
|
||||
|
||||
|
||||
@cliq.with_authenticated_http
|
||||
async def upload_plugin(output: str | IO, *, server: str, sess: ClientSession) -> None:
|
||||
server, token = get_token(server)
|
||||
if not token:
|
||||
return
|
||||
if isinstance(output, str):
|
||||
with open(output, "rb") as file:
|
||||
await upload_file(sess, file, server)
|
||||
else:
|
||||
await upload_file(sess, output, server)
|
||||
|
||||
|
||||
@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."
|
||||
),
|
||||
)
|
||||
@click.argument("path", default=os.getcwd())
|
||||
@click.option(
|
||||
"-o", "--output", help="Path to output built plugin to", type=PathValidator.click_type
|
||||
)
|
||||
@click.option(
|
||||
"-u", "--upload", help="Upload plugin to server after building", is_flag=True, default=False
|
||||
)
|
||||
@click.option("-s", "--server", help="Server to upload built plugin to")
|
||||
def build(path: str, output: str, upload: bool, server: str) -> None:
|
||||
meta = read_meta(path)
|
||||
if not meta:
|
||||
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)
|
||||
if isinstance(output, str):
|
||||
print(f"{Fore.GREEN}Plugin built to {Fore.CYAN}{output}{Fore.GREEN}.{Fore.RESET}")
|
||||
else:
|
||||
output.seek(0)
|
||||
if upload:
|
||||
asyncio.run(upload_plugin(output, server=server))
|
||||
99
maubot-src/maubot/cli/commands/init.py
Normal file
99
maubot-src/maubot/cli/commands/init.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
|
||||
from jinja2 import Template
|
||||
from packaging.version import Version
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .. import cliq
|
||||
from ..cliq import SPDXValidator, VersionValidator
|
||||
from ..util import spdx
|
||||
|
||||
loaded: bool = False
|
||||
meta_template: Template
|
||||
mod_template: Template
|
||||
base_config: str
|
||||
|
||||
|
||||
def load_templates():
|
||||
global mod_template, meta_template, base_config, loaded
|
||||
if loaded:
|
||||
return
|
||||
meta_template = Template(resource_string("maubot.cli", "res/maubot.yaml.j2").decode("utf-8"))
|
||||
mod_template = Template(resource_string("maubot.cli", "res/plugin.py.j2").decode("utf-8"))
|
||||
base_config = resource_string("maubot.cli", "res/config.yaml").decode("utf-8")
|
||||
loaded = True
|
||||
|
||||
|
||||
@cliq.command(help="Initialize a new maubot plugin")
|
||||
@cliq.option(
|
||||
"-n",
|
||||
"--name",
|
||||
help="The name of the project",
|
||||
required=True,
|
||||
default=os.path.basename(os.getcwd()),
|
||||
)
|
||||
@cliq.option(
|
||||
"-i",
|
||||
"--id",
|
||||
message="ID",
|
||||
required=True,
|
||||
help="The maubot plugin ID (Java package name format)",
|
||||
)
|
||||
@cliq.option(
|
||||
"-v",
|
||||
"--version",
|
||||
help="Initial version for project (PEP-440 format)",
|
||||
default="0.1.0",
|
||||
validator=VersionValidator,
|
||||
required=True,
|
||||
)
|
||||
@cliq.option(
|
||||
"-l",
|
||||
"--license",
|
||||
validator=SPDXValidator,
|
||||
default="AGPL-3.0-or-later",
|
||||
help="The license for the project (SPDX identifier)",
|
||||
required=False,
|
||||
)
|
||||
@cliq.option(
|
||||
"-c",
|
||||
"--config",
|
||||
message="Should the plugin include a config?",
|
||||
help="Include a config in the plugin stub",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
)
|
||||
def init(name: str, id: str, version: Version, license: str, config: bool) -> None:
|
||||
load_templates()
|
||||
main_class = name[0].upper() + name[1:]
|
||||
meta = meta_template.render(
|
||||
id=id, version=str(version), license=license, config=config, main_class=main_class
|
||||
)
|
||||
with open("maubot.yaml", "w") as file:
|
||||
file.write(meta)
|
||||
if license:
|
||||
with open("LICENSE", "w") as file:
|
||||
file.write(spdx.get(license)["licenseText"])
|
||||
if not os.path.isdir(name):
|
||||
os.mkdir(name)
|
||||
mod = mod_template.render(config=config, name=main_class)
|
||||
with open(f"{name}/__init__.py", "w") as file:
|
||||
file.write(mod)
|
||||
if config:
|
||||
with open("base-config.yaml", "w") as file:
|
||||
file.write(base_config)
|
||||
77
maubot-src/maubot/cli/commands/login.py
Normal file
77
maubot-src/maubot/cli/commands/login.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
import json
|
||||
import os
|
||||
|
||||
from colorama import Fore
|
||||
from yarl import URL
|
||||
import aiohttp
|
||||
|
||||
from ..cliq import cliq
|
||||
from ..config import config, save_config
|
||||
|
||||
|
||||
@cliq.command(help="Log in to a Maubot instance")
|
||||
@cliq.option(
|
||||
"-u",
|
||||
"--username",
|
||||
help="The username of your account",
|
||||
default=os.environ.get("USER", None),
|
||||
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(
|
||||
"-a",
|
||||
"--alias",
|
||||
help="Alias to reference the server without typing the full URL",
|
||||
default="",
|
||||
required=False,
|
||||
)
|
||||
@cliq.with_http
|
||||
async def login(
|
||||
server: str, username: str, password: str, alias: str, sess: aiohttp.ClientSession
|
||||
) -> None:
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
url = URL(server) / "_matrix/maubot/v1/auth/login"
|
||||
async with sess.post(url, json=data) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
config["servers"][server] = data["token"]
|
||||
if not config["default_server"]:
|
||||
print(Fore.CYAN, "Setting", server, "as the default server")
|
||||
config["default_server"] = server
|
||||
if alias:
|
||||
config["aliases"][alias] = server
|
||||
save_config()
|
||||
print(Fore.GREEN + "Logged in successfully")
|
||||
else:
|
||||
try:
|
||||
err = (await resp.json())["error"]
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
err = await resp.text()
|
||||
print(Fore.RED + err + Fore.RESET)
|
||||
107
maubot-src/maubot/cli/commands/logs.py
Normal file
107
maubot-src/maubot/cli/commands/logs.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientSession, WSMessage, WSMsgType
|
||||
from colorama import Fore
|
||||
import click
|
||||
|
||||
from mautrix.types import Obj
|
||||
|
||||
from ..base import app
|
||||
from ..config import get_token
|
||||
|
||||
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:
|
||||
server, token = get_token(server)
|
||||
if not token:
|
||||
return
|
||||
global history_count
|
||||
history_count = tail
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(view_logs(server, token))
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
if entry.exc_info:
|
||||
print(entry.exc_info)
|
||||
|
||||
|
||||
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
|
||||
58
maubot-src/maubot/cli/commands/upload.py
Normal file
58
maubot-src/maubot/cli/commands/upload.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
|
||||
from typing import IO
|
||||
import json
|
||||
|
||||
from colorama import Fore
|
||||
from yarl import URL
|
||||
import aiohttp
|
||||
import click
|
||||
|
||||
from ..cliq import cliq
|
||||
|
||||
|
||||
class UploadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@cliq.command(help="Upload a maubot plugin")
|
||||
@click.argument("path")
|
||||
@click.option("-s", "--server", help="The maubot instance to upload the plugin to")
|
||||
@cliq.with_authenticated_http
|
||||
async def upload(path: str, server: str, sess: aiohttp.ClientSession) -> None:
|
||||
with open(path, "rb") as file:
|
||||
await upload_file(sess, file, server)
|
||||
|
||||
|
||||
async def upload_file(sess: aiohttp.ClientSession, file: IO, server: str) -> None:
|
||||
url = (URL(server) / "_matrix/maubot/v1/plugins/upload").with_query({"allow_override": "true"})
|
||||
headers = {"Content-Type": "application/zip"}
|
||||
async with sess.post(url, data=file, headers=headers) as resp:
|
||||
if resp.status in (200, 201):
|
||||
data = await resp.json()
|
||||
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}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
err = await resp.json()
|
||||
if "stacktrace" in err:
|
||||
print(err["stacktrace"])
|
||||
err = err["error"]
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError, KeyError):
|
||||
err = await resp.text()
|
||||
print(f"{Fore.RED}Failed to upload plugin: {err}{Fore.RESET}")
|
||||
Reference in New Issue
Block a user