diff --git a/assets/profile.jpg b/assets/profile.jpg new file mode 100644 index 0000000..616f769 Binary files /dev/null and b/assets/profile.jpg differ diff --git a/bot/account.py b/bot/account.py index 8647b4b..8a1fd8a 100644 --- a/bot/account.py +++ b/bot/account.py @@ -1,8 +1,11 @@ from typing import Optional, Union, Generator, Any -import requests, datetime, re, logging, os, json, ast +import requests, datetime, re, logging, os, json, ast, shutil import pandas as pd +from PIL import Image +from PIL.JpegImagePlugin import JpegImageFile from .signal import Signal from .logger import Logger + class Account: # Enum mappings from original wakuext.py @@ -19,7 +22,8 @@ class Account: "messaging": "wakuext", "urls": "sharedurls", "wallets": "wallet", - "account": "accounts" + "account": "accounts", + "identity": "multiaccounts" } def __init__(self, domain: str = "localhost", port: int = 8080, is_secure: bool = False): """ @@ -38,6 +42,13 @@ class Account: # 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) + # 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(os.path.dirname(os.path.dirname(__file__)), "assets") + os.makedirs(self.__assets_local_folder, exist_ok=True) + self.__logger = Logger() self.__timestamp_divisor = 1_000 self.__kd_iterations = 256000 @@ -284,6 +295,69 @@ class Account: def bio(self): self.bio = "" + @property + def profile_picture(self) -> Optional[JpegImageFile]: + """ + Get current profile picture + """ + files = [ + os.path.join(self.__assets_local_folder, f) + for f in os.listdir(self.__assets_local_folder) + if os.path.isfile(os.path.join(self.__assets_local_folder, f)) + ] + if not files: + return + + latest_file_path = max(files, key=os.path.getctime) + for file in files: + if file == latest_file_path: + continue + os.remove(file) + + return Image.open(latest_file_path) + + @profile_picture.setter + def profile_picture(self, file_path: str): + + if not isinstance(file_path, str): + return + + if not os.path.exists(file_path): + raise Exception(f"File path {file_path} does not exist") + + suffix = (".jpg", ".png", ".jpeg") + if not file_path.endswith(suffix): + raise Exception(f"Image must be one of the following extensions: {suffix}") + + file_name = os.path.basename(file_path) + + 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 + 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 == file_path: + continue + os.remove(current_file_path) + + try: + shutil.copy(file_path, asset_file_path) + except shutil.SameFileError: + self.logger.info("File is already in asset path") + + img = Image.open(asset_file_path) + params = [ + self.info["key_uid"], + docker_file_path, + 0, + 0, + *img.size + ] + self.logger.info(f"Setting {file_path} as profile picture") + self.__call_rpc("identity", "storeIdentityImage", params) + self.logger.info(f"Profile picture has been updated!") + @property def contacts(self) -> dict[str, dict]: """ diff --git a/bot/requirements.txt b/bot/requirements.txt index 59132fd..7ab5331 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -2,3 +2,4 @@ requests websocket-client websockets pandas +pillow diff --git a/docker-compose.yaml b/docker-compose.yaml index fc456b3..b30370b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,6 +14,7 @@ services: command: '-address 0.0.0.0:8080' volumes: - ./backups:/root/.config/Status/backups + - ./assets:/assets:ro networks: - status-bridge healthcheck: diff --git a/docs/account.md b/docs/account.md index 7efecb6..8878bb3 100644 --- a/docs/account.md +++ b/docs/account.md @@ -733,6 +733,47 @@ account.login(**params) del account.bio ``` +### `profile_picture` + +Get or update the **profile picture** of the currently logged‑in account. The image is the same one shown on the user's profile in Status App. + +Returns `PIL.Image.Image` when reading the property, or `None` if no profile picture has been set. + +```python +from bot import Account + +account = Account() +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +# Read the current profile picture +image = account.profile_picture +if image: + image.show() +``` + +The file path assigned to `profile_picture` will be automatically set as the latest profile picture in Status App. If the given file does not exist or the extension is not supported, an **exception will be raised**. Supported image formats are `.jpg`, `.jpeg` and `.png`. + +```python +from bot import Account + +account = Account() +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +# Update the profile picture +account.profile_picture = "./full_path/to/my_image.png" +account.profile_picture.show() +``` + +When a new profile picture is set, any previous image in the **assets** folder is removed. The image is also copied into the Status Backend Docker volume so it is picked up by the backend when updating the account identity. + ### `signal` The property exists in `Account` because signals require an **active logged‑in session**. Attempting to use signals before calling `login()` will raise an exception. Signals are low‑level events emitted by the Status Backend. Examples include: diff --git a/monitor.py b/monitor.py index 0fbeb78..7bc6ddd 100644 --- a/monitor.py +++ b/monitor.py @@ -184,6 +184,7 @@ def create_bot(config: dict) -> Account: if account.info["compressed_key"] != config["bot"]["compressed_key"]: raise Exception("Target compressed key and logged in compressed key are different") + account.profile_picture = os.path.join(os.path.dirname(__file__), "assets", "profile.jpg") account.logger.info(f"Account Information:\nCompressed Key: {account.info['compressed_key']}\nPublic Key: {account.info['public_key']}\nURL: {account.info['url']}") return account