account: load backup

Related to https://github.com/status-im/status-bot/issues/7

- Volume for data directory has been swapped with backup one
This commit is contained in:
Nick Ninov
2026-03-14 23:36:34 +00:00
parent 2b2d5c0a1d
commit 994e5c8243
4 changed files with 98 additions and 17 deletions
+70 -14
View File
@@ -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
+27 -1
View File
@@ -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.<br>**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.<br>**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 loggedin 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
+1 -1
View File
@@ -1,5 +1,5 @@
from typing import Optional
import datetime, websocket, json, copy,queue, threading
import datetime, websocket, json, copy, queue, threading
class Signal:
"""
-1
View File
@@ -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