diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..79a6265
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [krypton-byte]
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index e9a5dda..bee8c8a 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -51,13 +51,9 @@ jobs:
python-version: "3.12"
- uses: astral-sh/setup-uv@v5
- uses: dtolnay/rust-toolchain@stable
- - name: Rust cache (tryx root)
+ - name: Rust cache
uses: Swatinem/rust-cache@v2
- - name: Rust cache (whatsapp-rust workspace)
- uses: Swatinem/rust-cache@v2
- with:
- workspaces: "libs/whatsapp-rust -> target"
- name: Install dev dependencies
run: uv sync --group dev --no-install-project
- name: Build native extension
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index e4ee365..37c933c 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -50,14 +50,9 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- - name: Rust cache (tryx root)
+ - name: Rust cache
uses: Swatinem/rust-cache@v2
- - name: Rust cache (whatsapp-rust workspace)
- uses: Swatinem/rust-cache@v2
- with:
- workspaces: "libs/whatsapp-rust -> target"
-
- name: Install dev dependencies
run: uv sync --group dev --no-install-project
diff --git a/Cargo.lock b/Cargo.lock
index 01a29a3..95bbe94 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -186,18 +186,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
-]
-
-[[package]]
-name = "audiopus_sys"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651"
-dependencies = [
- "cmake",
- "log",
- "pkg-config",
+ "syn 3.0.4",
]
[[package]]
@@ -277,34 +266,32 @@ dependencies = [
[[package]]
name = "bon"
-version = "3.9.3"
+version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561"
+checksum = "9e3fac94a66da67200398458a25412bcc3f9b6443b5119a6cad9cf3ccfcd8cc6"
dependencies = [
"bon-macros",
- "rustversion",
]
[[package]]
name = "bon-macros"
-version = "3.9.3"
+version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f"
+checksum = "d4654961ad0494e4774c5c60b4cb4cd0ae9b9d92d039d901638b1dba97ebebf5"
dependencies = [
- "darling 0.23.0",
+ "darling 0.24.1",
"ident_case",
- "prettyplease",
+ "prettyplease 0.3.0",
"proc-macro2",
"quote",
- "rustversion",
- "syn 2.0.119",
+ "syn 3.0.4",
]
[[package]]
name = "buffa"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf9e6224bc4ee1f189ad257120c156fb05f95b826f5369d620b24984476c200a"
+checksum = "a92f2f5df67a9d5ccfc65237bfc954c56328aee40a04a9b92381ecba370be246"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -320,9 +307,9 @@ dependencies = [
[[package]]
name = "buffa-build"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "247d43740a4854a9c1b98a5931d6d67d8f8b41ffeb2a43ca008d460e79b1eb2c"
+checksum = "eee6b6b4a8d23f1f98a043f188f4d9d2ef8bee43191f42e93ac577f3c3b0f334"
dependencies = [
"buffa",
"buffa-codegen",
@@ -331,13 +318,13 @@ dependencies = [
[[package]]
name = "buffa-codegen"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2074a9c2f76b2ebe6af40f7bf67b6a19d6ce3663253ef2a55124dc93f38b230e"
+checksum = "bb4bea10ef85ea3d06a20412c67cb612527feb25f33367eab850139441fb5dd9"
dependencies = [
"buffa",
"buffa-descriptor",
- "prettyplease",
+ "prettyplease 0.2.37",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -346,9 +333,9 @@ dependencies = [
[[package]]
name = "buffa-descriptor"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "964d735e0b06cb24fa15a68c5aa99549abcc7e0bbeb76298f2fec57912fb79c1"
+checksum = "2b7dd79a6898e70dac8adae959c8ad2cae51ed96397935c91c996280cedcb55e"
dependencies = [
"buffa",
"rustversion",
@@ -382,7 +369,7 @@ checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -680,12 +667,12 @@ dependencies = [
[[package]]
name = "darling"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
- "darling_core 0.23.0",
- "darling_macro 0.23.0",
+ "darling_core 0.24.1",
+ "darling_macro 0.24.1",
]
[[package]]
@@ -704,15 +691,15 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.119",
+ "syn 3.0.4",
]
[[package]]
@@ -728,13 +715,13 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
- "darling_core 0.23.0",
+ "darling_core 0.24.1",
"quote",
- "syn 2.0.119",
+ "syn 3.0.4",
]
[[package]]
@@ -854,7 +841,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -1023,7 +1010,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -1099,6 +1086,18 @@ dependencies = [
"polyval",
]
+[[package]]
+name = "gloo-timers"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1400,9 +1399,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
-version = "0.37.0"
+version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
+checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
dependencies = [
"cc",
"pkg-config",
@@ -1455,12 +1454,6 @@ dependencies = [
"digest 0.10.7",
]
-[[package]]
-name = "md5"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
-
[[package]]
name = "memchr"
version = "2.8.3"
@@ -1626,11 +1619,20 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "opus"
-version = "0.3.1"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33718946cc77d4032911d4efe03a66dbcbfbd2bb16c3da06aaeadcc637c32216"
+dependencies = [
+ "opusic-sys",
+]
+
+[[package]]
+name = "opusic-sys"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d3809943dff6fbad5f0484449ea26bdb9cb7d8efdf26ed50d3c7f227f69eb5c"
+checksum = "c9d1ecdf206421bc74343ab3bb2f30ad2abbfee41fa341f7181fecbaf957769a"
dependencies = [
- "audiopus_sys",
+ "cmake",
]
[[package]]
@@ -1732,6 +1734,16 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "prettyplease"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"
+dependencies = [
+ "proc-macro2",
+ "syn 3.0.4",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -1758,7 +1770,7 @@ checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -1996,7 +2008,7 @@ checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -2011,9 +2023,9 @@ dependencies = [
[[package]]
name = "rtc-crypto"
-version = "0.21.0-beta.1"
+version = "0.21.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d47521f43ea9a6e16feafa78ba06633ca85a51f0ad66b906a5f01ccfe08ba90"
+checksum = "df0f2e4801a79469b8094f715f563d946c76826336f8d9fefb47dc6628524f11"
dependencies = [
"aes 0.8.4",
"ccm",
@@ -2030,9 +2042,9 @@ dependencies = [
[[package]]
name = "rtc-datachannel"
-version = "0.21.0-beta.1"
+version = "0.21.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "374adcfc6f138839109d84c521ccedae9410b11f62c1ae577cb786ed91494f50"
+checksum = "0180e434128b3a3e9d8552ea958fd588839266cf08a2625741fbe65c292d1cb8"
dependencies = [
"bytes",
"log",
@@ -2043,9 +2055,9 @@ dependencies = [
[[package]]
name = "rtc-dtls"
-version = "0.21.0-beta.1"
+version = "0.21.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3baf3ee5b4d481d57fdee828ed4abd281ba45ad98594502cd4e866d14316b48f"
+checksum = "b6cb87e1ec1b828914aace884307974a9ed40747e2f6ccaab93a4fff42528a73"
dependencies = [
"bytecheck",
"byteorder",
@@ -2063,9 +2075,9 @@ dependencies = [
[[package]]
name = "rtc-sctp"
-version = "0.21.0-beta.1"
+version = "0.21.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db38ed06af7bea634b210c3e8cf0e78e3bb36669e120968f5961a3c12a209b67"
+checksum = "9d9dabdca726a7c5eed4d4ae5f3f9d4e9baf7a2ed53efaedaae35b4bcc24bc2a"
dependencies = [
"bytes",
"crc32c",
@@ -2079,9 +2091,9 @@ dependencies = [
[[package]]
name = "rtc-shared"
-version = "0.21.0-beta.1"
+version = "0.21.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "108063844e58fa8470139c2d6bea3a28362e07d43194acef7e0d57d829d27cd6"
+checksum = "998b045d95d158251842c5146918f3213346ca7724e8c6ac750c37d467dbbcdd"
dependencies = [
"bitflags 1.3.2",
"bytes",
@@ -2199,6 +2211,15 @@ version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -2235,7 +2256,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -2524,9 +2545,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.3"
+version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
@@ -2600,7 +2621,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -2691,7 +2712,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -2984,6 +3005,7 @@ dependencies = [
"buffa",
"buffa-build",
"bytes",
+ "cbc",
"chrono",
"compact_str",
"ctr 0.10.1",
@@ -2995,7 +3017,6 @@ dependencies = [
"hmac 0.13.0",
"itoa",
"log",
- "md5",
"portable-atomic",
"rand",
"serde",
@@ -3059,7 +3080,7 @@ version = "0.7.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -3209,6 +3230,7 @@ dependencies = [
"getrandom 0.4.3",
"hashbrown 0.17.1",
"hex",
+ "hmac 0.13.0",
"itoa",
"log",
"opus",
@@ -3219,8 +3241,10 @@ dependencies = [
"rtc-sctp",
"rtc-shared",
"scopeguard",
+ "send_wrapper",
"serde",
"serde_json",
+ "sha2",
"smallvec",
"thiserror 2.0.20",
"tokio",
@@ -3243,13 +3267,16 @@ dependencies = [
"bytes",
"diesel",
"diesel_migrations",
+ "gloo-timers",
"libsqlite3-sys",
"log",
"scheduled-thread-pool",
"serde_json",
"sha2",
+ "thiserror 2.0.20",
"tokio",
"wacore",
+ "wacore-binary",
]
[[package]]
@@ -3619,7 +3646,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 45c1e69..5af9b61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,7 @@ wacore-binary = { path = "libs/whatsapp-rust/wacore/binary", default-features =
tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "signal", "sync", "process"] }
async-trait = "0.1.89"
pyo3-async-runtimes = { version = "0.28.0", features = ["attributes", "tokio-runtime"] }
-buffa = { version = "0.9.1", default-features = false, features = ["json"] }
+buffa = { version = "0.9.2", default-features = false, features = ["json"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
chrono = "0.4.44"
diff --git a/README.id.md b/README.id.md
index e923495..0211f8f 100644
--- a/README.id.md
+++ b/README.id.md
@@ -1,8 +1,13 @@
+
+
+
# Tryx
[](https://pypi.org/project/tryx/)
[](https://pypi.org/project/tryx/)
[](LICENSE)
+
+
[](https://krypton-byte.github.io/tryx/)
**Bahasa:** [English](README.md) | [简体中文](README.zh.md) | Bahasa Indonesia
diff --git a/README.md b/README.md
index 95a24c2..fbbdeea 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+
+
+
# Tryx
[](https://pypi.org/project/tryx/)
@@ -7,6 +10,8 @@
**Language:** English | [简体中文](README.zh.md) | [Bahasa Indonesia](README.id.md)
+
+
Tryx is a Python automation library powered by Rust and PyO3. It provides a Python-friendly API on top of the `whatsapp-rust` crate, with async support, persistent sessions, and access to advanced WhatsApp Web protocol features.
> Tryx is an independent project. It is not affiliated with WhatsApp, Meta, or their official products.
diff --git a/README.zh.md b/README.zh.md
index bc51c1e..371ee42 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -1,8 +1,13 @@
+
+
+
# Tryx
[](https://pypi.org/project/tryx/)
[](https://pypi.org/project/tryx/)
[](LICENSE)
+
+
[](https://krypton-byte.github.io/tryx/)
**语言:** [English](README.md) | 简体中文 | [Bahasa Indonesia](README.id.md)
diff --git a/assets/mascot.png b/assets/mascot.png
new file mode 100644
index 0000000..7cf791c
Binary files /dev/null and b/assets/mascot.png differ
diff --git a/docs/api/client.md b/docs/api/client.md
index 39c5d16..bbf05c9 100644
--- a/docs/api/client.md
+++ b/docs/api/client.md
@@ -1,4 +1,4 @@
-# Client API Gateway
+# :material-api: Client API Gateway
::: tryx.client.Tryx
options:
diff --git a/docs/api/events.md b/docs/api/events.md
index 4a18b08..c52b3da 100644
--- a/docs/api/events.md
+++ b/docs/api/events.md
@@ -1,4 +1,4 @@
-# Events API
+# :material-broadcast: Events API
::: tryx.events.Dispatcher
options:
@@ -172,6 +172,37 @@ async def on_presence(client, event):
pass
```
+## Example: Group Subject Change with LID Addressing
+
+When a group subject (name) is changed, the `Subject` action carries the renamer's identity. In LID-addressed groups, the phone-number alias arrives in a separate field:
+
+```python
+from tryx.events import EvGroupUpdate, GroupNotificationAction
+
+
+@app.on(EvGroupUpdate)
+async def on_group_update(client, event):
+ action = event.data.action
+
+ if isinstance(action, GroupNotificationAction.Subject):
+ renamer = action.subject_owner # JID (may be a LID)
+ renamer_pn = action.subject_owner_pn # phone-number JID (when LID addressing)
+ renamer_name = action.subject_owner_username # username (when enabled)
+
+ # Prefer the phone-number JID for display when available
+ display_jid = renamer_pn or renamer
+ print(f"Group renamed to {action.subject!r} by {display_jid}")
+
+ # Log all identity fields for audit
+ if renamer_pn:
+ print(f" LID: {renamer}, PN: {renamer_pn}")
+ if renamer_name:
+ print(f" Username: {renamer_name}")
+```
+
+!!! tip "LID vs Phone Number"
+ In WhatsApp's LID addressing mode, `subject_owner` may be a `@lid` JID that is not human-readable. The `subject_owner_pn` field provides the corresponding phone-number JID for display. When LID addressing is not active, `subject_owner_pn` is `None` and `subject_owner` already contains the phone-number JID.
+
## Enum-like Support Types
Common reason/state classes used by event payloads:
diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md
index aa3c0bd..e40a726 100644
--- a/docs/api/exceptions.md
+++ b/docs/api/exceptions.md
@@ -1,4 +1,4 @@
-# Exceptions API
+# :material-alert-circle: Exceptions API
::: tryx.exceptions.FailedBuildClient
options:
diff --git a/docs/api/index.md b/docs/api/index.md
new file mode 100644
index 0000000..99ad245
--- /dev/null
+++ b/docs/api/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/api
+---
+
+# :material-api: API Reference
+
+Complete API documentation for all Tryx modules.
diff --git a/docs/api/types.md b/docs/api/types.md
index 05c9122..6e02eff 100644
--- a/docs/api/types.md
+++ b/docs/api/types.md
@@ -1,4 +1,4 @@
-# Types API
+# :material-shape: Types API
::: tryx.types.JID
options:
diff --git a/docs/assets/mascot.png b/docs/assets/mascot.png
new file mode 100644
index 0000000..7cf791c
Binary files /dev/null and b/docs/assets/mascot.png differ
diff --git a/docs/core-concepts/architecture.md b/docs/core-concepts/architecture.md
index 4194aea..b469839 100644
--- a/docs/core-concepts/architecture.md
+++ b/docs/core-concepts/architecture.md
@@ -1,4 +1,4 @@
-# Architecture
+# :material-puzzle: Architecture
Tryx splits performance-sensitive protocol work (Rust) from application
ergonomics (Python). This gives you WhatsApp protocol handling at native
diff --git a/docs/core-concepts/event-model.md b/docs/core-concepts/event-model.md
index f18709e..65e6bd5 100644
--- a/docs/core-concepts/event-model.md
+++ b/docs/core-concepts/event-model.md
@@ -1,4 +1,4 @@
-# Event Model
+# :material-bell-ring: Event Model
Tryx uses an event-driven architecture. All WhatsApp interactions — messages,
presence changes, group updates, contact syncs — flow through typed event
diff --git a/docs/core-concepts/index.md b/docs/core-concepts/index.md
new file mode 100644
index 0000000..d0cbb41
--- /dev/null
+++ b/docs/core-concepts/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/puzzle-outline
+---
+
+# :material-puzzle: Core Concepts
+
+Understand the architecture and design principles behind Tryx.
diff --git a/docs/core-concepts/storage-backends.md b/docs/core-concepts/storage-backends.md
index efc2f9a..7d203eb 100644
--- a/docs/core-concepts/storage-backends.md
+++ b/docs/core-concepts/storage-backends.md
@@ -1,4 +1,4 @@
-# Storage Backends
+# :material-database: Storage Backends
Tryx supports a **3-tier storage architecture** so you can pick the right balance between simplicity, performance, and flexibility for your project.
diff --git a/docs/core-concepts/type-system.md b/docs/core-concepts/type-system.md
index cd37b2d..4330d47 100644
--- a/docs/core-concepts/type-system.md
+++ b/docs/core-concepts/type-system.md
@@ -1,4 +1,4 @@
-# Type System
+# :material-code-braces: Type System
Tryx ships with complete `.pyi` stub files and a `py.typed` marker,
enabling full editor intelligence, static analysis, and API discoverability.
diff --git a/docs/faq/qna.md b/docs/faq/qna.md
index cebc82e..a885128 100644
--- a/docs/faq/qna.md
+++ b/docs/faq/qna.md
@@ -1,4 +1,4 @@
-# FAQ
+# :material-frequently-asked-questions: FAQ
Quick answers to common questions. Jump to the linked pages for implementation
details.
diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md
index f9d7555..8edbd32 100644
--- a/docs/getting-started/authentication.md
+++ b/docs/getting-started/authentication.md
@@ -1,4 +1,4 @@
-# Authentication Flow
+# :material-key-variant: Authentication Flow
Tryx follows the WhatsApp multi-device pairing flow. The first run links
a session, and later runs reuse stored state.
diff --git a/docs/getting-started/contributing.md b/docs/getting-started/contributing.md
index e92d8d8..3f5d25a 100644
--- a/docs/getting-started/contributing.md
+++ b/docs/getting-started/contributing.md
@@ -1,4 +1,4 @@
-# Contributing
+# :material-account-group: Contributing
This guide explains how to contribute to Tryx development.
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
new file mode 100644
index 0000000..ab1f94f
--- /dev/null
+++ b/docs/getting-started/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/rocket-launch-outline
+---
+
+# :material-rocket-launch: Getting Started
+
+Get up and running with Tryx quickly.
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 76e3c9c..80a8510 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -1,4 +1,4 @@
-# Installation
+# :material-package-variant: Installation
This guide sets up a working development environment for building with Tryx.
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
index 374aa80..1faa13f 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started/quickstart.md
@@ -1,4 +1,4 @@
-# Quick Start
+# :material-rocket-launch: Quick Start
Build a working WhatsApp bot in 5 minutes. This guide walks through the
minimal setup, then expands with real functionality.
diff --git a/docs/index.md b/docs/index.md
index 997a3ae..f34b44b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,3 +1,11 @@
+---
+icon: material/home
+---
+
+
+
+
+
# Tryx
**Rust-powered Python SDK for event-driven WhatsApp automation.**
diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md
index e9bd46a..f50f4de 100644
--- a/docs/operations/deployment.md
+++ b/docs/operations/deployment.md
@@ -1,4 +1,4 @@
-# Deployment Guide
+# :material-cloud-upload: Deployment Guide
Deploy Tryx bots safely in production with stable session storage and predictable restarts.
diff --git a/docs/operations/index.md b/docs/operations/index.md
new file mode 100644
index 0000000..90185c6
--- /dev/null
+++ b/docs/operations/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/cog-outline
+---
+
+# :material-cog: Operations
+
+Deployment, monitoring, and maintenance guides.
diff --git a/docs/operations/performance.md b/docs/operations/performance.md
index 88ab8b0..2b46c63 100644
--- a/docs/operations/performance.md
+++ b/docs/operations/performance.md
@@ -1,4 +1,4 @@
-# Performance Guide
+# :material-speedometer: Performance Guide
Optimize for predictable latency and stable memory under burst traffic.
diff --git a/docs/operations/reliability.md b/docs/operations/reliability.md
index 45759b6..501ff3a 100644
--- a/docs/operations/reliability.md
+++ b/docs/operations/reliability.md
@@ -1,4 +1,4 @@
-# Reliability Playbook
+# :material-shield-check: Reliability Playbook
This page focuses on idempotency, retry strategy, and safe handler design.
diff --git a/docs/operations/security.md b/docs/operations/security.md
index 642e73a..7a458a0 100644
--- a/docs/operations/security.md
+++ b/docs/operations/security.md
@@ -1,4 +1,4 @@
-# Security Practices
+# :material-lock: Security Practices
Secure your client around four risk zones: session state, operator controls, user input, and outbound behavior.
diff --git a/docs/operations/troubleshooting.md b/docs/operations/troubleshooting.md
index 8e07a4c..0fc5efd 100644
--- a/docs/operations/troubleshooting.md
+++ b/docs/operations/troubleshooting.md
@@ -1,4 +1,4 @@
-# Troubleshooting
+# :material-wrench: Troubleshooting
Use this page as a decision tree first, checklist second.
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 0af326a..8c4bcf4 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,5 +1,11 @@
{% extends "base.html" %}
+{% block scripts %}
+ {{ super() }}
+
+
+{% endblock %}
+
{% block outdated %}
You're not viewing the latest version.
diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md
index ca51fd0..7b3c1d3 100644
--- a/docs/reference/changelog.md
+++ b/docs/reference/changelog.md
@@ -1,4 +1,4 @@
-# Changelog Policy
+# :material-history: Changelog Policy
Tryx changelog is generated automatically with `python-semantic-release`.
diff --git a/docs/reference/error-handling.md b/docs/reference/error-handling.md
index c57fafa..05d8a1d 100644
--- a/docs/reference/error-handling.md
+++ b/docs/reference/error-handling.md
@@ -1,4 +1,4 @@
-# Error Handling
+# :material-bug: Error Handling
Use exception classes and failure classification to decide whether to retry,
fail fast, or trigger operator action.
diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md
index 2d0cd25..7cb37ff 100644
--- a/docs/reference/glossary.md
+++ b/docs/reference/glossary.md
@@ -1,4 +1,4 @@
-# Glossary
+# :material-book-alphabetical: Glossary
---
diff --git a/docs/reference/index.md b/docs/reference/index.md
new file mode 100644
index 0000000..39e2867
--- /dev/null
+++ b/docs/reference/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/bookshelf
+---
+
+# :material-bookshelf: Reference
+
+Additional resources, FAQ, and changelog.
diff --git a/docs/reference/roadmap.md b/docs/reference/roadmap.md
index 301658b..e252cd1 100644
--- a/docs/reference/roadmap.md
+++ b/docs/reference/roadmap.md
@@ -1,4 +1,4 @@
-# Roadmap
+# :material-map: Roadmap
## Near-Term
diff --git a/docs/tutorials/command-bot.md b/docs/tutorials/command-bot.md
index 208ed90..88a9da7 100644
--- a/docs/tutorials/command-bot.md
+++ b/docs/tutorials/command-bot.md
@@ -1,4 +1,4 @@
-# Tutorial: Command Bot
+# :material-console: Tutorial: Command Bot
Build a command-driven WhatsApp bot that stays maintainable as command count
grows. This tutorial progresses from a minimal router to a production-ready
diff --git a/docs/tutorials/group-automation.md b/docs/tutorials/group-automation.md
index 3ae71de..5927c51 100644
--- a/docs/tutorials/group-automation.md
+++ b/docs/tutorials/group-automation.md
@@ -1,4 +1,4 @@
-# Tutorial: Group Automation
+# :material-account-multiple: Tutorial: Group Automation
Automate full group lifecycle with safe participant governance.
diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md
new file mode 100644
index 0000000..9d98f9b
--- /dev/null
+++ b/docs/tutorials/index.md
@@ -0,0 +1,7 @@
+---
+icon: material/school-outline
+---
+
+# :material-school: Tutorials
+
+Step-by-step guides to build real-world integrations.
diff --git a/docs/tutorials/media-workflows.md b/docs/tutorials/media-workflows.md
index 67899e6..cf9f6e3 100644
--- a/docs/tutorials/media-workflows.md
+++ b/docs/tutorials/media-workflows.md
@@ -1,4 +1,4 @@
-# Tutorial: Media Workflows
+# :material-image-multiple: Tutorial: Media Workflows
This tutorial covers send, download, and recovery flows for media-heavy bots.
diff --git a/docs/tutorials/poll-survey.md b/docs/tutorials/poll-survey.md
index d0ed405..f06abac 100644
--- a/docs/tutorials/poll-survey.md
+++ b/docs/tutorials/poll-survey.md
@@ -1,4 +1,4 @@
-# Tutorial: Poll Survey Workflow
+# :material-poll: Tutorial: Poll Survey Workflow
Create poll surveys, collect encrypted votes, and aggregate results.
diff --git a/docs/tutorials/profile-privacy.md b/docs/tutorials/profile-privacy.md
index c825837..eeea536 100644
--- a/docs/tutorials/profile-privacy.md
+++ b/docs/tutorials/profile-privacy.md
@@ -1,4 +1,4 @@
-# Tutorial: Profile and Privacy
+# :material-account-cog: Tutorial: Profile and Privacy
Build admin-controlled profile and privacy management commands.
diff --git a/libs/whatsapp-rust b/libs/whatsapp-rust
index 1489b7d..539ed05 160000
--- a/libs/whatsapp-rust
+++ b/libs/whatsapp-rust
@@ -1 +1 @@
-Subproject commit 1489b7da9a6a7e3cadc4337ee46399da7de25915
+Subproject commit 539ed050817f351d080fb5092179b22cebaceffe
diff --git a/mkdocs.yml b/mkdocs.yml
index c724ea9..0d9ba16 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,60 +9,67 @@ edit_uri: edit/main/docs/
nav:
- Home: index.md
- Getting Started:
- - Installation: getting-started/installation.md
- - Quick Start: getting-started/quickstart.md
- - Authentication Flow: getting-started/authentication.md
- - Contributing: getting-started/contributing.md
+ - getting-started/index.md
+ - getting-started/installation.md
+ - getting-started/quickstart.md
+ - getting-started/authentication.md
+ - getting-started/contributing.md
- Core Concepts:
- - Architecture: core-concepts/architecture.md
- - Event Model: core-concepts/event-model.md
- - Storage Backends: core-concepts/storage-backends.md
- - Type System: core-concepts/type-system.md
+ - core-concepts/index.md
+ - core-concepts/architecture.md
+ - core-concepts/event-model.md
+ - core-concepts/storage-backends.md
+ - core-concepts/type-system.md
- API Reference:
- - Client API Gateway: api/client.md
- - Storage Backends: api/backend.md
- - Client Namespaces:
- - Contact: api/contact.md
- - Chat Actions: api/chat-actions.md
- - Community: api/community.md
- - Newsletter: api/newsletter.md
- - Groups: api/groups.md
- - Status: api/status.md
- - Chatstate: api/chatstate.md
- - Blocking: api/blocking.md
- - Polls: api/polls.md
- - Presence: api/presence.md
- - Privacy: api/privacy.md
- - Profile: api/profile.md
- - Events API: api/events.md
- - Types API: api/types.md
- - Exceptions: api/exceptions.md
- - Helpers: api/helpers.md
- - WACore: api/wacore.md
+ - api/index.md
+ - api/client.md
+ - api/backend.md
+ - api/contact.md
+ - api/chat-actions.md
+ - api/community.md
+ - api/newsletter.md
+ - api/groups.md
+ - api/status.md
+ - api/chatstate.md
+ - api/blocking.md
+ - api/polls.md
+ - api/presence.md
+ - api/privacy.md
+ - api/profile.md
+ - api/events.md
+ - api/types.md
+ - api/exceptions.md
+ - api/helpers.md
+ - api/wacore.md
- Tutorials:
- - Command Bot: tutorials/command-bot.md
- - Profile & Privacy: tutorials/profile-privacy.md
- - Poll Survey: tutorials/poll-survey.md
- - Media Workflows: tutorials/media-workflows.md
- - Group Automation: tutorials/group-automation.md
+ - tutorials/index.md
+ - tutorials/command-bot.md
+ - tutorials/profile-privacy.md
+ - tutorials/poll-survey.md
+ - tutorials/media-workflows.md
+ - tutorials/group-automation.md
- Operations:
- - Deployment: operations/deployment.md
- - Reliability: operations/reliability.md
- - Troubleshooting: operations/troubleshooting.md
- - Performance: operations/performance.md
- - Security: operations/security.md
+ - operations/index.md
+ - operations/deployment.md
+ - operations/reliability.md
+ - operations/troubleshooting.md
+ - operations/performance.md
+ - operations/security.md
- Reference:
- - FAQ: faq/qna.md
- - Glossary: reference/glossary.md
- - Error Handling: reference/error-handling.md
- - Roadmap: reference/roadmap.md
- - Changelog: reference/changelog.md
+ - reference/index.md
+ - faq/qna.md
+ - reference/glossary.md
+ - reference/error-handling.md
+ - reference/roadmap.md
+ - reference/changelog.md
theme:
name: material
variant: modern
language: en
custom_dir: docs/overrides
+ logo: assets/mascot.png
+ favicon: assets/mascot.png
font:
text: Inter
code: JetBrains Mono
@@ -70,6 +77,7 @@ theme:
- navigation.sections
- navigation.tabs
- navigation.tabs.sticky
+ - navigation.indexes
- navigation.top
- navigation.path
- navigation.tracking
@@ -87,7 +95,7 @@ theme:
- search.highlight
- search.share
- toc.follow
- - toc.integrate
+ # - toc.integrate # not compatible with navigation.indexes
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
diff --git a/python/tryx/backend.pyi b/python/tryx/backend.pyi
index 76f49b1..c9e222d 100644
--- a/python/tryx/backend.pyi
+++ b/python/tryx/backend.pyi
@@ -17,6 +17,8 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
+from .types import JID
+
# ── Built-in backends ────────────────────────────────────────────────────────
class BackendBase:
@@ -31,6 +33,15 @@ class BackendBase:
...
+class StoredDeviceSummary:
+ """Summary of a stored WhatsApp device session."""
+
+ id: int
+ pn: JID | None
+ lid: JID | None
+ push_name: str
+ linked: bool
+
class SqliteStore(BackendBase):
"""Built-in SQLite storage backend.
@@ -40,6 +51,7 @@ class SqliteStore(BackendBase):
Args:
path: Filesystem path to the SQLite database file.
Created automatically if it doesn't exist.
+ device_id: Device ID index (default 0).
Example::
@@ -49,13 +61,14 @@ class SqliteStore(BackendBase):
"""
path: str
+ device_id: int
- def __init__(self, path: str) -> None:
- """
- Create a SQLite storage backend.
+ def __init__(self, path: str, device_id: int = 0) -> None:
+ """Create a SQLite storage backend.
Args:
path: Filesystem path to the database file.
+ device_id: Device ID index (default 0).
Example::
@@ -63,6 +76,41 @@ class SqliteStore(BackendBase):
"""
...
+ async def create_new_device(self) -> int:
+ """Create a new device entry in the database and return its ID.
+
+ Returns:
+ The newly created device ID.
+ """
+ ...
+
+ async def list_devices(self) -> list[StoredDeviceSummary]:
+ """List all stored device sessions in the database.
+
+ Returns:
+ List of StoredDeviceSummary entries.
+ """
+ ...
+
+ async def remove_device(self, device_id: int) -> None:
+ """Delete a device session by ID.
+
+ Args:
+ device_id: ID of the device to remove.
+ """
+ ...
+
+ async def device_exists(self, device_id: int) -> bool:
+ """Check if a device with the given ID exists.
+
+ Args:
+ device_id: ID of the device to check.
+
+ Returns:
+ True if the device exists, False otherwise.
+ """
+ ...
+
# ── Internal Rust structs (JSON-serialized across FFI) ──────────────────────
class Device:
diff --git a/python/tryx/client.pyi b/python/tryx/client.pyi
index b0fb73d..103c462 100644
--- a/python/tryx/client.pyi
+++ b/python/tryx/client.pyi
@@ -28,6 +28,8 @@ class IsOnWhatsAppResult:
jid: JID
is_registered: bool
+ username: str | None
+ pn_jid: JID | None
class UserInfo:
"""Basic profile metadata for a single user."""
@@ -37,6 +39,15 @@ class UserInfo:
status: str | None
picture_id: str | None
is_business: bool
+ username: str | None
+
+class UsernameLookupUser:
+ """User lookup result by username."""
+
+ jid: JID
+ pn_jid: JID | None
+ username: str | None
+ is_business: bool
class ContactInfo:
"""Resolved contact information from phone lookups."""
@@ -586,6 +597,47 @@ class CallHandle:
"""
...
+ async def resume_video(
+ self, video_source: VideoSource, video_sink: VideoSink
+ ) -> None:
+ """Resume video transmission in the call.
+
+ Args:
+ video_source: Source providing outgoing video frames.
+ video_sink: Sink receiving incoming video frames.
+
+ Raises:
+ RuntimeError: If the call has ended or video adapter is missing.
+ """
+ ...
+
+ async def re_request_video_upgrade(self) -> None:
+ """Re-request video upgrade in the call.
+
+ Raises:
+ RuntimeError: If the call has ended.
+ """
+ ...
+
+ async def announce_video_enabled(self) -> None:
+ """Announce to call participants that local video is enabled.
+
+ Raises:
+ RuntimeError: If the call has ended.
+ """
+ ...
+
+ async def set_video_orientation(self, orientation: int) -> None:
+ """Set the local video orientation.
+
+ Args:
+ orientation: Orientation angle (e.g. 0, 90, 180, 270).
+
+ Raises:
+ RuntimeError: If the call has ended.
+ """
+ ...
+
async def invite_participant(self, target: JID) -> None:
"""Invite a participant to a group call.
@@ -1195,6 +1247,23 @@ class ContactClient:
"""
...
+ async def find_by_username(
+ self, username: str, username_key: str | None = None
+ ) -> UsernameLookupUser | None:
+ """Look up a contact by their WhatsApp username.
+
+ Args:
+ username: The username to search for.
+ username_key: Optional username lookup key.
+
+ Returns:
+ UsernameLookupUser if found, None otherwise.
+
+ Raises:
+ RuntimeError: If the client is not running.
+ """
+ ...
+
class ChatActionsClient:
"""Chat-level actions such as archive, pin, mute, and reactions.
@@ -1672,7 +1741,7 @@ class GroupMetadata:
"""Full metadata for a WhatsApp group."""
id: JID
- subject: str
+ subject: str | None
participants: list[GroupParticipant]
addressing_mode: str
creator: JID | None
@@ -2371,21 +2440,49 @@ class MembershipRequest:
request_time: int | None
class GroupInfo:
- """Basic group information returned by query_info."""
+ """Basic group information returned by query_info / routing_info."""
participants: list[JID]
addressing_mode: str
lid_to_pn_map: list[tuple[str, JID]]
+class GroupOverview:
+ """Overview of a participating group.
+
+ Contains identity, subject, hierarchy, and participant count.
+ """
+
+ id: JID
+ subject: str | None
+ hierarchy: str
+ parent_jid: JID | None
+ participant_count: int | None
+
class GroupsClient:
"""WhatsApp group management operations.
All async methods raise ``RuntimeError`` if the client is not running.
"""
+ async def routing_info(self, jid: JID) -> GroupInfo:
+ """Query basic group routing info (participants, addressing mode).
+
+ Args:
+ jid: Group JID.
+
+ Returns:
+ GroupInfo with participants and addressing mode.
+
+ Raises:
+ RuntimeError: If the client is not running.
+ """
+ ...
+
async def query_info(self, jid: JID) -> GroupInfo:
"""Query basic group info (participants, addressing mode).
+ Alias for :meth:`routing_info`.
+
Args:
jid: Group JID.
@@ -2402,11 +2499,24 @@ class GroupsClient:
"""
...
- async def get_participating(self) -> dict[str, GroupMetadata]:
+ async def list_participating(self) -> list[GroupOverview]:
+ """Get an overview of all groups the account is participating in.
+
+ Returns:
+ List of GroupOverview objects.
+
+ Raises:
+ RuntimeError: If the client is not running.
+ """
+ ...
+
+ async def get_participating(self) -> list[GroupOverview]:
"""Get all groups the account is participating in.
+ Alias for :meth:`list_participating`.
+
Returns:
- Dict mapping group JID strings to GroupMetadata.
+ List of GroupOverview objects.
Raises:
RuntimeError: If the client is not running.
@@ -2414,14 +2524,44 @@ class GroupsClient:
Example::
groups = await client.groups.get_participating()
- for jid_str, metadata in groups.items():
- print(metadata.subject)
+ for overview in groups:
+ print(overview.subject)
+ """
+ ...
+
+ async def fetch_overviews(self, jids: list[JID]) -> list[GroupOverview]:
+ """Fetch group overviews for multiple groups.
+
+ Args:
+ jids: List of group JIDs to fetch overviews for.
+
+ Returns:
+ List of GroupOverview objects.
+
+ Raises:
+ RuntimeError: If the client is not running.
+ """
+ ...
+
+ async def fetch_metadata(self, jid: JID) -> GroupMetadata:
+ """Fetch full group metadata including participants from network.
+
+ Args:
+ jid: Group JID.
+
+ Returns:
+ GroupMetadata with full participant list.
+
+ Raises:
+ RuntimeError: If the client is not running.
"""
...
async def get_metadata(self, jid: JID) -> GroupMetadata:
"""Get full group metadata including participants.
+ Alias for :meth:`fetch_metadata`.
+
Args:
jid: Group JID.
diff --git a/python/tryx/events.pyi b/python/tryx/events.pyi
index 118179d..a55ac7e 100644
--- a/python/tryx/events.pyi
+++ b/python/tryx/events.pyi
@@ -108,11 +108,32 @@ class BusinessStatusUpdateType:
Unknown: BusinessStatusUpdateType
class GroupNotificationAction:
- """Opaque group update action variant.
+ """Group update action variant.
The runtime returns one action object that describes what changed in a group.
+ Access the variant type via ``isinstance`` checks, for example::
+
+ from tryx.events import GroupNotificationAction
+
+ if isinstance(action, GroupNotificationAction.Subject):
+ print(action.subject)
+ print(action.subject_owner_pn)
"""
+ class Subject:
+ """Group subject (name) was changed."""
+
+ subject: str
+ """New group name."""
+ subject_owner: JID | None
+ """JID of the user who renamed the group (follows addressing mode)."""
+ subject_owner_pn: JID | None
+ """Phone-number JID of the renamer when ``subject_owner`` is a LID."""
+ subject_owner_username: str | None
+ """Renamer's username, when username addressing is enabled."""
+ subject_timestamp: datetime | None
+ """Server timestamp of the rename."""
+
class EvConnected:
"""Emitted when the session becomes connected."""
diff --git a/src/backend/ffi_bridge.rs b/src/backend/ffi_bridge.rs
index d4290d3..ce4b050 100644
--- a/src/backend/ffi_bridge.rs
+++ b/src/backend/ffi_bridge.rs
@@ -326,13 +326,22 @@ impl AppSyncStore for FfiBridgeStore {
if res == 0 { Ok(()) } else { Err(make_err("set_sync_key failed")) }
}
- async fn get_version(&self, name: &str) -> StoreResult {
+ async fn get_version(&self, name: &str) -> StoreResult> {
let cname = CString::new(name).unwrap();
let mut out = TryxBuffer { data: ptr::null_mut(), len: 0 };
let res = unsafe { (self.ffi.get_version)(self.ffi.handle, cname.as_ptr(), &mut out) };
if res == 0 {
- Ok(bincode::deserialize(&self.take_buffer(out)).unwrap_or_default())
- } else { Ok(HashState::default()) }
+ let buf = self.take_buffer(out);
+ if buf.is_empty() {
+ Ok(None)
+ } else {
+ Ok(bincode::deserialize(&buf).ok())
+ }
+ } else { Ok(None) }
+ }
+
+ async fn delete_version(&self, _name: &str) -> StoreResult<()> {
+ Ok(())
}
async fn set_version(&self, name: &str, state: HashState) -> StoreResult<()> {
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 80b03b5..7969de9 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -1,6 +1,6 @@
mod store;
pub mod ffi_bridge;
-pub use store::{BackendBase, SqliteStore, };
+pub use store::{BackendBase, SqliteStore, StoredDeviceSummary};
pub mod python_store;
pub mod store_types;
diff --git a/src/backend/python_store.rs b/src/backend/python_store.rs
index f466097..74196cd 100644
--- a/src/backend/python_store.rs
+++ b/src/backend/python_store.rs
@@ -459,7 +459,7 @@ impl AppSyncStore for PythonStore {
})
}
- async fn get_version(&self, name: &str) -> StoreResult {
+ async fn get_version(&self, name: &str) -> StoreResult> {
let name_c = name.to_string();
let fut = Python::attach(|_py| {
let kwargs = pyo3::types::PyDict::new(_py);
@@ -469,10 +469,35 @@ impl AppSyncStore for PythonStore {
})?;
let _res = fut.await.map_err(|e| make_err(e.to_string()))?;
Python::attach(|_py| {
- let v = _res.bind(_py).extract::>().map_err(|e| make_err(e.to_string()))?; serde_json::from_slice(&v).map_err(|e| make_err(e.to_string()))
+ let bound = _res.bind(_py);
+ if bound.is_none() {
+ Ok(None)
+ } else {
+ let v = bound.extract::>().map_err(|e| make_err(e.to_string()))?;
+ serde_json::from_slice(&v).map_err(|e| make_err(e.to_string())).map(Some)
+ }
})
}
+ async fn delete_version(&self, name: &str) -> StoreResult<()> {
+ let name_c = name.to_string();
+ let fut = Python::attach(|_py| -> StoreResult<_> {
+ let obj = self.py_obj.bind(_py);
+ if obj.hasattr("delete_version").unwrap_or(false) {
+ let kwargs = pyo3::types::PyDict::new(_py);
+ kwargs.set_item("name", name_c).unwrap();
+ let coro = obj.call_method("delete_version", (), Some(&kwargs)).map_err(|e| make_err(e.to_string()))?;
+ Ok(Some(pyo3_async_runtimes::tokio::into_future(coro).map_err(|e| make_err(e.to_string()))?))
+ } else {
+ Ok(None)
+ }
+ })?;
+ if let Some(f) = fut {
+ f.await.map_err(|e| make_err(e.to_string()))?;
+ }
+ Ok(())
+ }
+
async fn set_version(&self, name: &str, state: HashState) -> StoreResult<()> {
let name_c = name.to_string();
let state_c = serde_json::to_vec(&state).map_err(|e| make_err(e.to_string()))?;
diff --git a/src/backend/store.rs b/src/backend/store.rs
index 534b92c..896e5ad 100644
--- a/src/backend/store.rs
+++ b/src/backend/store.rs
@@ -1,36 +1,140 @@
-use whatsapp_rust_sqlite_storage::SqliteStore as SqliteStoreInner;
+use whatsapp_rust_sqlite_storage::{
+ SqliteStore as SqliteStoreInner,
+ StoredDeviceSummary as StoredDeviceSummaryInner,
+};
use pyo3::prelude::*;
use tracing::{error, info};
+use crate::types::JID;
+
#[pyclass(subclass)]
pub struct BackendBase;
+#[pyclass]
+pub struct StoredDeviceSummary {
+ #[pyo3(get)]
+ pub id: i32,
+ #[pyo3(get)]
+ pub pn: Option>,
+ #[pyo3(get)]
+ pub lid: Option>,
+ #[pyo3(get)]
+ pub push_name: String,
+ #[pyo3(get)]
+ pub linked: bool,
+}
+
+impl StoredDeviceSummary {
+ pub fn from_inner(py: Python<'_>, inner: StoredDeviceSummaryInner) -> PyResult {
+ Ok(Self {
+ id: inner.id,
+ pn: inner.pn.map(|j| Py::new(py, JID::from(j))).transpose()?,
+ lid: inner.lid.map(|j| Py::new(py, JID::from(j))).transpose()?,
+ push_name: inner.push_name,
+ linked: inner.linked,
+ })
+ }
+}
+
#[pyclass(extends=BackendBase)]
pub struct SqliteStore {
#[pyo3(get)]
path: String,
+ #[pyo3(get)]
+ device_id: i32,
}
#[pymethods]
impl SqliteStore {
#[new]
- fn new(path: String) -> (Self, BackendBase) {
- (SqliteStore { path }, BackendBase)
+ #[pyo3(signature = (path, device_id=0))]
+ fn new(path: String, device_id: i32) -> (Self, BackendBase) {
+ (SqliteStore { path, device_id }, BackendBase)
+ }
+
+ fn create_new_device<'py>(&self, py: Python<'py>) -> PyResult> {
+ let path = self.path.clone();
+ let locals = pyo3_async_runtimes::tokio::get_current_locals(py)?;
+ pyo3_async_runtimes::tokio::future_into_py_with_locals::<_, i32>(py, locals, async move {
+ let store = SqliteStoreInner::new(&path)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ store
+ .create_new_device()
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
+ })
+ }
+
+ fn list_devices<'py>(&self, py: Python<'py>) -> PyResult> {
+ let path = self.path.clone();
+ let locals = pyo3_async_runtimes::tokio::get_current_locals(py)?;
+ pyo3_async_runtimes::tokio::future_into_py_with_locals::<_, Vec>>(
+ py,
+ locals,
+ async move {
+ let store = SqliteStoreInner::new(&path)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ let devices = store
+ .list_devices()
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Python::attach(|py| {
+ devices
+ .into_iter()
+ .map(|d| {
+ let summary = StoredDeviceSummary::from_inner(py, d)?;
+ Py::new(py, summary)
+ })
+ .collect::>>()
+ })
+ },
+ )
+ }
+
+ fn remove_device<'py>(&self, py: Python<'py>, device_id: i32) -> PyResult> {
+ let path = self.path.clone();
+ let locals = pyo3_async_runtimes::tokio::get_current_locals(py)?;
+ pyo3_async_runtimes::tokio::future_into_py_with_locals(py, locals, async move {
+ let store = SqliteStoreInner::new(&path)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ store
+ .remove_device(device_id)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Ok(())
+ })
+ }
+
+ fn device_exists<'py>(&self, py: Python<'py>, device_id: i32) -> PyResult> {
+ let path = self.path.clone();
+ let locals = pyo3_async_runtimes::tokio::get_current_locals(py)?;
+ pyo3_async_runtimes::tokio::future_into_py_with_locals::<_, bool>(py, locals, async move {
+ let store = SqliteStoreInner::new(&path)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ store
+ .device_exists(device_id)
+ .await
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
+ })
}
}
impl SqliteStore {
pub async fn connect(&self) -> Result {
- info!(path = %self.path, "connecting sqlite backend");
- match SqliteStoreInner::new(&self.path).await {
+ info!(path = %self.path, device_id = self.device_id, "connecting sqlite backend");
+ match SqliteStoreInner::new_for_device(&self.path, self.device_id).await {
Ok(store) => {
- info!(path = %self.path, "sqlite backend connected");
+ info!(path = %self.path, device_id = self.device_id, "sqlite backend connected");
Ok(store)
}
Err(e) => {
- error!(path = %self.path, error = %e, "sqlite backend connection failed");
- Err(e.to_string().into())
+ error!(path = %self.path, device_id = self.device_id, error = %e, "sqlite backend connection failed");
+ Err(e.to_string())
}
}
}
diff --git a/src/clients/chat_actions.rs b/src/clients/chat_actions.rs
index 9d18920..c9b30b1 100644
--- a/src/clients/chat_actions.rs
+++ b/src/clients/chat_actions.rs
@@ -437,6 +437,7 @@ impl ChatActionsClient {
client
.edit_message(chat_jid_value, original_id, message_value)
.await
+ .map(|result| result.message_id)
.map_err(|e| PyErr::new::(e.to_string()))
})
}
diff --git a/src/clients/contacts.rs b/src/clients/contacts.rs
index 6b6622a..5724b00 100644
--- a/src/clients/contacts.rs
+++ b/src/clients/contacts.rs
@@ -13,7 +13,7 @@ use wacore::iq::usync::{
};
use crate::types::{JID, ProfilePicture};
-use crate::wacore::iq::usync::{ContactInfo, IsOnWhatsAppResult, UserInfo};
+use crate::wacore::iq::usync::{ContactInfo, IsOnWhatsAppResult, UserInfo, UsernameLookupUser};
#[pyclass]
pub struct ContactClient {
@@ -84,6 +84,7 @@ impl ContactClient {
info.is_business,
info.status,
info.picture_id,
+ info.username.map(|u| u.to_string()),
);
dict.set_item(JID::from(jid), contact_info)?;
}
@@ -92,6 +93,40 @@ impl ContactClient {
})
}
+ #[pyo3(signature = (username, username_key=None))]
+ fn find_by_username<'py>(
+ &self,
+ py: Python<'py>,
+ username: String,
+ username_key: Option,
+ ) -> PyResult> {
+ let client = self.get_client()?;
+ let locals = get_current_locals(py)?;
+
+ future_into_py_with_locals::<_, Option>>(py, locals, async move {
+ let result = client
+ .contacts()
+ .find_by_username(&username, username_key.as_deref())
+ .await
+ .map_err(|e| PyErr::new::(e.to_string()))?;
+
+ Python::attach(|py| match result {
+ wacore::iq::usync::UsernameLookup::Found(user) => {
+ let py_user = UsernameLookupUser {
+ jid: Py::new(py, JID::from(user.jid))?,
+ pn_jid: user.pn_jid.map(|j| Py::new(py, JID::from(j))).transpose()?,
+ username: user.username.map(|u| u.to_string()),
+ is_business: user.is_business,
+ };
+ Ok(Some(Py::new(py, py_user)?))
+ }
+ wacore::iq::usync::UsernameLookup::NotFound
+ | wacore::iq::usync::UsernameLookup::KeyRequired { .. }
+ | _ => Ok(None),
+ })
+ })
+ }
+
fn get_profile_picture<'py>(
&self,
py: Python<'py>,
diff --git a/src/clients/groups.rs b/src/clients/groups.rs
index 1c5b204..8afc467 100644
--- a/src/clients/groups.rs
+++ b/src/clients/groups.rs
@@ -1,7 +1,6 @@
use std::sync::Arc;
use pyo3::{Bound, Py, PyAny, PyErr, PyResult, Python, pyclass, pymethods};
-use pyo3::types::{PyDict, PyDictMethods};
use pyo3_async_runtimes::tokio::{future_into_py_with_locals, get_current_locals};
use tokio::sync::watch;
use whatsapp_rust::Client;
@@ -12,6 +11,7 @@ use crate::wacore::iq::groups::{
CreateGroupOptions,
CreateGroupResult,
GroupInfo,
+ GroupOverview,
JoinGroupResult,
MemberAddMode,
MemberLinkMode,
@@ -36,7 +36,7 @@ impl GroupsClient {
#[pymethods]
impl GroupsClient {
- fn query_info<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
+ fn routing_info<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
let client = self.get_client()?;
let locals = get_current_locals(py)?;
let jid_value = jid.bind(py).borrow().as_whatsapp_jid();
@@ -44,7 +44,7 @@ impl GroupsClient {
future_into_py_with_locals::<_, Py>(py, locals, async move {
let result = client
.groups()
- .query_info(&jid_value)
+ .routing_info(&jid_value)
.await
.map_err(|e| PyErr::new::(e.to_string()))?;
Python::attach(|py| {
@@ -54,29 +54,37 @@ impl GroupsClient {
})
}
- fn get_participating<'py>(&self, py: Python<'py>) -> PyResult> {
+ fn query_info<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
+ self.routing_info(py, jid)
+ }
+
+ fn list_participating<'py>(&self, py: Python<'py>) -> PyResult> {
let client = self.get_client()?;
let locals = get_current_locals(py)?;
- future_into_py_with_locals::<_, Py>(py, locals, async move {
+ future_into_py_with_locals::<_, Vec>>(py, locals, async move {
let result = client
.groups()
- .get_participating()
+ .list_participating()
.await
.map_err(|e| PyErr::new::(e.to_string()))?;
Python::attach(|py| {
- let dict = PyDict::new(py);
- for (jid, meta) in result {
- let py_meta = GroupMetadata::from_inner(py, meta)?;
- let py_jid = Py::new(py, JID::from(jid))?;
- dict.set_item(py_jid, Py::new(py, py_meta)?)?;
- }
- Ok(dict.unbind())
+ result
+ .into_iter()
+ .map(|overview| {
+ let py_overview = GroupOverview::from_inner(py, overview)?;
+ Py::new(py, py_overview)
+ })
+ .collect::>>()
})
})
}
- fn get_metadata<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
+ fn get_participating<'py>(&self, py: Python<'py>) -> PyResult> {
+ self.list_participating(py)
+ }
+
+ fn fetch_metadata<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
let client = self.get_client()?;
let locals = get_current_locals(py)?;
let jid_value = jid.bind(py).borrow().as_whatsapp_jid();
@@ -84,7 +92,7 @@ impl GroupsClient {
future_into_py_with_locals::<_, Py>(py, locals, async move {
let result = client
.groups()
- .get_metadata(&jid_value)
+ .fetch_metadata(&jid_value)
.await
.map_err(|e| PyErr::new::(e.to_string()))?;
Python::attach(|py| {
@@ -94,6 +102,10 @@ impl GroupsClient {
})
}
+ fn get_metadata<'py>(&self, py: Python<'py>, jid: Py) -> PyResult> {
+ self.fetch_metadata(py, jid)
+ }
+
fn create_group<'py>(
&self,
py: Python<'py>,
@@ -842,4 +854,44 @@ impl GroupsClient {
Ok(())
})
}
+
+ fn fetch_overviews<'py>(&self, py: Python<'py>, jids: Vec>) -> PyResult> {
+ let client = self.get_client()?;
+ let locals = get_current_locals(py)?;
+ let jids_value: Vec = jids.iter().map(|j| j.bind(py).borrow().as_whatsapp_jid()).collect();
+
+ future_into_py_with_locals::<_, Vec>>(py, locals, async move {
+ let result = client
+ .groups()
+ .fetch_overviews(&jids_value)
+ .await
+ .map_err(|e| PyErr::new::(e.to_string()))?;
+ Python::attach(|py| {
+ let overviews = result
+ .into_iter()
+ .filter_map(|res| match res {
+ whatsapp_rust::GroupOverviewResult::Found(overview) => {
+ let py_overview = GroupOverview::from_inner(py, overview).ok()?;
+ Py::new(py, py_overview).ok()
+ }
+ whatsapp_rust::GroupOverviewResult::Truncated {
+ id,
+ participant_count,
+ } => {
+ let py_overview = GroupOverview {
+ id: Py::new(py, JID::from(id)).ok()?,
+ subject: None,
+ hierarchy: "unknown".to_string(),
+ parent_jid: None,
+ participant_count: Some(participant_count),
+ };
+ Py::new(py, py_overview).ok()
+ }
+ _ => None,
+ })
+ .collect::>();
+ Ok(overviews)
+ })
+ })
+ }
}
diff --git a/src/clients/tryx.rs b/src/clients/tryx.rs
index 49eb52b..07f38ec 100644
--- a/src/clients/tryx.rs
+++ b/src/clients/tryx.rs
@@ -34,7 +34,7 @@ use super::status::StatusClient;
use super::tryx_client::TryxClient;
use crate::clients::chat_actions::ChatActionsClient;
use crate::log::init_logging;
-use crate::backend::{SqliteStore, BackendBase};
+use crate::backend::SqliteStore;
use crate::events::types::{
EvArchiveUpdate, EvBusinessStatusUpdate, EvChatPresence, EvClientOutDated, EvConnectFailure, EvConnected, EvContactNumberChanged, EvContactSyncRequested, EvContactUpdate, EvContactUpdated, EvDeleteChatUpdate, EvDeleteMessageForMeUpdate, EvDeviceListUpdate, EvDisappearingModeChanged, EvDisconnected, EvGroupUpdate, EvHistorySync, EvLoggedOut, EvMarkChatAsReadUpdate, EvMessage, EvMuteUpdate, EvNewsletterLiveUpdate, EvNotification, EvOfflineSyncCompleted, EvOfflineSyncPreview, EvPairError, EvPairSuccess, EvPairingCode, EvPairingQrCode, EvPictureUpdate, EvPinUpdate, EvPresence, EvPushNameUpdate, EvQrScannedWithoutMultidevice, EvReceipt, EvSelfPushNameUpdated, EvStarUpdate, EvStreamError, EvStreamReplaced, EvTemporaryBan, EvUndecryptableMessage, EvUserAboutUpdate
};
@@ -420,7 +420,7 @@ impl Tryx {
async move {
match event.as_ref() {
Event::IncomingCall(incoming) => {
- let incoming = incoming.clone();
+ let incoming = (**incoming).clone();
let client = client.clone();
Self::emit_built_event(&tryx_client, &callbacks.incoming_call, locals.clone(), "IncomingCall", |py| {
Py::new(py, crate::clients::voip::IncomingCallEvent {
@@ -440,7 +440,7 @@ impl Tryx {
}).await;
}
Event::LoggedOut(logout) => {
- let logout = logout.clone();
+ let logout = (**logout).clone();
Self::emit_built_event(&tryx_client, &callbacks.logout, locals.clone(), "LoggedOut", |py| {
Py::new(py, EvLoggedOut::new(logout)).map(|event| event.into_any())
}).await;
@@ -521,7 +521,7 @@ impl Tryx {
|_py| {
Ok(EvReceipt::new(
receipt.source,
- receipt.message_ids,
+ receipt.message_ids.into_iter().map(|id| id.to_string()).collect(),
receipt.timestamp,
receipt.r#type,
)
diff --git a/src/clients/voip.rs b/src/clients/voip.rs
index 4373a14..aa5dad3 100644
--- a/src/clients/voip.rs
+++ b/src/clients/voip.rs
@@ -113,15 +113,20 @@ impl CallHandle {
self.inner.is_muted()
}
- fn set_muted(&self, muted: bool) {
- self.inner.set_muted(muted);
+ fn set_muted<'py>(&self, py: Python<'py>, muted: bool) -> PyResult> {
+ let call = self.inner.clone();
+ let locals = get_current_locals(py)?;
+ future_into_py_with_locals(py, locals, async move {
+ let _ = call.set_muted(muted).await;
+ Ok(())
+ })
}
fn hangup<'py>(&self, py: Python<'py>) -> PyResult> {
let call = self.inner.clone();
let locals = get_current_locals(py)?;
future_into_py_with_locals(py, locals, async move {
- call.hangup().await;
+ call.hangup_local().await;
Ok(())
})
}
@@ -158,6 +163,47 @@ impl CallHandle {
})
}
+ fn resume_video<'py>(&self, py: Python<'py>, video_source: Py, video_sink: Py) -> PyResult> {
+ let bridge = crate::voip::bridge_from_python(py, None, None, Some(video_source), Some(video_sink))?;
+ let source = bridge.video_source.ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("video source adapter is missing"))?;
+ let sink = bridge.video_sink.ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("video sink adapter is missing"))?;
+ let tasks = bridge.tasks;
+ self.bridge_tasks.lock().map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("call task lock poisoned"))?.extend(tasks);
+ let call = self.inner.clone();
+ let locals = get_current_locals(py)?;
+ future_into_py_with_locals(py, locals, async move {
+ call.resume_video(source, sink).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Ok(())
+ })
+ }
+
+ fn re_request_video_upgrade<'py>(&self, py: Python<'py>) -> PyResult> {
+ let call = self.inner.clone();
+ let locals = get_current_locals(py)?;
+ future_into_py_with_locals(py, locals, async move {
+ call.re_request_video_upgrade().await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Ok(())
+ })
+ }
+
+ fn announce_video_enabled<'py>(&self, py: Python<'py>) -> PyResult> {
+ let call = self.inner.clone();
+ let locals = get_current_locals(py)?;
+ future_into_py_with_locals(py, locals, async move {
+ call.announce_video_enabled().await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Ok(())
+ })
+ }
+
+ fn set_video_orientation<'py>(&self, py: Python<'py>, orientation: u8) -> PyResult> {
+ let call = self.inner.clone();
+ let locals = get_current_locals(py)?;
+ future_into_py_with_locals(py, locals, async move {
+ call.set_video_orientation(orientation).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
+ Ok(())
+ })
+ }
+
fn invite_participant<'py>(&self, py: Python<'py>, target: Py) -> PyResult> {
let call = self.inner.clone();
let target = target.bind(py).borrow().as_whatsapp_jid();
diff --git a/src/events/types/message_and_updates.rs b/src/events/types/message_and_updates.rs
index 849c687..6a56f8d 100644
--- a/src/events/types/message_and_updates.rs
+++ b/src/events/types/message_and_updates.rs
@@ -41,6 +41,11 @@ impl From for EvTemporaryBan {
EvTemporaryBan::new(event)
}
}
+impl From> for EvTemporaryBan {
+ fn from(event: Box) -> Self {
+ EvTemporaryBan::new(*event)
+ }
+}
#[pymethods]
impl EvTemporaryBan {
#[getter]
@@ -719,6 +724,8 @@ pub enum GroupNotificationAction {
Subject {
subject: String,
subject_owner: Option>,
+ subject_owner_pn: Option>,
+ subject_owner_username: Option,
subject_timestamp: Option>,
},
Description {
@@ -864,7 +871,7 @@ impl EvGroupUpdate {
.collect::>()
};
- let action = match &self.inner.action {
+ let action = match &*self.inner.action {
wacore::stanza::groups::GroupNotificationAction::Add { participants, reason } => {
let py_participants = py_group_participants(participants);
GroupNotificationAction::Add { participants: py_participants, reason: reason.clone() }
@@ -885,14 +892,17 @@ impl EvGroupUpdate {
let py_participants = py_group_participants(participants);
GroupNotificationAction::Modify { participants: py_participants }
},
- wacore::stanza::groups::GroupNotificationAction::Subject { subject, subject_owner, subject_time } => {
+ wacore::stanza::groups::GroupNotificationAction::Subject { subject, subject_owner, subject_owner_pn, subject_owner_username, subject_time } => {
let py_subject_owner = subject_owner
.as_ref()
.map(|o| Py::new(py, JID::from(o.clone())).unwrap());
+ let py_subject_owner_pn = subject_owner_pn
+ .as_ref()
+ .map(|o| Py::new(py, JID::from(o.clone())).unwrap());
let py_subject_timestamp = subject_time.map(|t| {
PyDateTime::from_timestamp(py, t as f64, None).unwrap().into()
});
- GroupNotificationAction::Subject { subject: subject.clone(), subject_owner: py_subject_owner, subject_timestamp: py_subject_timestamp }
+ GroupNotificationAction::Subject { subject: subject.clone(), subject_owner: py_subject_owner, subject_owner_pn: py_subject_owner_pn, subject_owner_username: subject_owner_username.clone(), subject_timestamp: py_subject_timestamp }
},
wacore::stanza::groups::GroupNotificationAction::Description { id, description } => {
GroupNotificationAction::Description { id: id.clone(), description: description.clone() }
diff --git a/src/events/types/profile_sync.rs b/src/events/types/profile_sync.rs
index 5183874..0cd1ae7 100644
--- a/src/events/types/profile_sync.rs
+++ b/src/events/types/profile_sync.rs
@@ -12,14 +12,14 @@ pub struct EvPushNameUpdateData {
#[pyclass]
pub struct EvPushNameUpdate {
- inner: Box,
+ _inner: Box,
data_cache: OnceLock>,
}
impl EvPushNameUpdate {
pub fn new(inner: wacore::types::events::RetiredPushNameUpdate) -> Self {
Self {
- inner: Box::new(inner),
+ _inner: Box::new(inner),
data_cache: OnceLock::new(),
}
}
diff --git a/src/lib.rs b/src/lib.rs
index a0efae9..104048c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -114,13 +114,13 @@ use self::events::types::{
DeviceListUpdateData,
EvPushNameUpdateData,
};
-use self::backend::{BackendBase, SqliteStore, store_types::*};
+use self::backend::{BackendBase, SqliteStore, StoredDeviceSummary, store_types::*};
use self::exceptions::{EventDispatchError, FailedBuildClient, FailedToDecodeProto, PyPayloadBuildError, UnsupportedBackend, UnsupportedEventType};
use self::types::{DeviceSentMeta, JID, MediaReuploadResult, MessageInfo, MessageSource, MsgBotInfo, MsgMetaInfo, ProfilePicture, SendResult, UploadResponse};
use self::wacore::download::MediaType;
use self::wacore::node::{Attrs, Node, NodeContent, NodeValue};
use self::wacore::stanza::{BusinessSubscription, KeyIndexInfo};
-use self::wacore::iq::usync::{ContactInfo, IsOnWhatsAppResult, UserInfo};
+use self::wacore::iq::usync::{ContactInfo, IsOnWhatsAppResult, UserInfo, UsernameLookupUser};
use self::wacore::iq::community::{
CommunitySubgroup,
CreateCommunityOptions,
@@ -146,6 +146,7 @@ use self::wacore::iq::groups::{
CreateGroupOptions,
CreateGroupResult,
GroupInfo,
+ GroupOverview,
GroupParticipantOptions,
JoinGroupResult,
MemberAddMode,
@@ -200,6 +201,7 @@ fn _tryx(_py: &Bound) -> PyResult<()> {
client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
+ client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
@@ -227,6 +229,7 @@ fn _tryx(_py: &Bound) -> PyResult<()> {
client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
+ client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
client_module.add_class::()?;
@@ -331,6 +334,7 @@ fn _tryx(_py: &Bound) -> PyResult<()> {
let backend_module = PyModule::new(_py.py(), "backend")?;
backend_module.add_class::()?;
backend_module.add_class::()?;
+ backend_module.add_class::()?;
backend_module.add_class::()?;
backend_module.add_class::()?;
backend_module.add_class::()?;
diff --git a/src/types.rs b/src/types.rs
index a1df1af..a830de2 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -194,9 +194,9 @@ impl From for MessageInfo {
fn from(info: WhatsAppMessageInfo) -> Self {
MessageInfo {
inner: Arc::new(info.clone()),
- id: info.id.clone(),
+ id: info.id.to_string(),
r#type: info.r#type.map(|t| t.to_string()).unwrap_or_default(),
- push_name: info.push_name.clone(),
+ push_name: info.push_name.to_string(),
}
}
}
@@ -253,37 +253,51 @@ impl MessageInfo {
fn bot_info(&self) -> Option {
match &self.inner.bot_info {
Some(msg) => {
- Some(MsgBotInfo { inner: Arc::new(msg.clone()), edit_target_id: match msg.edit_target_id {
- Some(ref s) => Some(s.clone()),
- None => None,
-
- } })
- },
+ Some(MsgBotInfo {
+ inner: Arc::new((**msg).clone()),
+ edit_target_id: msg.edit_target_id.as_deref().map(String::from),
+ })
+ }
None => None,
}
}
#[getter]
- fn meta_info(&self, py: Python<'_>) -> MsgMetaInfo{
- MsgMetaInfo {
- target_id: self.inner.meta_info.target_id.as_deref().map(String::from),
- target_sender: match self.inner.meta_info.target_sender {
- Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
- None => None,
- },
- target_chat: match self.inner.meta_info.target_chat {
- Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
- None => None,
- },
- thread_message_id: self.inner.meta_info.thread_message_id.as_deref().map(String::from),
- thread_message_sender_jid: match self.inner.meta_info.thread_message_sender_jid {
- Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
- None => None,
- },
- content_type: self.inner.meta_info.content_type.as_deref().map(String::from),
- appdata: self.inner.meta_info.appdata.as_deref().map(String::from),
- reporting_tag: self.inner.meta_info.reporting_tag.as_deref().map(|b| b.to_vec()),
- reporting_token: self.inner.meta_info.reporting_token.as_deref().map(|b| b.to_vec()),
- reporting_token_version: self.inner.meta_info.reporting_token_version,
+ fn meta_info(&self, py: Python<'_>) -> MsgMetaInfo {
+ if let Some(meta) = &self.inner.meta_info {
+ MsgMetaInfo {
+ target_id: meta.target_id.as_deref().map(String::from),
+ target_sender: match meta.target_sender {
+ Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
+ None => None,
+ },
+ target_chat: match meta.target_chat {
+ Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
+ None => None,
+ },
+ thread_message_id: meta.thread_message_id.as_deref().map(String::from),
+ thread_message_sender_jid: match meta.thread_message_sender_jid {
+ Some(ref jid) => Some(pyo3::Py::new(py, JID::from(jid.clone())).unwrap()),
+ None => None,
+ },
+ content_type: meta.content_type.as_deref().map(String::from),
+ appdata: meta.appdata.as_deref().map(String::from),
+ reporting_tag: meta.reporting_tag.as_deref().map(|b| b.to_vec()),
+ reporting_token: meta.reporting_token.as_deref().map(|b| b.to_vec()),
+ reporting_token_version: meta.reporting_token_version,
+ }
+ } else {
+ MsgMetaInfo {
+ target_id: None,
+ target_sender: None,
+ target_chat: None,
+ thread_message_id: None,
+ thread_message_sender_jid: None,
+ content_type: None,
+ appdata: None,
+ reporting_tag: None,
+ reporting_token: None,
+ reporting_token_version: None,
+ }
}
}
#[getter]
@@ -320,7 +334,7 @@ impl MessageInfo {
}
#[getter]
fn ephemeral_expiration(&self) -> Option {
- self.inner.ephemeral_expiration
+ None
}
#[getter]
fn is_offline(&self) -> bool {
diff --git a/src/wacore/iq/community.rs b/src/wacore/iq/community.rs
index b6571c8..b6b20bd 100644
--- a/src/wacore/iq/community.rs
+++ b/src/wacore/iq/community.rs
@@ -214,7 +214,7 @@ pub struct GroupMetadata {
#[pyo3(get)]
pub id: Py,
#[pyo3(get)]
- pub subject: String,
+ pub subject: Option,
#[pyo3(get)]
pub participants: Vec>,
#[pyo3(get)]
diff --git a/src/wacore/iq/groups.rs b/src/wacore/iq/groups.rs
index dd359e0..678ac18 100644
--- a/src/wacore/iq/groups.rs
+++ b/src/wacore/iq/groups.rs
@@ -300,7 +300,7 @@ pub struct GroupInfo {
}
impl GroupInfo {
- pub fn from_inner(py: Python<'_>, value: &wacore::client::context::GroupInfo) -> PyResult {
+ pub fn from_inner(py: Python<'_>, value: &wacore::client::context::GroupRoutingInfo) -> PyResult {
let lid_to_pn_map = Vec::new();
let participants = value
@@ -321,3 +321,37 @@ impl GroupInfo {
})
}
}
+
+#[pyclass]
+pub struct GroupOverview {
+ #[pyo3(get)]
+ pub id: Py,
+ #[pyo3(get)]
+ pub subject: Option,
+ #[pyo3(get)]
+ pub hierarchy: String,
+ #[pyo3(get)]
+ pub parent_jid: Option>,
+ #[pyo3(get)]
+ pub participant_count: Option,
+}
+
+impl GroupOverview {
+ pub fn from_inner(py: Python<'_>, value: whatsapp_rust::GroupOverview) -> PyResult {
+ let (hierarchy, parent_jid) = match value.hierarchy {
+ whatsapp_rust::GroupHierarchy::Standalone => ("standalone".to_string(), None),
+ whatsapp_rust::GroupHierarchy::Community => ("community".to_string(), None),
+ whatsapp_rust::GroupHierarchy::Subgroup { parent, .. } => {
+ ("subgroup".to_string(), Some(Py::new(py, JID::from(parent))?))
+ }
+ _ => ("unknown".to_string(), None),
+ };
+ Ok(Self {
+ id: Py::new(py, JID::from(value.id))?,
+ subject: value.subject,
+ hierarchy,
+ parent_jid,
+ participant_count: value.participant_count,
+ })
+ }
+}
diff --git a/src/wacore/iq/usync.rs b/src/wacore/iq/usync.rs
index 2665aec..fdaa129 100644
--- a/src/wacore/iq/usync.rs
+++ b/src/wacore/iq/usync.rs
@@ -5,43 +5,53 @@ use crate::types::JID;
#[pyclass]
pub struct IsOnWhatsAppResult {
#[pyo3(get)]
- jid: Py,
+ pub jid: Py,
#[pyo3(get)]
- is_registered: bool,
+ pub is_registered: bool,
+ #[pyo3(get)]
+ pub pn_jid: Option>,
+ #[pyo3(get)]
+ pub username: Option,
}
impl IsOnWhatsAppResult {
- pub fn new(jid: JID, is_registered: bool) -> Self {
+ pub fn new(jid: JID, is_registered: bool, pn_jid: Option, username: Option) -> Self {
Python::attach(|py| {
Self {
jid: Py::new(py, JID::from(jid)).unwrap(),
is_registered,
+ pn_jid: pn_jid.map(|j| Py::new(py, JID::from(j)).unwrap()),
+ username,
}
})
}
}
+
impl From for IsOnWhatsAppResult {
fn from(result: wacore::iq::usync::IsOnWhatsAppResult) -> Self {
- IsOnWhatsAppResult::new(result.jid.into(), result.is_registered)
+ IsOnWhatsAppResult::new(
+ result.jid.into(),
+ result.is_registered,
+ result.pn_jid.map(Into::into),
+ result.username.map(|u| u.to_string()),
+ )
}
}
-
-
#[pyclass]
pub struct ContactInfo {
#[pyo3(get)]
- jid: Py,
+ pub jid: Py,
#[pyo3(get)]
- lid: Option>,
+ pub lid: Option>,
#[pyo3(get)]
- is_registered: bool,
+ pub is_registered: bool,
#[pyo3(get)]
- is_business: bool,
+ pub is_business: bool,
#[pyo3(get)]
- status: Option,
+ pub status: Option,
#[pyo3(get)]
- picture_id: Option,
+ pub picture_id: Option,
}
impl ContactInfo {
@@ -66,7 +76,6 @@ impl ContactInfo {
}
}
-
impl From for ContactInfo {
fn from(info: wacore::iq::usync::IsOnWhatsAppResult) -> Self {
ContactInfo::new(
@@ -83,15 +92,17 @@ impl From for ContactInfo {
#[pyclass]
pub struct UserInfo {
#[pyo3(get)]
- jid: Py,
+ pub jid: Py,
+ #[pyo3(get)]
+ pub lid: Option>,
#[pyo3(get)]
- lid: Option>,
+ pub status: Option,
#[pyo3(get)]
- status: Option,
+ pub picture_id: Option,
#[pyo3(get)]
- picture_id: Option,
+ pub is_business: bool,
#[pyo3(get)]
- is_business: bool,
+ pub username: Option,
}
impl UserInfo {
@@ -101,6 +112,7 @@ impl UserInfo {
is_business: bool,
status: Option,
picture_id: Option,
+ username: Option,
) -> Self {
Python::attach(|py| {
Self {
@@ -109,6 +121,7 @@ impl UserInfo {
status,
picture_id,
is_business,
+ username,
}
})
}
@@ -122,8 +135,20 @@ impl From for UserInfo {
info.is_business,
info.status,
info.picture_id,
-
+ info.username.map(|u| u.to_string()),
)
}
}
+#[pyclass]
+pub struct UsernameLookupUser {
+ #[pyo3(get)]
+ pub jid: Py,
+ #[pyo3(get)]
+ pub pn_jid: Option>,
+ #[pyo3(get)]
+ pub username: Option,
+ #[pyo3(get)]
+ pub is_business: bool,
+}
+
diff --git a/tests/test_new_features.py b/tests/test_new_features.py
new file mode 100644
index 0000000..985f8d5
--- /dev/null
+++ b/tests/test_new_features.py
@@ -0,0 +1,50 @@
+"""Tests for new features: multi-device SQLite store and new types."""
+
+from __future__ import annotations
+
+import asyncio
+from pathlib import Path
+
+from tryx.backend import SqliteStore, StoredDeviceSummary
+from tryx.client import (
+ ContactClient,
+ GroupsClient,
+ IsOnWhatsAppResult,
+ UserInfo,
+ UsernameLookupUser,
+)
+
+
+def test_sqlite_store_multidevice(tmp_path: Path) -> None:
+ async def run() -> None:
+ db_file = str(tmp_path / "devices.db")
+ store = SqliteStore(db_file, 0)
+ assert store.device_id == 0
+ assert store.path == db_file
+
+ dev_1 = await store.create_new_device()
+ assert dev_1 > 0
+ assert await store.device_exists(dev_1)
+
+ devices = await store.list_devices()
+ assert isinstance(devices, list)
+ assert any(d.id == dev_1 for d in devices)
+ for d in devices:
+ assert isinstance(d, StoredDeviceSummary)
+ assert isinstance(d.id, int)
+ assert isinstance(d.push_name, str)
+ assert isinstance(d.linked, bool)
+
+ await store.remove_device(dev_1)
+ assert not await store.device_exists(dev_1)
+
+ asyncio.run(run())
+
+
+def test_type_exports() -> None:
+ assert hasattr(UsernameLookupUser, "__doc__")
+ assert hasattr(IsOnWhatsAppResult, "__doc__")
+ assert hasattr(UserInfo, "__doc__")
+ assert hasattr(StoredDeviceSummary, "__doc__")
+ assert hasattr(ContactClient, "find_by_username")
+ assert hasattr(GroupsClient, "fetch_overviews")