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/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/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"
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/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"):
diff --git a/status_sdk/models/message.py b/status_sdk/models/message.py
index 8372bd9..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,11 +72,12 @@ 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
bridge_id: Optional[str] = None
payment_requests: list[PaymentRequest] = field(default_factory=list)
@@ -89,43 +91,46 @@ 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:
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["content_type"] = "sticker"
+ params["image_urls"] += [params["content"]]
+ 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"]]
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)
- 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