From 8bec5082992ea63660bac1b338677d5d24b91aba Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Wed, 16 Sep 2026 08:54:43 +0300 Subject: [PATCH 01/11] account: Set build folders --- pyproject.toml | 2 +- status_sdk/account.py | 50 +++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 11e0516..960ad90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "status-sdk" -version = "1.1.6" +version = "1.2.0" description = "Private chat. Communities. Multi-chain wallet. Browser. dApps all in one app, powered by SNT." readme = "README.md" requires-python = ">=3.11" diff --git a/status_sdk/account.py b/status_sdk/account.py index c2783d6..fff0de6 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -57,23 +57,7 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po # Wallet transactions self.__alchemy_token = None self.__transactions: Optional[pd.DataFrame] = None - # Path of the account data in the Docker container for Status Backend - self.__docker_data_folder = "./data" - # Path of the backups in the Docker container for Status Backend - self.__docker_backup_folder = "./root/.config/Status/backups" - self.__backup_folder = backup_folder - # PyPI installation folder - sdk_folder = volume_folder if volume_folder else os.path.dirname(__file__) - # As the docker-compose.yaml folder is at the moment - self.__backup_sdk_folder = os.path.join(sdk_folder, "backups") - os.makedirs(self.__backup_sdk_folder, exist_ok=True) - # Path of where images will be uploaded to Status Backend - self.__docker_asset_folder = "./assets" - # As the docker-compose.yaml folder is at the moment - # NOTE: This might change for initial release - self.__assets_local_folder = os.path.join(sdk_folder, "assets") - os.makedirs(self.__assets_local_folder, exist_ok=True) self.__logger = logging.getLogger(__name__) self.__timestamp_divisor = 1_000 self.__kd_iterations = 256000 @@ -95,6 +79,7 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po self.__ws_base_url = f"ws://{domain}:{backend_port}/" self.__urls = { "http": { + "health": f"{self.__http_base_url.replace('statusgo/', '')}health", "initialize": f"{self.__http_base_url}InitializeApplication", "login": f"{self.__http_base_url}LoginAccount", "create": f"{self.__http_base_url}CreateAccountAndLogin", @@ -112,6 +97,34 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po "signals": f"{self.__ws_base_url}signals" } } + self.__is_docker = not bool(requests.get(self.__urls["http"]["health"]).json()) + + base_folder = os.path.dirname(__file__) + if not self.__is_docker: + os.makedirs(base_folder, exist_ok=True) + + # Path of the account data in the Docker container / build for Status Backend + self.__docker_data_folder = "./data" if self.__is_docker else os.path.join(base_folder, "data") + if not self.__is_docker: + os.makedirs(self.__docker_data_folder, exist_ok=True) + + # Path of the backups in the Docker container / build for Status Backend + self.__backup_folder = backup_folder + # PyPI installation folder + sdk_folder = volume_folder if volume_folder and self.__is_docker else base_folder + # As the docker-compose.yaml folder is at the moment + self.__backup_sdk_folder = os.path.join(sdk_folder, "backups") + os.makedirs(self.__backup_sdk_folder, exist_ok=True) + + # Path of where images will be uploaded to Status Backend + self.__docker_asset_folder = "./assets" if self.__is_docker else os.path.join(base_folder, "assets") + if not self.__is_docker: + os.makedirs(self.__docker_asset_folder, exist_ok=True) + # As the docker-compose.yaml folder is at the moment + # NOTE: This might change for initial release + self.__assets_local_folder = os.path.join(sdk_folder, "assets") + os.makedirs(self.__assets_local_folder, exist_ok=True) + self.__status = "on" self.__media_port = media_port self.__signal = Signal(self.__urls["socket"]["signals"]) @@ -385,7 +398,7 @@ def profile_picture(self, file_path: str): extension = file_name.split(".")[-1] asset_file_name = f"profile.{extension}" asset_file_path = os.path.join(self.__assets_local_folder, asset_file_name) - docker_file_path = self.__docker_asset_folder + "/" + asset_file_name + docker_file_path = os.path.join(self.__docker_asset_folder, asset_file_name) for file_name in os.listdir(self.__assets_local_folder): current_file_path = os.path.join(self.__assets_local_folder, file_name) if not os.path.isfile(current_file_path) or current_file_path.lower() == file_path.lower(): @@ -1765,8 +1778,9 @@ def _load_backup(self): if sdk_file_path != file_path: shutil.copy(file_path, sdk_file_path) + backup_folder = self._call_rpc("settings", "backupPath", []).get("result") params = { - "filePath": os.path.join(self.__docker_backup_folder, file_name).replace("\\", "/") + "filePath": f"{backup_folder}/{file_name}" if self.__is_docker else os.path.join(backup_folder, file_name) } self.logger.info(f"Loading backup file: {file_path}") response = requests.post(self.__urls["http"]["load_backup"], json=params) From cfac00dae2ae951e8e01974de258cf083727cb49 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Wed, 16 Sep 2026 12:45:53 +0300 Subject: [PATCH 02/11] bug: No profile picture set --- status_sdk/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/status_sdk/account.py b/status_sdk/account.py index fff0de6..e919764 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -372,7 +372,8 @@ def profile_picture(self) -> Optional[Union[JpegImageFile, PngImageFile]]: Get current profile picture """ identity_images = self._call_rpc("identity", "getIdentityImages", [self.info["key_uid"]]) - latest = max(identity_images.get("result", []), key=lambda item: item["clock"], default=None) + result: list[dict] = identity_images.get("result", []) or [] + latest = max(result, key=lambda item: item["clock"], default=None) if not latest: return None latest_url = latest["localUrl"] From 900dc82fd6cef97e868d3b7cf0c746689563169b Mon Sep 17 00:00:00 2001 From: Andrey Bocharnikov Date: Wed, 16 Sep 2026 17:28:01 +0400 Subject: [PATCH 03/11] docker: report the built status-go ref in /health and fix the healthcheck status-go's Docker build context has no .git, so its embedded version is empty and /health answers {}. Pass the ref being built through the existing build_flags build arg as an -ldflags -X override, so the container reports what it was built from without any status-go change. The healthcheck passed the whole command as one argv entry, so Docker looked for an executable literally named "curl http://0.0.0.0:8080/health" and the container never became healthy. --- status_sdk/docker-compose.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/status_sdk/docker-compose.yaml b/status_sdk/docker-compose.yaml index 79b6b21..b9713de 100644 --- a/status_sdk/docker-compose.yaml +++ b/status_sdk/docker-compose.yaml @@ -2,6 +2,9 @@ services: backend: build: context: https://github.com/status-im/status-go.git#${STATUS_GO_COMMIT:-develop} + args: + # The build context has no .git, so the embedded version is empty; link the ref being built in instead + build_flags: -ldflags=-X=github.com/status-im/status-go/pkg/version.version=${STATUS_GO_COMMIT:-develop} platform: ${PLATFORM:-linux/amd64} container_name: status-backend ports: @@ -20,7 +23,7 @@ services: networks: - status-bridge healthcheck: - test: ["CMD", "curl http://0.0.0.0:8080/health"] + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"] networks: status-bridge: From 7ee8ef59757a5f137591dc50934acb14b09b7c3e Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Wed, 16 Sep 2026 19:17:02 +0300 Subject: [PATCH 04/11] docker: Check if backend is running in docker or binary --- status_sdk/account.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/status_sdk/account.py b/status_sdk/account.py index e919764..daceff0 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -97,7 +97,13 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po "signals": f"{self.__ws_base_url}signals" } } - self.__is_docker = not bool(requests.get(self.__urls["http"]["health"]).json()) + + health_info: dict = requests.get(self.__urls["http"]["health"]).json() + version_regex = re.compile( + r'^v(?P\d+)\.(?P\d+)\.(?P\d+)' + r'(?:-(?P\d+)-g(?P[0-9a-f]{7,40}))?$' + ) + self.__is_docker = not bool(version_regex.match(health_info.get("version", ""))) base_folder = os.path.dirname(__file__) if not self.__is_docker: From f4c97c64c8b70a8f6f494a39690d18d8f95bd084 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Wed, 16 Sep 2026 20:53:43 +0300 Subject: [PATCH 05/11] docker: Create build --- docs/utils.md | 62 ++++++++++++++++++++++ status_sdk/__init__.py | 2 +- status_sdk/exceptions.py | 3 ++ status_sdk/utils/__init__.py | 2 +- status_sdk/utils/external.py | 99 +++++++++++++++++++++++++++++++++++- 5 files changed, 165 insertions(+), 3 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index a47fb15..6be08cf 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -96,6 +96,68 @@ sudo chown -R $USER:$USER /path/to/status_sdk sudo chmod -R a+rw /path/to/status_sdk ``` +### `build_and_launch(commit=None, repo_dir=None, address="localhost:8080", wait_seconds=30, install_deps=True)` + +Build `status-backend` **natively** from a local clone of [`status-im/status-go`](https://github.com/status-im/status-go), then run it as a normal process on your machine. This is the **alternative to [`launch_docker_container`](./utils.md#launch_docker_containercommitnone-wait_seconds5-platformlinuxamd64-data_foldernone)** for setups without Docker. Everything else in the SDK works the same afterwards - [`Account`](./account.md) talks to the backend over HTTP either way. + +| | [`launch_docker_container`](./utils.md#launch_docker_containercommitnone-wait_seconds5-platformlinuxamd64-data_foldernone) | `build_and_launch` | +|---|---|---| +| Needs | Docker (and WSL on Windows) | Nix and `git` | +| Windows | Supported | **Not supported** - use Docker, or run from inside WSL | +| Backend runs as | A container | A process on your machine | +| Stopping it | `docker compose down` | Terminate the returned process | + +The build runs inside the repository's **Nix dev shell**, so the Go toolchain and every build dependency come from Nix rather than your system - nothing has to be installed by hand beyond Nix itself. + +| Name | Type | Required | Description | +|-----|-----|-----|-------------| +| `commit` | `str` | No | The `status-im/status-go` git commit SHA,. When omitted, the latest `develop` branch is built. | +| `repo_dir` | `str` | No | Local folder to clone `status-go` into, and reuse on later calls. Defaults to a `status-go` folder next to this package's installation. When the folder does not already hold a clone, the repository is fetched from GitHub. | +| `address` | `str` | No | The `host:port` to run `status-backend` on. Defaults to `localhost:8080`, which matches the defaults of [`Account`](./account.md#accountdomainlocalhost-backend_port8080-media_port9000-is_securefalse-backup_foldernone-volume_foldernone). If you change it, pass the matching `domain` and `backend_port` when creating the `Account`. | +| `wait_seconds` | `int` | No | How long to poll the backend's `/health` endpoint before giving up, in seconds. Defaults to `30`. **The build itself is not subject to this timeout** - only the startup that follows it. | +| `install_deps` | `bool` | No | Whether to run `make status-go-deps` before building. This also runs `go clean -cache` and `go clean -modcache`, which throws away every cached Go build on your machine and makes the next build much slower. Only needed on a first build or after a Go toolchain upgrade. | + +Returns `subprocess.Popen` - the handle of the running `status-backend` process, so you can terminate it when you are done. + +```python +from status_sdk import build_and_launch, Account + +# First run clones status-go and builds it - this takes a while +process = build_and_launch() + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +print(account.info["public_key"]) + +# Stop the backend when you are done +process.terminate() +``` + +Build a specific ref into a folder of your choosing: + +```python +from status_sdk import build_and_launch + +process = build_and_launch( + commit="2bee8b6a38cdc8f92d74e2dbb8c4e77fbbeea149", + repo_dir="/home/thedatabro/src/status-go" +) +``` + +Run on a different port, and point the `Account` at it: + +```python +from status_sdk import build_and_launch, Account + +process = build_and_launch(address="localhost:9500", wait_seconds=60) +account = Account(backend_port=9500) +``` + ## Properties ### `__version__` diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index fecd7b3..1bf1a3e 100644 --- a/status_sdk/__init__.py +++ b/status_sdk/__init__.py @@ -3,7 +3,7 @@ from .account import Account from .group_chat import GroupChat from .community.base import Community, Channel -from .utils import launch_docker_container +from .utils import launch_docker_container, build_and_launch from . import exceptions, models try: diff --git a/status_sdk/exceptions.py b/status_sdk/exceptions.py index aaa68f0..3a32d26 100644 --- a/status_sdk/exceptions.py +++ b/status_sdk/exceptions.py @@ -133,6 +133,9 @@ class ProfilePictureError(Exception): class DockerError(Exception): pass +class BuildError(Exception): + pass + class SignalError(Exception): pass diff --git a/status_sdk/utils/__init__.py b/status_sdk/utils/__init__.py index 17a6a7e..18bed37 100644 --- a/status_sdk/utils/__init__.py +++ b/status_sdk/utils/__init__.py @@ -1 +1 @@ -from .external import launch_docker_container +from .external import launch_docker_container, build_and_launch diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index 41aacbf..9385acc 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -1,7 +1,7 @@ """ Used outside of `status_sdk` """ -import shutil, os, subprocess, sys, time, yaml, logging +import shutil, os, subprocess, sys, time, yaml, logging, requests from pathlib import Path from typing import Optional from .. import exceptions @@ -95,3 +95,100 @@ def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, logger.info(f"Docker Container successfully launched! Sleeping for {wait_seconds}s") time.sleep(wait_seconds) + +def build_and_launch(commit: Optional[str] = None, repo_dir: Optional[str] = None, address: str = "localhost:8080", wait_seconds: int = 30, install_deps: bool = True) -> subprocess.Popen: + """ + This is an alternative to `launch_docker_container` for setups that do not use Docker or + would like to use `status-im/status-go` outside of a Docker container. This will build a + native clone of `status-im/status-go` inside its Nix dev shell and launch it. + + NOTE: Requires the Nix package manager (https://nixos.org/download) and `git` to be installed. + + Parameters: + - `commit` - the `status-go` commit SHA, branch or tag to build. If no commit is provided, the latest `develop` branch is built. + - `repo_dir` - local folder to clone `status-go` into (and reuse on subsequent calls). Defaults to a `status-go` folder next to this package's installation. If the repo is not found then it will be fetched from GitHub + - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. + - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting. Building itself is not subject to this timeout. + - `install_deps` - whether to run `make status-go-deps` before building. This also runs `go clean -cache` / `go clean -modcache`, so it is skipped by default - only needed on the first build, or after a Go toolchain upgrade. + + Output: + - `subprocess.Popen` handle of the running `status-backend` process, so it can be terminated later. + """ + logger = logging.getLogger(__name__) + + def run(cmd: list[str], cwd: Optional[str] = None) -> subprocess.CompletedProcess: + logger.info(f"Running:\n{' '.join(cmd)}") + return subprocess.run(cmd, cwd=cwd, stderr=subprocess.PIPE, text=True) + + system = sys.platform + + if system == "win32": + raise exceptions.BuildError("build_and_launch requires Nix and is not supported on Windows. Use launch_docker_container instead, or run this from within WSL.") + + if not shutil.which("nix"): + raise exceptions.BuildError("Please install Nix - https://nixos.org/download.") + + if not shutil.which("git"): + raise exceptions.BuildError("Please install git.") + + repo_dir = repo_dir or os.path.join(os.path.dirname(os.path.dirname(__file__)), "status-go") + ref = commit if commit else "develop" + + if not os.path.isdir(os.path.join(repo_dir, ".git")): + result = run(["git", "clone", "https://github.com/status-im/status-go.git", repo_dir]) + else: + result = run(["git", "fetch", "origin"], cwd=repo_dir) + + if result.returncode != 0: + raise exceptions.BuildError(result.stderr.strip()) + + result = run(["git", "checkout", ref], cwd=repo_dir) + if result.returncode != 0: + result = run(["git", "checkout", "-B", ref, f"origin/{ref}"], cwd=repo_dir) + if result.returncode != 0: + raise exceptions.BuildError(result.stderr.strip()) + elif ref == "develop": + result = run(["git", "merge", "--ff-only", "origin/develop"], cwd=repo_dir) + if result.returncode != 0: + raise exceptions.BuildError(result.stderr.strip()) + + commands = [ + "make status-go-deps", + "make generate", + "make status-backend" + ] + if not install_deps: + commands = commands[1:] + + make_targets = " && ".join(commands) + logger.info("Building status-backend inside the Nix dev shell - this can take a while...") + result = run(["nix", "--extra-experimental-features", "nix-command flakes", "develop", "--command", "bash", "-c", make_targets], cwd=repo_dir) + if result.returncode != 0: + raise exceptions.BuildError(result.stderr.strip()) + + binary_path = os.path.join(repo_dir, "build", "bin", "status-backend") + if not os.path.isfile(binary_path): + raise exceptions.BuildError(f"Build finished but {binary_path} was not found.") + + logger.info(f"Launching status-backend on {address}...") + log_path = os.path.join(repo_dir, "status-backend.log") + with open(log_path, "w") as log_file: + process = subprocess.Popen([binary_path, f"-address={address}"], cwd=repo_dir, stdout=log_file, stderr=subprocess.STDOUT) + + health_url = f"http://{address}/health" + deadline = time.time() + wait_seconds + last_error = None + while time.time() < deadline: + if process.poll() is not None: + raise exceptions.BuildError(f"status-backend exited early with code {process.returncode}. See {log_path} for details.") + try: + response = requests.get(health_url, timeout=1) + if response.ok: + logger.info("status-backend is up!") + return process + except requests.exceptions.RequestException as error: + last_error = error + time.sleep(0.5) + + process.terminate() + raise exceptions.BuildError(f"status-backend did not become reachable at {health_url} within {wait_seconds}s ({last_error}).") From 52ea96a09e2d87f2b332d949aed8e7e79663435f Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Thu, 17 Sep 2026 14:20:12 +0300 Subject: [PATCH 06/11] docker: Download build --- .gitignore | 2 + docs/utils.md | 52 +++++++++++++-- status_sdk/__init__.py | 2 +- status_sdk/utils/__init__.py | 2 +- status_sdk/utils/external.py | 126 ++++++++++++++++++++++++++++++++--- 5 files changed, 167 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 16d6f54..ed5ef66 100644 --- a/.gitignore +++ b/.gitignore @@ -217,3 +217,5 @@ __marimo__/ # Docker volumes for Status data/ *.bkp +# Status Backend builds +status-backend-bundle/ diff --git a/docs/utils.md b/docs/utils.md index 6be08cf..1bc48d4 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -117,8 +117,6 @@ The build runs inside the repository's **Nix dev shell**, so the Go toolchain an | `wait_seconds` | `int` | No | How long to poll the backend's `/health` endpoint before giving up, in seconds. Defaults to `30`. **The build itself is not subject to this timeout** - only the startup that follows it. | | `install_deps` | `bool` | No | Whether to run `make status-go-deps` before building. This also runs `go clean -cache` and `go clean -modcache`, which throws away every cached Go build on your machine and makes the next build much slower. Only needed on a first build or after a Go toolchain upgrade. | -Returns `subprocess.Popen` - the handle of the running `status-backend` process, so you can terminate it when you are done. - ```python from status_sdk import build_and_launch, Account @@ -143,7 +141,7 @@ Build a specific ref into a folder of your choosing: ```python from status_sdk import build_and_launch -process = build_and_launch( +build_and_launch( commit="2bee8b6a38cdc8f92d74e2dbb8c4e77fbbeea149", repo_dir="/home/thedatabro/src/status-go" ) @@ -154,10 +152,56 @@ Run on a different port, and point the `Account` at it: ```python from status_sdk import build_and_launch, Account -process = build_and_launch(address="localhost:9500", wait_seconds=60) +build_and_launch(address="localhost:9500", wait_seconds=60) account = Account(backend_port=9500) ``` +### `download_build_and_launch(file_name=None, repo_name="status-im/status-go", tag=None, token=None, address="localhost:8080", wait_seconds=30)` + +Download a prebuilt `status-backend` bundle from a GitHub release and launch it. This is an **alternative to [`build_and_launch`](./utils.md#build_and_launchcommitnone-repo_dirnone-addresslocalhost8080-wait_seconds30-install_depstrue)** for setups that do not want to build [`status-im/status-go`](https://github.com/status-im/status-go) from source. + +| Name | Type | Required | Description | +|-----|-----|-----|-------------| +| `file_name` | `str` | No | The release asset to download, exactly as it appears on the release page. When omitted, the asset matching this machine is picked using the pattern `status-backend_{platform}_{arch}_{version}`, e.g. `status-backend_linux_x86_64_2.34.0`. | +| `repo_name` | `str` | No | The `owner/name` of the repository to pull the release from. Defaults to `status-im/status-go`. | +| `tag` | `str` | No | A release tag. When omitted, the latest release is used. | +| `token` | `str` | No | A GitHub token. Required for private repositories. | +| `address` | `str` | No | The `host:port` to run `status-backend` on. Defaults to `localhost:8080`, which matches the defaults of [`Account`](./account.md#accountdomainlocalhost-backend_port8080-media_port9000-is_securefalse-backup_foldernone-volume_foldernone). If you change it, pass the matching `domain` and `backend_port` when creating the `Account`. | +| `wait_seconds` | `int` | No | How long to poll the backend's `/health` endpoint before giving up, in seconds. Defaults to `30`. **Downloading itself is not subject to this timeout** - only the startup that follows it. | + +The bundle is extracted into a `status-backend-bundle` folder in the current working directory, and reused on later calls instead of downloading again. + +```python +from status_sdk import download_build_and_launch, Account + +# Downloads the latest release's asset matching this machine +download_build_and_launch() + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +print(account.info["public_key"]) + +# Stop the backend when you are done +process.terminate() +``` + +Download a specific release tag from a private repository: + +```python +from status_sdk import download_build_and_launch + +download_build_and_launch( + repo_name="status-im/status-go", + tag="v2.34.0", + token="ghp_xxxxxxxxxxxxxxxxxxxx" +) +``` + ## Properties ### `__version__` diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index 1bf1a3e..35f633d 100644 --- a/status_sdk/__init__.py +++ b/status_sdk/__init__.py @@ -3,7 +3,7 @@ from .account import Account from .group_chat import GroupChat from .community.base import Community, Channel -from .utils import launch_docker_container, build_and_launch +from .utils import launch_docker_container, build_and_launch, download_build_and_launch from . import exceptions, models try: diff --git a/status_sdk/utils/__init__.py b/status_sdk/utils/__init__.py index 18bed37..0cc89bb 100644 --- a/status_sdk/utils/__init__.py +++ b/status_sdk/utils/__init__.py @@ -1 +1 @@ -from .external import launch_docker_container, build_and_launch +from .external import launch_docker_container, build_and_launch, download_build_and_launch diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index 9385acc..d89f9c0 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -1,8 +1,9 @@ """ Used outside of `status_sdk` """ -import shutil, os, subprocess, sys, time, yaml, logging, requests +import shutil, os, subprocess, sys, time, yaml, logging, requests, stat, tarfile from pathlib import Path +from platform import machine from typing import Optional from .. import exceptions @@ -96,7 +97,7 @@ def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, logger.info(f"Docker Container successfully launched! Sleeping for {wait_seconds}s") time.sleep(wait_seconds) -def build_and_launch(commit: Optional[str] = None, repo_dir: Optional[str] = None, address: str = "localhost:8080", wait_seconds: int = 30, install_deps: bool = True) -> subprocess.Popen: +def build_and_launch(commit: Optional[str] = None, repo_dir: Optional[str] = None, address: str = "localhost:8080", wait_seconds: int = 30, install_deps: bool = True): """ This is an alternative to `launch_docker_container` for setups that do not use Docker or would like to use `status-im/status-go` outside of a Docker container. This will build a @@ -110,9 +111,6 @@ def build_and_launch(commit: Optional[str] = None, repo_dir: Optional[str] = Non - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting. Building itself is not subject to this timeout. - `install_deps` - whether to run `make status-go-deps` before building. This also runs `go clean -cache` / `go clean -modcache`, so it is skipped by default - only needed on the first build, or after a Go toolchain upgrade. - - Output: - - `subprocess.Popen` handle of the running `status-backend` process, so it can be terminated later. """ logger = logging.getLogger(__name__) @@ -170,22 +168,128 @@ def run(cmd: list[str], cwd: Optional[str] = None) -> subprocess.CompletedProces if not os.path.isfile(binary_path): raise exceptions.BuildError(f"Build finished but {binary_path} was not found.") - logger.info(f"Launching status-backend on {address}...") - log_path = os.path.join(repo_dir, "status-backend.log") - with open(log_path, "w") as log_file: - process = subprocess.Popen([binary_path, f"-address={address}"], cwd=repo_dir, stdout=log_file, stderr=subprocess.STDOUT) + __launch_build(binary_path, repo_dir, address, wait_seconds) + +def download_build_and_launch(file_name: Optional[str] = None, repo_name: str = "status-im/status-go", tag: Optional[str] = None, token: Optional[str] = None, address: str = "localhost:8080", wait_seconds: int = 30): + """ + Download a prebuilt `status-backend` bundle from a GitHub release, extract it and launch it. + This is an alternative to `build_and_launch` for setups that do not want to build + `status-im/status-go` from source - no Nix or Go toolchain is needed. + + Parameters: + - `file_name` - the release asset to download, exactly as it appears on the release page. If not provided, the asset matching this machine is picked + - `repo_name` - the `owner/name` of the repository + - `tag` - a release tag. If not provided, the latest release is used + - `token` - a GitHub token. Required for private repositories + - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. + - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting. Downloading itself is not subject to this timeout. + """ + logger = logging.getLogger(__name__) + headers = {"Accept": "application/vnd.github+json"} + if token: + headers["Authorization"] = f"Bearer {token}" + + endpoint = f"tags/{tag}" if tag else "latest" + url = f"https://api.github.com/repos/{repo_name}/releases/{endpoint}" + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + release: dict = response.json() + + name = f"status-backend_{sys.platform}_{machine()}_{release['tag_name'][1:]}".lower() + assets: list[dict] = [asset for asset in release["assets"] if name in asset["name"]] + if not assets: + available_assets = [asset["name"] for asset in release["assets"]] + raise exceptions.BuildError(f"'{name}' is not in release '{release['tag_name']}'. Available: {', '.join(available_assets) or 'none'}") + + selected = assets[0] + file_name = selected["name"] + destination = os.getcwd() + archive_path = os.path.join(destination, file_name) + bundle_dir = os.path.join(destination, "status-backend-bundle") + if not os.path.exists(bundle_dir): + logger.info(f"Downloading '{file_name}' from release '{release['tag_name']}'...") + with requests.get(selected["url"], headers={**headers, "Accept": "application/octet-stream"}, stream=True, timeout=300) as download: + download.raise_for_status() + with open(archive_path, "wb") as f: + for chunk in download.iter_content(chunk_size=1024 * 1024): + f.write(chunk) + + with tarfile.open(archive_path) as archive: + # Archives created on macOS carry a `._*` metadata file next to every real file + members = [ + member + for member in archive.getmembers() + if not os.path.basename(member.name).startswith("._") + ] + + # A bundle from a previous release must not mix with the new one + shutil.rmtree(bundle_dir, ignore_errors=True) + logger.info(f"Extracting '{file_name}' into {bundle_dir}...") + # The `data` filter blocks paths escaping `destination`. Only available from Python 3.11.4 + extract_options = {"filter": "data"} if hasattr(tarfile, "data_filter") else {} + archive.extractall(destination, members=members, **extract_options) + + os.remove(archive_path) + else: + logger.info("Bundle already exists. Skipping download.") + # Linux bundles start through `run.sh` so the bundled loader is used, macOS bundles start the binary directly + launcher = os.path.join(bundle_dir, "run.sh") + if not os.path.isfile(launcher): + launcher = os.path.join(bundle_dir, "status-backend") + + if not os.path.isfile(launcher): + raise exceptions.BuildError(f"'{file_name}' has neither run.sh nor status-backend in {bundle_dir}.") + + if not os.access(launcher, os.X_OK): + mode = os.stat(launcher).st_mode + os.chmod(launcher, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + if sys.platform == "darwin" and shutil.which("xattr"): + # Downloaded files are tagged with `com.apple.quarantine`, which makes Gatekeeper + result = subprocess.run(["xattr", "-dr", "com.apple.quarantine", bundle_dir], stderr=subprocess.PIPE, text=True) + if result.returncode != 0 and "No such xattr" not in result.stderr: + logger.warning(f"Failed to clear quarantine attribute on {bundle_dir}: {result.stderr.strip()}") + + __launch_build(launcher, destination, address, wait_seconds) + +def __launch_build(binary_path: str, working_dir: str, address: str = "localhost:8080", wait_seconds: int = 30): + """ + Launch a `status-backend` binary and wait until it answers on its health endpoint. + Shared by `build_and_launch` and `download_build_and_launch`. + + Parameters: + - `binary_path` - the `status-backend` binary to launch + - `working_dir` - the working directory of the process. The backend resolves its relative `assets` / `backups` folders against it + - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. + - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting + """ + logger = logging.getLogger(__name__) + if not os.path.isfile(binary_path): + raise exceptions.BuildError(f"{binary_path} was not found.") health_url = f"http://{address}/health" + try: + response = requests.get(health_url, timeout=1) + if response.ok: + logger.info(f"status-backend is already running on {address}. Skipping launch.") + return None + except requests.exceptions.RequestException: + pass + + logger.info(f"Launching status-backend on {address}...") + # stdout / stderr are inherited, so the backend's logs are printed in the terminal + process = subprocess.Popen([binary_path, f"-address={address}"], cwd=working_dir) + deadline = time.time() + wait_seconds last_error = None while time.time() < deadline: if process.poll() is not None: - raise exceptions.BuildError(f"status-backend exited early with code {process.returncode}. See {log_path} for details.") + raise exceptions.BuildError(f"status-backend exited early with code {process.returncode}. See the status-backend output above for details.") try: response = requests.get(health_url, timeout=1) if response.ok: logger.info("status-backend is up!") - return process + return except requests.exceptions.RequestException as error: last_error = error time.sleep(0.5) From 0b5d4822018f284ecc54f8999097c358f6666c90 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Thu, 17 Sep 2026 22:06:30 +0300 Subject: [PATCH 07/11] docker: Launch already created build --- .gitignore | 1 + status_sdk/utils/builds.py | 87 ++++++++++++++++++++++++++++++++++++ status_sdk/utils/external.py | 69 +++++++++------------------- 3 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 status_sdk/utils/builds.py diff --git a/.gitignore b/.gitignore index ed5ef66..2bb5160 100644 --- a/.gitignore +++ b/.gitignore @@ -219,3 +219,4 @@ data/ *.bkp # Status Backend builds status-backend-bundle/ +*/status-go diff --git a/status_sdk/utils/builds.py b/status_sdk/utils/builds.py new file mode 100644 index 0000000..11e0652 --- /dev/null +++ b/status_sdk/utils/builds.py @@ -0,0 +1,87 @@ +import shutil, os, subprocess, time, logging, requests +from .. import exceptions + +def fix_nix_build_paths(binary_path: str) -> None: + """ + Nix's `nim-sds` package sometimes bakes the transient build-sandbox path of `libsds.dylib` + (e.g. `/nix/var/nix/builds/nix-/source/build/libsds.dylib`) into `status-backend` instead + of the stable `/nix/store/...` path. That sandbox is removed after the build finishes, so the + binary fails to launch with a `Library not loaded` dyld error. This finds any such dangling + dependency and repoints it at the matching library actually present in `/nix/store`. + """ + logger = logging.getLogger(__name__) + if not shutil.which("install_name_tool") or not shutil.which("otool"): + return + + result = subprocess.run(["otool", "-L", binary_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode != 0: + logger.warning(f"Failed to inspect {binary_path} with otool: {result.stderr.strip()}") + return + + changed = False + for line in result.stdout.splitlines()[1:]: + dep_path = line.strip().split(" ", 1)[0] + if os.path.isfile(dep_path) or "/nix/var/nix/builds/" not in dep_path: + continue + + lib_name = os.path.basename(dep_path) + logger.warning(f"{binary_path} references a stale Nix build path for {lib_name}. Searching /nix/store for a replacement...") + find_result = subprocess.run(["find", "/nix/store", "-iname", lib_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + candidates = [candidate for candidate in find_result.stdout.splitlines() if candidate] + if not candidates: + logger.warning(f"No replacement for {lib_name} found in /nix/store. {binary_path} may fail to launch.") + continue + + replacement = candidates[0] + subprocess.run(["install_name_tool", "-change", dep_path, replacement, binary_path], stderr=subprocess.PIPE, text=True) + logger.info(f"Repointed {lib_name} to {replacement}") + changed = True + + if changed: + # install_name_tool invalidates the code signature - macOS refuses to run an unsigned/altered binary + subprocess.run(["codesign", "--sign", "-", "--force", binary_path], stderr=subprocess.PIPE, text=True) + +def launch_build(binary_path: str, working_dir: str, address: str = "localhost:8080", wait_seconds: int = 30): + """ + Launch a `status-backend` binary and wait until it answers on its health endpoint. + Shared by `build_and_launch` and `download_build_and_launch`. + + Parameters: + - `binary_path` - the `status-backend` binary to launch + - `working_dir` - the working directory of the process. The backend resolves its relative `assets` / `backups` folders against it + - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. + - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting + """ + logger = logging.getLogger(__name__) + if not os.path.isfile(binary_path): + raise exceptions.BuildError(f"{binary_path} was not found.") + + health_url = f"http://{address}/health" + try: + response = requests.get(health_url, timeout=1) + if response.ok: + logger.info(f"status-backend is already running on {address}. Skipping launch.") + return None + except requests.exceptions.RequestException: + pass + + logger.info(f"Launching status-backend on {address}...") + # stdout / stderr are inherited, so the backend's logs are printed in the terminal + process = subprocess.Popen([binary_path, f"-address={address}"], cwd=working_dir) + + deadline = time.time() + wait_seconds + last_error = None + while time.time() < deadline: + if process.poll() is not None: + raise exceptions.BuildError(f"status-backend exited early with code {process.returncode}. See the status-backend output above for details.") + try: + response = requests.get(health_url, timeout=1) + if response.ok: + logger.info("status-backend is up!") + return + except requests.exceptions.RequestException as error: + last_error = error + time.sleep(0.5) + + process.terminate() + raise exceptions.BuildError(f"status-backend did not become reachable at {health_url} within {wait_seconds}s ({last_error}).") diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index d89f9c0..e7f6be5 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -6,6 +6,7 @@ from platform import machine from typing import Optional from .. import exceptions +from . import builds def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, platform: str = "linux/amd64", data_folder: Optional[str] = None): """ @@ -103,6 +104,11 @@ def build_and_launch(commit: Optional[str] = None, repo_dir: Optional[str] = Non would like to use `status-im/status-go` outside of a Docker container. This will build a native clone of `status-im/status-go` inside its Nix dev shell and launch it. + If `status-backend` already exists at `repo_dir/build/bin/status-backend`, the build is + skipped entirely (including cloning/fetching `repo_dir` and installing Nix/git) and the + existing binary is launched directly. Remove the binary, or pass a different `repo_dir`, to + force a rebuild. + NOTE: Requires the Nix package manager (https://nixos.org/download) and `git` to be installed. Parameters: @@ -123,13 +129,22 @@ def run(cmd: list[str], cwd: Optional[str] = None) -> subprocess.CompletedProces if system == "win32": raise exceptions.BuildError("build_and_launch requires Nix and is not supported on Windows. Use launch_docker_container instead, or run this from within WSL.") + repo_dir = repo_dir or os.path.join(os.path.dirname(os.path.dirname(__file__)), "status-go") + binary_path = os.path.join(repo_dir, "build", "bin", "status-backend") + + if os.path.isfile(binary_path): + logger.info(f"{binary_path} already exists. Skipping build.") + if system == "darwin": + builds.fix_nix_build_paths(binary_path) + builds.launch_build(binary_path, repo_dir, address, wait_seconds) + return + if not shutil.which("nix"): raise exceptions.BuildError("Please install Nix - https://nixos.org/download.") if not shutil.which("git"): raise exceptions.BuildError("Please install git.") - repo_dir = repo_dir or os.path.join(os.path.dirname(os.path.dirname(__file__)), "status-go") ref = commit if commit else "develop" if not os.path.isdir(os.path.join(repo_dir, ".git")): @@ -164,11 +179,13 @@ def run(cmd: list[str], cwd: Optional[str] = None) -> subprocess.CompletedProces if result.returncode != 0: raise exceptions.BuildError(result.stderr.strip()) - binary_path = os.path.join(repo_dir, "build", "bin", "status-backend") if not os.path.isfile(binary_path): raise exceptions.BuildError(f"Build finished but {binary_path} was not found.") - __launch_build(binary_path, repo_dir, address, wait_seconds) + if system == "darwin": + builds.fix_nix_build_paths(binary_path) + + builds.launch_build(binary_path, repo_dir, address, wait_seconds) def download_build_and_launch(file_name: Optional[str] = None, repo_name: str = "status-im/status-go", tag: Optional[str] = None, token: Optional[str] = None, address: str = "localhost:8080", wait_seconds: int = 30): """ @@ -250,49 +267,5 @@ def download_build_and_launch(file_name: Optional[str] = None, repo_name: str = if result.returncode != 0 and "No such xattr" not in result.stderr: logger.warning(f"Failed to clear quarantine attribute on {bundle_dir}: {result.stderr.strip()}") - __launch_build(launcher, destination, address, wait_seconds) + builds.launch_build(launcher, destination, address, wait_seconds) -def __launch_build(binary_path: str, working_dir: str, address: str = "localhost:8080", wait_seconds: int = 30): - """ - Launch a `status-backend` binary and wait until it answers on its health endpoint. - Shared by `build_and_launch` and `download_build_and_launch`. - - Parameters: - - `binary_path` - the `status-backend` binary to launch - - `working_dir` - the working directory of the process. The backend resolves its relative `assets` / `backups` folders against it - - `address` - the `host:port` to launch `status-backend` on. Defaults to `localhost:8080`. - - `wait_seconds` - number of seconds to wait, polling the health endpoint, before giving up on the backend starting - """ - logger = logging.getLogger(__name__) - if not os.path.isfile(binary_path): - raise exceptions.BuildError(f"{binary_path} was not found.") - - health_url = f"http://{address}/health" - try: - response = requests.get(health_url, timeout=1) - if response.ok: - logger.info(f"status-backend is already running on {address}. Skipping launch.") - return None - except requests.exceptions.RequestException: - pass - - logger.info(f"Launching status-backend on {address}...") - # stdout / stderr are inherited, so the backend's logs are printed in the terminal - process = subprocess.Popen([binary_path, f"-address={address}"], cwd=working_dir) - - deadline = time.time() + wait_seconds - last_error = None - while time.time() < deadline: - if process.poll() is not None: - raise exceptions.BuildError(f"status-backend exited early with code {process.returncode}. See the status-backend output above for details.") - try: - response = requests.get(health_url, timeout=1) - if response.ok: - logger.info("status-backend is up!") - return - except requests.exceptions.RequestException as error: - last_error = error - time.sleep(0.5) - - process.terminate() - raise exceptions.BuildError(f"status-backend did not become reachable at {health_url} within {wait_seconds}s ({last_error}).") From 7e041bbb1e35b36efdb5a2350c7b178f1b1ee63a Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Fri, 18 Sep 2026 21:18:21 +0300 Subject: [PATCH 08/11] account: Get status-go commit --- docs/account.md | 32 ++++++++++++++++++++++++++++++++ status_sdk/account.py | 8 +++++++- status_sdk/utils/external.py | 21 ++++++++++++++++----- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/docs/account.md b/docs/account.md index d7aa203..7783a16 100644 --- a/docs/account.md +++ b/docs/account.md @@ -1811,6 +1811,38 @@ account.logger.warning("This is a warning") account.logger.error("Something went wrong") ``` +### `status_go_commit_sha` + +Which build of [`status-im/status-go`](https://github.com/status-im/status-go) the connected Status Backend is running. Use it when reporting an issue, and to confirm that the backend you are talking to is the commit you meant to launch. + +Returns `str`. The value is read from the backend's health endpoint when the `Account` is created, so it describes the backend that was running **at that moment** - relaunching the backend on a different commit is not picked up until a new `Account` is created. + +The exact shape depends on how the backend was launched: + +| Launched with | Reported version | `status_go_commit_sha` | +|--------|--------|-------------| +| [`build_and_launch`](./utils.md#build_and_launchcommitnone-repo_dirnone-addresslocalhost8080-wait_seconds30-install_depstrue) / [`download_build_and_launch`](./utils.md#download_build_and_launchfile_namenone-repo_namestatus-imstatus-go-tagnone-tokennone-addresslocalhost8080-wait_seconds30) | `v10.35.0-48-g9f09f9027` | `9f09f9027` - the short SHA, taken out of the `git describe` version | +| [`launch_docker_container`](./utils.md#launch_docker_containercommitnone-wait_seconds5-platformlinuxamd64-data_foldernone) | `9f09f902762292fbe8264dd75a68e739ecd17444` | the same value, unchanged | + +The two differ because a native build is compiled inside a git clone, so `status-go` embeds a `git describe` version that the SDK extracts the SHA from. The Docker image is built from a git **context** that carries no `.git` folder, so `docker-compose.yaml` injects the ref it was told to build instead - meaning the full SHA comes back as-is. + +```python +from status_sdk import Account + +account = Account() +print(f"status-go: {account.status_go_commit_sha}") +``` + +Include it, together with [`__version__`](./utils.md#__version__), when [reporting an issue](https://github.com/status-im/status-python-sdk/issues): + +```python +import status_sdk +from status_sdk import Account + +account = Account() +print(f"status-sdk {status_sdk.__version__} / status-go {account.status_go_commit_sha}") +``` + ### Chat #### `contacts` diff --git a/status_sdk/account.py b/status_sdk/account.py index daceff0..f14a05b 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -103,7 +103,9 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po r'^v(?P\d+)\.(?P\d+)\.(?P\d+)' r'(?:-(?P\d+)-g(?P[0-9a-f]{7,40}))?$' ) - self.__is_docker = not bool(version_regex.match(health_info.get("version", ""))) + version = health_info.get("version", "") + self.__is_docker = not bool(version_regex.match(version)) + self.__status_go_commit_sha = version if self.__is_docker else version_regex.match(version)["sha"] base_folder = os.path.dirname(__file__) if not self.__is_docker: @@ -287,6 +289,10 @@ def logout(self): def logger(self) -> logging.Logger: return self.__logger + @property + def status_go_commit_sha(self) -> str: + return self.__status_go_commit_sha + @property def available_accounts(self) -> list[dict]: """ diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index e7f6be5..45e1bc9 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -38,14 +38,25 @@ def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, to_docker_path = lambda path: f"/mnt/{Path(path).drive.rstrip(':').lower()}/" + "/".join(Path(path).parts[1:]) if is_windows else path docker_path = to_docker_path(DOCKER_COMPOSE_PATH) - env_params = { - "STATUS_GO_COMMIT": ref, - "PLATFORM": platform - } - with open(DOCKER_COMPOSE_PATH, "r") as f: docker_yaml_data: dict = yaml.load(f, Loader=yaml.SafeLoader) + if not commit: + url, _, _ = docker_yaml_data["services"]["backend"]["build"]["context"].partition("#") + org, repo = url.split("/")[-2:] + repo = repo.replace(".git", "") + response = requests.get( + f"https://api.github.com/repos/{org}/{repo}/commits", + headers={"Accept": "application/vnd.github.sha"}, + timeout=30 + ) + commit = response.json()[0]["sha"] + + logger.info(f"status-im/status-go SHA: {commit}") + env_params = { + "STATUS_GO_COMMIT": commit, + "PLATFORM": platform + } data_volume = '${DATA_DIR:-./data}:/data' current_volumes: list[str] = docker_yaml_data["services"]["backend"]["volumes"] if data_folder: From 9af82d9c20cfcdfb416ac6f25cc592cef1c50bb5 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Tue, 22 Sep 2026 20:53:44 +0300 Subject: [PATCH 09/11] communities: Remove privilege checks - Decrease RPC calls so `status-im/status-go` does not crash during many calls at once. If the user does not have privileges to send messages in the channel, a custom error will be raised --- status_sdk/community/channel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/status_sdk/community/channel.py b/status_sdk/community/channel.py index 0d7e573..87fa5cf 100644 --- a/status_sdk/community/channel.py +++ b/status_sdk/community/channel.py @@ -267,7 +267,7 @@ def send_message(self, message: str, reply_to_message_id: Optional[str] = None) Output: - The message ID """ - return self.__account.send_message(self.id, message, reply_to_message_id) if self.can_post else None + return self.__account.send_message(self.id, message, reply_to_message_id) def send_image(self, file_path: str, message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> Optional[str]: """ @@ -281,7 +281,7 @@ def send_image(self, file_path: str, message: Optional[str] = None, reply_to_mes Output: - The message ID """ - return self.__account.send_image(self.id, file_path, message, reply_to_message_id) if self.can_post else None + return self.__account.send_image(self.id, file_path, message, reply_to_message_id) def send_bridged_message(self, message: str, name: Optional[str] = None, username: Optional[str] = None, user_id: Optional[str] = None, message_id: Optional[str] = None, reply_to_message_id: Optional[str] = None, image_url: Optional[str] = None) -> Optional[str]: """ @@ -299,7 +299,7 @@ def send_bridged_message(self, message: str, name: Optional[str] = None, usernam Output: - The message ID in Status App """ - return self.__account.send_bridged_message(self.id, message, name, username, user_id, message_id, reply_to_message_id, image_url) if self.can_post else None + return self.__account.send_bridged_message(self.id, message, name, username, user_id, message_id, reply_to_message_id, image_url) def send_emoji_reaction(self, message_id: str, emoji_shortname: str): """ From 387bfa233184fae2169fb6bd38b98c9f9a37568f Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Tue, 22 Sep 2026 14:09:22 +0300 Subject: [PATCH 10/11] docs: Add CHANGELOG --- CHANGELOG.md | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6d4c91f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,195 @@ +# Changelog + +All notable changes to `status-python-sdk` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [1.1.6] - 2026-09-09 + +### Fixed + +- Real time + - `def listen_messages` monitors `status-im/status-go` property `messages` only instead of `messages` and `chats` + - Unfiltered messages will return `dict` only instead of `dict` or `list[dict]` + +## [1.1.5] - 2026-09-09 + +### Fixed + +- Real time + - `def listen_messages` returns a single `Message` with multiple images, instead of a `Message` per image + - Remove duplicated `status-im/status-go` signals that have already been yielded + +## [1.1.4] - 2026-09-09 + +### Added + +- Real time + - User removes the account from their contacts + - Listen for bridged messages +- `class Channel` + - move position up and down + - change current category + - add and delete member permissions +- Send bridged messages + - `class Account` + - `class GroupChat` + - `class Channel` + +### Fixed + +- `account.add_contact` accepting a friend request via `wakuext_addContact` no longer sends the user "Please add me to your contacts." +- Sending a reply to a message returns the ID of the sent message instead of the replied-to message's ID +- `docker-compose.yaml` ENV parameters match the ones used in `launch_docker_container` +- Data class `Message` supports bridged messages +- `listen_messages` would break if an unhandled `content_type` was found + +### Removed + +- `class Logger` has been deprecated. Logger config must be set outside of `status-sdk` with `logging.basicConfig`. + +### Changed + +- Property `state` in data class `CommunityRequest` has been replaced with boolean values `pending`, `reject`, `accept` and `cancel`. + +## [1.1.3] - 2026-08-26 + +### Added + +- Emoji Reactions + - `class Account` + - `class GroupChat` + - `class Channel` +- Real time + - Listen for message mentions + - Accepted contact requests + +### Changed + +- Real time listening will return custom `dataclass` instead of `dict` +- Internal `class Signal` can now listen for multiple signal types at once, or leave them unfiltered to listen for all + +## [1.1.2] - 2026-08-18 + +### Added + +- Block / Unblock contacts from `class Account` + +### Changed + +- Deleted messages would be flagged as deleted but not removed from the internal `status-im/status-go` database. +- `class Account` no longer logs out an already logged in account on initialization. Calling `def login` with the same account continues normally; calling it with a different account logs out the original one first. +- `get_messages` supports `str` in format `YYYY-MM-DD` + +## [1.1.1] - 2026-08-12 + +### Added + +- Send images + - `class Account` + - `class GroupChat` + - `class Channel` +- Listen for new contact requests in real time +- Swap Community Control Nodes between Status App and `status-im/status-go` + - New `launch_docker_container` property + - Add `data_folder` to `class Community` +- Get community collectables + +### Fixed + +- Centre `profile_picture` when setting the value + +### Changed + +- `login` functionality in `class Account` supports Keccak-256 hash if the `data` folder has been copied over from another Status instance (`status-im/status-go` or Status App) + +will try to log in without a hashed password + +## [1.1.0] - 2026-08-04 + +### Added + +- Convert Chat Key (`compressed_key`) and URL (`url`) to Status App `public_key` +- Get and set current status of the logged in account +- Support async signal fetching +- Sync `status-im/status-python-sdk` with Status App via **Installation ID** +- Create `class GroupChat` + - Create chat + - Leave chat + - Edit chat name + - Edit Group Picture + - Add member + - Remove member + - Get current chat members + - Support messaging similar to `class Account` +- Create `class Community` + - Kick member + - Ban member + - Unban previously banned members + - Leave + - Accept new member + - Decline new member + - Get members + - Current channels (community chats) +- Create `class Channel` + - Create chat + - Delete chat + - Get messages + - Send messages + - Delete messages + - Properties + - Name - get and set + - Colour - get and set + - Description - get and set + - Emoji - get and set + + +### Removed + +- `class Account` properties + - `community_members` + +## [1.0.0] - 2026-07-15 + +### Added + +- Create initial design of `class Account` + - Create account + - Log in + - Account recovery - load and create `.bkp` files + - Log out + - Send friend request + - Accept friend request - when the bot and the user have sent a friend request + - Decline friend request + - Send community join request + - Messaging + - Read messages from given start and end timestamps + - Read new messages in real time + - Send message + - Properties + - Overall account information + - Contacts the bot has seen + - Communities the bot has access to + - Chats the bot has access to + - Display Name - get and set + - Bio - get and set + - Profile Picture - get and set + - Wallet + - Get account balance + - Get market information + - Get transactions + - Send crypto + - Swap crypto + - ETH to ERC-20 + - ERC-20 to ETH + - ERC-20 to ERC-20 +- Launch `status-im/status-go` Docker container with Python instead of manual `docker compose up -d` setup. +- Custom library errors + +[1.1.6]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.6 +[1.1.5]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.5 +[1.1.4]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.4 +[1.1.3]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.3 +[1.1.2]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.2 +[1.1.1]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.1 +[1.1.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.0 +[1.0.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.0.0 From 2fabcc389b11f0e84c6b202e3ad99640f5a8ae8a Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Tue, 22 Sep 2026 21:33:19 +0300 Subject: [PATCH 11/11] docs: CHANGELOG 1.2.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4c91f..9769a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,29 @@ All notable changes to `status-python-sdk` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-09-22 + +### Added + +- Build and launch [status-go] locally / inside already existing Docker container. Requires [nix] and [git] set up. +- Download build and launch [status-go] locally / inside already existing Docker container. +- `class Account` has new property `status_go_commit_sha` + +### Fixed + +- Image RPC call for getting the current `profile_picture` returned `None` instead of an empty `list`. +- If no `commit` is passed to `launch_docker_container`, the latest commit from the `develop` branch is now used. + +### Removed + +- Remove `can_post` check when sending community messages, reducing RPC calls to [status-go] to prevent it from crashing under heavy use. A custom error is now raised instead if the user lacks privileges to send messages in the channel. ## [1.1.6] - 2026-09-09 ### Fixed - Real time - - `def listen_messages` monitors `status-im/status-go` property `messages` only instead of `messages` and `chats` + - `def listen_messages` monitors [status-go] property `messages` only instead of `messages` and `chats` - Unfiltered messages will return `dict` only instead of `dict` or `list[dict]` ## [1.1.5] - 2026-09-09 @@ -17,7 +33,7 @@ All notable changes to `status-python-sdk` will be documented in this file. The - Real time - `def listen_messages` returns a single `Message` with multiple images, instead of a `Message` per image - - Remove duplicated `status-im/status-go` signals that have already been yielded + - Remove duplicated [status-go] signals that have already been yielded ## [1.1.4] - 2026-09-09 @@ -76,7 +92,7 @@ All notable changes to `status-python-sdk` will be documented in this file. The ### Changed -- Deleted messages would be flagged as deleted but not removed from the internal `status-im/status-go` database. +- Deleted messages would be flagged as deleted but not removed from the internal [status-go] database. - `class Account` no longer logs out an already logged in account on initialization. Calling `def login` with the same account continues normally; calling it with a different account logs out the original one first. - `get_messages` supports `str` in format `YYYY-MM-DD` @@ -89,7 +105,7 @@ All notable changes to `status-python-sdk` will be documented in this file. The - `class GroupChat` - `class Channel` - Listen for new contact requests in real time -- Swap Community Control Nodes between Status App and `status-im/status-go` +- Swap Community Control Nodes between Status App and [status-go] - New `launch_docker_container` property - Add `data_folder` to `class Community` - Get community collectables @@ -100,7 +116,7 @@ All notable changes to `status-python-sdk` will be documented in this file. The ### Changed -- `login` functionality in `class Account` supports Keccak-256 hash if the `data` folder has been copied over from another Status instance (`status-im/status-go` or Status App) +- `login` functionality in `class Account` supports Keccak-256 hash if the `data` folder has been copied over from another Status instance ([status-go] or Status App) will try to log in without a hashed password @@ -182,9 +198,10 @@ will try to log in without a hashed password - ETH to ERC-20 - ERC-20 to ETH - ERC-20 to ERC-20 -- Launch `status-im/status-go` Docker container with Python instead of manual `docker compose up -d` setup. +- Launch [status-go] Docker container with Python instead of manual `docker compose up -d` setup. - Custom library errors +[1.2.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.2.0 [1.1.6]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.6 [1.1.5]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.5 [1.1.4]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.4 @@ -193,3 +210,8 @@ will try to log in without a hashed password [1.1.1]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.1 [1.1.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.1.0 [1.0.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.0.0 + +[nix]: https://nixos.org/ +[git]: https://git-scm.com/ +[status-go]: https://github.com/status-im/status-go +[status-backend]: https://github.com/status-im/status-go