diff --git a/bot/account.py b/bot/account.py index 0044a44..0b16a91 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, os, json, ast, shutil, eth_abi +import requests, datetime, re, logging, os, json, ast, shutil, eth_abi, shutil import pandas as pd from PIL import Image from PIL.JpegImagePlugin import JpegImageFile @@ -29,7 +29,7 @@ class Account: "transfer": "a9059cbb" # keccak256("transfer(address,uint256)")[:4] } - def __init__(self, domain: str = "localhost", port: int = 8080, is_secure: bool = False): + def __init__(self, domain: str = "localhost", port: int = 8080, is_secure: bool = False, backup_folder: Optional[str] = None): """ Work with your own Status App account @@ -37,15 +37,18 @@ class Account: - `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 + - `backup_folder` - where backup files will be created and stored """ # 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" + self.__backup_folder = backup_folder # 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") - os.makedirs(self.__backup_local_folder, exist_ok=True) + self.__backup_sdk_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), "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 @@ -798,7 +801,7 @@ class Account: 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. + - the file path of the backup. The name is unique per account. """ self.info response = requests.post(self.__urls["http"]["create_backup"]) @@ -808,6 +811,13 @@ class Account: 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']}") + file_name = os.path.basename(file_path) + sdk_file_path = os.path.join(self.__backup_sdk_folder, file_name) + file_path = sdk_file_path + if self.__backup_folder: + file_path = os.path.join(self.__backup_folder, file_name) + shutil.move(sdk_file_path, file_path) + return file_path def get_tokens(self) -> pd.DataFrame: @@ -1096,13 +1106,26 @@ class Account: 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): + folder = self.__backup_folder if self.__backup_folder else self.__backup_sdk_folder + for file_name in os.listdir(folder): + if not file_name.endswith(".bkp"): + continue + + file_path = os.path.join(folder, file_name) + sdk_file_path = os.path.join(self.__backup_sdk_folder, file_name) + if sdk_file_path != file_path: + shutil.copy(file_path, sdk_file_path) + params = { "filePath": os.path.join(self.__docker_backup_folder, file_name).replace("\\", "/") } - self.logger.info(f"Trying to load {file_name}") + self.logger.info(f"Trying to load {file_path}") response = requests.post(self.__urls["http"]["load_backup"], json=params) error: str = response.json().get("error", "") + + if sdk_file_path != file_path: + os.remove(sdk_file_path) + if len(error) == 0: self.__signal.get("messages.new") self.logger.info(f"Successfully loaded file!") diff --git a/docs/account.md b/docs/account.md index 35ed053..3b038cd 100644 --- a/docs/account.md +++ b/docs/account.md @@ -62,6 +62,50 @@ Wallet features are optional and can be omitted if not required for your use cas ![Status App Wallet](./images/wallet.png) +## `Account(domain="localhost", port=8080, is_secure=False, backup_folder=None)` + +Create a new `Account` instance ready to be logged in. The constructor wires the SDK to a running [Status Backend](https://github.com/status-im/status-go) at the given `domain` and `port`, prepares the local `assets/` folder (used for image uploads, such as the [profile picture](./account.md#profile_picture)) and `backups/` folder (used for [backup uploads](./account.md#backups) and recovery). + +| Name | Type | Required | Description | +|-----|-----|-----|-------------| +| `domain` | `str` | No | Domain where Status Backend is reachable. Defaults to `localhost` when running through [`launch_docker_container`](./utils.md#launch_docker_container) on the same machine. **Use the container name when the SDK runs inside the same Docker network as Status Backend.** | +| `port` | `int` | No | Port exposed by Status Backend. Defaults to `8080`. Verify the value in `docker-compose.yaml` if you have customized the setup. | +| `is_secure` | `bool` | No | When `True`, the SDK communicates over `https`; otherwise `http` is used. Defaults to `False`. | +| `backup_folder` | `str` | No | Absolute path on the host machine where `.bkp` files will be stored and loaded from. If not provided, the SDK's own `backups/` folder is used. See [Backups](./account.md#backups). | + +The constructor does not log into any account on its own - call [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennonecoingecko_api_keynone) afterwards. To discover what accounts already exist in the configured data directory, use the [`available_accounts`](./account.md#available_accounts) property, which is also populated automatically during initialization. + +Default setup (localhost, port 8080, http): + +```python +from bot import Account + +account = Account() +``` + +Use a custom backup folder: + +```python +from bot import Account + +account = Account(backup_folder="C:/Users/me/status-backups") +``` + +Connect to a Status Backend running on a different host or port: + +```python +from bot import Account + +account = Account( + domain="status-backend.internal", + port=9090, + is_secure=True +) +``` + +**Note**: Status Backend must be running before initializing `Account`. You can launch the backend container with [`launch_docker_container`](./utils.md#launch_docker_container). If the backend is not reachable on `domain:port`, calls to [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennonecoingecko_api_keynone) will fail. + +**Note**: When `backup_folder` is set, [`backup`](./account.md#backup) moves the generated `.bkp` file out of the SDK's internal `backups/` folder into the provided path, and recovery via `mnemonic` will look in this same folder for `.bkp` files to auto-load. Make sure the folder exists and is writable. ## Methods @@ -697,7 +741,7 @@ tx_hash = account.send_transaction( **Note**: This is a wallet method, so it requires both `infura_token` and `coingecko_api_key` to be provided in [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennonecoingecko_api_keynone). If either is missing, an exception will be raised when this method is called. -**Note**: The sender and receiver must be on the **same chain**. Cross-chain transfers are not supported by this method — set `chain_id` to the chain where the funds currently exist. +**Note**: The sender and receiver must be on the **same chain**. Cross-chain transfers are not supported by this method - set `chain_id` to the chain where the funds currently exist. **Note**: An **exception will be raised** when: - the `symbol` (or contract address) does not exist on the given `chain_id` @@ -908,8 +952,8 @@ The property exists in `Account` because signals require an **active logged‑in The property exposes two primary methods: -- `signal.get()` — fetch a single event. If the event is not found, you may end up in an infinite loop. -- `signal.listen()` — stream events continuously. Example usage of this is found in [`listen_messages()`](./account.md#listen_messages) +- `signal.get()` - fetch a single event. If the event is not found, you may end up in an infinite loop. +- `signal.listen()` - stream events continuously. Example usage of this is found in [`listen_messages()`](./account.md#listen_messages) ### `logger` @@ -1093,8 +1137,8 @@ print(community_members.head().to_markdown(index=False)) #### `chats` 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** +- [`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).