From 0761a8337d37638fbdc11ffd165e5bad9aa96617 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Thu, 12 Mar 2026 16:45:47 +0000 Subject: [PATCH 1/5] account: mnemonic recovery Related to https://github.com/status-im/status-bot/issues/7 - Recover account with mnemonic phrase in `login` --- bot/account.py | 69 ++++++++++++++++++++++++++++++--------------- bot/docs/account.md | 25 ++++++++++++++-- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/bot/account.py b/bot/account.py index 7036d08..41c9787 100644 --- a/bot/account.py +++ b/bot/account.py @@ -41,6 +41,7 @@ class Account: "initialize": f"{self.http_base_url}InitializeApplication", "login": f"{self.http_base_url}LoginAccount", "create": f"{self.http_base_url}CreateAccountAndLogin", + "restore": f"{self.http_base_url}RestoreAccountAndLogin", "logout": f"{self.http_base_url}Logout", "rpc": f"{self.http_base_url}CallRPC" }, @@ -49,26 +50,12 @@ class Account: } } self.__signal = Signal(self.urls["socket"]["signals"]) - response = requests.post(self.urls["http"]["initialize"], json={ - "dataDir": self.__unix_folder - }) - data: dict = response.json() - accounts: list[dict] = data.get("accounts", []) - if not isinstance(accounts, list): - accounts = [] - - self.__available_accounts = [ - { - "display_name": account["name"], - "key_uid": account["key-uid"], - "created_at": datetime.datetime.fromtimestamp(account["timestamp"]) - } - for account in accounts - ] + # Initialize profile + self.available_accounts # In case if there is a hanging logged in session self.logout() - def login(self, password: str, key_uid: Optional[str] = None, display_name: Optional[str] = None): + def login(self, password: str, key_uid: Optional[str] = None, display_name: Optional[str] = None, mnemonic: Optional[str] = None): """ Login to the given account. If it does not exist, it will be created and automatically logged in. @@ -77,13 +64,15 @@ class Account: - `password` - your Status password - `key_uid` - your key unique identifier. If not provided `display_name` will be used to fetch it. This means that each `display_name` can be linked to one `key_uid` - `display_name` - your Status display name. Use `display_name` and `password` parameter combination if you have a 1 to 1 mapping (each display name has a unique `key_uid`) + - `mnemonic` - the mnemonic when creating an account. Use this field with `password` and `display_name` to recover an account """ if not key_uid and not display_name: raise ValueError("Please provide either a Key Unique Identifier (key_uid) or a Display Name (display_name)...") + available_accounts = self.available_accounts # Login combination: display_name + password if not key_uid: - for account in self.__available_accounts: + for account in available_accounts: if account["display_name"] != display_name: continue @@ -91,19 +80,34 @@ class Account: break # Login combination: key_uid + password else: - available_key_uids = [current["key_uid"] for current in self.__available_accounts] + available_key_uids = [current["key_uid"] for current in available_accounts] if key_uid not in available_key_uids: info = "\n".join([f"{current['key_uid']} - {current['display_name']}" for current in self.__available_accounts]) raise ValueError(f"Given Key Unique Identifier is invalid...\nAvailable Key Unique Identifiers:\n{info}") is_new_account = isinstance(key_uid, type(None)) + is_recovery = not isinstance(mnemonic, type(None)) + url_key = "login" params = { "keyUid": key_uid, "password": password, 'kdfIterations': self.__kd_iterations } - if is_new_account: + if is_recovery: + self.__validate_display_name(display_name) + params = { + "mnemonic": mnemonic, + "rootDataDir": self.__unix_folder, + "kdfIterations": self.__kd_iterations, + "displayName": display_name, + "password": password, + "customizationColor": "primary", + "wakuV2LightClient": False, + "thirdpartyServicesEnabled": True + } + url_key = "restore" + elif is_new_account: self.__validate_display_name(display_name) params = { "rootDataDir": self.__unix_folder, @@ -114,8 +118,10 @@ class Account: "wakuV2LightClient": False, "thirdpartyServicesEnabled": True, } + url_key = "create" + self.logout() - url = self.urls["http"]["login" if not is_new_account else "create"] + url = self.urls["http"][url_key] response = requests.post(url, json=params) signal_event = self.__signal.get("node.login") if signal_event["is_error"]: @@ -127,7 +133,7 @@ class Account: "emojis": event["emojiHash"], "key_uid": event["key-uid"], "compressed_key": event["compressedKey"], - "mnemonic": event["mnemonic"], + "mnemonic": event.get("mnemonic", mnemonic), "display_name": event["display-name"], "bio": event.get("bio", ""), "password": password, @@ -152,7 +158,23 @@ class Account: """ All locally available accounts """ - return self.__available_accounts + response = requests.post(self.urls["http"]["initialize"], json={ + "dataDir": self.__unix_folder + }) + data: dict = response.json() + accounts: list[dict] = data.get("accounts", []) + if not isinstance(accounts, list): + accounts = [] + + current_available_accounts = [ + { + "display_name": account["name"], + "key_uid": account["key-uid"], + "created_at": datetime.datetime.fromtimestamp(account["timestamp"]) + } + for account in accounts + ] + return current_available_accounts @property def info(self) -> dict: @@ -484,6 +506,7 @@ class Account: if self.__is_messenger_launched: return self.__call_rpc("messaging", "startMessenger") + self.__signal.get("wakuv2.peerstats") self.__is_messenger_launched = True def __del__(self): diff --git a/bot/docs/account.md b/bot/docs/account.md index 217b153..12e6b45 100644 --- a/bot/docs/account.md +++ b/bot/docs/account.md @@ -42,18 +42,22 @@ If a display name does not follow these rules, a **`ValueError`** will be raised ## Methods -### `login(password, key_uid=None, display_name=None)` +### `login(password, key_uid=None, display_name=None, mnemonic=None)` Login to an existing Status account. If the account does not exist in the initialized data directory, a new account will be created and automatically logged in. After a successful login, the decentralized messenger service is automatically started so the account can send and receive messages. +An account can also be recovered if the `mnemonic` is passed. + | Name | Type | Required | Description | |-----|-----|-----|-------------| | `password` | `str` | Yes | Password used to encrypt the account | | `key_uid` | `str` | Yes* | Unique key identifier of the account. If provided, the account will be logged in directly using this identifier. If not provided, then you must use `display_name` and `password` to login. | -| `display_name` | `str` | Yes* | Display name of the account. Used to resolve the `key_uid` if it is not provided, or to create a new account if one does not already exist. | +| `display_name` | `str` | Yes* | Display name of the account. Used to resolve the `key_uid` if it is not provided, or to create a new account if one does not already exist. This field is required if an account needs to be recovered with `mnemonic`. | +| `mnemonic` | `str` | No | The mnemonic from [`info`](./account.md#info). Use this field with `password` and `display_name` to recover the account.
**Note**: You can pass a different `display_name` but that will be internal only. When an account is recovered setting [`display_name`](./account.md#display_name) can be buggy. Ideally when recovering the account, use the original `display_name` of the account. | Returns the current `Account` instance, allowing method chaining. +Login with `display_name`: ```python from bot import Account @@ -65,6 +69,9 @@ params = { account.login(**params) ``` +**Note**: This assumes that `display_name` and is unique for every `key_uid`. If there are duplicated `display_names` then the first found match will be used. You can log in with `key_uid` if you have `display_name` duplicates. + +Login with `key_uid`: ```python from bot import Account @@ -76,6 +83,20 @@ params = { account.login(**params) ``` +Recover account: + +```python +from bot import Account + +account = Account() +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "mnemonic" : "phrase_1 phrase_2 phrase_3 phrase_4 phrase_5 phrase_6 phrase_7 phrase_8 phrase_9 phrase_10 phrase_11 phrase_12" +} +account.login(**params) +``` + ### `logout()` Logout from the currently logged-in Status account. This method also clears the internal account state and stops the active messenger session. This function is also supported in `del` and when the script automatically finishes. From 1c67a08b41feb82f78895ae9ac2b2f364626eb3f Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Thu, 12 Mar 2026 22:14:57 +0000 Subject: [PATCH 2/5] account: custom logger Related to https://github.com/status-im/status-bot/issues/7 - Add custom logger to `Account` class - Add `display_name` validation based on Status App - Update new `display_name` for other accounts when account has been recovered with mnemonics - Update documentation --- bot/account.py | 24 +++++++++++++++++++++--- bot/docs/account.md | 29 +++++++++++++++++++++++++++++ bot/logger.py | 24 ++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 bot/logger.py diff --git a/bot/account.py b/bot/account.py index 41c9787..74e3250 100644 --- a/bot/account.py +++ b/bot/account.py @@ -1,7 +1,7 @@ from typing import Optional, Union, Generator, Any -import requests, datetime, re +import requests, datetime, re, logging from .signal import Signal - +from .logger import Logger class Account: # Enum mappings from original wakuext.py @@ -28,6 +28,7 @@ class Account: - `port` - the port to connect to Status Backend. Verify the port in the Docker files. - `is_secure` - if `http` or `https` should be used """ + self.__logger = Logger() self.__timestamp_divisor = 1_000 self.__kd_iterations = 256000 self.__unix_folder = unix_folder @@ -86,7 +87,7 @@ class Account: raise ValueError(f"Given Key Unique Identifier is invalid...\nAvailable Key Unique Identifiers:\n{info}") is_new_account = isinstance(key_uid, type(None)) - is_recovery = not isinstance(mnemonic, type(None)) + is_recovery = not isinstance(mnemonic, type(None)) and not key_uid url_key = "login" params = { @@ -107,6 +108,7 @@ class Account: "thirdpartyServicesEnabled": True } url_key = "restore" + self.logger.info(f"Restoring account for given mnemonics") elif is_new_account: self.__validate_display_name(display_name) params = { @@ -119,6 +121,9 @@ class Account: "thirdpartyServicesEnabled": True, } url_key = "create" + self.logger.info(f"Creating account with display_name {display_name}") + else: + self.logger.info(f"Logging in with Key UID - {key_uid}") self.logout() url = self.urls["http"][url_key] @@ -127,6 +132,7 @@ class Account: if signal_event["is_error"]: raise Exception(f"There was an error with Status Backend...\n{signal_event['error_message']}") + self.logger.info("Successfully logged in!") event: dict = signal_event["event"]["settings"] self.__info = { "public_key": event["public-key"], @@ -142,6 +148,9 @@ class Account: } # Messenger can be activated only when logged in self.__start_messenger() + if is_recovery: + self.display_name = event["display-name"] + return self def logout(self): @@ -153,6 +162,10 @@ class Account: self.__is_messenger_launched = False return self + @property + def logger(self) -> logging.Logger: + return self.__logger + @property def available_accounts(self) -> list[dict]: """ @@ -505,9 +518,11 @@ class Account: """ if self.__is_messenger_launched: return + self.logger.info("Starting messaging") self.__call_rpc("messaging", "startMessenger") self.__signal.get("wakuv2.peerstats") self.__is_messenger_launched = True + self.logger.info("Messaging launched") def __del__(self): """ @@ -593,6 +608,9 @@ class Account: if len(name) < 5: raise ValueError("Display name must be at least 5 characters long.") + if len(name) > 24: + raise ValueError("Display name cannot be more than 24 characters long.") + if not re.fullmatch(r"[A-Za-z0-9_-]+", name): raise ValueError("Display name can contain only A-Z, 0-9, hyphens (-), and underscores (_).") diff --git a/bot/docs/account.md b/bot/docs/account.md index 12e6b45..2fc0950 100644 --- a/bot/docs/account.md +++ b/bot/docs/account.md @@ -13,6 +13,7 @@ Display names must follow strict validation rules enforced by the library and ex - It may contain **hyphens (`-`)** - It may contain **underscores (`_`)** - It must be **at least 5 characters long** +- It **cannot be more than 24 characters long** - It **cannot start or end with a space** Characters such as spaces, punctuation, emojis, or other symbols are **not allowed**. @@ -97,6 +98,10 @@ params = { account.login(**params) ``` +**Note**: When in recovery mode, the display name is updated on Status App as well so it is consistent locally and to other users. + + + ### `logout()` Logout from the currently logged-in Status account. This method also clears the internal account state and stops the active messenger session. This function is also supported in `del` and when the script automatically finishes. @@ -624,3 +629,27 @@ account.login(**params) # account.bio = None del account.bio ``` + +### `logger` + +Provides access to the internal **Python logger** for monitoring the lifecycle of the account and backend operations such as login, account creation, messenger startup, and recovery. + +Returns `logging.Logger`. + +Default logger configuration: + +- **Name**: `status-bot` +- **Level**: `INFO` +- **Output**: standard output (terminal) + +Example: + +```python +from bot import Account + +account = Account() + +account.logger.info("Starting Status bot") +account.logger.warning("This is a warning") +account.logger.error("Something went wrong") +``` diff --git a/bot/logger.py b/bot/logger.py new file mode 100644 index 0000000..850b254 --- /dev/null +++ b/bot/logger.py @@ -0,0 +1,24 @@ +from typing import Optional +import logging + +class Logger: + instance: Optional[logging.Logger] = None + + def __new__(cls) -> logging.Logger: + + if cls.instance: + return cls.instance + + cls.instance = logging.getLogger("status-bot") + cls.instance.setLevel(logging.INFO) + cls.instance.propagate = False + + handler = logging.StreamHandler() + formatter = logging.Formatter( + f"[%(asctime)s] [%(levelname)s]\t%(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + handler.setFormatter(formatter) + cls.instance.addHandler(handler) + return cls.instance From 6106e2f69ae49ec3e3a0e3f03ada0c6a853ed5a0 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Fri, 13 Mar 2026 21:27:50 +0000 Subject: [PATCH 3/5] account: add backup folder volume Related to https://github.com/status-im/status-bot/issues/7 - Make backup folder into a volume so `.bkp` files can be used --- .gitignore | 4 +++- docker-compose.yaml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f7a6069..d7caf06 100644 --- a/.gitignore +++ b/.gitignore @@ -211,8 +211,10 @@ __marimo__/ .vscode/ *.json *.ipynb -/data-dir /uploads *.DS_Store *.pkl *.dockerignore +# Docker volumes for Status +/data-dir +*.bkp diff --git a/docker-compose.yaml b/docker-compose.yaml index 31c5246..def6cce 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,7 @@ services: command: '-address 0.0.0.0:8080' volumes: - ./data-dir:/data-dir + - ./backups:/root/.config/Status/backups networks: - status-bridge profiles: From 2b2d5c0a1df03d721af6d99e53c6b24285673d33 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sat, 14 Mar 2026 20:19:35 +0000 Subject: [PATCH 4/5] account: add group chats support Related to https://github.com/status-im/status-bot/issues/8 --- bot/account.py | 10 +++++++++- bot/docs/account.md | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bot/account.py b/bot/account.py index 74e3250..255f51d 100644 --- a/bot/account.py +++ b/bot/account.py @@ -365,7 +365,15 @@ class Account: {"type": "contact", "id": contact["chat_id"], "name": contact["display_name"]} for contact in self.contacts.values() ] - return contacts + communities + + # Group chats in RPC endpoint are chat type 3 + data = self.__call_rpc("messaging", "activeChats") + group_chats = [ + {"type": "group_chat", "id": active_chat["id"], "name": active_chat["name"]} + for active_chat in data.get("result", []) + if active_chat["chatType"] == 3 + ] + return contacts + communities + group_chats def send_message(self, chat_id: str, message: str): """ diff --git a/bot/docs/account.md b/bot/docs/account.md index 2fc0950..e1b4df6 100644 --- a/bot/docs/account.md +++ b/bot/docs/account.md @@ -496,12 +496,13 @@ for community in account.communities: Get all chats that the account can **send messages to**. This includes: - [`contacts`](./account.md#contacts) — direct messages with users - [`communities`](./account.md#communities) — community channels where the account has **posting permission** +- Group chats that the account is in Returns `list[dict]` where each `dict` represents a chat that can be used with [`send_message`](./account.md#send_messagechat_id-message) and [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone). | Key | Type | Description | |----|----|-------------| -| `type` | `str` | Type of chat (`contact` or `channel`). | +| `type` | `str` | Type of chat (`contact`, `channel` or `group_chat`). | | `id` | `str` | Chat identifier used when sending messages. | | `name` | `str` | Either the display name of the user or the community channel name. | From 994e5c8243cd131ef5f03c1efd883930953ed4f5 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sat, 14 Mar 2026 23:36:34 +0000 Subject: [PATCH 5/5] account: load backup Related to https://github.com/status-im/status-bot/issues/7 - Volume for data directory has been swapped with backup one --- bot/account.py | 84 +++++++++++++++++++++++++++++++++++++-------- bot/docs/account.md | 28 ++++++++++++++- bot/signal.py | 2 +- docker-compose.yaml | 1 - 4 files changed, 98 insertions(+), 17 deletions(-) diff --git a/bot/account.py b/bot/account.py index 255f51d..16bc8af 100644 --- a/bot/account.py +++ b/bot/account.py @@ -1,5 +1,5 @@ from typing import Optional, Union, Generator, Any -import requests, datetime, re, logging +import requests, datetime, re, logging, os from .signal import Signal from .logger import Logger class Account: @@ -18,20 +18,25 @@ class Account: "messaging": "wakuext", "urls": "sharedurls" } - def __init__(self, unix_folder: str = "./data-dir", domain: str = "localhost", port: int = 8080, is_secure: bool = False): + def __init__(self, domain: str = "localhost", port: int = 8080, is_secure: bool = False): """ Work with your own Status App account Parameters: - - `unix_folder` - where Status Backend files will be initialized. **This folder is required in the Docker container**. Ideally it should be a volume so the account data is persistant if the image is deleted. Folder path is automatically created. - `domain` - the domain name where Status Backend is running. If running locally it would be `localhost` and if it's running in a container it would be the image's name. - `port` - the port to connect to Status Backend. Verify the port in the Docker files. - `is_secure` - if `http` or `https` should be used """ + # Path of the account data in the Docker container for Status Backend + self.__docker_data_folder = "./data-dir" + # Path of the backups in the Docker container for Status Backend + self.__docker_backup_folder = "./root/.config/Status/backups" + # As the docker-compose.yaml folder is at the moment + # NOTE: This might change for initial release + self.__backup_local_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), "backups") self.__logger = Logger() self.__timestamp_divisor = 1_000 self.__kd_iterations = 256000 - self.__unix_folder = unix_folder self.__is_messenger_launched = False # Information for the logged in account self.__info = {} @@ -44,7 +49,9 @@ class Account: "create": f"{self.http_base_url}CreateAccountAndLogin", "restore": f"{self.http_base_url}RestoreAccountAndLogin", "logout": f"{self.http_base_url}Logout", - "rpc": f"{self.http_base_url}CallRPC" + "create_backup": f"{self.http_base_url}PerformLocalBackup", + "load_backup": f"{self.http_base_url}LoadLocalBackup", + "rpc": f"{self.http_base_url}CallRPC", }, "socket": { "signals": f"{self.ws_base_url}signals" @@ -99,7 +106,7 @@ class Account: self.__validate_display_name(display_name) params = { "mnemonic": mnemonic, - "rootDataDir": self.__unix_folder, + "rootDataDir": self.__docker_data_folder, "kdfIterations": self.__kd_iterations, "displayName": display_name, "password": password, @@ -112,7 +119,7 @@ class Account: elif is_new_account: self.__validate_display_name(display_name) params = { - "rootDataDir": self.__unix_folder, + "rootDataDir": self.__docker_data_folder, "kdfIterations": self.__kd_iterations, "displayName": display_name, "password": password, @@ -149,7 +156,10 @@ class Account: # Messenger can be activated only when logged in self.__start_messenger() if is_recovery: + self.logger.info("Updating remote display name") self.display_name = event["display-name"] + self.logger.info("Successfully updated display name!") + self.__load_backup() return self @@ -172,7 +182,7 @@ class Account: All locally available accounts """ response = requests.post(self.urls["http"]["initialize"], json={ - "dataDir": self.__unix_folder + "dataDir": self.__docker_data_folder }) data: dict = response.json() accounts: list[dict] = data.get("accounts", []) @@ -368,9 +378,13 @@ class Account: # Group chats in RPC endpoint are chat type 3 data = self.__call_rpc("messaging", "activeChats") + result: Optional[list[dict]] = data.get("result", []) + if not result: + result = [] + group_chats = [ {"type": "group_chat", "id": active_chat["id"], "name": active_chat["name"]} - for active_chat in data.get("result", []) + for active_chat in result if active_chat["chatType"] == 3 ] return contacts + communities + group_chats @@ -422,10 +436,17 @@ class Account: while not finished: data = self.__call_rpc("messaging", "chatMessages", list(params.values())) result: dict[str, Union[str, list[dict]]] = data.get("result", {}) - if result["messages"] and not timestamp_keys: + messages: Optional[list[dict]] = result.get("messages") + cursor: Optional[str] = result.get("cursor") + if not cursor: + cursor = "" + if not messages: + messages = [] + + if messages and not timestamp_keys: timestamp_keys = [key for key in result["messages"][0].keys() if "timestamp" in key.lower()] - for message in result["messages"]: + for message in messages: point = { self.__camel_to_snake(key): value if key not in timestamp_keys else datetime.datetime.fromtimestamp(value / self.__timestamp_divisor) for key, value in message.items() @@ -440,8 +461,8 @@ class Account: all_messages.append(point) - if len(result["cursor"]) > 0: - params["cursor"] = result["cursor"] + if len(cursor) > 0: + params["cursor"] = cursor else: finished = True @@ -519,6 +540,23 @@ class Account: data = self.__call_rpc("messaging", "requestToJoinCommunity", params) return datetime.datetime.fromtimestamp(raw.get("requestedToJoinAt", datetime.datetime.now().timestamp())) + def backup(self) -> str: + """ + Create a `.bkp` (Backup) for the account. If the backup was not successful, a custom exception will be raised. + + Output: + - the Docker backup path (linked to a volume). The file name is unique per account. + """ + self.info + response = requests.post(self.urls["http"]["create_backup"]) + result: dict = response.json() + file_path = result.get("filePath") + + if not file_path or (isinstance(file_path, str) and len(file_path) == 0): + raise Exception(f"There was an error with creating a backup for {self.info['display_name']}") + + return file_path + def __start_messenger(self): """ Start the decentralized messaging service. @@ -546,6 +584,25 @@ class Account: """ return self.__call_rpc(prefix, method_name, params) + def __load_backup(self): + """ + Try to load every file in the Docker volume + when an account recover is done. + """ + for file_name in os.listdir(self.__backup_local_folder): + params = { + "filePath": os.path.join(self.__docker_backup_folder, file_name) + } + self.logger.info(f"Trying to load {file_name}") + response = requests.post(self.urls["http"]["load_backup"], json=params) + error: str = response.json().get("error", "") + if len(error) == 0: + self.__signal.get("history.request.completed") + self.logger.info(f"Successfully loaded file!") + break + + self.logger.warning(error) + def __call_rpc(self, prefix: str, method_name: str, params: Optional[Union[list, dict]] = None) -> dict: """ Make RPC calls to Status Backend @@ -623,4 +680,3 @@ class Account: raise ValueError("Display name can contain only A-Z, 0-9, hyphens (-), and underscores (_).") return True - diff --git a/bot/docs/account.md b/bot/docs/account.md index e1b4df6..67aaaf7 100644 --- a/bot/docs/account.md +++ b/bot/docs/account.md @@ -40,6 +40,14 @@ node-42 If a display name does not follow these rules, a **`ValueError`** will be raised by the account validation logic. +## Backups + +Backup files (`.bkp`) can be both created in [Status App](https://our.status.im/status-desktop-v2-35-local-backups-new-home-page-performance-boosts-and-more/) and the [Python SDK](./account.md#backup). Status Backend backup folder is exposed in a Docker volume so users can: + +- **Upload backup** - by dropping`.bkp` files in the `backups` folder locally (linked to Status Backend Docker container). Backups are automatically uploaded if a [`mnemonic` is provided during `login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone). +- **Create backup** - by using [`backup()`](./account.md#backup) or creating one in [Status App](https://our.status.im/status-desktop-v2-35-local-backups-new-home-page-performance-boosts-and-more/). + +**Note**: Status App will not automatically backup messages. This has to be manually overridden on the app. When using the Python SDK, the messages are automatically stored in the `.bkp` files. ## Methods @@ -54,7 +62,7 @@ An account can also be recovered if the `mnemonic` is passed. | `password` | `str` | Yes | Password used to encrypt the account | | `key_uid` | `str` | Yes* | Unique key identifier of the account. If provided, the account will be logged in directly using this identifier. If not provided, then you must use `display_name` and `password` to login. | | `display_name` | `str` | Yes* | Display name of the account. Used to resolve the `key_uid` if it is not provided, or to create a new account if one does not already exist. This field is required if an account needs to be recovered with `mnemonic`. | -| `mnemonic` | `str` | No | The mnemonic from [`info`](./account.md#info). Use this field with `password` and `display_name` to recover the account.
**Note**: You can pass a different `display_name` but that will be internal only. When an account is recovered setting [`display_name`](./account.md#display_name) can be buggy. Ideally when recovering the account, use the original `display_name` of the account. | +| `mnemonic` | `str` | No | The mnemonic from [`info`](./account.md#info). Use this field with `password` and `display_name` to recover the account. If you have [`.bkp`](./account.md#backup) files, in the backup Docker volume they will be automatically picked up and loaded.
**Note**: You can pass a different `display_name` but that will be internal only. When an account is recovered setting [`display_name`](./account.md#display_name) can be buggy. Ideally when recovering the account, use the original `display_name` of the account. | Returns the current `Account` instance, allowing method chaining. @@ -322,7 +330,25 @@ account.send_request_community( "https://status.app/c/community-invite-link" ) ``` +### `backup()` +Create a **local backup file** (`.bkp`) for the currently logged‑in account. The backup is generated by the Status Backend and stored inside the configured Docker backup volume. Each file is uniquely associated with an account. If the backup creation fails, an **exception will be raised**. + +Returns `str` representing the **Docker path** of the generated backup file. The returned path refers to the **Docker container path** where the backup was created. If the backup directory is mounted as a Docker volume, the file will also appear on the host machine in the mapped folder. + +```python +from bot import Account + +account = Account() +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +backup_path = account.backup() +print(f"Backup created at: {backup_path}") +``` ## Properties diff --git a/bot/signal.py b/bot/signal.py index 0e6d2f8..1ea75a8 100644 --- a/bot/signal.py +++ b/bot/signal.py @@ -1,5 +1,5 @@ from typing import Optional -import datetime, websocket, json, copy,queue, threading +import datetime, websocket, json, copy, queue, threading class Signal: """ diff --git a/docker-compose.yaml b/docker-compose.yaml index def6cce..7503df3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,6 @@ services: entrypoint: 'status-backend' command: '-address 0.0.0.0:8080' volumes: - - ./data-dir:/data-dir - ./backups:/root/.config/Status/backups networks: - status-bridge