From cc8c77e7802f81a04f71b1c7769d69b7412cdfc4 Mon Sep 17 00:00:00 2001 From: saces Date: Tue, 17 Mar 2026 18:16:21 +0100 Subject: [PATCH] more click, more api, even more fixes --- libmxclient/mxapi/login.go | 61 ++++++++++++++--- pygomx/.gitignore | 1 + pygomx/src/pygomx/__init__.py | 1 + pygomx/src/pygomx/apiv0.py | 72 ++++++++++++++++++++ pygomx/src/pygomx/client.py | 29 ++++----- pygomx/src/pygomx/cliv0.py | 72 ++++++-------------- pygomx/src/pygomx/errors.py | 39 ++++++----- pygomx/src/pygomx/simple/__init__.py | 1 + pygomx/src/pygomx/simple/client.py | 30 ++++----- pygomx/src/pygomx/util.py | 32 +++++++++ smal/.gitignore | 1 + smal/src/demobot/__init__.py | 1 + smal/src/demobot/simple/__init__.py | 1 + smal/src/pymxutils/mxutils/discoverhs.py | 22 +++---- smal/src/pymxutils/mxutils/logout.py | 32 +++++---- smal/src/pymxutils/mxutils/passitem.py | 25 ++++--- smal/src/pymxutils/mxutils/whoami.py | 4 +- smal/src/smal/smalsetup/smalsetup.py | 83 ++++++++++++++---------- 18 files changed, 318 insertions(+), 189 deletions(-) create mode 100644 pygomx/src/pygomx/apiv0.py create mode 100644 pygomx/src/pygomx/simple/__init__.py create mode 100644 pygomx/src/pygomx/util.py diff --git a/libmxclient/mxapi/login.go b/libmxclient/mxapi/login.go index f7af283..5c1dda9 100644 --- a/libmxclient/mxapi/login.go +++ b/libmxclient/mxapi/login.go @@ -4,9 +4,11 @@ package mxapi import ( "context" - "crypto/sha256" + "crypto/rand" "encoding/json" + "errors" "fmt" + "os" "time" "maunium.net/go/mautrix" @@ -14,10 +16,15 @@ import ( ) type login_data struct { - Homeserver string `json:"homeserver"` - Mxid string `json:"mxid"` - Loginname string `json:"loginname"` - Password string `json:"password"` + Homeserver string `json:"homeserver"` + Mxid string `json:"mxid"` + Loginname string `json:"loginname"` + Password string `json:"password"` + DeviceID string `json:"deviceid"` + DeviceName string `json:"devicename"` + MXPassFile string `json:"mxpassfile"` + MakeMasterKey bool `json:"make_master_key"` + MakeRecoveryKey bool `json:"make_recovery_key"` } func Login(data string) (string, error) { @@ -27,12 +34,27 @@ func Login(data string) (string, error) { return "", err } + if ld.MXPassFile != "" { + if _, err := os.Stat(ld.MXPassFile); err == nil { + return "", fmt.Errorf("mxpassfile '%s' already exists", ld.MXPassFile) + } else if !errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("error while checking mxpassfile: %v", err) + } + } + mauclient, err := mautrix.NewClient(ld.Homeserver, id.UserID(ld.Mxid), "") if err != nil { return "", err } - deviceName := fmt.Sprintf("smalbot-%d", time.Now().Unix()) + now := time.Now() + if ld.DeviceID == "" { + ld.DeviceID = fmt.Sprintf("libmxclient-%d", now.Unix()) + } + + if ld.DeviceName == "" { + ld.DeviceName = fmt.Sprintf("libmxclient-%s", now.Format(time.RFC3339)) + } resp, err := mauclient.Login(context.Background(), &mautrix.ReqLogin{ Type: "m.login.password", @@ -41,8 +63,8 @@ func Login(data string) (string, error) { User: ld.Loginname, }, Password: ld.Password, - DeviceID: id.DeviceID(deviceName), - InitialDeviceDisplayName: deviceName, + DeviceID: id.DeviceID(ld.DeviceID), + InitialDeviceDisplayName: ld.DeviceName, StoreCredentials: false, StoreHomeserverURL: false, RefreshToken: false, @@ -51,8 +73,27 @@ func Login(data string) (string, error) { return "", err } - tpl := "%s | %s | %s | %s\nrecovery | | | %x\nmaster | | | %x\n" - res := fmt.Sprintf(tpl, ld.Homeserver, ld.Loginname, id.UserID(ld.Mxid).Homeserver(), resp.AccessToken, sha256.Sum256([]byte(ld.Password)), sha256.Sum256([]byte(deviceName))) + res := fmt.Sprintf("%s | %s | %s | %s\n", ld.Homeserver, ld.Loginname, id.UserID(ld.Mxid).Homeserver(), resp.AccessToken) + + if ld.MakeMasterKey { + masterkey := make([]byte, 32) + rand.Read(masterkey) + res = fmt.Sprintf("%smaster | | | %x\n", res, masterkey) + } + + if ld.MakeRecoveryKey { + recoverykey := make([]byte, 32) + rand.Read(recoverykey) + res = fmt.Sprintf("%srecovery | | | %x\n", res, recoverykey) + } + + if ld.MXPassFile != "" { + err := os.WriteFile(ld.MXPassFile, []byte(res), 0600) + if err != nil { + return "", fmt.Errorf("unable to write file: %w", err) + } + return "SUCCESS.", nil + } return res, nil } diff --git a/pygomx/.gitignore b/pygomx/.gitignore index 3c4e140..7de8f48 100644 --- a/pygomx/.gitignore +++ b/pygomx/.gitignore @@ -6,3 +6,4 @@ build/ *.c *.o *.a +__pycache__ diff --git a/pygomx/src/pygomx/__init__.py b/pygomx/src/pygomx/__init__.py index eb259c1..82fee55 100644 --- a/pygomx/src/pygomx/__init__.py +++ b/pygomx/src/pygomx/__init__.py @@ -1 +1,2 @@ from .cliv0 import CliV0Api, CliV0 +from .client import _AsyncClient diff --git a/pygomx/src/pygomx/apiv0.py b/pygomx/src/pygomx/apiv0.py new file mode 100644 index 0000000..b68144a --- /dev/null +++ b/pygomx/src/pygomx/apiv0.py @@ -0,0 +1,72 @@ +# Copyright (C) 2026 saces@c-base.org +# SPDX-License-Identifier: AGPL-3.0-only +import logging + +from _pygomx import lib + +from .util import _stringresult, _autostring, _autodict +from .errors import CheckApiError, CheckApiErrorOnly, CheckApiResult + +logger = logging.getLogger(__name__) + + +class ApiV0Api: + """api_v0 api + some c-api wrappers & helpers + inputs: str or bytes + output: str (utf8) + """ + + @staticmethod + def discover(mxid): + return _stringresult(lib.apiv0_discover(_autostring(mxid))) + + @staticmethod + def login(data): + return _stringresult(lib.apiv0_login(_autodict(data))) + + @staticmethod + def joinedrooms(cid): + return _stringresult(lib.apiv0_joinedrooms(cid)) + + @staticmethod + def sendmessage(cid, data): + return _stringresult(lib.apiv0_sendmessage(cid, _autodict(data))) + + @staticmethod + def startclient(cid): + return _stringresult(lib.apiv0_startclient(cid)) + + @staticmethod + def stopclient(cid): + return _stringresult(lib.apiv0_stopclient(cid)) + + @staticmethod + def leaveroom(cid, roomid): + return _stringresult(lib.apiv0_leaveroom(cid, _autostring(roomid))) + + @staticmethod + def createroom(cid, data): + return _stringresult(lib.apiv0_createroom(cid, _autodict(data))) + + +class ApiV0: + """ApiV0""" + + @staticmethod + def Discover(mxid): + res = ApiV0Api.discover(mxid) + return CheckApiResult(res) + + @staticmethod + def Login(data: dict, mxpassfile: str): + withpass = False + if mxpassfile is not None and len(mxpassfile.strip()) > 0: + data["mxpassfile"] = mxpassfile + withpass = True + res = ApiV0Api.login(data) + if withpass: + CheckApiError(res) + else: + CheckApiErrorOnly(res) + return res diff --git a/pygomx/src/pygomx/client.py b/pygomx/src/pygomx/client.py index 4d79db6..a43bea3 100644 --- a/pygomx/src/pygomx/client.py +++ b/pygomx/src/pygomx/client.py @@ -1,13 +1,14 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only +import asyncio import json import logging +import threading from _pygomx import ffi, lib -from .errors import APIError, CheckApiError, CheckApiResult -import asyncio -import threading +from .apiv0 import ApiV0Api +from .errors import CheckApiError, CheckApiResult, PygomxAPIError logger = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class _AsyncClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) r = lib.apiv0_set_on_message_handler( self.client_id, on_message_callback, self._ffi_selfhandle @@ -37,7 +38,7 @@ class _AsyncClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) r = lib.apiv0_set_on_sys_handler( self.client_id, on_sys_callback, self._ffi_selfhandle @@ -45,7 +46,7 @@ class _AsyncClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) def _createMXClient(self): r = lib.apiv0_createclient_pass(b".mxpass", b".", b"*", b"*", b"*") @@ -53,7 +54,7 @@ class _AsyncClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) result_dict = json.loads(result) self.client_id = result_dict["id"] @@ -61,29 +62,27 @@ class _AsyncClient: self.DeviceID = result_dict["deviceid"] async def _sync(self): - r = lib.apiv0_startclient(self.client_id) + r = ApiV0Api.startclient(self.client_id) CheckApiError(r) def _stopsync(self): - r = lib.apiv0_stopclient(self.client_id) + r = ApiV0Api.stopclient(self.client_id) CheckApiError(r) async def _sendmessage(self, data_dict): - data = json.dumps(data_dict).encode(encoding="utf-8") - r = lib.apiv0_sendmessage(self.client_id, data) + r = ApiV0Api.sendmessage(self.client_id, data_dict) return CheckApiResult(r) def leaveroom(self, roomid): - r = lib.apiv0_leaveroom(self.client_id, roomid.encode(encoding="utf-8")) + r = ApiV0Api.leaveroom(self.client_id, roomid) CheckApiError(r) async def joinedrooms(self): - r = lib.apiv0_joinedrooms(self.client_id) + r = ApiV0Api.joinedrooms(self.client_id) return CheckApiResult(r) def _createroom(self, data_dict): - data = json.dumps(data_dict).encode(encoding="utf-8") - r = lib.apiv0_createroom(self.client_id, data) + r = ApiV0Api.createroom(self.client_id, data_dict) return CheckApiError(r) def process_event(self, evt): diff --git a/pygomx/src/pygomx/cliv0.py b/pygomx/src/pygomx/cliv0.py index d2ea72e..de52306 100644 --- a/pygomx/src/pygomx/cliv0.py +++ b/pygomx/src/pygomx/cliv0.py @@ -1,57 +1,9 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only -from _pygomx import ffi, lib -import json -from .errors import APIError +from _pygomx import lib +from .errors import CheckApiResult - -def _stringresult(cstr): - result = ffi.string(cstr) - lib.FreeCString(cstr) - return result.decode("utf-8") - - -def _autostring(xstr): - match xstr: - case bytes(): - return xstr - case str(): - return xstr.encode(encoding="utf-8") - case _: - raise TypeError("only str or bytes allowed") - - -def _autodict(xdict): - match xdict: - case bytes(): - return xdict - case str(): - return xdict.encode(encoding="utf-8") - case dict(): - return json.dumps(xdict).encode(encoding="utf-8") - case _: - raise TypeError("only str or bytes or dict allowed") - - -def CheckApiError(rstr): - if rstr.startswith("ERR:"): - raise APIError(rstr) - - if rstr == "SUCCESS.": - return None - - raise ValueError(f"unexpected result: {rstr[:20]}") - - -def CheckApiResult(rstr): - if rstr.startswith("ERR:"): - raise APIError(rstr) - - if rstr == "SUCCESS.": - return None - - result_dict = json.loads(rstr) - return result_dict +from .util import _stringresult, _autostring, _autodict class CliV0Api: @@ -86,6 +38,14 @@ class CliV0Api: ) ) + @staticmethod + def discover(domain): + return _stringresult( + lib.cliv0_discoverhs( + _autostring(domain), + ) + ) + class CliV0: """cli_v0 api class @@ -102,6 +62,16 @@ class CliV0: result_dict = CheckApiResult(res) return cls(result_dict["Matrixhost"], result_dict["Token"]) + @staticmethod + def Discover(domain): + res = CliV0Api.discover(domain) + return CheckApiResult(res) + + @staticmethod + def MXPassItem(mxpassfile, hs_url, localpart, domain): + res = CliV0Api.mxpassitem(mxpassfile, hs_url, localpart, domain) + return CheckApiResult(res) + def Whoami(self): res = CliV0Api.whoami(self.hs_url, self.token) return CheckApiResult(res) diff --git a/pygomx/src/pygomx/errors.py b/pygomx/src/pygomx/errors.py index 73c1c08..f2e311c 100644 --- a/pygomx/src/pygomx/errors.py +++ b/pygomx/src/pygomx/errors.py @@ -1,10 +1,9 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only -from _pygomx import ffi, lib import json -class APIError(Exception): +class PygomxAPIError(Exception): """Exception raised for api usage errors. Attributes: @@ -16,27 +15,27 @@ class APIError(Exception): super().__init__(self.message) -def apiResult(cstr): - result = ffi.string(cstr).decode("utf-8") - lib.FreeCString(cstr) - return result +def CheckApiErrorOnly(rstr): + if rstr.startswith("ERR:"): + raise PygomxAPIError(rstr) -def CheckApiError(cstr): - result = apiResult(cstr) +def CheckApiError(rstr): + if rstr.startswith("ERR:"): + raise PygomxAPIError(rstr) - if result.startswith("ERR:"): - raise APIError(result) - - -def CheckApiResult(cstr): - result = apiResult(cstr) - - if result.startswith("ERR:"): - raise APIError(result) - - if result == "SUCCESS.": + if rstr == "SUCCESS.": return None - result_dict = json.loads(result) + raise ValueError(f"unexpected result: {rstr[:60]}") + + +def CheckApiResult(rstr): + if rstr.startswith("ERR:"): + raise PygomxAPIError(rstr) + + if rstr == "SUCCESS.": + return None + + result_dict = json.loads(rstr) return result_dict diff --git a/pygomx/src/pygomx/simple/__init__.py b/pygomx/src/pygomx/simple/__init__.py new file mode 100644 index 0000000..35e8893 --- /dev/null +++ b/pygomx/src/pygomx/simple/__init__.py @@ -0,0 +1 @@ +from .client import _SimpleClient diff --git a/pygomx/src/pygomx/simple/client.py b/pygomx/src/pygomx/simple/client.py index 160467f..96c534d 100644 --- a/pygomx/src/pygomx/simple/client.py +++ b/pygomx/src/pygomx/simple/client.py @@ -5,7 +5,8 @@ import logging from _pygomx import ffi, lib -from .errors import APIError, CheckApiError +from ..apiv0 import ApiV0Api +from ..errors import CheckApiError, CheckApiResult, PygomxAPIError logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class _SimpleClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) r = lib.apiv0_set_on_message_handler( self.client_id, on_message_callback, self._ffi_selfhandle @@ -35,7 +36,7 @@ class _SimpleClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) r = lib.apiv0_set_on_sys_handler( self.client_id, on_sys_callback, self._ffi_selfhandle @@ -43,7 +44,7 @@ class _SimpleClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) def _createMXClient(self): r = lib.apiv0_createclient_pass(b".mxpass", b".", b"*", b"*", b"*") @@ -51,7 +52,7 @@ class _SimpleClient: result = ffi.string(r) lib.FreeCString(r) if result.startswith(b"ERR:"): - raise APIError(result) + raise PygomxAPIError(result) result_dict = json.loads(result) self.client_id = result_dict["id"] @@ -59,30 +60,27 @@ class _SimpleClient: self.DeviceID = result_dict["deviceid"] def _sync(self): - r = lib.apiv0_startclient(self.client_id) + r = ApiV0Api.startclient(self.client_id) CheckApiError(r) def _stopsync(self): - r = lib.apiv0_stopclient(self.client_id) + r = ApiV0Api.stopclient(self.client_id) CheckApiError(r) def _sendmessage(self, data_dict): - data = json.dumps(data_dict).encode(encoding="utf-8") - r = lib.apiv0_sendmessage(self.client_id, data) - result = CheckApiError(r) - return result + r = ApiV0Api.sendmessage(self.client_id, data_dict) + return CheckApiResult(r) def leaveroom(self, roomid): - r = lib.apiv0_leaveroom(self.client_id, roomid.encode(encoding="utf-8")) + r = ApiV0Api.leaveroom(self.client_id, roomid) CheckApiError(r) def joinedrooms(self): - r = lib.apiv0_joinedrooms(self.client_id) - return CheckApiError(r) + r = ApiV0Api.joinedrooms(self.client_id) + return CheckApiResult(r) def _createroom(self, data_dict): - data = json.dumps(data_dict).encode(encoding="utf-8") - r = lib.apiv0_createroom(self.client_id, data) + r = ApiV0Api.createroom(self.client_id, data_dict) return CheckApiError(r) def process_event(self, evt): diff --git a/pygomx/src/pygomx/util.py b/pygomx/src/pygomx/util.py new file mode 100644 index 0000000..92369fd --- /dev/null +++ b/pygomx/src/pygomx/util.py @@ -0,0 +1,32 @@ +# Copyright (C) 2026 saces@c-base.org +# SPDX-License-Identifier: AGPL-3.0-only +from _pygomx import ffi, lib +import json + + +def _stringresult(cstr): + result = ffi.string(cstr) + lib.FreeCString(cstr) + return result.decode("utf-8") + + +def _autostring(xstr): + match xstr: + case bytes(): + return xstr + case str(): + return xstr.encode(encoding="utf-8") + case _: + raise TypeError("only str or bytes allowed") + + +def _autodict(xdict): + match xdict: + case bytes(): + return xdict + case str(): + return xdict.encode(encoding="utf-8") + case dict(): + return json.dumps(xdict).encode(encoding="utf-8") + case _: + raise TypeError("only str or bytes or dict allowed") diff --git a/smal/.gitignore b/smal/.gitignore index 3fafd07..d7eadc0 100644 --- a/smal/.gitignore +++ b/smal/.gitignore @@ -1,2 +1,3 @@ __pycache__ *.egg-info +build diff --git a/smal/src/demobot/__init__.py b/smal/src/demobot/__init__.py index e69de29..8cf9cb3 100644 --- a/smal/src/demobot/__init__.py +++ b/smal/src/demobot/__init__.py @@ -0,0 +1 @@ +from .demobot import main diff --git a/smal/src/demobot/simple/__init__.py b/smal/src/demobot/simple/__init__.py index e69de29..8cf9cb3 100644 --- a/smal/src/demobot/simple/__init__.py +++ b/smal/src/demobot/simple/__init__.py @@ -0,0 +1 @@ +from .demobot import main diff --git a/smal/src/pymxutils/mxutils/discoverhs.py b/smal/src/pymxutils/mxutils/discoverhs.py index 275b0b0..85b9724 100644 --- a/smal/src/pymxutils/mxutils/discoverhs.py +++ b/smal/src/pymxutils/mxutils/discoverhs.py @@ -1,9 +1,11 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only -import sys -from _pygomx import lib, ffi import click -import json +from pygomx.errors import PygomxAPIError + +from pygomx import CliV0 + +from .click import click_catch_exception @click.command() @@ -11,18 +13,12 @@ import json "--json", "show_json", is_flag=True, help="show json as returned from server." ) @click.argument("domain", metavar="string") +@click_catch_exception(handle=(PygomxAPIError)) def discoverhs(domain, show_json): """Attempts to discover the homeserver from the given string""" - mxid = domain.encode(encoding="utf-8") + result = CliV0.Discover(domain) - r = lib.cliv0_discoverhs(mxid) - result = ffi.string(r).decode("utf-8") - lib.FreeCString(r) - if result.startswith("ERR:"): - print(result) - sys.exit(1) if show_json: - print(result) + click.echo(result) else: - result_dict = json.loads(result) - print(result_dict["m.homeserver"]["base_url"]) + click.echo(result["m.homeserver"]["base_url"]) diff --git a/smal/src/pymxutils/mxutils/logout.py b/smal/src/pymxutils/mxutils/logout.py index 22a0896..b9c91ce 100644 --- a/smal/src/pymxutils/mxutils/logout.py +++ b/smal/src/pymxutils/mxutils/logout.py @@ -4,9 +4,12 @@ import getpass from datetime import datetime import click +from pygomx.errors import PygomxAPIError from pygomx import CliV0 +from .click import click_catch_exception + @click.command() @click.option("-u", "--url", "hs_url", metavar="url", help="homeserver url") @@ -22,17 +25,18 @@ from pygomx import CliV0 type=click.Choice(["all", "other", "self"]), help="logout devices", ) +@click_catch_exception(handle=(PygomxAPIError)) def logout(hs_url, token, devices, logout_type, show_json): """List or logout devices. \b mxlogout [--json] list all devices - mxlogout --all + mxlogout --logout all logout all devices - mxlogout --self + mxlogout --logout self logout this device - mxlogout --other + mxlogout --logout other logout all other devices (requires auth) mxlogout deviceid [deviceid]... logout given devices (requires auth) @@ -79,7 +83,7 @@ def logout(hs_url, token, devices, logout_type, show_json): return if show_json: - print(raw_device_dict) + click.echo(raw_device_dict) return max_len = 0 @@ -88,12 +92,16 @@ def logout(hs_url, token, devices, logout_type, show_json): for device in raw_device_dict["devices"]: date_object = datetime.fromtimestamp(device["last_seen_ts"] / 1000) - print( - device["device_id"], - " " * (max_len - len(device["device_id"])), - date_object, - device["last_seen_ip"], - device["display_name"], + click.echo( + " ".join( + [ + device["device_id"], + " " * (max_len - len(device["device_id"])), + str(date_object), + device["last_seen_ip"], + device["display_name"], + ] + ) ) date_object = datetime.fromtimestamp(device["last_seen_ts"] / 1000) @@ -106,7 +114,7 @@ def do_logout(cli, all): if all: reqData["path"] += ["all"] res = cli.Generic(reqData) - print(res) + click.echo(res) def do_logout_devices(cli, devices, user_id): @@ -126,4 +134,4 @@ def do_logout_devices(cli, devices, user_id): }, } res = cli.Generic(reqData) - print(res) + click.echo(res) diff --git a/smal/src/pymxutils/mxutils/passitem.py b/smal/src/pymxutils/mxutils/passitem.py index 91a2c1f..be2199d 100644 --- a/smal/src/pymxutils/mxutils/passitem.py +++ b/smal/src/pymxutils/mxutils/passitem.py @@ -1,9 +1,14 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only -from _pygomx import lib, ffi -import click import json +import click +from pygomx.errors import PygomxAPIError + +from pygomx import CliV0 + +from .click import click_catch_exception + @click.command() @click.option( @@ -15,6 +20,7 @@ import json ) @click.option("-d", "--domain", "domain", metavar="domain", help="domain selector") @click.argument("mxpassfile", metavar="mxpassfilepath", required=False) +@click_catch_exception(handle=(PygomxAPIError)) def passitem(mxpassfile, show_secret, hs_url, localpart, domain): """utility to get items from mxpasss files""" @@ -28,19 +34,10 @@ def passitem(mxpassfile, show_secret, hs_url, localpart, domain): if domain is None: domain = "*" - r = lib.cliv0_mxpassitem( - mxpassfile.encode(encoding="utf-8"), - hs_url.encode(encoding="utf-8"), - localpart.encode(encoding="utf-8"), - domain.encode(encoding="utf-8"), - ) - result = ffi.string(r).decode("utf-8") - lib.FreeCString(r) - - result_dict = json.loads(result) + result_dict = CliV0.MXPassItem(mxpassfile, hs_url, localpart, domain) if show_secret: - print(result_dict["Token"]) + click.echo(result_dict["Token"]) else: result_dict["Token"] = "***" - print(json.dumps(result_dict)) + click.echo(json.dumps(result_dict)) diff --git a/smal/src/pymxutils/mxutils/whoami.py b/smal/src/pymxutils/mxutils/whoami.py index 8b55e45..23e5213 100644 --- a/smal/src/pymxutils/mxutils/whoami.py +++ b/smal/src/pymxutils/mxutils/whoami.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-only import click from pygomx import CliV0 -import pygomx +from pygomx.errors import PygomxAPIError from .click import click_catch_exception @@ -10,7 +10,7 @@ from .click import click_catch_exception @click.command() @click.option("-u", "--url", "hs_url", metavar="url", help="homeserver url") @click.option("-t", "--token", "token", metavar="token", help="access token") -@click_catch_exception(handle=(pygomx.errors.APIError)) +@click_catch_exception(handle=(PygomxAPIError)) def whoami(hs_url, token): """this token belongs to?""" diff --git a/smal/src/smal/smalsetup/smalsetup.py b/smal/src/smal/smalsetup/smalsetup.py index 7f3431a..c11cafd 100644 --- a/smal/src/smal/smalsetup/smalsetup.py +++ b/smal/src/smal/smalsetup/smalsetup.py @@ -1,49 +1,60 @@ # Copyright (C) 2026 saces@c-base.org # SPDX-License-Identifier: AGPL-3.0-only -import sys -import os +import datetime import getpass -import json -from _pygomx import lib, ffi +import os +import time +from functools import partial, wraps + +import click +from pygomx.errors import PygomxAPIError + +from pygomx import ApiV0 -def smalsetup(): - if len(sys.argv) != 2: - print("usage: ", sys.argv[0], " matrixid") - return 1 +def catch_exception(func=None, *, handle): + if not func: + return partial(catch_exception, handle=handle) - mxid = sys.argv[1].encode(encoding="utf-8") + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except handle as e: + raise click.ClickException(e) - r = lib.apiv0_discover(mxid) - result = ffi.string(r).decode("utf-8") - lib.FreeCString(r) + return wrapper - if result.startswith("ERR:"): - print(result) - return 1 - result_dict = json.loads(result) +@click.command() +@click.option( + "--mxpass", + "mxpassfile", + metavar="filepath", + default=".mxpass", + help="mxpass file name", +) +@click.argument("mxid", metavar="MatrixID") +@catch_exception(handle=(PygomxAPIError)) +def smalsetup(mxid, mxpassfile): + """Utility for creating smalbot mxpass files""" + + create_mxpass = len(mxpassfile.strip()) > 0 + + if create_mxpass: + if os.path.exists(mxpassfile): + raise click.ClickException(f"file {mxpassfile} exists.") + + result_dict = ApiV0.Discover(mxid) + result_dict["password"] = getpass.getpass(prompt="Password: ") - data = json.dumps(result_dict).encode(encoding="utf-8") + result_dict["make_master_key"] = True + result_dict["make_recovery_key"] = True - r = lib.apiv0_login(data) - result = ffi.string(r).decode("utf-8") - lib.FreeCString(r) + now = int(time.time()) + result_dict["deviceid"] = f"smalbot-{now}" + result_dict["devicename"] = f"smalbot-{datetime.fromtimestamp(now)}" - if result.startswith("ERR:"): - print(result) - return 1 + ApiV0.Login(result_dict, ".mxpass") - # Set restrictive umask (owner only) - new_umask = 0o077 - old_umask = os.umask(new_umask) - - # Create file with new umask - with open(".mxpass", "w") as f: - f.write(result) - - # Restore original umask - os.umask(old_umask) - - print("login created. start your bot now.") - return 0 + click.echo("login created. start your bot now.")