From e0ea03d85d6ed3e87902f3916f0d9b752cdddb60 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sat, 10 Oct 2026 09:58:02 +0300 Subject: [PATCH 1/4] models: Add image URLs to Message --- status_sdk/models/message.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index 8372bd9..573768b 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -76,6 +76,7 @@ class Message: timestamp: datetime.datetime chat_type: str source: str + image_urls: list[str] = field(default_factory=list) reply_id: Optional[str] = None bridge_id: Optional[str] = None payment_requests: list[PaymentRequest] = field(default_factory=list) @@ -89,7 +90,8 @@ def from_raw(cls, raw: dict) -> Self: "chat_id": raw["chatId"], "from_public_key": raw["from"], "timestamp": datetime.datetime.fromtimestamp(raw["whisperTimestamp"] / 1_000), - "source": raw.get("bridgeMessage", {}).get("bridgeName", "status") + "source": raw.get("bridgeMessage", {}).get("bridgeName", "status"), + "image_urls": [] } if len(raw["responseTo"]) > 0: @@ -111,6 +113,7 @@ def from_raw(cls, raw: dict) -> Self: # Sticker elif content_type == 2: params["content"] = raw["sticker"]["url"] + params["image_urls"] += [params["content"]] params["content_type"] = "sticker" # Image elif content_type == 7: @@ -118,6 +121,7 @@ def from_raw(cls, raw: dict) -> Self: raw["image"] = [raw["image"]] img_paths: list[str] = raw["image"] + params["image_urls"] += img_paths text = raw["text"] caption = f"{text}\n\n" if len(text) > 0 else "" params["content"] = caption + "\n".join(img_paths) From edd9276908b55cc5dff4539a161209b496161c3e Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sat, 10 Oct 2026 12:28:29 +0300 Subject: [PATCH 2/4] types: Add Content & Chat classes --- status_sdk/__init__.py | 2 +- status_sdk/models/message.py | 27 ++++++++++++++------------- status_sdk/types.py | 13 +++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 status_sdk/types.py diff --git a/status_sdk/__init__.py b/status_sdk/__init__.py index 35f633d..95a44e9 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, models, types try: __version__ = _version("status-sdk") diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py index 573768b..5f3dccc 100644 --- a/status_sdk/models/message.py +++ b/status_sdk/models/message.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, field from typing import Self, Optional, Union +from .. import types import datetime, uuid @dataclass @@ -71,10 +72,10 @@ class Message: id: str chat_id: str content: str - content_type: str + content_type: types.Content from_public_key: str timestamp: datetime.datetime - chat_type: str + chat_type: types.Chat source: str image_urls: list[str] = field(default_factory=list) reply_id: Optional[str] = None @@ -98,25 +99,25 @@ def from_raw(cls, raw: dict) -> Self: params["reply_id"] = raw["responseTo"] if msg_type == 5: - params["chat_type"] = "community" + params["chat_type"] = types.Chat.COMMUNITY elif msg_type == 1: - params["chat_type"] = "private" + params["chat_type"] = types.Chat.PRIVATE elif msg_type in [2, 3]: - params["chat_type"] = "group" + params["chat_type"] = types.Chat.GROUP # Text & Emojis - if content_type in [1, 4]: + if content_type in [types.Content.TEXT.value, types.Content.EMOJI.value]: params["content"] = raw["text"] - params["content_type"] = "text" if content_type == 1 else "image" + params["content_type"] = types.Content.TEXT if content_type == types.Content.TEXT.value else types.Content.EMOJI # Sticker - elif content_type == 2: + elif content_type == types.Content.STICKER.value: params["content"] = raw["sticker"]["url"] params["image_urls"] += [params["content"]] - params["content_type"] = "sticker" + params["content_type"] = types.Content.STICKER # Image - elif content_type == 7: + elif content_type == types.Content.IMAGE.value: if isinstance(raw["image"], str): raw["image"] = [raw["image"]] @@ -125,11 +126,11 @@ def from_raw(cls, raw: dict) -> Self: text = raw["text"] caption = f"{text}\n\n" if len(text) > 0 else "" params["content"] = caption + "\n".join(img_paths) - params["content_type"] = "image" + params["content_type"] = types.Content.IMAGE # Bridged Message - elif content_type == 18: + elif content_type == types.Content.BRIDGED.value: params["content"] = raw["bridgeMessage"]["content"] - params["content_type"] = "text" + params["content_type"] = types.Content.BRIDGED params["bridge_id"] = raw["bridgeMessage"]["messageID"] reply_id = raw["bridgeMessage"].get("parentMessageID") if isinstance(reply_id, str) and len(reply_id) == 0: diff --git a/status_sdk/types.py b/status_sdk/types.py new file mode 100644 index 0000000..209e42d --- /dev/null +++ b/status_sdk/types.py @@ -0,0 +1,13 @@ +from enum import Enum + +class Content(Enum): + TEXT = 1 + STICKER = 2 + EMOJI = 4 + IMAGE = 7 + BRIDGED = 18 + +class Chat(Enum): + PRIVATE = 1 + COMMUNITY = 2 + GROUP = 3 From 0cac6dddc094eab0235b7f634a9199811033d5d0 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sat, 10 Oct 2026 12:30:13 +0300 Subject: [PATCH 3/4] account: Listen for specific content type --- docs/account.md | 50 ++++++++++----- docs/community.md | 28 ++------- docs/models.md | 142 ++++++++++++++++++++++++++++++++++++++++++ docs/types.md | 61 ++++++++++++++++++ status_sdk/account.py | 21 +++---- 5 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 docs/models.md create mode 100644 docs/types.md diff --git a/docs/account.md b/docs/account.md index 5af6270..7aa2cc9 100644 --- a/docs/account.md +++ b/docs/account.md @@ -395,7 +395,7 @@ A message can be **at most 2000 characters long**, matching the limit enforced b |-----|-----|-----|-------------| | `chat_id` | `str` | Yes | Identifier of the chat where the message will be sent. All available chat IDs can be obtained from the [`chats`](./account.md#chats) property. | | `message` | `str` | Yes | The text message to send. Cannot be longer than **2000 characters**. | -| `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 message is sent as a standalone message. | +| `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_messagescontentnone) event. When omitted (default), the message is sent as a standalone message. | Returns `str` - the `id` of the message that was just sent. It is the same identifier that appears under the `id` key in [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone), 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, without having to fetch the chat's messages first. @@ -449,9 +449,9 @@ Send one or more images to a specific chat, with an optional text message. Image | `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_paths` | `list[str]`
`str` | Yes | Local full path(s) to the image file(s). A single value can be passed as a `str`. Passing more than one path sends all of them together as a single **album**. | | `message` | `str` | No | Caption sent together with the image(s). Cannot be longer than **2000 characters**. When omitted (default), the image is 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. | +| `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_messagescontentnone) 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. For an album, this is the id of its last image - [`listen_messages`](./account.md#listen_messages) yields the whole album under this same id once every image in it has arrived, but [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone) returns each image in the album as its own entry rather than merging them. +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. For an album, this is the id of its last image - [`listen_messages`](./account.md#listen_messagescontentnone) yields the whole album under this same id once every image in it has arrived, but [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone) returns each image in the album as its own entry rather than merging them. ```python from status_sdk import Account @@ -606,11 +606,11 @@ React to a message with an emoji, the same as reacting to a message in Status Ap Emojis are identified by their **shortname**, exactly as Status App names them (`:thumbsup:`, `:heart_eyes:`). The surrounding colons are optional - `thumbsup` and `:thumbsup:` are the same emoji - and the full list of supported shortnames is documented under [Emojis](./utils.md#emojis). -Passing `chat_id` is purely an **optimisation**. Without it the chat has to be resolved from the message first, which costs one extra round trip to the Status Backend per reaction - worth avoiding when reacting to many messages in a chat that is already known, such as inside a [`listen_messages`](./account.md#listen_messages) loop. A `chat_id` that does not match the message is rejected by the backend and raises a custom exception, so pass it only when it is certain. +Passing `chat_id` is purely an **optimisation**. Without it the chat has to be resolved from the message first, which costs one extra round trip to the Status Backend per reaction - worth avoiding when reacting to many messages in a chat that is already known, such as inside a [`listen_messages`](./account.md#listen_messagescontentnone) loop. A `chat_id` that does not match the message is rejected by the backend and raises a custom exception, so pass it only when it is certain. | 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_paths-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_messagescontentnone) 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_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. | @@ -724,9 +724,13 @@ deleted = account.delete_message(message_id) print(f"Deleted: {deleted}") ``` -#### `listen_messages()` +#### `listen_messages(content=None)` -Listen for new incoming messages **in real time**. This method yields raw message events as they are received from the Status Backend [signal](./account.md#signal) `messages.new`. This method is ideal for developing real time chat applications +Listen for new incoming messages **in real time**. This method is ideal for developing real time chat applications. + +| Name | Type | Required | Description | +|-----|-----|-----|-------------| +| `content` | `list[types.Content]`
`types.Content` | No | Only yield messages of the given [content type(s)](./types.md#content). | ```python from status_sdk import Account @@ -739,21 +743,39 @@ params = { account.login(**params) for msg in account.listen_messages(): - print(msg) + print(msg.content) +``` + +Only listen for text and emoji messages: + +```python +from status_sdk import Account, types + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +for msg in account.listen_messages(content=[types.Content.TEXT, types.Content.EMOJI]): + print(msg.content) ``` **Note**: If you receive multiple messages at once, `contacts` and `chats` will grow. #### `listen_contact_requests()` -Listen for contact requests **in real time**. . Every yielded event carries a `request_type` key that tells them apart: +Listen for contact requests **in real time**. This method yields [`ContactRequest`](./models.md#contactrequest) objects, whose `incoming`, `accepted` and `removed` booleans tell the events apart: -| `request_type` | Meaning | +| Attribute | Meaning | |-----|-----| -| `incoming` | Another user sent a contact request to the account. Approve it with [`add_contact`](./account.md#add_contactpublic_key-request_idnone-display_namenone). Use `id` and `public_key` properties from `ContactRequest` | +| `incoming` | Another user sent a contact request to the account. Approve it with [`add_contact`](./account.md#add_contactpublic_key-request_idnone-display_namenone). | | `accepted` | Another user accepted a contact request that the account had sent. The contact is now mutual. | | `removed` | When a user has removed the account from their contacts. | +See [`ContactRequest`](./models.md#contactrequest) for the full list of attributes. + ```python from status_sdk import Account @@ -791,7 +813,7 @@ for request in account.listen_contact_requests(): #### `listen_message_mentions()` -Listen for `@0x...` mentions **in real time**. +Listen for `@0x...` mentions **in real time**. This method yields [`Message`](./models.md#message) objects. ```python from status_sdk import Account @@ -804,7 +826,7 @@ params = { account.login(**params) for mention in account.listen_message_mentions(): - print(mention) + print(mention.content) ``` #### `add_contact(public_key, request_id=None, display_name=None)` @@ -1781,7 +1803,7 @@ The property exists in `Account` because signals require an **active logged‑in The property exposes the following 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.listen()` - stream events continuously. Example usage of this is found in [`listen_messages()`](./account.md#listen_messagescontentnone) - `signal.connect()` - open a persistent websocket connection that buffers **every** incoming signal in the background. Must be called before `expect()`. - `signal.disconnect()` - tear down the persistent connection opened by `connect()`. - `signal.expect()` - return a context manager that waits for one or more matching signals to arrive **after** you perform an action. This is the recommended way to make **async message calls**, since it removes the race conditions and infinite-loop risk of `get()`. diff --git a/docs/community.md b/docs/community.md index cb6ae48..0275609 100644 --- a/docs/community.md +++ b/docs/community.md @@ -532,20 +532,9 @@ community.delete_channel("announcements") ### `listen_requests()` -Listen for join requests to the community **in real time**. +Listen for join requests to the community **in real time**. This method yields [`CommunityRequest`](./models.md#communityrequest) objects, so the fields are reached as attributes (`request.pending`) rather than dictionary keys. -Returns a `Generator` that yields one `models.CommunityRequest` **dataclass** per request event, so the fields are reached as attributes (`request.pending`) rather than dictionary keys: - -| Attribute | Type | Description | -|----|----|-------------| -| `id` | `str` | The join request id. Pass this to [`accept`](./community.md#acceptpending_request_id) or [`decline`](./community.md#declinepending_request_id). | -| `public_key` | `str` | Public key of the requesting member. | -| `pending` | `bool` | The request is waiting to be [accepted](./community.md#acceptpending_request_id) or [declined](./community.md#declinepending_request_id). | -| `reject` | `bool` | The request was declined. | -| `accept` | `bool` | The request was accepted and the member joined. | -| `cancel` | `bool` | The request was cancelled. | - -The state is carried by **four booleans** rather than one string, and **exactly one of them is `True`** on any yielded request - the one matching the state the request just moved into. The rest stay `False`. +The state is carried by **four booleans** rather than one string, and **exactly one of them is `True`** on any yielded request - the one matching the state the request just moved into. The rest stay `False`. See [`CommunityRequest`](./models.md#communityrequest) for the full list of attributes. Events belonging to **other communities**, and requests whose state code is not one of the four above, are skipped - so everything yielded is a request for this community. @@ -1186,7 +1175,7 @@ Permissions are [added](./community.md#add_permissionpermission-tokensnone), [li The `permission` argument is a **case-insensitive** string. Any other value raises a custom exception listing the valid ones. | Scope | Description | -|-----|-----|-------------| +|-----|-------------| | `view` | Members who meet the criteria can **read** the channel. | | `view_post` | Members who meet the criteria can **read and post** in the channel. | | `admin` | Members who meet the criteria become **administrators**. | @@ -1198,14 +1187,7 @@ The `permission` argument is a **case-insensitive** string. Any other value rais ### Token criteria -Token criteria are described with the `models.TokenPermission` **dataclass**: - -| Attribute | Type | Required | Description | -|----|----|----|-------------| -| `symbol` | `str` | Yes | The token symbol, as it appears in [`get_tokens`](./account.md#get_tokens) on `Account`. | -| `amount` | `float` | Yes | The minimum amount a member must hold. Converted to wei using the token's own `decimals`, so it is written in human units - `10` means 10 tokens, not 10 wei. | -| `chain_id` | `int` | No | The chain the token lives on. Defaults to `1` (Ethereum mainnet). | -| `address` | `str` | No | The token's contract address. Needed only when the same `symbol` exists more than once on `chain_id`. | +Token criteria are described with the [`TokenPermission`](./models.md#tokenpermission) dataclass. Passing several `TokenPermission` objects requires a member to satisfy **all** of them. @@ -1440,7 +1422,7 @@ Add a permission to the channel. See [Channel permissions](./community.md#channe | Name | Type | Required | Description | |-----|-----|-----|-------------| | `permission` | `str` | Yes | The [scope](./community.md#permission-scopes) to grant - `view`, `view_post`, `admin`, `member`, `token_master` or `token_owner`. Case-insensitive. | -| `tokens` | `models.TokenPermission`
`list[models.TokenPermission]` | No | The [token criteria](./community.md#token-criteria) a member must meet. A single object or a list of them. When omitted, the permission applies to **every** member. | +| `tokens` | [`models.TokenPermission`](./models.md#tokenpermission)
`list[models.TokenPermission]` | No | The [token criteria](./community.md#token-criteria) a member must meet. A single object or a list of them. When omitted, the permission applies to **every** member. | ```python from status_sdk import Account, Community, models diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..60e857f --- /dev/null +++ b/docs/models.md @@ -0,0 +1,142 @@ +# Models + +## `Message` + +Returned by [`listen_messages`](./account.md#listen_messagescontentnone) and [`listen_message_mentions`](./account.md#listen_message_mentions) from `Account`. + +| Attribute | Type | Description | +|----|----|-------------| +| `id` | `str` | The message id. Can be passed to [`delete_message`](./account.md#delete_messageid) or used as a `reply_to_message_id`. | +| `chat_id` | `str` | The chat the message was sent in. | +| `content` | `str` | The message's text. For an [image](./types.md#content), this is the caption (if any) followed by the image path(s). | +| `content_type` | [`types.Content`](./types.md#content) | What kind of content the message carries. | +| `from_public_key` | `str` | Public key of the sender. | +| `timestamp` | `datetime.datetime` | When the message was sent. | +| `chat_type` | [`types.Chat`](./types.md#chat) | What kind of chat the message was sent in. | +| `source` | `str` | `"status"` for a regular message, or the platform name (e.g. `"Discord"`) for a [bridged message](./account.md#send_bridged_messagechat_id-message-namenone-usernamenone-user_idnone-message_idnone-reply_to_message_idnone-image_urlnone). | +| `image_urls` | `list[str]` | Local paths of attached images. Empty unless `content_type` is [`IMAGE`](./types.md#content) or [`STICKER`](./types.md#content). Contains more than one entry for an [album](./account.md#send_imagechat_id-file_paths-messagenone-reply_to_message_idnone). | +| `reply_id` | `str` | The `id` of the message this one replies to. `None` if it is not a reply. | +| `bridge_id` | `str` | The message's id **on the other platform**, for a [bridged message](./account.md#send_bridged_messagechat_id-message-namenone-usernamenone-user_idnone-message_idnone-reply_to_message_idnone-image_urlnone). `None` otherwise. | +| `payment_requests` | `list[`[`PaymentRequest`](./models.md#paymentrequest)`]` | Payment requests attached to the message. Empty for most messages. | + +```python +from status_sdk import Account + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +for message in account.listen_messages(): + print(message.from_public_key, message.content) +``` + +## `PaymentRequest` + +A payment request attached to a [`Message`](./models.md#message). + +| Attribute | Type | Description | +|----|----|-------------| +| `to_address` | `str` | The address the payment should be sent to. | +| `token_symbol` | `str` | The requested token's symbol. | +| `token_address` | `str` | The requested token's contract address. | +| `chain_id` | `int` | The chain the token lives on. | +| `amount` | `str` | The requested amount, in the token's smallest unit (wei). | + +```python +from status_sdk import Account + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +for message in account.listen_messages(): + for payment in message.payment_requests: + print(f"{payment.amount} {payment.token_symbol} -> {payment.to_address}") +``` + +## `ContactRequest` + +Returned by [`listen_contact_requests`](./account.md#listen_contact_requests) from `Account`. + +| Attribute | Type | Description | +|----|----|-------------| +| `id` | `str` | The contact request id. Pass this as `request_id` to [`add_contact`](./account.md#add_contactpublic_key-request_idnone-display_namenone) to approve it. | +| `public_key` | `str` | Public key of the other account. | +| `incoming` | `bool` | Another user sent a contact request to the account. Approve it with [`add_contact`](./account.md#add_contactpublic_key-request_idnone-display_namenone). | +| `accepted` | `bool` | Another user accepted a contact request that the account had sent. The contact is now mutual. | +| `removed` | `bool` | A user has removed the account from their contacts. | + +The state is carried by **three booleans** rather than one string, and **exactly one of them is `True`** on any yielded request - the one matching the state the request just moved into. The rest stay `False`. + +```python +from status_sdk import Account + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +for request in account.listen_contact_requests(): + if request.incoming: + print("New contact request received") + elif request.accepted: + print("Contact request was accepted") + elif request.removed: + print(f"{request.public_key} has removed you") +``` + +## `CommunityRequest` + +Returned by [`listen_requests`](./community.md#listen_requests) from `Community`. + +| Attribute | Type | Description | +|----|----|-------------| +| `id` | `str` | The join request id. Pass this to [`accept`](./community.md#acceptpending_request_id) or [`decline`](./community.md#declinepending_request_id). | +| `public_key` | `str` | Public key of the requesting member. | +| `pending` | `bool` | The request is waiting to be [accepted](./community.md#acceptpending_request_id) or [declined](./community.md#declinepending_request_id). | +| `reject` | `bool` | The request was declined. | +| `accept` | `bool` | The request was accepted and the member joined. | +| `cancel` | `bool` | The request was cancelled. | + + +```python +from status_sdk import Account, Community + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +url = "https://status.app/c/G3QAAMQn9ueHRsR3W5Ouuy25fkCxziknAIEkCbYAoC04HjyGeQ6X8k45q3GVeyZiksbd38tQ4S_EfhrJKhRV3sDvjhmrCuSoDBIf2QJiEKwAOZipxis8ntNRVyPhC5IoWaEsj9X4P5zw093pcLofZzTV2gM=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9" +community = Community(account, url=url) + +for request in community.listen_requests(): + print(f"{request.public_key}\tpending={request.pending}") +``` + +## `TokenPermission` + +Describe a token a community member must hold for a [permission](./community.md#add_permissionpermission-tokensnone). This model is **constructed by the user** and passed *into* the SDK - see [Token criteria](./community.md#token-criteria). + +| Attribute | Type | Required | Description | +|----|----|----|-------------| +| `symbol` | `str` | Yes | The token symbol, as it appears in [`get_tokens`](./account.md#get_tokens) on `Account`. | +| `amount` | `float` | Yes | The minimum amount a member must hold. Converted to wei using the token's own `decimals`, so it is written in human units - `10` means 10 tokens, not 10 wei. | +| `chain_id` | `int` | No | The chain the token lives on. Defaults to `1` (Ethereum mainnet). | +| `address` | `str` | No | The token's contract address. Needed only when the same `symbol` exists more than once on `chain_id`. | + +```python +from status_sdk import models + +token = models.TokenPermission(symbol="SNT", amount=10) +``` diff --git a/docs/types.md b/docs/types.md new file mode 100644 index 0000000..257e6e2 --- /dev/null +++ b/docs/types.md @@ -0,0 +1,61 @@ +# Types + +## `Content` + +The content type of a message, matching the `contentType` Status Backend uses internally. + +| Name | Value | Meaning | +|-----|-----|-------------| +| `TEXT` | `1` | A plain text message. | +| `STICKER` | `2` | A sticker. | +| `EMOJI` | `4` | An emoji-only message. | +| `IMAGE` | `7` | An image, or an album of images sent together - see [`send_image`](./account.md#send_imagechat_id-file_paths-messagenone-reply_to_message_idnone). | +| `BRIDGED` | `18` | A message relayed from another platform - see [`send_bridged_message`](./account.md#send_bridged_messagechat_id-message-namenone-usernamenone-user_idnone-message_idnone-reply_to_message_idnone-image_urlnone). | + +```python +from status_sdk import Account, types + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +# Only yield text and emoji messages +for msg in account.listen_messages(content=[types.Content.TEXT, types.Content.EMOJI]): + print(msg.content) +``` + +A single value can be passed instead of a list: + +```python +# Only yield images (and albums) +for msg in account.listen_messages(content=types.Content.IMAGE): + print(msg.image_urls) +``` + +## `Chat` + +The type of chat a message was sent in. + +| Name | Value | Meaning | +|-----|-----|-------------| +| `PRIVATE` | `1` | A one-to-one chat with a [mutual contact](./account.md#contacts). | +| `COMMUNITY` | `2` | A [`Channel`](./community.md) inside a community. | +| `GROUP` | `3` | A [`GroupChat`](./group-chat.md). | + +```python +from status_sdk import Account, types + +account = Account() +params = { + "name": "status-app-bot", + "password": "SNTPUMP" +} +account.login(**params) + +for msg in account.listen_messages(): + if msg.chat_type == types.Chat.GROUP: + print(f"Group message: {msg.content}") +``` diff --git a/status_sdk/account.py b/status_sdk/account.py index b4eb8e6..21526f1 100644 --- a/status_sdk/account.py +++ b/status_sdk/account.py @@ -9,7 +9,7 @@ from PIL import Image from PIL.JpegImagePlugin import JpegImageFile from PIL.PngImagePlugin import PngImageFile -from . import constants, models +from . import constants, models, types from .signal import Signal class Account: @@ -967,17 +967,16 @@ 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, content: Optional[Union[list[types.Content], types.Content]] = None) -> Generator[models.Message, None, None]: """ - Listen for new **RAW** messages continuously. Can be used for real time processing. + Listen for new messages continuously. Can be used for real time processing. """ - ALLOWED_CONTENT_TYPES = [ - 1, # Text - 2, # Sticker - 4, # Emojis - 7, # Image - 18, # Bridged Message - ] + if isinstance(content, type(None)): + content = [] + elif isinstance(content, types.Content): + content = [content] + + allowed_content_types = [content_type.value for content_type in (content if content else types.Content)] albums: dict[str, list[dict]] = {} processed: list[str] = [] SIZE = 100 @@ -985,7 +984,7 @@ 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: + if raw["contentType"] not in allowed_content_types or raw["id"] in processed: continue if not raw.get("albumId"): From d0110c106534e6843ed42cb6569ce8656df7e8dc Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Sun, 11 Oct 2026 21:20:24 +0300 Subject: [PATCH 4/4] docs: CHANGELOG 1.3.0 --- CHANGELOG.md | 14 ++++++++++++++ pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05e5529..d3483f8 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] - 2026-10-11 + +### Added + +- Enums for Message Content Type (`Content`) and Chat Types (`Chat`) + +### Changed + +- `listen_messages` can listen for specific `Content` type. +- `Message` model + - Property `content_type` and `chat_type` will return `Content` and `Chat` types instead of `str`. + - Store content URLs in `list[str]` property `image_urls` + ## [1.2.3] - 2026-10-10 ### Fixed @@ -229,6 +242,7 @@ will try to log in without a hashed password - Launch [status-go] Docker container with Python instead of manual `docker compose up -d` setup. - Custom library errors +[1.3.0]: https://github.com/status-im/status-python-sdk/releases/tag/1.3.0 [1.2.3]: https://github.com/status-im/status-python-sdk/releases/tag/1.2.3 [1.2.2]: https://github.com/status-im/status-python-sdk/releases/tag/1.2.2 [1.2.1]: https://github.com/status-im/status-python-sdk/releases/tag/1.2.1 diff --git a/pyproject.toml b/pyproject.toml index 5e08141..73163a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "status-sdk" -version = "1.2.3" +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"