From 3f3107c6cf0e3386d411c1032d91f5c77bef53c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:16:15 -0300 Subject: [PATCH 01/14] docs: BusinessHoursConfig open_time/close_time are now Option (PR #1060) --- api/business.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/business.mdx b/api/business.mdx index 45233e6..c6220ce 100644 --- a/api/business.mdx +++ b/api/business.mdx @@ -63,7 +63,7 @@ if let Some(profile) = client.get_business_profile(&jid).await? { if let Some(configs) = &profile.business_hours.business_config { for config in configs { println!( - " {:?}: {:?} ({}–{})", + " {:?}: {:?} ({:?}–{:?})", config.day_of_week, config.mode, config.open_time, @@ -126,8 +126,8 @@ pub struct BusinessHours { pub struct BusinessHoursConfig { pub day_of_week: DayOfWeek, pub mode: BusinessHourMode, - pub open_time: u32, - pub close_time: u32, + pub open_time: Option, + pub close_time: Option, } ``` From 2d03f15ea59aeb1ca7c1c0006d75477aa9c66ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:18:18 -0300 Subject: [PATCH 02/14] docs: mention get_profile_picture_with_timeout (PR #1060) --- api/contacts.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/contacts.mdx b/api/contacts.mdx index 7e62276..fd59283 100644 --- a/api/contacts.mdx +++ b/api/contacts.mdx @@ -153,6 +153,10 @@ if let Some(pic) = client.contacts().get_profile_picture(&group_jid, true).await } ``` + +To override the default request timeout for a single fetch, use `get_profile_picture_with_timeout(jid, preview, timeout)`, which takes an extra `timeout: Option` argument. Internally the request is built via `ProfilePictureSpec`'s `with_timeout(...)` builder method; pass `None` to fall back to the default timeout. + + ### get_user_info Get user information by JID. From 3613c112d4995a49680171b803ace14dd3d456fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:18:52 -0300 Subject: [PATCH 03/14] docs: document community subgroup/remove_participants/get_participating APIs from PR #1060 --- api/community.mdx | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/api/community.mdx b/api/community.mdx index 38c5055..4a680bd 100644 --- a/api/community.mdx +++ b/api/community.mdx @@ -51,6 +51,26 @@ if let Some(desc) = &result.metadata.description { Since v0.6, `community().create()` returns the full `GroupMetadata` instead of just the JID. The library inlines the community description directly into the create stanza (matching WA Web), so the returned metadata already contains it — no separate `set_description` round-trip is needed. If you previously read `result.gid`, switch to `result.metadata.id` (`GroupMetadata` uses `id: Jid`). +### get_participating + +Fetch all parent/community groups the logged-in account currently participates in. + +```rust +pub async fn get_participating(&self) -> Result, CommunityError> +``` + +**Returns:** +- `HashMap` — Map of community JID to metadata + +**Example:** +```rust +let communities = client.community().get_participating().await?; + +for (jid, metadata) in communities { + println!("Community: {} ({})", metadata.subject, jid); +} +``` + ### deactivate Deactivate (delete) a community. Subgroups are unlinked but not deleted. @@ -103,6 +123,44 @@ for (jid, error_code) in &result.failed_groups { } ``` +### create_subgroup + +Create a new group that is already linked as a subgroup of a community, in one call. + +```rust +pub async fn create_subgroup( + &self, + name: &str, + participants: &[Jid], + parent_jid: &Jid, +) -> Result +``` + +**Parameters:** +- `name` — Name of the new subgroup +- `participants` — Initial participant JIDs to add to the subgroup +- `parent_jid` — JID of the parent community to link the new subgroup under + +**Returns:** +- `CreateCommunityResult` — Contains the full `metadata: GroupMetadata` for the created subgroup + +**Example:** +```rust +let participants = vec![ + "5511999999999@s.whatsapp.net".parse()?, +]; + +let result = client.community() + .create_subgroup("My Subgroup", &participants, &community_jid) + .await?; + +println!("Created subgroup: {} ({})", result.metadata.subject, result.metadata.id); +``` + + +Equivalent to creating a group and then calling [`link_subgroups`](#link_subgroups), but done in a single round-trip. + + ### unlink_subgroups Unlink subgroups from a community. @@ -274,6 +332,38 @@ for p in &participants { } ``` +### remove_participants + +Remove participants from a community. + +```rust +pub async fn remove_participants( + &self, + community_jid: &Jid, + participants: &[Jid], +) -> Result, CommunityError> +``` + +**Parameters:** +- `community_jid` — JID of the community +- `participants` — Array of participant JIDs to remove + +**Returns:** +- `Vec` — Result for each participant (see [`ParticipantChangeResponse`](/api/groups#participantchangeresponse)) + +**Example:** +```rust +let to_remove = vec!["15551234567@s.whatsapp.net".parse()?]; + +let results = client.community() + .remove_participants(&community_jid, &to_remove) + .await?; + +for result in results { + println!("{}: status {:?}", result.jid, result.status); +} +``` + ## Types ### CreateCommunityOptions @@ -336,6 +426,8 @@ pub struct CommunitySubgroup { pub participant_count: Option, pub is_default_sub_group: bool, pub is_general_chat: bool, + pub creation: Option, + pub owner: Option, } ``` @@ -347,6 +439,8 @@ pub struct CommunitySubgroup { - `participant_count` — Number of participants (if available) - `is_default_sub_group` — Whether this is the default announcement subgroup - `is_general_chat` — Whether this is the general chat subgroup +- `creation` — Subgroup creation timestamp (Unix seconds), if available +- `owner` — JID of the subgroup owner, if available ### LinkSubgroupsResult From b7fba51b083fddc1f4bf6c7a28a9659f4960f970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:19:25 -0300 Subject: [PATCH 04/14] docs: document community subgroup/remove_participants/get_participating APIs from PR #1060 --- guides/communities.mdx | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/guides/communities.mdx b/guides/communities.mdx index c2c30d4..bc6a4dd 100644 --- a/guides/communities.mdx +++ b/guides/communities.mdx @@ -89,6 +89,26 @@ for (jid, error_code) in &result.failed_groups { See [Community API reference](/api/community#link_subgroups) for details. +### Create a new subgroup + +Create a brand new group that's already linked as a subgroup of a community, in a single call: + +```rust +let participants = vec![ + "15551234567@s.whatsapp.net".parse()?, +]; + +let result = client.community() + .create_subgroup("Announcements", &participants, &community_jid) + .await?; + +println!("Created subgroup: {} ({})", result.metadata.subject, result.metadata.id); +``` + +This is a shortcut for creating a group and then calling `link_subgroups` — both steps happen in a single round-trip instead of two. + +See [Community API reference](/api/community#create_subgroup) for details. + ### Unlink subgroups Unlink subgroups from a community: @@ -109,6 +129,24 @@ When `remove_orphan_members` is `true`, members who are only in the community th See [Community API reference](/api/community#unlink_subgroups) for details. +### Remove participants from a community + +Remove participants directly from a community (as opposed to from a single subgroup): + +```rust +let to_remove = vec!["15551234567@s.whatsapp.net".parse()?]; + +let results = client.community() + .remove_participants(&community_jid, &to_remove) + .await?; + +for result in results { + println!("{}: status {:?}", result.jid, result.status); +} +``` + +See [Community API reference](/api/community#remove_participants) for details. + ### Join a subgroup Join a linked subgroup via the parent community: @@ -126,6 +164,20 @@ See [Community API reference](/api/community#join_subgroup) for details. ## Querying community information +### List communities you're in + +Fetch all parent/community groups the logged-in account currently participates in: + +```rust +let communities = client.community().get_participating().await?; + +for (jid, metadata) in communities { + println!("{}: {} ({} participants)", jid, metadata.subject, metadata.participants.len()); +} +``` + +See [Community API reference](/api/community#get_participating) for details. + ### List subgroups Fetch all subgroups of a community via MEX (GraphQL): From 8a9211a1f05a1e1cb3c0ef6bf0d579272ca6a79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:21:53 -0300 Subject: [PATCH 05/14] docs: document new session/sender-key/pre-key APIs from PR #1060 --- api/signal.mdx | 265 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/api/signal.mdx b/api/signal.mdx index 0f4605c..a254232 100644 --- a/api/signal.mdx +++ b/api/signal.mdx @@ -160,6 +160,112 @@ let plaintext = client.signal().decrypt_group_message( ).await?; ``` +### sender_key_distribution + +Create (or lazily initialize) and serialize the current outgoing sender-key distribution message for a group. + +```rust +pub async fn sender_key_distribution( + &self, + group_jid: &Jid, + sender_jid: &Jid, +) -> Result, SignalError> +``` + +**Parameters:** +- `group_jid` - Group JID +- `sender_jid` - Your own JID as it should appear to other group members (the sender key chain owner) + +**Returns:** +- `Vec` - Serialized `SenderKeyDistributionMessage` bytes, ready to send to a new or existing group member (e.g. when adding a participant who needs to decrypt future messages) + +This is the same distribution payload [`encrypt_group_message`](#encrypt_group_message) returns automatically on first use — call it directly when you need to (re)distribute a sender key out of band, such as when a new member joins and needs the current chain without waiting for the next group message. The distribution is durably persisted before this method returns. + +**Example:** +```rust +let distribution = client.signal().sender_key_distribution(&group_jid, &my_jid).await?; +// Send `distribution` to the new participant, e.g. wrapped in a SenderKeyDistributionMessage protocol node +``` + +### process_sender_key_distribution + +Process an incoming sender-key distribution message for a group, installing the sender's chain so future `skmsg` stanzas from them can be decrypted. + +```rust +pub async fn process_sender_key_distribution( + &self, + group_jid: &Jid, + sender_jid: &Jid, + distribution: &[u8], +) -> Result<(), SignalError> +``` + +**Parameters:** +- `group_jid` - Group JID +- `sender_jid` - JID of the participant who distributed the sender key +- `distribution` - Serialized `SenderKeyDistributionMessage` bytes received from the sender (typically extracted from an incoming SKDM node) + +The sender-key chain is durably persisted before this method returns. + +**Example:** +```rust +client.signal().process_sender_key_distribution( + &group_jid, + &sender_jid, + &distribution_bytes, +).await?; +``` + +### has_sender_key + +Check whether sender-key state already exists for a group and sender. + +```rust +pub async fn has_sender_key( + &self, + group_jid: &Jid, + sender_jid: &Jid, +) -> Result +``` + +**Parameters:** +- `group_jid` - Group JID +- `sender_jid` - Sender's JID within the group + +**Returns:** +- `bool` - `true` if a sender-key chain is already stored for this `(group_jid, sender_jid)` pair + +**Example:** +```rust +if !client.signal().has_sender_key(&group_jid, &author_jid).await? { + // No chain yet — process the SKDM before decrypting skmsg from this sender +} +``` + +### delete_sender_key + +Durably delete a sender-key chain for a group and sender, e.g. on group exit or key rotation. + +```rust +pub async fn delete_sender_key( + &self, + group_jid: &Jid, + sender_jid: &Jid, +) -> Result<(), SignalError> +``` + +**Parameters:** +- `group_jid` - Group JID +- `sender_jid` - Sender's JID within the group + +The deletion waits for any in-flight chain mutation (e.g. a concurrent `encrypt_group_message` ratchet advance) to finish before removing the chain, and is flushed to the persistent backend before returning. + +**Example:** +```rust +// On leaving a group, drop the local copy of your own sender key chain +client.signal().delete_sender_key(&group_jid, &my_jid).await?; +``` + ### validate_session Check whether a Signal session exists for a JID. @@ -183,6 +289,29 @@ if client.signal().validate_session(&jid).await? { } ``` +### session_info + +Inspect an existing pairwise Signal session without mutating it. + +```rust +pub async fn session_info(&self, jid: &Jid) -> Result, SignalError> +``` + +**Parameters:** +- `jid` - JID to inspect. PN JIDs are resolved to LID and `Hosted` JIDs to `HostedLid` when a mapping exists. + +**Returns:** +- `Option` - `Some` with the session's base key and remote registration id if a session exists, `None` otherwise + +If only a legacy PN-addressed session exists and the resolved address is a LID, this method migrates it first (moving session and identity state to the LID namespace) and then reports on the migrated session — mirroring the on-the-fly migration used by the decrypt path. See [`SignalSessionInfo`](#signalsessioninfo) and [PN→LID session migration](/advanced/signal-protocol). + +**Example:** +```rust +if let Some(info) = client.signal().session_info(&jid).await? { + println!("registration_id={} base_key_len={}", info.registration_id, info.base_key.len()); +} +``` + ### delete_sessions Delete Signal sessions and identity keys for the given JIDs. @@ -202,6 +331,72 @@ This matches WhatsApp Web's `deleteRemoteSession` behavior, which removes both t client.signal().delete_sessions(&[jid1, jid2]).await?; ``` +### install_prekey_bundle + +Durably install a supplied pre-key bundle for a JID, establishing (or replacing) a pairwise session from it. + +```rust +pub async fn install_prekey_bundle( + &self, + jid: &Jid, + bundle: &PreKeyBundle, +) -> Result +``` + +**Parameters:** +- `jid` - JID to install the session for. Resolved the same way as [`encrypt_message`](#encrypt_message). +- `bundle` - A `PreKeyBundle` obtained out of band — e.g. from a manual/custom prekey fetch — rather than through [`assert_sessions`](#assert_sessions)'s normal usync + fetch flow + +**Returns:** +- `IdentityChange` - `IdentityChange::NewOrUnchanged` if the peer had no identity key or it matched, or `IdentityChange::ReplacedExisting` if this bundle's identity key replaced a previously trusted one + +The session is durably persisted before this method returns. + +**Example:** +```rust +let bundle: PreKeyBundle = /* fetched via a custom IQ */; +let identity_change = client.signal().install_prekey_bundle(&jid, &bundle).await?; + +if identity_change == IdentityChange::ReplacedExisting { + println!("warning: {jid}'s identity key changed"); +} +``` + +### migrate_sessions + +Move pairwise session and identity state from one JID namespace to another for the same underlying account (PN→LID, or Hosted→HostedLid). + +```rust +pub async fn migrate_sessions( + &self, + from: &Jid, + to: &Jid, +) -> Result +``` + +**Parameters:** +- `from` - Source JID namespace (must be `Pn` or `Hosted`) +- `to` - Destination JID namespace (must be `Lid` for a `Pn` source, or `HostedLid` for a `Hosted` source) + +**Returns:** +- `SignalSessionMigration` - Counts of sessions and identities moved, discarded, or skipped. See [`SignalSessionMigration`](#signalsessionmigration). + +Scans known device slots under `from`, moving each pairwise session and identity to `to` when the destination doesn't already have one, and discarding the stale source entry when it does. This is the same logic the client runs automatically on LID discovery and on-the-fly during decryption — exposed here for callers that want to trigger it manually. See [PN→LID session migration](/advanced/signal-protocol). + +Mismatched namespace pairs (e.g. a `Pn` source with a `HostedLid` destination, or two otherwise unrelated JIDs) are rejected with `SignalError::InvalidInput`. + +**Example:** +```rust +let outcome = client.signal().migrate_sessions(&pn_jid, &lid_jid).await?; + +if outcome.has_state_changes() { + println!( + "migrated {} sessions, {} identities", + outcome.migrated, outcome.migrated_identities + ); +} +``` + ### create_participant_nodes Create encrypted participant `` nodes for the given recipient JIDs. @@ -298,6 +493,48 @@ pub enum EncType { `EncType` exposes two predicate helpers: `is_session()` (true for `Message` / `PreKeyMessage`, **excludes** `MessageSecret`) and `is_bot_secret()` (true only for `MessageSecret`). +## SignalSessionInfo + +Read-only information from a currently open pairwise session, returned by [`session_info`](#session_info): + +```rust +pub struct SignalSessionInfo { + /// Local base key identifying the active session state. + pub base_key: Vec, + /// Remote registration identifier recorded by the session. + pub registration_id: u32, +} +``` + +`SignalSessionInfo` is re-exported from the crate root, so it's also available as `whatsapp_rust::SignalSessionInfo`. + +## SignalSessionMigration + +Result of moving pairwise session state between address namespaces, returned by [`migrate_sessions`](#migrate_sessions): + +```rust +#[non_exhaustive] +pub struct SignalSessionMigration { + /// Pairwise sessions moved to the destination namespace. + pub migrated: usize, + /// Pairwise session lookups skipped after a storage error. + pub skipped: usize, + /// Pairwise sessions found or unsuccessfully queried. + pub total: usize, + /// Identity records moved when the destination had no identity. + pub migrated_identities: usize, + /// Source identity records removed in favor of an existing destination. + pub discarded_identities: usize, + /// Identity lookups skipped after a storage error. + pub skipped_identities: usize, +} +``` + +**Methods:** +- `has_state_changes(self) -> bool` - `true` if any source state was moved or removed (`migrated != 0 || migrated_identities != 0 || discarded_identities != 0`). Useful for deciding whether a migration was a meaningful no-op (nothing to move) versus one that changed persisted state. + +`SignalSessionMigration` is `#[non_exhaustive]` and re-exported from the crate root as `whatsapp_rust::SignalSessionMigration`. + ## Bot message decryption (msmsg) When you message Meta AI or another `@bot` account, the bot's replies arrive as `` stanzas. These are **not** Signal-session encrypted — they use a dual-HKDF derivation over the 32-byte `messageSecret` from the prompt you sent, then AES-256-GCM. @@ -357,6 +594,18 @@ if skdm.is_some() { } ``` +### Add a participant to an existing sender-key group + +```rust +// New member needs the current sender key chain to decrypt future skmsg +if !client.signal().has_sender_key(&group_jid, &my_jid).await? { + // Nothing sent to this group yet — no chain to distribute +} else { + let distribution = client.signal().sender_key_distribution(&group_jid, &my_jid).await?; + // Send `distribution` to the new participant +} +``` + ### Reset a broken session ```rust @@ -370,6 +619,19 @@ client.signal().assert_sessions(&[jid.clone()]).await?; let (enc_type, ciphertext) = client.signal().encrypt_message(&jid, plaintext).await?; ``` +### Manually migrate a session to LID addressing + +```rust +if let Some(info) = client.signal().session_info(&pn_jid).await? { + println!("existing PN-addressed session, registration_id={}", info.registration_id); +} + +let outcome = client.signal().migrate_sessions(&pn_jid, &lid_jid).await?; +if outcome.has_state_changes() { + println!("moved {} session(s) to LID addressing", outcome.migrated); +} +``` + ## Error types ### `SignalError` @@ -383,6 +645,8 @@ pub enum SignalError { Protocol(#[from] SignalProtocolError), #[error("unsupported signal operation: {0}")] Unsupported(String), + #[error("invalid signal input: {0}")] + InvalidInput(String), #[error(transparent)] Internal(#[from] anyhow::Error), } @@ -391,6 +655,7 @@ pub enum SignalError { **Variants:** - `Protocol` — Signal protocol error (session mismatch, decode failure, etc.) - `Unsupported` — Operation not supported for the given parameters +- `InvalidInput` — The operation is supported but one of its inputs is malformed — e.g. a sender-key distribution message that fails to decode, or a [`migrate_sessions`](#migrate_sessions) call with a source/destination pair that isn't a valid PN→LID or Hosted→HostedLid namespace match - `Internal` — Catch-all for other errors ## See also From b5e14006a995864d081ffd5d1957e2807d3e8483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:23:38 -0300 Subject: [PATCH 06/14] docs: update group docs for PR #1060 (ephemeral field, promote/demote return type) --- api/groups.mdx | 164 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 6 deletions(-) diff --git a/api/groups.mdx b/api/groups.mdx index e53943d..20ca316 100644 --- a/api/groups.mdx +++ b/api/groups.mdx @@ -67,25 +67,37 @@ This map is keyed by `Jid`. Call `.to_string()` on the key if you need the strin **GroupMetadata fields:** - `id: Jid` - Group JID - `subject: String` - Group name +- `notify: Option` - Display notification string reported by the server (from the `notify` attribute) - `participants: Vec` - List of participants - `addressing_mode: AddressingMode` - Phone number or LID mode - `creator: Option` - Group creator JID +- `creator_pn: Option` - Creator's phone-number JID, when `creator` is a LID +- `creator_username: Option` - Creator's Meta username, when present +- `creator_country_code: Option` - Creator's ISO country code, when present - `creation_time: Option` - Group creation timestamp (Unix seconds) +- `participant_version_id: Option` - Participant-list version identifier (from `p_v_id`) +- `admin_version_id: Option` - Admin-list version identifier (from `a_v_id`) +- `open_thread_id: Option` - Open thread identifier associated with the group +- `has_missing_participant_identification: bool` - Whether participant identity information was incomplete in this response - `subject_time: Option` - Subject modification timestamp (Unix seconds) - `subject_owner: Option` - Subject owner JID +- `subject_owner_pn: Option` - Subject owner's phone-number JID (from `s_o_pn`) +- `subject_owner_username: Option` - Subject owner's Meta username (from `s_o_username`) - `description: Option` - Group description body text - `description_id: Option` - Description ID (for conflict detection) - `description_owner: Option` - JID of the participant who set the description +- `description_owner_pn: Option` - Description owner's phone-number JID +- `description_owner_username: Option` - Description owner's Meta username - `description_time: Option` - Timestamp when the description was set (Unix seconds) - `is_locked: bool` - Whether only admins can edit group info - `is_announcement: bool` - Whether only admins can send messages -- `ephemeral_expiration: u32` - Disappearing messages timer in seconds (0 = disabled) -- `ephemeral_trigger: Option` - Disappearing mode trigger value (from the `trigger` attribute on ``) +- `ephemeral: Option` - Disappearing-message settings. `None` when the server response has no `` node at all; `Some(GroupEphemeralSettings { expiration, trigger })` when the node is present — `expiration` is `None` if the node omitted the attribute, which is distinct from `Some(0)` (timer explicitly disabled) - `membership_approval: bool` - Whether admin approval is required to join - `member_add_mode: Option` - Who can add members - `member_link_mode: Option` - Who can use invite links - `size: Option` - Total participant count - `is_parent_group: bool` - Whether this group is a community parent group +- `parent_membership_approval_required: bool` - Whether joins to this parent group require approval by default - `parent_group_jid: Option` - JID of the parent community (for subgroups) - `is_default_sub_group: bool` - Whether this is the default announcement subgroup of a community - `is_general_chat: bool` - Whether this is the general chat subgroup of a community @@ -94,11 +106,25 @@ This map is keyed by `Jid`. Call `.to_string()` on the key if you need the strin - `member_share_history_mode: Option` - Who can share message history with new members - `growth_locked: Option` - Growth lock status (invite links temporarily disabled by the system) - `is_suspended: bool` - Whether the group is suspended +- `suspension_can_auto_file: bool` - Whether a suspension appeal may be filed automatically +- `appeal_status: Option` - Current suspension-appeal state +- `appeal_update_time: Option` - Last suspension-appeal update timestamp (Unix seconds) +- `is_support_group: bool` - Whether the group is marked as a support group - `allow_admin_reports: bool` - Whether admin reports are allowed - `is_hidden_group: bool` - Whether the group is hidden - `is_incognito: bool` - Whether incognito mode is enabled - `has_group_history: bool` - Whether group history is enabled +- `is_auto_add_disabled: bool` - Whether automatic participant addition is disabled +- `has_capi: bool` - Whether the group carries the CAPI capability marker +- `evolution_version: Option` - Group schema evolution version +- `has_group_safety_check: bool` - Whether the group safety-check feature is enabled +- `participant_label_enabled: bool` - Whether participant labels are enabled - `is_limit_sharing_enabled: bool` - Whether limit sharing is enabled +- `limit_sharing_trigger: Option` - Source trigger for limit-sharing enablement + + +`ephemeral_expiration: u32` and `ephemeral_trigger: Option` were replaced by the single `ephemeral: Option` field. Migrate reads like `metadata.ephemeral_expiration` to `metadata.ephemeral.as_ref().and_then(|e| e.expiration).unwrap_or(0)`. + See [Community API](/api/community) for community-specific operations. @@ -107,7 +133,10 @@ See [Community API](/api/community) for community-specific operations. **GroupParticipant fields:** - `jid: Jid` - Participant JID - `phone_number: Option` - Phone number JID (for LID groups) +- `lid: Option` - Participant's LID JID, when the server includes one +- `username: Option` - Participant's Meta username, when present - `participant_type: ParticipantType` - Participant role (member, admin, or super admin) +- `details: Option>` - Less-common participant metadata (label, join time, display name, etc.); boxed and only populated when at least one field is present **Example:** ```rust @@ -340,6 +369,33 @@ let to_remove = vec!["15551234567@s.whatsapp.net".parse()?]; client.groups().remove_participants(&group_jid, &to_remove).await?; ``` +### remove_participants_including_linked_groups + +Remove participants from a group and cascade the removal to its linked/child groups. Used for community-linked groups, where removing someone from the community should also remove them from subgroups. + +```rust +pub async fn remove_participants_including_linked_groups( + &self, + jid: &Jid, + participants: &[Jid], +) -> Result, GroupError> +``` + +**Parameters:** +- `jid` - Group JID (typically the community parent group) +- `participants` - Array of participant JIDs to remove + +**Returns:** +- `Vec` - Result for each participant + +**Example:** +```rust +let group_jid: Jid = "123456789@g.us".parse()?; +let to_remove = vec!["15551234567@s.whatsapp.net".parse()?]; + +let results = client.groups().remove_participants_including_linked_groups(&group_jid, &to_remove).await?; +``` + ### promote_participants Promote participants to admin. @@ -349,21 +405,32 @@ pub async fn promote_participants( &self, jid: &Jid, participants: &[Jid], -) -> Result<(), GroupError> +) -> Result, GroupError> ``` **Parameters:** - `jid` - Group JID - `participants` - Array of participant JIDs to promote +**Returns:** +- `Vec` - Result for each participant + **Example:** ```rust let group_jid: Jid = "123456789@g.us".parse()?; let to_promote = vec!["15551234567@s.whatsapp.net".parse()?]; -client.groups().promote_participants(&group_jid, &to_promote).await?; +let results = client.groups().promote_participants(&group_jid, &to_promote).await?; + +for result in results { + println!("Promoted {}: status {:?}", result.jid, result.status); +} ``` + +`promote_participants` returns `Vec` (one entry per participant) instead of `()`. + + ### demote_participants Demote admin participants to regular members. @@ -373,21 +440,32 @@ pub async fn demote_participants( &self, jid: &Jid, participants: &[Jid], -) -> Result<(), GroupError> +) -> Result, GroupError> ``` **Parameters:** - `jid` - Group JID - `participants` - Array of admin JIDs to demote +**Returns:** +- `Vec` - Result for each participant + **Example:** ```rust let group_jid: Jid = "123456789@g.us".parse()?; let to_demote = vec!["15551234567@s.whatsapp.net".parse()?]; -client.groups().demote_participants(&group_jid, &to_demote).await?; +let results = client.groups().demote_participants(&group_jid, &to_demote).await?; + +for result in results { + println!("Demoted {}: status {:?}", result.jid, result.status); +} ``` + +`demote_participants` returns `Vec` (one entry per participant) instead of `()`. + + ### get_invite_link Get or reset the group invite link. @@ -971,6 +1049,36 @@ let group_jid: Jid = "123456789@g.us".parse()?; client.groups().acknowledge(&group_jid).await?; ``` +### update_member_label_with_id + +Set or clear the bot's per-group member label, sent as a `ProtocolMessage` over the normal message path (not an IQ). Returns the sent stanza's message ID. + +```rust +pub async fn update_member_label_with_id( + &self, + group_jid: &Jid, + label: impl Into, +) -> Result +``` + +**Parameters:** +- `group_jid` - Group JID +- `label` - New label text, or an empty string to clear the label + +**Returns:** +- `String` - Message ID of the sent stanza + +**Example:** +```rust +let group_jid: Jid = "123456789@g.us".parse()?; +let message_id = client.groups().update_member_label_with_id(&group_jid, "VIP").await?; +println!("Label update sent as message {}", message_id); +``` + + +`update_member_label` is a thin wrapper around `update_member_label_with_id` that discards the message ID and returns `Result<(), GroupError>`, for callers that don't need it. + + ### batch_get_info Batch query group info for multiple groups at once. @@ -1180,6 +1288,17 @@ impl GroupDescription { } ``` +### GroupEphemeralSettings + +Disappearing-message settings carried by a group's `` node. + +```rust +pub struct GroupEphemeralSettings { + pub expiration: Option, + pub trigger: Option, +} +``` + ### MemberAddMode ```rust @@ -1204,6 +1323,26 @@ pub enum ParticipantType { **Methods:** - `is_admin(&self) -> bool` - Returns `true` if admin or super admin +### GroupParticipantDetails + +Less-common participant metadata. Boxed on `GroupParticipant`/`GroupParticipantResponse` and only populated when at least one field is present. + +```rust +#[non_exhaustive] +pub struct GroupParticipantDetails { + pub participant_label: Option, + pub participant_label_mtime: Option, + pub join_time: Option, + pub group_history_sent: Option, + pub display_name: Option, + pub is_addressable: bool, +} +``` + + +`GroupParticipantDetails` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`. + + ### MemberLinkMode Controls who can use invite links to join the group. @@ -1235,6 +1374,19 @@ pub enum MembershipApprovalMode { } ``` +### GroupAppealStatus + +Review state for an appeal on a suspended group. + +```rust +pub enum GroupAppealStatus { + Approved, + InReview, + NoAppeal, + Rejected, +} +``` + ### GroupParticipantOptions Options for specifying a participant when creating or modifying a group. From 37d1bcc9c0df89f669bcd39638f9c4f1114d9248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:24:49 -0300 Subject: [PATCH 07/14] docs: update group docs for PR #1060 (ephemeral field, promote/demote return type) --- guides/group-management.mdx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/guides/group-management.mdx b/guides/group-management.mdx index caa00fe..f028d7f 100644 --- a/guides/group-management.mdx +++ b/guides/group-management.mdx @@ -286,17 +286,21 @@ for response in responses { See [Groups API reference](/api/groups#remove_participants) for details. +To also remove participants from a community group's linked/child groups, use `remove_participants_including_linked_groups` instead — same signature, but the removal cascades to subgroups. See [Groups API reference](/api/groups#remove_participants_including_linked_groups) for details. + ### Promote to admin ```rust let participants = vec!["1234567890@s.whatsapp.net".parse()?]; -client.groups().promote_participants( +let results = client.groups().promote_participants( &group_jid, &participants, ).await?; -println!("Promoted to admin"); +for result in results { + println!("Promoted {}: status {:?}", result.jid, result.status); +} ``` See [Groups API reference](/api/groups#promote_participants) for details. @@ -306,12 +310,14 @@ See [Groups API reference](/api/groups#promote_participants) for details. ```rust let participants = vec!["1234567890@s.whatsapp.net".parse()?]; -client.groups().demote_participants( +let results = client.groups().demote_participants( &group_jid, &participants, ).await?; -println!("Demoted from admin"); +for result in results { + println!("Demoted {}: status {:?}", result.jid, result.status); +} ``` See [Groups API reference](/api/groups#demote_participants) for details. @@ -724,6 +730,17 @@ client.groups().set_limit_sharing(&group_jid, false).await?; See [Groups API reference](/api/groups#set_limit_sharing) for details. +### Member label + +Set or clear the bot's per-group member label. This is sent as a message (`ProtocolMessage`), not an IQ, and returns the sent message's ID: + +```rust +let message_id = client.groups().update_member_label_with_id(&group_jid, "VIP").await?; +println!("Label update sent as message {}", message_id); +``` + +See [Groups API reference](/api/groups#update_member_label_with_id) for details. + ## Privacy tokens on group operations When server-side A/B experiment flags are enabled, the library automatically attaches privacy tokens (`tc_token`) to participants during group creation and participant addition. This matches WhatsApp Web's behavior and requires no changes to your code. From 0465df70d1df31f9028b30864b2ceb96fff34a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:29:11 -0300 Subject: [PATCH 08/14] docs: document new session/sender-key/pre-key APIs from PR #1060 --- advanced/signal-protocol.mdx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx index a449788..c8054cc 100644 --- a/advanced/signal-protocol.mdx +++ b/advanced/signal-protocol.mdx @@ -342,6 +342,10 @@ Location: `wacore/src/send.rs:675-820`, `src/send.rs` WhatsApp's multi-device architecture uses two addressing schemes: phone number JIDs (PN, `@s.whatsapp.net`) and Linked Identity JIDs (LID, `@lid`). WhatsApp Web always resolves PN→LID before any session operation via `createSignalAddress()`. whatsapp-rust mirrors this behavior — when a LID mapping is discovered for a phone number, any Signal sessions stored under the PN address are automatically migrated to the corresponding LID address. + + The automatic migration described below is also exposed for manual invocation: [`Signal::migrate_sessions(from, to)`](/api/signal#migrate_sessions) runs the same move for a caller-chosen JID pair, and [`Signal::session_info(jid)`](/api/signal#session_info) inspects a session (migrating a legacy PN-addressed one first if needed) without mutating it further. See the [Signal API reference](/api/signal) for both. + + ### Signal address resolution `Client::resolve_encryption_jid()` mirrors WA Web's `SignalAddress.toString()` (`WAWeb/Signal/Address.js`). It upgrades the JID's `server` to its LID counterpart when a mapping is known, and otherwise returns the input unchanged: @@ -428,6 +432,10 @@ Identity keys are migrated independently of sessions — they can outlive delete The migration reads through the cache because the backend may contain stale session data when unflushed cache mutations exist. Reading directly from the backend could skip in-flight ratchet advances, causing the migrated session to decrypt with an outdated chain key. + + `add_lid_pn_mapping` also has a batch form, `Client::add_lid_pn_mappings(mappings, source)`, which durably records many LID↔PN pairs in one call and runs the same per-mapping migration as the single-entry path. It returns how many mappings were actually written, deduplicated against existing records. + + ### On-the-fly migration during decryption If a message arrives from a LID address and decryption fails with `SessionNotFound` or `InvalidPreKeyId`, the client attempts PN→LID migration as a fallback before requesting a retry: @@ -996,6 +1004,11 @@ client.refresh_pre_keys().await?; Internally, this acquires `prekey_upload_lock` to prevent races with the count-based and digest-repair upload paths, then calls `upload_pre_keys_with_retry(force: true)` which uses Fibonacci backoff (1s, 2s, 3s, 5s, 8s, ... capped at 610s). +Two related public methods build on the same `prekey_upload_lock`-guarded path: + +- `Client::refresh_pre_keys_with_count(count)` — same force-upload as `refresh_pre_keys()`, but with a caller-chosen batch size instead of the configured [`wanted_pre_key_count`](#configuration). +- `Client::ensure_pre_keys()` — a non-forced check-and-top-up: uploads only if the server-side pool is below the low-water mark, rather than unconditionally replacing it. + Location: `src/prekeys.rs:263-266` ### Digest key validation @@ -1048,6 +1061,10 @@ After connection, the client validates that the server's copy of the key bundle Hash mismatches or missing local pre-keys are logged but do **not** trigger a re-upload. Only a 404 response (server has no record) triggers re-upload. This matches WhatsApp Web's behavior where `validateLocalKeyBundle` exceptions are caught without re-uploading — the normal [`RotateKeyJob`](#signed-pre-key-rotation-rotatekeyjob) eventually refreshes the signed pre-key. + + `Client::validate_digest_key()` is a public method — callers can trigger this validation pass on demand instead of only relying on the automatic post-connection check. + + Location: `src/prekeys.rs:218-344`, `wacore/src/iq/prekeys.rs:170-302` ### Signed pre-key rotation (RotateKeyJob) @@ -1097,6 +1114,10 @@ Before this feature, `Device::load_signed_prekey` (`src/store/signal.rs`) return **Retry, not NACK, once the id ages past retention:** A sender's `PreKeySignalMessage` can still name a signed pre-key id that has since aged past `SIGNED_PRE_KEY_RETENTION` (3 total: current + 2 rotated-out) — the backend fallback above has nothing left to return, and `InvalidSignedPreKeyId` is the correct, permanent answer. On the 1:1 decrypt path (`src/message/receive.rs`), this now routes to a retry receipt (`RetryReason::InvalidKeyId`) carrying the current bundle, mirroring the sibling `InvalidPreKeyId` arm — instead of falling through to the catch-all `UnhandledError` nack, which would drop the stanza from the offline queue and lose the 1:1 message permanently and silently. + + `Client::rotate_signed_pre_key()` is a public method — callers can force an out-of-cadence rotation directly instead of waiting for the weekly check. It shares `signed_pre_key_rotation_lock` with the automatic path (so a manual call can't race a background rotation) and propagates upload failures to the caller rather than swallowing them. + + Location: `src/features/rotate_key.rs`, `src/store/signal.rs`, `src/message/receive.rs`, `wacore/src/iq/prekeys.rs`, `wacore/src/store/commands.rs` ### Re-pair pre-key healing (v0.6) From 7563b473ee945c8e65f943e377636b29b2b3a482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:34:45 -0300 Subject: [PATCH 09/14] docs: clarify session_info can migrate PN session state (review feedback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses greptile-apps review comment on PR #418: session_info is not a purely read-only inspection when it triggers legacy PN→LID migration. --- api/signal.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/signal.mdx b/api/signal.mdx index a254232..63cd20b 100644 --- a/api/signal.mdx +++ b/api/signal.mdx @@ -291,7 +291,7 @@ if client.signal().validate_session(&jid).await? { ### session_info -Inspect an existing pairwise Signal session without mutating it. +Inspect an existing pairwise Signal session, migrating legacy PN-addressed state to its resolved LID namespace when needed. ```rust pub async fn session_info(&self, jid: &Jid) -> Result, SignalError> @@ -303,7 +303,9 @@ pub async fn session_info(&self, jid: &Jid) -> Result, **Returns:** - `Option` - `Some` with the session's base key and remote registration id if a session exists, `None` otherwise -If only a legacy PN-addressed session exists and the resolved address is a LID, this method migrates it first (moving session and identity state to the LID namespace) and then reports on the migrated session — mirroring the on-the-fly migration used by the decrypt path. See [`SignalSessionInfo`](#signalsessioninfo) and [PN→LID session migration](/advanced/signal-protocol). + + If only a legacy PN-addressed session exists and the resolved address is a LID, this method migrates it first (moving session and identity state to the LID namespace) and then reports on the migrated session — mirroring the on-the-fly migration used by the decrypt path. This means `session_info` is not a purely read-only probe: it can itself perform the migration, so a subsequent [`migrate_sessions`](#migrate_sessions) call on the same pair may find there is nothing left to move. See [`SignalSessionInfo`](#signalsessioninfo) and [PN→LID session migration](/advanced/signal-protocol). + **Example:** ```rust From b12d8c3213e21f646c8929c871a95933078697e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:36:24 -0300 Subject: [PATCH 10/14] docs: fix manual migration example to not self-defeat via session_info side effect Addresses chatgpt-codex-connector review comment on PR #418: calling session_info(&pn_jid) before migrate_sessions would already perform the migration as a side effect, making migrate_sessions report no changes. Reordered to migrate first, then inspect the result under the LID address. --- api/signal.mdx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api/signal.mdx b/api/signal.mdx index 63cd20b..a119725 100644 --- a/api/signal.mdx +++ b/api/signal.mdx @@ -624,14 +624,17 @@ let (enc_type, ciphertext) = client.signal().encrypt_message(&jid, plaintext).aw ### Manually migrate a session to LID addressing ```rust -if let Some(info) = client.signal().session_info(&pn_jid).await? { - println!("existing PN-addressed session, registration_id={}", info.registration_id); -} - +// Migrate first — session_info(&pn_jid) would trigger this same migration as a +// side effect, which would leave nothing here for migrate_sessions to move. let outcome = client.signal().migrate_sessions(&pn_jid, &lid_jid).await?; if outcome.has_state_changes() { println!("moved {} session(s) to LID addressing", outcome.migrated); } + +// Inspect the session under its new LID address +if let Some(info) = client.signal().session_info(&lid_jid).await? { + println!("registration_id={}", info.registration_id); +} ``` ## Error types From 660610feb98d525c73579790edaf05360b55618e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:52:02 -0300 Subject: [PATCH 11/14] docs: reconcile sender-key locking claims (review feedback on PR #418) --- api/signal.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/signal.mdx b/api/signal.mdx index a119725..1cd9e81 100644 --- a/api/signal.mdx +++ b/api/signal.mdx @@ -109,9 +109,9 @@ pub async fn encrypt_group_message( **Returns:** - `(Option>, Vec)` - A tuple of optional SKDM bytes and ciphertext bytes. The SKDM is `Some` only when a new sender key was created (first encrypt for this group or after key rotation). You must distribute the SKDM to all group participants when present. - - Not safe to call concurrently with `decrypt_group_message` for the same group — sender key state is not internally locked. - + + Concurrent calls are serialized on a per-`(group_jid, sender_jid)` chain lock (`sender_key_lock`), keyed here by your own JID as the sender. Two overlapping `encrypt_group_message` calls for the same group safely queue behind one another. A concurrent `decrypt_group_message` call only shares this lock when its `sender_jid` is your own JID (an unusual case) — decrypting messages from other participants uses a different chain and runs fully in parallel. See [sender-key chain locking](/advanced/signal-protocol#parallelized-group-encrypt-fan-out). + **Example:** ```rust @@ -147,9 +147,9 @@ pub async fn decrypt_group_message( **Returns:** - `Vec` - Raw padded plaintext. Use `MessageUtils::unpad_message_ref` with the stanza's `v` attribute if WhatsApp message unpadding is needed. - - Not safe to call concurrently with `encrypt_group_message` for the same group — sender key state is not internally locked. - + + Concurrent calls are serialized on a per-`(group_jid, sender_jid)` chain lock (`sender_key_lock`), so two overlapping `decrypt_group_message` calls for the same sender safely queue behind one another. Calls for different senders in the same group — or a concurrent `encrypt_group_message` call, which uses your own JID as the chain identity — touch a different chain and run in parallel. See [sender-key chain lock (group receive)](/concepts/architecture#sender-key-chain-lock-group-receive). + **Example:** ```rust From 5cf856895eaaf829b65780f0f68e002771c99c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:55:55 -0300 Subject: [PATCH 12/14] docs: fix stale ephemeral_expiration/display_name references (review feedback on PR #418) --- api/groups.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/groups.mdx b/api/groups.mdx index 20ca316..b502613 100644 --- a/api/groups.mdx +++ b/api/groups.mdx @@ -229,7 +229,7 @@ for participant in &result.metadata.participants { ``` -Since v0.6, `create_group` returns the full `GroupMetadata` (matching `get_metadata`) instead of just the JID. Inspect `result.metadata` for participants, addressing mode, ephemeral timer, and parent linkage in a single round-trip. `GroupMetadata.participants` is a `Vec` (the IQ-response shape), not the `GroupParticipantInfo` used by group notification events — so masked-number `display_name` labels are only available on the event-side `GroupParticipantInfo`, not here. +Since v0.6, `create_group` returns the full `GroupMetadata` (matching `get_metadata`) instead of just the JID. Inspect `result.metadata` for participants, addressing mode, ephemeral timer, and parent linkage in a single round-trip. `GroupMetadata.participants` is a `Vec` (the IQ-response shape), not the `GroupParticipantInfo` used by group notification events — but masked-number `display_name` labels are available here too, via `participant.details.as_ref().and_then(|d| d.display_name.as_deref())`, in addition to the event-side `GroupParticipantInfo.display_name` for live group-update events. ### set_subject @@ -1543,7 +1543,7 @@ pub struct CreateGroupResult { `CreateGroupResult` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`. -The `metadata` field carries the full group state returned by the server (same shape as [`get_metadata`](#get_metadata)). `metadata.participants` is `Vec` — note that masked-number `display_name` labels live on `GroupParticipantInfo` (which carries `` children of *notification* events), not on `GroupParticipant` here. +The `metadata` field carries the full group state returned by the server (same shape as [`get_metadata`](#get_metadata)). `metadata.participants` is `Vec` — masked-number `display_name` labels are reachable via `participant.details.as_ref().and_then(|d| d.display_name.as_deref())`, or from the event-side `GroupParticipantInfo` (which carries `` children of *notification* events) when handling live group-update events. Prior to v0.6 this struct only exposed a `gid: Jid`. Replace `result.gid` with `result.metadata.id` when upgrading (`GroupMetadata.id`, not `jid`). The same migration applies to `CreateCommunityResult`. From 85f3a9b5d2e0fe44d13b1e0b94aa462446adb0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:57:32 -0300 Subject: [PATCH 13/14] docs: fix stale ephemeral_expiration/display_name references (review feedback on PR #418) --- guides/sending-messages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/sending-messages.mdx b/guides/sending-messages.mdx index 37396a2..d48f984 100644 --- a/guides/sending-messages.mdx +++ b/guides/sending-messages.mdx @@ -690,7 +690,7 @@ For groups, read the timer from group metadata: ```rust let metadata = client.groups().get_metadata(&group_jid).await?; -let expiration = metadata.ephemeral_expiration; // 0 if disabled +let expiration = metadata.ephemeral.as_ref().and_then(|e| e.expiration).unwrap_or(0); ``` For incoming messages, read it from `MessageInfo`: From debc75fc2d158da7f19c1e770a531b434beb3ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:59:47 -0300 Subject: [PATCH 14/14] docs: fix stale ephemeral_expiration/display_name references (review feedback on PR #418) --- api/send.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/send.mdx b/api/send.mdx index 7572a21..c3d3494 100644 --- a/api/send.mdx +++ b/api/send.mdx @@ -343,7 +343,7 @@ let result = client.send_message_with_options( ``` -The `ephemeral_expiration` value should match the chat's disappearing messages timer. You can get this from `GroupMetadata.ephemeral_expiration` for groups, or from `MessageInfo.ephemeral_expiration` on received messages. See the [sending messages guide](/guides/sending-messages#ephemeral-disappearing-messages) for a complete walkthrough. +The `ephemeral_expiration` value should match the chat's disappearing messages timer. You can get this from `GroupMetadata.ephemeral` (via `metadata.ephemeral.as_ref().and_then(|e| e.expiration)`) for groups, or from `MessageInfo.ephemeral_expiration` on received messages. See the [sending messages guide](/guides/sending-messages#ephemeral-disappearing-messages) for a complete walkthrough. ### Example: Send with extra stanza nodes