From f0507aae82b2fca466a77aa72a6a0d2296bd0383 Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 09:56:04 +0200 Subject: [PATCH 1/7] refactor: move models in directory Signed-off-by: apentori --- status_sdk/__init__.py | 2 +- status_sdk/account.py | 5 +- status_sdk/models/__init__.py | 8 ++ status_sdk/models/community.py | 18 ++++ status_sdk/{models.py => models/message.py} | 97 +++++++-------------- status_sdk/models/payement.py | 24 +++++ 6 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 status_sdk/models/__init__.py create mode 100644 status_sdk/models/community.py rename status_sdk/{models.py => models/message.py} (82%) create mode 100644 status_sdk/models/payement.py diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index 35f633d..f63f6f9 100644 --- a/status_sdk/__init__.py +++ b/status_sdk/__init__.py @@ -4,7 +4,7 @@ from .group_chat import GroupChat from .community.base import Community, Channel from .utils import launch_docker_container, build_and_launch, download_build_and_launch -from . import exceptions, models +from . import exceptions try: __version__ = _version("status-sdk") diff --git a/status_sdk/account.py b/status_sdk/account.py index f14a05b..3a16d70 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -8,7 +8,7 @@ from PIL import Image from PIL.JpegImagePlugin import JpegImageFile from PIL.PngImagePlugin import PngImageFile -from . import constants, models +from status_sdk import models, constants from .signal import Signal class Account: @@ -952,6 +952,9 @@ def listen_messages(self) -> Generator[models.Message, None, None]: 2, # Sticker 4, # Emojis 7, # Image + 11, # Contact Request + 15, # Send Contact Request + 17, # Remove Contact 18, # Bridged Message ] albums: dict[str, list[dict]] = {} diff --git a/status_sdk/models/__init__.py b/status_sdk/models/__init__.py new file mode 100644 index 0000000..86564ab --- /dev/null +++ b/status_sdk/models/__init__.py @@ -0,0 +1,8 @@ +from .payement import PaymentRequest +from .message import Message, BridgedContent, ContactRequest +__all__ = [ + "PaymentRequest", + "ContactRequest", + "Message", + "BridgedContent" +] diff --git a/status_sdk/models/community.py b/status_sdk/models/community.py new file mode 100644 index 0000000..1a609a5 --- /dev/null +++ b/status_sdk/models/community.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass +from typing import Optional + +@dataclass +class CommunityRequest: + id: str + public_key: str + pending: bool = False + reject: bool = False + accept: bool = False + cancel: bool = False + +@dataclass +class TokenPermission: + symbol: str + amount: float + chain_id: int = 1 + address: Optional[str] = None diff --git a/status_sdk/models.py b/status_sdk/models/message.py similarity index 82% rename from status_sdk/models.py rename to status_sdk/models/message.py index 4fcd774..a2d727f 100644 --- a/status_sdk/models.py +++ b/status_sdk/models/message.py @@ -1,6 +1,10 @@ from dataclasses import dataclass, field from typing import Self, Optional, Union -import datetime, uuid +import datetime +import uuid + +from status_sdk.models import PaymentRequest + @dataclass class ContactRequest: @@ -14,56 +18,6 @@ def __post_init__(self): if not (self.incoming or self.accepted or self.removed): raise ValueError(f"A {self.__class__.__name__} must be `incoming`, `accepted` or `removed`") -@dataclass -class PaymentRequest: - to_address: str - token_symbol: str - token_address: str - chain_id: int - amount: str - - @classmethod - def from_raw(cls, raw: dict) -> Self: - chain_id, token_address = raw["tokenKey"].split("-") - params = { - "to_address": raw["receiver"], - "token_symbol": raw["symbol"], - "token_address": token_address, - "chain_id": int(chain_id), - "amount": raw["amount"] - } - return cls(**params) - -@dataclass -class BridgedContent: - message: str - name: Optional[str] = None - username: Optional[str] = None - user_id: Optional[Union[str, int]] = None - reply_to_message_id: Optional[Union[str, int]] = None - message_id: Optional[Union[str, int]] = None - image_url: Optional[str] = None - - def __post_init__(self): - to_string = lambda value: str(value) if isinstance(value, int) else value - - self.user_id = to_string(self.user_id) - self.reply_to_message_id = to_string(self.reply_to_message_id) - self.message_id = to_string(self.message_id) - - @property - def status_go_params(self) -> dict: - content = { - "bridgeName": self.name or "Unknown", - "userName": self.username or "Anon", - "userAvatar": self.image_url or "", - "userID": self.user_id or str(uuid.uuid4()), - "content": self.message, - "messageID": self.message_id or str(uuid.uuid4()), - "parentMessageID": self.reply_to_message_id or "", - } - return content - @dataclass class Message: id: str @@ -139,18 +93,33 @@ def from_raw(cls, raw: dict) -> Self: return cls(**params) -@dataclass -class CommunityRequest: - id: str - public_key: str - pending: bool = False - reject: bool = False - accept: bool = False - cancel: bool = False @dataclass -class TokenPermission: - symbol: str - amount: float - chain_id: int = 1 - address: Optional[str] = None +class BridgedContent: + message: str + name: Optional[str] = None + username: Optional[str] = None + user_id: Optional[Union[str, int]] = None + reply_to_message_id: Optional[Union[str, int]] = None + message_id: Optional[Union[str, int]] = None + image_url: Optional[str] = None + + def __post_init__(self): + to_string = lambda value: str(value) if isinstance(value, int) else value + + self.user_id = to_string(self.user_id) + self.reply_to_message_id = to_string(self.reply_to_message_id) + self.message_id = to_string(self.message_id) + + @property + def status_go_params(self) -> dict: + content = { + "bridgeName": self.name or "Unknown", + "userName": self.username or "Anon", + "userAvatar": self.image_url or "", + "userID": self.user_id or str(uuid.uuid4()), + "content": self.message, + "messageID": self.message_id or str(uuid.uuid4()), + "parentMessageID": self.reply_to_message_id or "", + } + return content diff --git a/status_sdk/models/payement.py b/status_sdk/models/payement.py new file mode 100644 index 0000000..4b75638 --- /dev/null +++ b/status_sdk/models/payement.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import Self + +@dataclass +class PaymentRequest: + to_address: str + token_symbol: str + token_address: str + chain_id: int + amount: str + + @classmethod + def from_raw(cls, raw: dict) -> Self: + chain_id, token_address = raw["tokenKey"].split("-") + params = { + "to_address": raw["receiver"], + "token_symbol": raw["symbol"], + "token_address": token_address, + "chain_id": int(chain_id), + "amount": raw["amount"] + } + return cls(**params) + + From eb8aa13425a4d5051ba7d180e7d2e8ec09d6cee8 Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 10:05:14 +0200 Subject: [PATCH 2/7] fix import Signed-off-by: apentori --- status_sdk/__init__.py | 15 ++++++++++++--- status_sdk/account.py | 5 ++--- status_sdk/community/__init__.py | 6 ++++++ status_sdk/community/base.py | 7 +++---- status_sdk/community/channel.py | 5 ++--- status_sdk/group_chat.py | 3 +-- status_sdk/signal.py | 2 +- status_sdk/utils/__init__.py | 12 ++++++++++++ status_sdk/utils/builds.py | 2 +- status_sdk/utils/community.py | 3 +-- status_sdk/utils/external.py | 4 ++-- 11 files changed, 43 insertions(+), 21 deletions(-) diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index f63f6f9..7f54e71 100644 --- a/status_sdk/__init__.py +++ b/status_sdk/__init__.py @@ -1,10 +1,19 @@ from importlib.metadata import PackageNotFoundError, version as _version +from . import exceptions from .account import Account -from .group_chat import GroupChat from .community.base import Community, Channel -from .utils import launch_docker_container, build_and_launch, download_build_and_launch -from . import exceptions +from .group_chat import GroupChat +from .signal import Signal + +__all__ = [ + "Account", + "GroupChat", + "Community", + "Channel", + "exceptions", + "Signal" +] try: __version__ = _version("status-sdk") diff --git a/status_sdk/account.py b/status_sdk/account.py index 3a16d70..280eed0 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -2,14 +2,13 @@ import uuid as uuid_lib import requests, datetime, re, logging, os, json, ast, shutil, eth_abi, shutil import pandas as pd -from . import exceptions +from status_sdk import exceptions from Crypto.Hash import keccak from io import BytesIO from PIL import Image from PIL.JpegImagePlugin import JpegImageFile from PIL.PngImagePlugin import PngImageFile -from status_sdk import models, constants -from .signal import Signal +from status_sdk import models, constants, Signal class Account: # Enum mappings from original wakuext.py diff --git a/status_sdk/community/__init__.py b/status_sdk/community/__init__.py index a537ad8..365a162 100644 --- a/status_sdk/community/__init__.py +++ b/status_sdk/community/__init__.py @@ -1 +1,7 @@ from .base import Community +from .channel import Channel + +__all__ = [ + "Community", + "Channel" +] diff --git a/status_sdk/community/base.py b/status_sdk/community/base.py index 0763320..4c49784 100644 --- a/status_sdk/community/base.py +++ b/status_sdk/community/base.py @@ -1,8 +1,7 @@ -from ..account import Account -from .. import exceptions, models -from .channel import Channel +from status_sdk import exceptions, models, Account +from status_sdk.community import Channel from typing import Union, Optional, Generator -from ..utils import community as utils +from status_sdk.utils import community as utils import pandas as pd import datetime, copy, os, shutil diff --git a/status_sdk/community/channel.py b/status_sdk/community/channel.py index 87fa5cf..c0c8e1e 100644 --- a/status_sdk/community/channel.py +++ b/status_sdk/community/channel.py @@ -1,6 +1,5 @@ -from ..account import Account -from .. import exceptions, models -from ..utils import community as utils +from status_sdk import exceptions, models, Account +from status_sdk.utils import community as utils from typing import Union, Optional import pandas as pd import re, datetime, random diff --git a/status_sdk/group_chat.py b/status_sdk/group_chat.py index 2fd1267..d1aad9f 100644 --- a/status_sdk/group_chat.py +++ b/status_sdk/group_chat.py @@ -1,5 +1,4 @@ -from .account import Account -from . import exceptions +from status_sdk import Account, exceptions from typing import Union, Optional import pandas as pd import re, datetime diff --git a/status_sdk/signal.py b/status_sdk/signal.py index 67a2ee6..e8c0493 100644 --- a/status_sdk/signal.py +++ b/status_sdk/signal.py @@ -1,6 +1,6 @@ from typing import Optional, Callable, Union import datetime, websocket, json, copy, queue, threading, time -from . import exceptions +from status_sdk import exceptions class Signal: """ diff --git a/status_sdk/utils/__init__.py b/status_sdk/utils/__init__.py index 0cc89bb..b3fd1ea 100644 --- a/status_sdk/utils/__init__.py +++ b/status_sdk/utils/__init__.py @@ -1 +1,13 @@ from .external import launch_docker_container, build_and_launch, download_build_and_launch +from .community import get_channel_permissions, get_community_info +from .builds import fix_nix_build_paths, launch_build + +__all__ = [ + "launch_docker_container", + "build_and_launch", + "download_build_and_launch", + "get_channel_permissions", + "get_community_info", + "fix_nix_build_paths", + "launch_build" +] diff --git a/status_sdk/utils/builds.py b/status_sdk/utils/builds.py index 11e0652..99fba2d 100644 --- a/status_sdk/utils/builds.py +++ b/status_sdk/utils/builds.py @@ -1,5 +1,5 @@ import shutil, os, subprocess, time, logging, requests -from .. import exceptions +from status_sdk import exceptions def fix_nix_build_paths(binary_path: str) -> None: """ diff --git a/status_sdk/utils/community.py b/status_sdk/utils/community.py index 7d81ad0..dc0f403 100644 --- a/status_sdk/utils/community.py +++ b/status_sdk/utils/community.py @@ -1,5 +1,4 @@ -from ..account import Account -from .. import exceptions +from status_sdk import Account, exceptions from typing import Optional import pandas as pd diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index f4ec6f6..3b09174 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -5,8 +5,8 @@ from pathlib import Path from platform import machine from typing import Optional -from .. import exceptions -from . import builds +from status_sdk import exceptions +from status_sdk.utils import builds def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, platform: str = "linux/amd64", data_folder: Optional[str] = None): """ From 3327091f22f4a076a2249d92b29fb1155334b3c3 Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 12:40:40 +0200 Subject: [PATCH 3/7] fix: account and message issues * add missing param for messages * Listen to all messages * handle sending multiple images Signed-off-by: apentori --- CHANGELOG.md | 13 ++++++++ status_sdk/account.py | 63 ++++++++++++++++------------------- status_sdk/models/__init__.py | 5 +-- status_sdk/models/message.py | 41 ++++++++++++++++------- 4 files changed, 74 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56c7e1..86cf4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ 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.3.0 - + +### Changed + +- `def listen_messages()` now listen to all messages by default + - Possibility to listen to specific messages with the parameters `listen_types` +- `def send_images()` accept list of `file_path` instead of one. + - `def __send_content()` accept a list of `file_path` to send at the same time. + +### Added + +* Completed Message parameters + ## [1.2.1] - 2026-09-25 ### Changed diff --git a/status_sdk/account.py b/status_sdk/account.py index 280eed0..d776ab4 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -671,7 +671,7 @@ def __getitem__(self, key: str) -> pd.DataFrame: return balance.copy() - def send_image(self, chat_id: str, file_path: str, message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str: + def send_image(self, chat_id: str, file_path: list[str], message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str: """ Send an image to the given chat. @@ -761,7 +761,14 @@ def send_emoji_reaction(self, message_id: str, emoji_shortname: str, chat_id: Op if error: raise exceptions.ChatNotFoundError(error.get("message")) - def __send_content(self, chat_id: str, message: Optional[str] = None, reply_to_message_id: Optional[str] = None, image_path: Optional[str] = None, bridged_content: Optional[models.BridgedContent] = None) -> str: + def __send_content( + self, + chat_id: str, + message: Optional[str] = None, + reply_to_message_id: Optional[str] = None, + image_path: list[str] = [], + bridged_content: Optional[models.BridgedContent] = None + ) -> str: """ Send a message with optional media attached to the given chat. @@ -769,7 +776,7 @@ def __send_content(self, chat_id: str, message: Optional[str] = None, reply_to_m - `chat_id` - the chat ID can be found in `self.chats` - `message` - the text that will be sent. Optional when media is attached, so an image can be sent on its own - `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message. - - `image_path` - local path to the image to attach. + - `image_path` - list of local path to the images to attach. Output: - The message ID @@ -800,21 +807,11 @@ def validate_path(path: str): "contentType": 1, # Normal message "responseTo": reply_to_message_id if reply_to_message_id else "" } - # Content status-go key name for RPC request - content_key = None - # Non `status-im/status-go` path - file_path = None # File path in `status-im/status-go` docker_file_path = [self.__docker_asset_folder] # subfolder name in ./assets/ (if necessary) - asset_subfolder = None + asset_subfolder = "images" - if image_path: - image_path = validate_path(image_path) - file_path = image_path - msg_params["contentType"] = 7 - content_key = "imagePath" - asset_subfolder = "images" if bridged_content: msg_params["text"] = "" @@ -823,21 +820,25 @@ def validate_path(path: str): if asset_subfolder: docker_file_path.append(asset_subfolder) - + content_image_path = "" asset_file_path = None - if file_path: - docker_file_path.append(os.path.basename(file_path)) - asset_file_path = os.path.join(self.__assets_local_folder, asset_subfolder, os.path.basename(file_path)) + for path in image_path: + validate_path(path) + docker_file_path.append(os.path.basename(path)) + asset_file_path = os.path.join( + self.__assets_local_folder, asset_subfolder, os.path.basename(path)) os.makedirs(os.path.dirname(asset_file_path), exist_ok=True) if os.path.exists(asset_file_path): os.remove(asset_file_path) - shutil.copy(file_path, asset_file_path) + shutil.copy(p, asset_file_path) + content_image_path = f"{content_image_path}//{docker_file_path}" - if content_key: + if len(image_path): + msg_params["contentType"] = 7 msg_params.update({ - content_key: "/".join(docker_file_path) - }) + "imagePath": content_image_path + }) if msg_params["contentType"] == 1 and len(msg_params["text"]) == 0: raise exceptions.SendContentError("Cannot send empty text messages") @@ -942,20 +943,13 @@ def listen_message_mentions(self) -> Generator[models.Message, None, None]: if mention_everyone in current_text or account_mention in current_text: yield models.Message.from_raw(event["body"]["message"]) - def listen_messages(self) -> Generator[models.Message, None, None]: + def listen_messages(self, listen_types: list[int] = []) -> Generator[models.Message, None, None]: """ Listen for new **RAW** messages continuously. Can be used for real time processing. + Parameters: + - listen_types: List(int), limit the messages to only the specified type. + Default all messages """ - ALLOWED_CONTENT_TYPES = [ - 1, # Text - 2, # Sticker - 4, # Emojis - 7, # Image - 11, # Contact Request - 15, # Send Contact Request - 17, # Remove Contact - 18, # Bridged Message - ] albums: dict[str, list[dict]] = {} processed: list[str] = [] SIZE = 100 @@ -963,7 +957,8 @@ def listen_messages(self) -> Generator[models.Message, None, None]: event: dict = message.get("event", {}) raw_messages: list[dict] = event.get("messages", []) for raw in raw_messages: - if raw["contentType"] not in ALLOWED_CONTENT_TYPES or raw["id"] in processed: + is_message_type_allowed = len(listen_types) == 0 or raw["contentType"] in listen_types + if not is_message_type_allowed or raw["id"] in processed: continue if not raw.get("albumId"): diff --git a/status_sdk/models/__init__.py b/status_sdk/models/__init__.py index 86564ab..b615032 100644 --- a/status_sdk/models/__init__.py +++ b/status_sdk/models/__init__.py @@ -1,8 +1,9 @@ from .payement import PaymentRequest -from .message import Message, BridgedContent, ContactRequest +from .message import Message, BridgedContent, ContactRequest, MessageContentTypeEnum __all__ = [ "PaymentRequest", "ContactRequest", "Message", - "BridgedContent" + "BridgedContent", + "MessageContentTypeEnum", ] diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index a2d727f..47c963d 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from enum import Enum from typing import Self, Optional, Union import datetime import uuid @@ -6,6 +7,16 @@ from status_sdk.models import PaymentRequest +class MessageContentTypeEnum: + TEXT = 1, + STICKER = 2, + EMOJIS = 4, + IMAGE = 7, + CONTACT_REQUEST = 11, + SENT_CONTACT_REQUEST = 15, + REMOVED_CONTACT = 17, + BRIDGED_MESSAGE = 18 + @dataclass class ContactRequest: id: str @@ -23,13 +34,18 @@ class Message: id: str chat_id: str content: str + text: str content_type: str from_public_key: str + + compressed_key: str timestamp: datetime.datetime chat_type: str + payload_urls: list[str] source: str reply_id: Optional[str] = None bridge_id: Optional[str] = None + response_to: Optional[str] = None payment_requests: list[PaymentRequest] = field(default_factory=list) @classmethod @@ -38,23 +54,26 @@ def from_raw(cls, raw: dict) -> Self: msg_type: int = raw["messageType"] params = { "id": raw["id"], + "text": raw["text"], "chat_id": raw["chatId"], "from_public_key": raw["from"], + "compressed_key": raw["compressed_key"], "timestamp": datetime.datetime.fromtimestamp(raw["whisperTimestamp"] / 1_000), - "source": raw.get("bridgeMessage", {}).get("bridgeName", "status") + "source": raw.get("bridgeMessage", {}).get("bridgealloweName", "status"), + "message_type": raw['contentType'], } if len(raw["responseTo"]) > 0: params["reply_id"] = raw["responseTo"] - if msg_type == 5: - params["chat_type"] = "community" - - elif msg_type == 1: + if msg_type == 1: params["chat_type"] = "private" - elif msg_type in [2, 3]: params["chat_type"] = "group" + elif msg_type == 5: + params["chat_type"] = "community" + else: + params["chat_type"] = "unknown" # Text & Emojis if content_type in [1, 4]: @@ -64,15 +83,12 @@ def from_raw(cls, raw: dict) -> Self: elif content_type == 2: params["content"] = raw["sticker"]["url"] params["content_type"] = "sticker" + params["payload_urls"].append(raw["sticker"]["url"]) # Image elif content_type == 7: if isinstance(raw["image"], str): raw["image"] = [raw["image"]] - - img_paths: list[str] = raw["image"] - text = raw["text"] - caption = f"{text}\n\n" if len(text) > 0 else "" - params["content"] = caption + "\n".join(img_paths) + params["payload_urls"] = [raw["image"]] params["content_type"] = "image" # Bridged Message elif content_type == 18: @@ -83,7 +99,8 @@ def from_raw(cls, raw: dict) -> Self: if isinstance(reply_id, str) and len(reply_id) == 0: reply_id = None params["reply_id"] = reply_id - + if raw["responseTo"]: + params["response_to"] = raw["responseTo"] payments = raw.get("paymentRequests", []) if payments: params["payment_requests"] = [ From f5cb3970e46acd64ddfdcca6f885d63ae0f09ed0 Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 15:41:44 +0200 Subject: [PATCH 4/7] fix import Signed-off-by: apentori --- status_sdk/account.py | 23 ++++++++++++----------- status_sdk/community/base.py | 7 ++++--- status_sdk/community/channel.py | 7 ++++--- status_sdk/models/__init__.py | 3 +++ 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/status_sdk/account.py b/status_sdk/account.py index d776ab4..610c04c 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -8,7 +8,8 @@ from PIL import Image from PIL.JpegImagePlugin import JpegImageFile from PIL.PngImagePlugin import PngImageFile -from status_sdk import models, constants, Signal +from status_sdk import constants, Signal +from status_sdk.models import Message, BridgedContent, ContactRequest class Account: # Enum mappings from original wakuext.py @@ -717,7 +718,7 @@ def send_bridged_message(self, chat_id: str, message: str, name: Optional[str] = Output: - The message ID in Status App """ - content = models.BridgedContent( + content = BridgedContent( message=message, name=name, username=username, @@ -767,7 +768,7 @@ def __send_content( message: Optional[str] = None, reply_to_message_id: Optional[str] = None, image_path: list[str] = [], - bridged_content: Optional[models.BridgedContent] = None + bridged_content: Optional[BridgedContent] = None ) -> str: """ Send a message with optional media attached to the given chat. @@ -891,7 +892,7 @@ def delete_message(self, id: str) -> bool: return not any(errors) if errors else False - def listen_contact_requests(self) -> Generator[models.ContactRequest, None, None]: + def listen_contact_requests(self) -> Generator[ContactRequest, None, None]: """ Listen for incoming contact requests and for contact requests that were accepted. Can be used for real time processing. """ @@ -905,7 +906,7 @@ def listen_contact_requests(self) -> Generator[models.ContactRequest, None, None if message["type"] == "local-notifications": category = event.get("category") if category == "contactRequest": - yield models.ContactRequest( + yield ContactRequest( message["event"]["body"]["message"]["id"], message["event"]["body"]["message"]["from"], incoming=True @@ -924,9 +925,9 @@ def listen_contact_requests(self) -> Generator[models.ContactRequest, None, None "public_key": public_key, key_mapping[message["contentType"]]: True } - yield models.ContactRequest(**params) + yield ContactRequest(**params) - def listen_message_mentions(self) -> Generator[models.Message, None, None]: + def listen_message_mentions(self) -> Generator[Message, None, None]: """ Listen for `@0xpublic-key` mentions. Can be used for real time processing. """ @@ -941,9 +942,9 @@ def listen_message_mentions(self) -> Generator[models.Message, None, None]: current_text: str = event["body"]["message"]["text"] if mention_everyone in current_text or account_mention in current_text: - yield models.Message.from_raw(event["body"]["message"]) + yield Message.from_raw(event["body"]["message"]) - def listen_messages(self, listen_types: list[int] = []) -> Generator[models.Message, None, None]: + def listen_messages(self, listen_types: list[int] = []) -> Generator[Message, None, None]: """ Listen for new **RAW** messages continuously. Can be used for real time processing. Parameters: @@ -962,7 +963,7 @@ def listen_messages(self, listen_types: list[int] = []) -> Generator[models.Mess continue if not raw.get("albumId"): - yield models.Message.from_raw(raw) + yield Message.from_raw(raw) processed.append(raw["id"]) continue @@ -978,7 +979,7 @@ def listen_messages(self, listen_types: list[int] = []) -> Generator[models.Mess point["image"] for point in albums[album_id] ] - yield models.Message.from_raw(raw) + yield Message.from_raw(raw) processed.append(raw["id"]) albums.pop(album_id) diff --git a/status_sdk/community/base.py b/status_sdk/community/base.py index 4c49784..1278431 100644 --- a/status_sdk/community/base.py +++ b/status_sdk/community/base.py @@ -1,4 +1,5 @@ -from status_sdk import exceptions, models, Account +from status_sdk import exceptions, Account +from status_sdk.models import CommunityRequest from status_sdk.community import Channel from typing import Union, Optional, Generator from status_sdk.utils import community as utils @@ -281,7 +282,7 @@ def delete_channel(self, channel_name: str): params = [self.id, channel.id.replace(self.id, "")] self.__account._call_rpc("messaging", "deleteCommunityChat", params) - def listen_requests(self) -> Generator[models.CommunityRequest, None, None]: + def listen_requests(self) -> Generator[CommunityRequest, None, None]: """ Listen for commnunity requests """ @@ -305,7 +306,7 @@ def listen_requests(self) -> Generator[models.CommunityRequest, None, None]: "public_key": request["publicKey"], state: True } - yield models.CommunityRequest(**params) + yield CommunityRequest(**params) @property def categories(self) -> dict[str, str]: diff --git a/status_sdk/community/channel.py b/status_sdk/community/channel.py index c0c8e1e..7c3115e 100644 --- a/status_sdk/community/channel.py +++ b/status_sdk/community/channel.py @@ -1,5 +1,6 @@ -from status_sdk import exceptions, models, Account +from status_sdk import exceptions, Account from status_sdk.utils import community as utils +from status_sdk.models import TokenPermission from typing import Union, Optional import pandas as pd import re, datetime, random @@ -341,7 +342,7 @@ def delete_message(self, id: str) -> bool: self.name return self.__account.delete_message(id) - def add_permission(self, permission: str, tokens: Optional[Union[list[models.TokenPermission], models.TokenPermission]] = None): + def add_permission(self, permission: str, tokens: Optional[Union[list[TokenPermission], TokenPermission]] = None): """ Add a new permission for the channel @@ -359,7 +360,7 @@ def add_permission(self, permission: str, tokens: Optional[Union[list[models.Tok token_criteria = [] if tokens: - if isinstance(tokens, models.TokenPermission): + if isinstance(tokens, TokenPermission): tokens = [tokens] available_tokens = self.__account.get_tokens() diff --git a/status_sdk/models/__init__.py b/status_sdk/models/__init__.py index b615032..29d8a99 100644 --- a/status_sdk/models/__init__.py +++ b/status_sdk/models/__init__.py @@ -1,9 +1,12 @@ from .payement import PaymentRequest from .message import Message, BridgedContent, ContactRequest, MessageContentTypeEnum +from .community import TokenPermission, CommunityRequest __all__ = [ "PaymentRequest", "ContactRequest", "Message", "BridgedContent", "MessageContentTypeEnum", + "TokenPermission", + "CommunityRequest" ] From 3b07ddfdee3a20f4ea5a8d678f51aa42fdda0648 Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 15:57:21 +0200 Subject: [PATCH 5/7] fix imports Signed-off-by: apentori --- pyproject.toml | 2 +- status_sdk/__init__.py | 3 ++- status_sdk/account.py | 7 ++++--- status_sdk/community/base.py | 9 +++++---- status_sdk/community/channel.py | 7 ++++--- status_sdk/group_chat.py | 3 ++- status_sdk/models/message.py | 2 +- status_sdk/signal.py | 2 +- status_sdk/utils/builds.py | 2 +- status_sdk/utils/community.py | 3 ++- status_sdk/utils/external.py | 4 ++-- 11 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c9b5045..3d68b9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ Issues = "https://github.com/status-im/status-python-sdk/issues" "Status Backend" = "https://github.com/status-im/status-go" [tool.setuptools] -packages = ["status_sdk", "status_sdk.community", "status_sdk.utils"] +packages = ["status_sdk", "status_sdk.community", "status_sdk.utils", "status_sdk.models"] [tool.setuptools.package-data] status_sdk = ["docker-compose.yaml"] diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index 7f54e71..fa433e2 100644 --- a/status_sdk/__init__.py +++ b/status_sdk/__init__.py @@ -1,10 +1,11 @@ from importlib.metadata import PackageNotFoundError, version as _version from . import exceptions +from . import constants +from .signal import Signal from .account import Account from .community.base import Community, Channel from .group_chat import GroupChat -from .signal import Signal __all__ = [ "Account", diff --git a/status_sdk/account.py b/status_sdk/account.py index 610c04c..d573d67 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -2,14 +2,15 @@ import uuid as uuid_lib import requests, datetime, re, logging, os, json, ast, shutil, eth_abi, shutil import pandas as pd -from status_sdk import exceptions +from . import exceptions from Crypto.Hash import keccak from io import BytesIO from PIL import Image from PIL.JpegImagePlugin import JpegImageFile from PIL.PngImagePlugin import PngImageFile -from status_sdk import constants, Signal -from status_sdk.models import Message, BridgedContent, ContactRequest +from . import constants +from .signal import Signal +from .models import Message, BridgedContent, ContactRequest class Account: # Enum mappings from original wakuext.py diff --git a/status_sdk/community/base.py b/status_sdk/community/base.py index 1278431..aec9605 100644 --- a/status_sdk/community/base.py +++ b/status_sdk/community/base.py @@ -1,8 +1,9 @@ -from status_sdk import exceptions, Account -from status_sdk.models import CommunityRequest -from status_sdk.community import Channel +from ..account import Account +from .. import exceptions +from ..models import CommunityRequest +from .channel import Channel from typing import Union, Optional, Generator -from status_sdk.utils import community as utils +from ..utils import community as utils import pandas as pd import datetime, copy, os, shutil diff --git a/status_sdk/community/channel.py b/status_sdk/community/channel.py index 7c3115e..e184aa2 100644 --- a/status_sdk/community/channel.py +++ b/status_sdk/community/channel.py @@ -1,6 +1,7 @@ -from status_sdk import exceptions, Account -from status_sdk.utils import community as utils -from status_sdk.models import TokenPermission +from ..account import Account +from .. import exceptions +from ..utils import community as utils +from ..models import TokenPermission from typing import Union, Optional import pandas as pd import re, datetime, random diff --git a/status_sdk/group_chat.py b/status_sdk/group_chat.py index d1aad9f..2fd1267 100644 --- a/status_sdk/group_chat.py +++ b/status_sdk/group_chat.py @@ -1,4 +1,5 @@ -from status_sdk import Account, exceptions +from .account import Account +from . import exceptions from typing import Union, Optional import pandas as pd import re, datetime diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index 47c963d..30b8639 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -4,7 +4,7 @@ import datetime import uuid -from status_sdk.models import PaymentRequest +from . import PaymentRequest class MessageContentTypeEnum: diff --git a/status_sdk/signal.py b/status_sdk/signal.py index e8c0493..67a2ee6 100644 --- a/status_sdk/signal.py +++ b/status_sdk/signal.py @@ -1,6 +1,6 @@ from typing import Optional, Callable, Union import datetime, websocket, json, copy, queue, threading, time -from status_sdk import exceptions +from . import exceptions class Signal: """ diff --git a/status_sdk/utils/builds.py b/status_sdk/utils/builds.py index 99fba2d..11e0652 100644 --- a/status_sdk/utils/builds.py +++ b/status_sdk/utils/builds.py @@ -1,5 +1,5 @@ import shutil, os, subprocess, time, logging, requests -from status_sdk import exceptions +from .. import exceptions def fix_nix_build_paths(binary_path: str) -> None: """ diff --git a/status_sdk/utils/community.py b/status_sdk/utils/community.py index dc0f403..7d81ad0 100644 --- a/status_sdk/utils/community.py +++ b/status_sdk/utils/community.py @@ -1,4 +1,5 @@ -from status_sdk import Account, exceptions +from ..account import Account +from .. import exceptions from typing import Optional import pandas as pd diff --git a/status_sdk/utils/external.py b/status_sdk/utils/external.py index 3b09174..f4ec6f6 100644 --- a/status_sdk/utils/external.py +++ b/status_sdk/utils/external.py @@ -5,8 +5,8 @@ from pathlib import Path from platform import machine from typing import Optional -from status_sdk import exceptions -from status_sdk.utils import builds +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): """ From e8b9616dc02566520ce1fd8967318137e49dc41b Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 16:41:59 +0200 Subject: [PATCH 6/7] fix message models Signed-off-by: apentori --- status_sdk/account.py | 6 +++--- status_sdk/models/message.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/status_sdk/account.py b/status_sdk/account.py index d573d67..5f5a12e 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -822,7 +822,7 @@ def validate_path(path: str): if asset_subfolder: docker_file_path.append(asset_subfolder) - content_image_path = "" + content_image_path = [] asset_file_path = None for path in image_path: validate_path(path) @@ -833,8 +833,8 @@ def validate_path(path: str): if os.path.exists(asset_file_path): os.remove(asset_file_path) - shutil.copy(p, asset_file_path) - content_image_path = f"{content_image_path}//{docker_file_path}" + shutil.copy(path, asset_file_path) + content_image_path.append(docker_file_path) if len(image_path): msg_params["contentType"] = 7 diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index 30b8639..ad9cbc6 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -3,11 +3,13 @@ from typing import Self, Optional, Union import datetime import uuid +import logging +import json from . import PaymentRequest -class MessageContentTypeEnum: +class MessageContentTypeEnum(Enum): TEXT = 1, STICKER = 2, EMOJIS = 4, @@ -37,11 +39,11 @@ class Message: text: str content_type: str from_public_key: str - compressed_key: str timestamp: datetime.datetime + message_type: int chat_type: str - payload_urls: list[str] + images: list[str] source: str reply_id: Optional[str] = None bridge_id: Optional[str] = None @@ -50,6 +52,7 @@ class Message: @classmethod def from_raw(cls, raw: dict) -> Self: + logging.info(json.dumps(raw)) content_type: int = raw["contentType"] msg_type: int = raw["messageType"] params = { @@ -57,15 +60,17 @@ def from_raw(cls, raw: dict) -> Self: "text": raw["text"], "chat_id": raw["chatId"], "from_public_key": raw["from"], - "compressed_key": raw["compressed_key"], + "compressed_key": raw["compressedKey"], "timestamp": datetime.datetime.fromtimestamp(raw["whisperTimestamp"] / 1_000), "source": raw.get("bridgeMessage", {}).get("bridgealloweName", "status"), "message_type": raw['contentType'], + "images": [] } if len(raw["responseTo"]) > 0: params["reply_id"] = raw["responseTo"] + params["content"] = raw["text"] if msg_type == 1: params["chat_type"] = "private" elif msg_type in [2, 3]: @@ -83,12 +88,10 @@ def from_raw(cls, raw: dict) -> Self: elif content_type == 2: params["content"] = raw["sticker"]["url"] params["content_type"] = "sticker" - params["payload_urls"].append(raw["sticker"]["url"]) # Image elif content_type == 7: if isinstance(raw["image"], str): - raw["image"] = [raw["image"]] - params["payload_urls"] = [raw["image"]] + raw["images"] = raw["image"] params["content_type"] = "image" # Bridged Message elif content_type == 18: From 9ec09c51530f3c5b263247c07d7e8dc93b1d659a Mon Sep 17 00:00:00 2001 From: apentori Date: Tue, 29 Sep 2026 21:57:08 +0200 Subject: [PATCH 7/7] account: support only one image per __send_content Signed-off-by: apentori --- docs/account.md | 18 ++++----- docs/community.md | 16 ++++---- docs/group-chat.md | 16 ++++---- pyproject.toml | 2 +- status_sdk/account.py | 72 ++++++++++++++++++++++----------- status_sdk/community/channel.py | 6 +-- status_sdk/group_chat.py | 6 +-- status_sdk/models/message.py | 40 +++++++++++------- 8 files changed, 105 insertions(+), 71 deletions(-) diff --git a/docs/account.md b/docs/account.md index 74e66fa..3143861 100644 --- a/docs/account.md +++ b/docs/account.md @@ -439,15 +439,15 @@ account.send_message( ) ``` -#### `send_image(chat_id, file_path, message=None, reply_to_message_id=None)` +#### `send_image(chat_id, image_paths, message=None, reply_to_message_id=None)` -Send an image to a specific chat, with an optional text message. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone), it can be sent as a **reply** to an existing message. +Send one or more images to a specific chat, with an optional text message. Multiple images are grouped into a single album, the same as attaching several images in Status App. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone), it can be sent as a **reply** to an existing message. | Name | Type | Required | Description | |-----|-----|-----|-------------| | `chat_id` | `str` | Yes | Identifier of the chat where the image will be sent. All available chat IDs can be obtained from the [`chats`](./account.md#chats) property. | -| `file_path` | `str` | Yes | Local full path to the image file. | -| `message` | `str` | No | Caption sent together with the image. Cannot be longer than **2000 characters**. When omitted (default), the image is sent without any text. | +| `image_paths` | `list[str]` | Yes | Local full paths to the image files. | +| `message` | `str` | No | Caption sent together with the images. Cannot be longer than **2000 characters**. When omitted (default), the images are sent without any text. | | `reply_to_message_id` | `str` | No | The `id` of the message being replied to. Message IDs can be obtained from the `id` key of [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone) or from a [`listen_messages`](./account.md#listen_messages) event. When omitted (default), the image is sent as a standalone message. | Returns `str` - the `id` of the message that was just sent, exactly as [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) does, so it can be passed straight into [`delete_message`](./account.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message. @@ -464,7 +464,7 @@ account.login(**params) # This is under the assumption you already have a contact / joined a community chat = account.chats[0] -message_id = account.send_image(chat["id"], "/full/file-path/meme-67.png") +message_id = account.send_image(chat["id"], ["/full/file-path/meme-67.png"]) print(f"Sent image: {message_id}") ``` @@ -484,7 +484,7 @@ chat = account.chats[0] account.send_image( chat_id=chat["id"], - file_path="/full/file-path/meme-67.png", + image_paths=["/full/file-path/meme-67.png"], message="Du bist gut genug" ) ``` @@ -509,7 +509,7 @@ latest = messages[0] account.send_image( chat_id=chat["id"], - file_path="/full/file-path/meme-67.png", + image_paths=["/full/file-path/meme-67.png"], message="Du bist gut genug", reply_to_message_id=latest["id"] ) @@ -527,7 +527,7 @@ Relay a message that came from **another messaging platform** - Discord, Telegra | `username` | `str` | No | The author's username **on the other platform**, shown as the sender. Defaults to `Anon`. | | `user_id` | `str` | No | The author's id on the other platform. Status uses it to tell one bridged author from another, so **pass the real id** - see the note below. | | `message_id` | `str` | No | The original message's id on the other platform. Pass it if you want later messages to be able to reply to this one. | -| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id. Unlike in [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) and [`send_image`](./account.md#send_imagechat_id-file_path-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. Passing a Status id will not thread. | +| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id. Unlike in [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) and [`send_image`](./account.md#send_imagechat_id-image_paths-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. Passing a Status id will not thread. | | `image_url` | `str` | No | URL of the author's avatar on the other platform. Defaults to no avatar. | Returns `str` - the `id` of the message **in Status App**, which is a different value from the `message_id` you passed in. It can be used with [`delete_message`](./account.md#delete_messageid) like any other sent message. @@ -588,7 +588,7 @@ Passing `chat_id` is purely an **optimisation**. Without it the chat has to be r | Name | Type | Required | Description | |-----|-----|-----|-------------| -| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone), from the `lastMessage` of a [`listen_messages`](./account.md#listen_messages) event, or directly from the return value of [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) / [`send_image`](./account.md#send_imagechat_id-file_path-messagenone-reply_to_message_idnone). | +| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone), from the `lastMessage` of a [`listen_messages`](./account.md#listen_messages) event, or directly from the return value of [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) / [`send_image`](./account.md#send_imagechat_id-image_paths-messagenone-reply_to_message_idnone). | | `emoji_shortname` | `str` | Yes | The emoji shortname as in Status App, with or without the surrounding colons. See [Emojis](./utils.md#emojis) for all supported values. | | `chat_id` | `str` | No | Identifier of the chat the message belongs to, as found in the [`chats`](./account.md#chats) property. When omitted (default), it is resolved from `message_id` with an extra call to the Status Backend. | diff --git a/docs/community.md b/docs/community.md index 39858d8..d485bc9 100644 --- a/docs/community.md +++ b/docs/community.md @@ -1246,17 +1246,17 @@ message_id = channel.send_message("Hello from my Status bot!") print(f"Sent message: {message_id}") ``` -### `send_image(file_path, message=None, reply_to_message_id=None)` +### `send_image(file_paths, message=None, reply_to_message_id=None)` -Send an image to the channel, with an optional text **caption**. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone), it can be sent as a **reply** to an existing message in the channel. +Send one or more images to the channel, with an optional text **caption**. Multiple images are grouped into a single album. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone), it can be sent as a **reply** to an existing message in the channel. | Name | Type | Required | Description | |-----|-----|-----|-------------| -| `file_path` | `str` | Yes | Local full path to the image file. | -| `message` | `str` | No | Caption sent together with the image. | +| `file_paths` | `list[str]` | Yes | Local full paths to the image files. | +| `message` | `str` | No | Caption sent together with the images. | | `reply_to_message_id` | `str` | No | The `id` of the message being replied to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone). When omitted (default), the image is sent as a standalone message. | -Returns `str` - the `id` of the message that was just sent, delegated from [`send_image`](./account.md#send_imagechat_id-file_path-messagenone-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./community.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./community.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message. +Returns `str` - the `id` of the message that was just sent, delegated from [`send_image`](./account.md#send_imagechat_id-image_paths-messagenone-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./community.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./community.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message. ```python from status_sdk import Account, Community @@ -1272,7 +1272,7 @@ url = "https://status.app/c/G3QAAMQn9ueHRsR3W5Ouuy25fkCxziknAIEkCbYAoC04HjyGeQ6X community = Community(account, url=url) channel = community["general"] -message_id = channel.send_image("./meme-67.png", "Daily random meme") +message_id = channel.send_image(["./meme-67.png"], "Daily random meme") print(f"Sent image: {message_id}") ``` @@ -1287,7 +1287,7 @@ Relay a message that came from **another messaging platform** - Discord, Telegra | `username` | `str` | No | The author's username **on the other platform**, shown as the sender. Defaults to `Anon`. | | `user_id` | `str` | No | The author's id on the other platform. Status uses it to tell one bridged author from another, so **pass the real id** - see the note below. | | `message_id` | `str` | No | The original message's id on the other platform. Pass it if you want later messages to be able to reply to this one. | -| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id, unlike in [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone) and [`send_image`](./community.md#send_imagefile_path-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. | +| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id, unlike in [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone) and [`send_image`](./community.md#send_imagefile_paths-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. | | `image_url` | `str` | No | URL of the author's avatar on the other platform. Defaults to no avatar. | Returns `str` - the `id` of the message **in Status App**, delegated from [`send_bridged_message`](./account.md#send_bridged_messagechat_id-message-namenone-usernamenone-user_idnone-message_idnone-reply_to_message_idnone-image_urlnone) on `Account`. This is a different value from the `message_id` you passed in, and it can be used with [`delete_message`](./community.md#delete_messageid) like any other sent message. Returns `None` when [`can_post`](./community.md#can_post) is `False` - nothing is relayed and no error is raised. @@ -1326,7 +1326,7 @@ Emojis are identified by their **shortname**, exactly as Status App names them ( | Name | Type | Required | Description | |-----|-----|-----|-------------| -| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./community.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone) / [`send_image`](./community.md#send_imagefile_path-messagenone-reply_to_message_idnone). | +| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./community.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./community.md#send_messagemessage-reply_to_message_idnone) / [`send_image`](./community.md#send_imagefile_paths-messagenone-reply_to_message_idnone). | | `emoji_shortname` | `str` | Yes | The emoji shortname as in Status App, with or without the surrounding colons. See [Emojis](./utils.md#emojis) for all supported values. | ```python diff --git a/docs/group-chat.md b/docs/group-chat.md index ec70f4e..2a07220 100644 --- a/docs/group-chat.md +++ b/docs/group-chat.md @@ -223,17 +223,17 @@ latest = messages[0] group_chat.send_message("Thanks for the update!", latest["id"]) ``` -### `send_image(file_path, message=None, reply_to_message_id=None)` +### `send_image(file_paths, message=None, reply_to_message_id=None)` -Send an image to the group chat, with an optional text **caption**. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone), it can be sent as a **reply** to an existing message in the chat. +Send one or more images to the group chat, with an optional text **caption**. Multiple images are grouped into a single album. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone), it can be sent as a **reply** to an existing message in the chat. | Name | Type | Required | Description | |-----|-----|-----|-------------| -| `file_path` | `str` | Yes | Local full path to the image file. | -| `message` | `str` | No | Caption sent together with the image. | +| `file_paths` | `list[str]` | Yes | Local full paths to the image files. | +| `message` | `str` | No | Caption sent together with the images. | | `reply_to_message_id` | `str` | No | The `id` of the message being replied to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone). When omitted (default), the image is sent as a standalone message. | -Returns `str` - the `id` of the message that was just sent, delegated from [`send_image`](./account.md#send_imagechat_id-file_path-messagenone-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./group-chat.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message. +Returns `str` - the `id` of the message that was just sent, delegated from [`send_image`](./account.md#send_imagechat_id-image_paths-messagenone-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./group-chat.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message. ```python from status_sdk import Account, GroupChat @@ -248,7 +248,7 @@ account.login(**params) chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0] group_chat = GroupChat(account, chat["id"]) -message_id = group_chat.send_image("./meme-67.png", "Daily random meme") +message_id = group_chat.send_image(["./meme-67.png"], "Daily random meme") print(f"Sent image: {message_id}") ``` @@ -263,7 +263,7 @@ Relay a message that came from **another messaging platform** - Discord, Telegra | `username` | `str` | No | The author's username **on the other platform**, shown as the sender. Defaults to `Anon`. | | `user_id` | `str` | No | The author's id on the other platform. Status uses it to tell one bridged author from another, so **pass the real id** - see the note below. | | `message_id` | `str` | No | The original message's id on the other platform. Pass it if you want later messages to be able to reply to this one. | -| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id, unlike in [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone) and [`send_image`](./group-chat.md#send_imagefile_path-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. | +| `reply_to_message_id` | `str` | No | The **other platform's** id of the message being replied to - *not* a Status message id, unlike in [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone) and [`send_image`](./group-chat.md#send_imagefile_paths-messagenone-reply_to_message_idnone). It threads correctly only when the message it points at was itself relayed with that same value as its `message_id`. | | `image_url` | `str` | No | URL of the author's avatar on the other platform. Defaults to no avatar. | Returns `str` - the `id` of the message **in Status App**, delegated from [`send_bridged_message`](./account.md#send_bridged_messagechat_id-message-namenone-usernamenone-user_idnone-message_idnone-reply_to_message_idnone-image_urlnone) on `Account`. This is a different value from the `message_id` you passed in, and it can be used with [`delete_message`](./group-chat.md#delete_messageid) like any other sent message. @@ -300,7 +300,7 @@ Emojis are identified by their **shortname**, exactly as Status App names them ( | Name | Type | Required | Description | |-----|-----|-----|-------------| -| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone) / [`send_image`](./group-chat.md#send_imagefile_path-messagenone-reply_to_message_idnone). | +| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone) / [`send_image`](./group-chat.md#send_imagefile_paths-messagenone-reply_to_message_idnone). | | `emoji_shortname` | `str` | Yes | The emoji shortname as in Status App, with or without the surrounding colons. See [Emojis](./utils.md#emojis) for all supported values. | ```python diff --git a/pyproject.toml b/pyproject.toml index 3d68b9f..73163a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "status-sdk" -version = "1.2.1" +version = "1.3.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 5f5a12e..7fa92d2 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -2,6 +2,7 @@ import uuid as uuid_lib import requests, datetime, re, logging, os, json, ast, shutil, eth_abi, shutil import pandas as pd + from . import exceptions from Crypto.Hash import keccak from io import BytesIO @@ -12,6 +13,16 @@ from .signal import Signal from .models import Message, BridgedContent, ContactRequest + +def in_docker() -> bool: + if os.path.exists("/.dockerenv"): + return True + try: + with open("/proc/1/cgroup") as f: + return any("docker" in line or "kubepods" in line for line in f) + except OSError: + return False + class Account: # Enum mappings from original wakuext.py __mappings = { @@ -105,8 +116,8 @@ def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_po r'(?:-(?P\d+)-g(?P[0-9a-f]{7,40}))?$' ) 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"] + self.__is_docker = in_docker() + 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: @@ -673,20 +684,20 @@ def __getitem__(self, key: str) -> pd.DataFrame: return balance.copy() - def send_image(self, chat_id: str, file_path: list[str], message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str: + def send_image(self, chat_id: str, image_paths: list[str], message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str: """ Send an image to the given chat. Parameters: - `chat_id` - the chat ID can be found in `self.chats` - - `file_path` - the file path of the image + - `image_paths` - the file paths of the images - `message` - the message that will be sent - `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message. Output: - The message ID """ - return self.__send_content(chat_id, message, reply_to_message_id, image_path = file_path) + return self.__send_content(chat_id, message, reply_to_message_id, image_paths) def send_message(self, chat_id: str, message: str, reply_to_message_id: Optional[str] = None) -> str: """ @@ -768,7 +779,7 @@ def __send_content( chat_id: str, message: Optional[str] = None, reply_to_message_id: Optional[str] = None, - image_path: list[str] = [], + image_paths: Optional[list[str]] = None, bridged_content: Optional[BridgedContent] = None ) -> str: """ @@ -778,7 +789,7 @@ def __send_content( - `chat_id` - the chat ID can be found in `self.chats` - `message` - the text that will be sent. Optional when media is attached, so an image can be sent on its own - `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message. - - `image_path` - list of local path to the images to attach. + - `image_paths` - List of local paths to the images to attach. No images are attached when omitted. Output: - The message ID @@ -796,13 +807,14 @@ def validate_path(path: str): return path - self.info if not message: message = "" if len(message) > self.message_length: raise exceptions.MessageTooLongError(f"Message cannot be longer than 2000 characters (got {len(message)})...") + image_paths = image_paths or [] + params = [] msg_params = { "chatId": chat_id, "text": message, @@ -814,7 +826,6 @@ def validate_path(path: str): # subfolder name in ./assets/ (if necessary) asset_subfolder = "images" - if bridged_content: msg_params["text"] = "" msg_params["contentType"] = 18 @@ -822,33 +833,46 @@ def validate_path(path: str): if asset_subfolder: docker_file_path.append(asset_subfolder) - content_image_path = [] - asset_file_path = None - for path in image_path: - validate_path(path) - docker_file_path.append(os.path.basename(path)) + + asset_file_paths = [] + print(image_paths) + for image_path in image_paths: + validate_path(image_path) asset_file_path = os.path.join( - self.__assets_local_folder, asset_subfolder, os.path.basename(path)) + self.__assets_local_folder, asset_subfolder, + os.path.basename(image_path)) os.makedirs(os.path.dirname(asset_file_path), exist_ok=True) if os.path.exists(asset_file_path): os.remove(asset_file_path) - shutil.copy(path, asset_file_path) - content_image_path.append(docker_file_path) + shutil.copy(image_path, asset_file_path) + asset_file_paths.append(asset_file_path) - if len(image_path): msg_params["contentType"] = 7 + # Path as seen by `status-im/status-go` - the mounted assets folder + _image_path = image_path + if self.__is_docker: + _image_path = "/".join(docker_file_path + [os.path.basename(image_path)]) + msg_params.update({ - "imagePath": content_image_path - }) + "imagePath": _image_path + }) + # One message per image - `msg_params` is mutated, so copy it + params.append(msg_params.copy()) + print(f"Messages {params}") if msg_params["contentType"] == 1 and len(msg_params["text"]) == 0: raise exceptions.SendContentError("Cannot send empty text messages") - params = [msg_params] - response = self._call_rpc("messaging", "sendChatMessage", params) - if asset_file_path and os.path.exists(asset_file_path): - os.remove(asset_file_path) + if len(image_paths) == 0: + params = [msg_params] + + # `status-im/status-go` expects the list of messages as a single + # RPC argument (`[]*common.Message`) + response = self._call_rpc("messaging", "sendChatMessages", [params]) + for asset_file_path in asset_file_paths: + if os.path.exists(asset_file_path): + os.remove(asset_file_path) error = response.get("error", {}) or {} if error: diff --git a/status_sdk/community/channel.py b/status_sdk/community/channel.py index e184aa2..7b5469d 100644 --- a/status_sdk/community/channel.py +++ b/status_sdk/community/channel.py @@ -270,19 +270,19 @@ def send_message(self, message: str, reply_to_message_id: Optional[str] = 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]: + def send_image(self, file_paths: list[str], message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> Optional[str]: """ Send a image to the group chat. Parameters: - - `file_path` - the file path of the image + - `file_paths` - the file paths of the images - `message` - the message that will be sent. Currently only text messages are supported - `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message. Output: - The message ID """ - return self.__account.send_image(self.id, file_path, message, reply_to_message_id) + return self.__account.send_image(self.id, file_paths, 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]: """ diff --git a/status_sdk/group_chat.py b/status_sdk/group_chat.py index 2fd1267..f3e8623 100644 --- a/status_sdk/group_chat.py +++ b/status_sdk/group_chat.py @@ -70,19 +70,19 @@ def send_message(self, message: str, reply_to_message_id: Optional[str] = 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) -> str: + def send_image(self, file_paths: list[str], message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str: """ Send a image to the group chat. Parameters: - - `file_path` - the file path of the image + - `file_paths` - the file paths of the images - `message` - the message that will be sent. Currently only text messages are supported - `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message. Output: - The message ID """ - return self.__account.send_image(self.id, file_path, message, reply_to_message_id) + return self.__account.send_image(self.id, file_paths, 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) -> str: """ diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index ad9cbc6..c00a461 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from enum import Enum +from enum import IntEnum from typing import Self, Optional, Union import datetime import uuid @@ -9,15 +9,26 @@ from . import PaymentRequest -class MessageContentTypeEnum(Enum): - TEXT = 1, - STICKER = 2, - EMOJIS = 4, - IMAGE = 7, - CONTACT_REQUEST = 11, - SENT_CONTACT_REQUEST = 15, - REMOVED_CONTACT = 17, - BRIDGED_MESSAGE = 18 +class MessageContentTypeEnum(IntEnum): + UNKNOWN_CONTENT_TYPE = 0 + TEXT_PLAIN = 1 + STICKER = 2 + STATUS = 3 + EMOJI = 4 + TRANSACTION_COMMAND = 5 # deprecated + SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP = 6 # local only + IMAGE = 7 + AUDIO = 8 + COMMUNITY = 9 + SYSTEM_MESSAGE_GAP = 10 # local only + CONTACT_REQUEST = 11 + DISCORD_MESSAGE = 12 + IDENTITY_VERIFICATION = 13 + SYSTEM_MESSAGE_PINNED_MESSAGE = 14 # local only + SYSTEM_MESSAGE_MUTUAL_EVENT_SENT = 15 # local only + SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED = 16 # local only + SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED = 17 # local only + BRIDGE_MESSAGE = 18 @dataclass class ContactRequest: @@ -43,7 +54,7 @@ class Message: timestamp: datetime.datetime message_type: int chat_type: str - images: list[str] + images_url: list[str] source: str reply_id: Optional[str] = None bridge_id: Optional[str] = None @@ -52,7 +63,7 @@ class Message: @classmethod def from_raw(cls, raw: dict) -> Self: - logging.info(json.dumps(raw)) + logging.info(f"Raw message {json.dumps(raw)}") content_type: int = raw["contentType"] msg_type: int = raw["messageType"] params = { @@ -64,7 +75,7 @@ def from_raw(cls, raw: dict) -> Self: "timestamp": datetime.datetime.fromtimestamp(raw["whisperTimestamp"] / 1_000), "source": raw.get("bridgeMessage", {}).get("bridgealloweName", "status"), "message_type": raw['contentType'], - "images": [] + "images_url": [] } if len(raw["responseTo"]) > 0: @@ -90,8 +101,7 @@ def from_raw(cls, raw: dict) -> Self: params["content_type"] = "sticker" # Image elif content_type == 7: - if isinstance(raw["image"], str): - raw["images"] = raw["image"] + params["images_url"] = [raw["image"]] if isinstance(raw["image"], str) else raw["image"] params["content_type"] = "image" # Bridged Message elif content_type == 18: