diff --git a/advanced/inbound-durability.mdx b/advanced/inbound-durability.mdx index 963f2c1d..fbe0e4f0 100644 --- a/advanced/inbound-durability.mdx +++ b/advanced/inbound-durability.mdx @@ -104,6 +104,8 @@ pub trait InboundDurabilityHook { Each `InboundMessage` carries: ```rust +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct InboundMessage { pub message: Arc, pub info: Arc, diff --git a/api/bot.mdx b/api/bot.mdx index 68dbedf1..9eb4c88f 100644 --- a/api/bot.mdx +++ b/api/bot.mdx @@ -35,7 +35,7 @@ let mut bot = Bot::builder() .on_event(|event, client| async move { match &*event { Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { println!("Message from {}: {:?}", info.source.sender, msg); } } @@ -577,7 +577,7 @@ Configures pair code authentication to run automatically after connecting. ```rust use whatsapp_rust::pair_code::PairCodeOptions; use wacore::companion_reg::CompanionWebClientType; -use wacore::types::events::Event; +use wacore::types::events::{Event, PairingCode}; Bot::builder() .with_pair_code(PairCodeOptions { @@ -593,7 +593,7 @@ Bot::builder() }) .on_event(|event, _client| async move { match &*event { - Event::PairingCode { code, timeout } => { + Event::PairingCode(PairingCode { code, timeout, .. }) => { println!("Enter this code on your phone: {}", code); println!("Expires in: {} seconds", timeout.as_secs()); } @@ -981,7 +981,7 @@ Newsletter (channel) messages don't flow through `MessageContext::react`. Use [` use whatsapp_rust::bot::Bot; use whatsapp_rust::TokioRuntime; use whatsapp_rust::store::SqliteStore; -use wacore::types::events::{Event, InboundMessage}; +use wacore::types::events::{Event, InboundMessage, PairingQrCode}; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; use waproto::whatsapp as wa; @@ -1003,7 +1003,7 @@ async fn main() -> anyhow::Result<()> { match &*event { Event::Messages(batch) => { // Echo messages back - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { if let Some(text) = &msg.conversation { let reply = wa::Message { conversation: Some(format!("You said: {}", text)), @@ -1018,7 +1018,7 @@ async fn main() -> anyhow::Result<()> { // Set status let _ = client.presence().set_available().await; } - Event::PairingQrCode { code, .. } => { + Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("Scan this QR code:"); println!("{}", code); } diff --git a/api/client.mdx b/api/client.mdx index bfbd8c8a..2a42bcf4 100644 --- a/api/client.mdx +++ b/api/client.mdx @@ -1618,7 +1618,7 @@ impl EventHandler for MyHandler { fn handle_event(&self, event: Arc) { match &*event { Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { println!("New message from {}: {:?}", info.source.sender, msg); } } diff --git a/api/receipt.mdx b/api/receipt.mdx index 2075d6a4..ab1a7249 100644 --- a/api/receipt.mdx +++ b/api/receipt.mdx @@ -381,6 +381,7 @@ let mut bot = Bot::builder() ### Receipt event structure ```rust +#[non_exhaustive] pub struct Receipt { pub source: MessageSource, pub message_ids: Vec, @@ -390,6 +391,10 @@ pub struct Receipt { } ``` + +`Receipt` is `#[non_exhaustive]` and constructed internally via a `bon` builder (`Receipt::builder()…build()`), so a struct-pattern destructure needs a `..` rest — e.g. `Receipt { source, r#type, .. }`. See [Payload stability](/concepts/events#event-enum) for the full policy. + + Source information: - `chat` - Chat JID where the receipt originated diff --git a/concepts/authentication.mdx b/concepts/authentication.mdx index da8ec280..b9fc7c75 100644 --- a/concepts/authentication.mdx +++ b/concepts/authentication.mdx @@ -84,7 +84,7 @@ use whatsapp_rust::TokioRuntime; use whatsapp_rust::store::SqliteStore; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; -use wacore::types::events::Event; +use wacore::types::events::{Event, PairingQrCode}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -97,7 +97,7 @@ async fn main() -> Result<(), Box> { .with_runtime(TokioRuntime) .on_event(|event, _client| async move { match &*event { - Event::PairingQrCode { code, timeout } => { + Event::PairingQrCode(PairingQrCode { code, timeout, .. }) => { println!("Scan this QR code (valid for {}s):", timeout.as_secs()); println!("{}", code); } @@ -117,16 +117,22 @@ async fn main() -> Result<(), Box> { ### QR code events -**Event:** `Event::PairingQrCode` +**Event:** `Event::PairingQrCode(PairingQrCode)` ```rust // wacore/src/types/events.rs -Event::PairingQrCode { - code: String, // ASCII art QR or data string - timeout: Duration, // Validity duration (60s first, 20s subsequent) +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingQrCode { + pub code: String, // ASCII art QR or data string + pub timeout: std::time::Duration, // Validity duration (60s first, 20s subsequent) } ``` + +Breaking change: `PairingQrCode` moved from inline fields on `Event::PairingQrCode { code, timeout }` to a dedicated `#[non_exhaustive]` struct sealed with a `bon` builder — `Event::PairingQrCode(PairingQrCode)`. Update `match`/`if let` patterns to destructure through the newtype (with a `..` rest), and construct via `PairingQrCode::builder().code(code).timeout(timeout).build()` instead of a struct literal. + + **Generated in:** `src/pair.rs:63-116` The rotation loop includes a **safety guard** that checks `is_logged_in()` before emitting each QR code. This prevents stale QR events from firing after pairing completes — important for single-threaded runtimes, fast auto-pair scenarios, and mock servers where the spawned task may not be polled until after pairing succeeds. @@ -146,7 +152,9 @@ for code in codes_clone { Duration::from_secs(20) }; - client.core.event_bus.dispatch(Event::PairingQrCode { code, timeout }); + client.core.event_bus.dispatch(Event::PairingQrCode( + PairingQrCode::builder().code(code).timeout(timeout).build(), + )); let sleep = client_clone.runtime.sleep(timeout); let stop = stop_rx.recv(); @@ -193,7 +201,7 @@ pub const NATIVE_CAMERA_DEEP_LINK_PREFIX: &str = "https://wa.me/settings/linked_ ```rust use whatsapp_rust::pair::NATIVE_CAMERA_DEEP_LINK_PREFIX; -use wacore::types::events::Event; +use wacore::types::events::{Event, PairingQrCode}; // Depends on the `qrcode` crate: qrcode = "0.14" fn render_qr(payload: &str) { @@ -208,7 +216,7 @@ fn render_qr(payload: &str) { // ...inside your event handler: .on_event(|event, _client| async move { - if let Event::PairingQrCode { code, timeout } = &*event { + if let Event::PairingQrCode(PairingQrCode { code, timeout, .. }) = &*event { // Prepend the prefix so iOS's native Camera opens WhatsApp directly. let deep_link = format!("{NATIVE_CAMERA_DEEP_LINK_PREFIX}{code}"); @@ -370,13 +378,15 @@ The server also validates that the display string is 1..=100 bytes. ### Pair code events -**Event:** `Event::PairingCode` +**Event:** `Event::PairingCode(PairingCode)` ```rust // wacore/src/types/events.rs -Event::PairingCode { - code: String, // The 8-character pairing code - timeout: Duration, // Validity (~180 seconds), remaining at time of emission +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingCode { + pub code: String, // The 8-character pairing code + pub timeout: std::time::Duration, // Validity (~180 seconds), remaining at time of emission } ``` @@ -391,20 +401,28 @@ let elapsed = wacore::time::now_secs().saturating_sub(code_generation_ts).max(0) let remaining = PairCodeUtils::code_validity() .saturating_sub(std::time::Duration::from_secs(elapsed)); -self.core.event_bus.dispatch(Event::PairingCode { - code: code.clone(), - timeout: remaining, -}); +self.core.event_bus.dispatch(Event::PairingCode( + PairingCode::builder() + .code(code.clone()) + .timeout(remaining) + .build(), +)); ``` + +Breaking change: `PairingCode` and `PairingCodeRefresh` moved from inline enum-variant fields (`Event::PairingCode { code, timeout }`, `Event::PairingCodeRefresh { force_manual }`) to dedicated `#[non_exhaustive]` structs sealed with a `bon` builder — `Event::PairingCode(PairingCode)` / `Event::PairingCodeRefresh(PairingCodeRefresh)`. Destructuring patterns need a `..` rest; construction goes through `PairingCode::builder()…build()`. + + ### Pair code refresh events -**Event:** `Event::PairingCodeRefresh` +**Event:** `Event::PairingCodeRefresh(PairingCodeRefresh)` ```rust // wacore/src/types/events.rs -Event::PairingCodeRefresh { - force_manual: bool, // true when the server requires an explicit re-request +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingCodeRefresh { + pub force_manual: bool, // true when the server requires an explicit re-request } ``` @@ -642,6 +660,10 @@ Event::PairPasskeyError(PairPasskeyError { Linking completes through the ordinary [`PairSuccess`/`PairError`](#success-events) events — there is no separate "passkey success" event. + +`PairPasskeyRequest`, `PairPasskeyConfirmation`, and `PairPasskeyError` are `#[non_exhaustive]`, sealed with a `bon` builder (e.g. `PairPasskeyRequest::builder().request_options_json(json).build()`). Field access by name (`req.request_options_json`) is unaffected; only an exhaustive struct-pattern destructure would need a `..` rest. + + ### Client methods | Method | Purpose | @@ -873,7 +895,8 @@ The `is_logged_in()` safety guard in the rotation loop acts as a fallback — ev ```rust // wacore/src/types/events.rs -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairSuccess { pub id: Jid, // Device JID (e.g., "15551234567.0:1@s.whatsapp.net") pub lid: Jid, // LID JID (e.g., "100000012345678.0:1@lid") @@ -881,13 +904,21 @@ pub struct PairSuccess { pub platform: String, // Platform identifier } -Event::PairSuccess(PairSuccess { id, lid, business_name, platform }) +Event::PairSuccess( + PairSuccess::builder() + .id(id) + .lid(lid) + .business_name(business_name) + .platform(platform) + .build(), +) ``` ### PairError ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairError { pub id: Jid, pub lid: Jid, @@ -896,9 +927,13 @@ pub struct PairError { pub error: String, // Error description } -Event::PairError(PairError { /* ... */ }) +Event::PairError(PairError::builder() /* .id(..).lid(..)… */ .build()) ``` + +Breaking change: `PairSuccess`, `PairError`, and `LoggedOut` (below) are now `#[non_exhaustive]` and sealed with a `bon` builder. A struct literal from outside `wacore`/`whatsapp-rust` no longer compiles — construct via `Type::builder()…build()`, and add a `..` rest to any destructuring pattern. + + ## Error Handling ### QR code errors @@ -1008,10 +1043,12 @@ bot.run().await?; // Uses saved session client.logout().await?; // Event emitted: -Event::LoggedOut(LoggedOut { - on_connect: false, - reason: ConnectFailureReason::LoggedOut, -}) +Event::LoggedOut( + LoggedOut::builder() + .on_connect(false) + .reason(ConnectFailureReason::LoggedOut) + .build(), +) ``` ## Best Practices @@ -1042,15 +1079,15 @@ let options = PairCodeOptions { ```rust .on_event(|event, client| async move { match &*event { - Event::PairingQrCode { code, timeout } => { + Event::PairingQrCode(PairingQrCode { code, timeout, .. }) => { // Display QR to user println!("Valid for: {}s", timeout.as_secs()); } - Event::PairingCode { code, timeout } => { + Event::PairingCode(PairingCode { code, timeout, .. }) => { // Display code to user println!("Enter {} on your phone", code); } - Event::PairingCodeRefresh { force_manual } => { + Event::PairingCodeRefresh(PairingCodeRefresh { force_manual, .. }) => { // Previous code is no longer guaranteed valid — request a new one println!("Refresh requested (force_manual={})", force_manual); } diff --git a/concepts/events.mdx b/concepts/events.mdx index 99d8c14f..ce34ce3b 100644 --- a/concepts/events.mdx +++ b/concepts/events.mdx @@ -69,7 +69,7 @@ impl EventHandler for MyHandler { fn handle_event(&self, event: Arc) { match &*event { Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { println!("Message from {}: {:?}", info.source.sender, msg); } } @@ -99,10 +99,14 @@ impl EventHandler for MyHandler { ``` - **`EventKind`** is a `#[repr(u8)]` discriminant — one variant per `Event` variant (`Messages`, `Connected`, `Receipt`, …). The enum is `#[non_exhaustive]`, so `match` blocks on `EventKind` must include a wildcard arm (`_ => …`); new kinds may be added in minor releases as the library tracks new server events. -- **`EventKind::CAPACITY`** is a public `u8` constant (currently `64`) that bounds the number of kinds. It exists because each discriminant is packed as a bit in `EventInterest`'s `u64` mask, and a future variant that would overflow it fails compilation rather than silently corrupting the mask at runtime. Treat it as a read-only ceiling — you don't need to check it at runtime. -- **`EventInterest`** is a 64-bit set of kinds. Build it with `EventInterest::of(&[…])`, `EventInterest::ALL` (the default), `EventInterest::none()`, or chain `.with(kind)`. Query it with `.wants(kind)`. +- **`EventKind::CAPACITY`** is a public `u8` constant (currently `128`) that bounds the number of kinds. It exists because each discriminant is packed as a bit in `EventInterest`'s `u128` mask, and a future variant that would overflow it fails compilation rather than silently corrupting the mask at runtime. Treat it as a read-only ceiling — you don't need to check it at runtime. +- **`EventInterest`** is a 128-bit set of kinds. Build it with `EventInterest::of(&[…])`, `EventInterest::ALL` (the default), `EventInterest::none()`, or chain `.with(kind)`. Query it with `.wants(kind)`. - The bus exposes `has_handler_for(kind)` and only produces an event when at least one registered handler wants its kind. + +`EventInterest` was widened from a `u64` to a `u128` mask (and `EventKind::CAPACITY` from `64` to `128`) as part of the pre-1.0 event-payload API freeze, since the kind count had reached 58/64. The public surface (`EventInterest::of`, `.with(kind)`, `.wants(kind)`, `EventInterest::ALL`) is unchanged — only the internal bit width doubled, giving headroom for future event kinds. + + With the [`Bot`](/api/bot) builder, the same narrowing is available via `on_event_for`: ```rust @@ -130,9 +134,9 @@ pub enum Event { TemporaryBan(TemporaryBan), // Pairing - PairingQrCode { code: String, timeout: Duration }, - PairingCode { code: String, timeout: Duration }, - PairingCodeRefresh { force_manual: bool }, + PairingQrCode(PairingQrCode), + PairingCode(PairingCode), + PairingCodeRefresh(PairingCodeRefresh), PairSuccess(PairSuccess), PairError(PairError), QrScannedWithoutMultidevice(QrScannedWithoutMultidevice), @@ -212,7 +216,7 @@ The `Event` enum is `#[non_exhaustive]`, so your `match` statements must include -**Payload stability:** payload structs are being sealed with `#[non_exhaustive]` plus a [`bon`](https://docs.rs/bon) builder for construction (`Type::builder()…build()`), so a payload can gain fields without breaking consumers — the seal is rolling out per struct. `ServerAck` was sealed first; the notification/presence/contact/group payloads (`SelfPushNameUpdated`, `ChatPresenceUpdate`, `PresenceUpdate`, `PictureUpdate`, `UserAboutUpdate`, `ContactUpdated`, `ContactNumberChanged`, `ContactSyncRequested`, `GroupUpdate`, `PushNameUpdate`) and every app-state-sync mutation payload (`ContactUpdate`, `PinUpdate`, `MuteUpdate`, `ArchiveUpdate`, `StarUpdate`, `MarkChatAsReadUpdate`, `DeleteChatUpdate`, `ClearChatUpdate`, `UserStatusMuteUpdate`, `DeleteMessageForMeUpdate`, `LabelEditUpdate`, `LabelAssociationUpdate`) followed. Payloads not yet sealed (message/newsletter events, pairing events) keep the pre-1.0 unsealed shape for now. Either way, read the fields you need (e.g. `ack.class`) or keep a `..` rest when destructuring, rather than binding every field. A maybe-absent field is always modeled as `Option` (with a `maybe_*` builder setter once sealed), never an empty-string or zero sentinel. +**Payload stability:** every event payload struct is sealed with `#[non_exhaustive]` plus a [`bon`](https://docs.rs/bon) builder for construction (`Type::builder()…build()`), so a payload can gain fields later without breaking consumers. The freeze rolled out in stages — `ServerAck` first, then the notification/presence/contact/group payloads and the app-state-sync mutation payloads, then the remaining message/newsletter/device/pairing payloads and the four unit-marker events (`Connected`, `ClientOutdated`, `QrScannedWithoutMultidevice`, `StreamReplaced`, each an empty sealed struct built as `Connected::builder().build()`) — and is now complete across the whole `Event` surface. Read the fields you need (e.g. `ack.class`) or keep a `..` rest when destructuring (required for a `#[non_exhaustive]` struct pattern from outside the defining crate — e.g. `InboundMessage { message, info, .. }`), rather than binding every field. A maybe-absent field is always modeled as `Option` (with a `maybe_*` builder setter), never an empty-string or zero sentinel. The library itself constructs every payload via its builder — a struct literal from outside `wacore`/`whatsapp-rust` no longer compiles (`E0639`). ## Connection Events @@ -222,10 +226,11 @@ The `Event` enum is `#[non_exhaustive]`, so your `match` statements must include **Emitted:** After successful connection and authentication ```rust -#[derive(Debug, Clone, Serialize)] -pub struct Connected; +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct Connected {} -Event::Connected(Connected) +Event::Connected(Connected::builder().build()) ``` **Usage:** @@ -241,12 +246,13 @@ Event::Connected(_) => { **Emitted:** When the connection ends without the client itself intentionally closing or reconnecting it — covers both a routine server-initiated stream recycle and a genuine transport failure (see `reason` below to tell them apart) ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct Disconnected { pub reason: DisconnectReason, } -Event::Disconnected(Disconnected { reason }) +Event::Disconnected(Disconnected::builder().reason(reason).build()) ``` **Fields:** @@ -255,7 +261,7 @@ Event::Disconnected(Disconnected { reason }) **Behavior:** Client automatically attempts reconnection -Breaking change: `Disconnected` gained the `reason` field (previously a unit struct). Update `Event::Disconnected(Disconnected)` patterns to `Event::Disconnected(Disconnected { reason })` or `Event::Disconnected(_)`. +Breaking change: `Disconnected` gained the `reason` field (previously a unit struct). `Disconnected` is now `#[non_exhaustive]` too, so a destructuring pattern needs a `..` rest: `Event::Disconnected(Disconnected { reason, .. })`, or just `Event::Disconnected(_)`. ### ConnectFailure @@ -263,10 +269,12 @@ Breaking change: `Disconnected` gained the `reason` field (previously a unit str **Emitted:** When connection fails with a specific reason ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct ConnectFailure { pub reason: ConnectFailureReason, - pub message: String, + /// The server's `message` attribute on the `` stanza, when present. + pub message: Option, pub raw: Option, } @@ -290,6 +298,10 @@ pub enum ConnectFailureReason { } ``` + +Breaking change: `ConnectFailure.message` changed from `String` (empty-string sentinel when the server omitted the `message` attribute) to `Option`, matching the "maybe-absent field is always `Option`" convention. `unwrap_or_default()` at a call site becomes `.unwrap_or_default()` on the `Option` (same fallback) or, better, `match`/`if let Some(msg) = &failure.message`. + + **Helper methods:** ```rust if reason.is_logged_out() { @@ -310,7 +322,8 @@ The 403 variant was renamed `MainDeviceGone` → `AccountLocked` in v0.6 to matc **Emitted:** When account is temporarily banned ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct TemporaryBan { pub code: TempBanReason, pub expire: chrono::Duration, @@ -338,6 +351,13 @@ Event::TemporaryBan(ban) => { **Emitted:** When another device connects with the same credentials (stream error code 409 or ``) +```rust +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct StreamReplaced {} +``` + +**Usage:** ```rust Event::StreamReplaced(_) => { println!("⚠️ Another instance connected - disconnecting"); @@ -352,7 +372,8 @@ Event::StreamReplaced(_) => { **Emitted:** When the session is invalidated by the server (stream error code 401 or 516) or when `client.logout()` is called ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct LoggedOut { pub on_connect: bool, pub reason: ConnectFailureReason, @@ -379,7 +400,8 @@ Event::LoggedOut(logout) => { **Emitted:** For unrecognized stream error codes (codes not matching 401, 409, 429, 503, 515, or 516) ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct StreamError { pub code: String, pub raw: Option, @@ -410,28 +432,43 @@ Recognized stream error codes emit specific events instead of `StreamError`: **Emitted:** For each QR code in rotation ```rust -Event::PairingQrCode { - code: String, // ASCII art QR or data string - timeout: Duration, // 60s first, 20s subsequent +/// A QR code the consumer renders during multi-device pairing. +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingQrCode { + /// The QR payload to render — ASCII art QR or data string. + pub code: String, + /// How long this code stays valid before the next one rotates in (60s first, 20s subsequent). + pub timeout: std::time::Duration, } ``` **Example:** ```rust -Event::PairingQrCode { code, timeout } => { +Event::PairingQrCode(PairingQrCode { code, timeout, .. }) => { println!("Scan this QR (valid {}s):", timeout.as_secs()); println!("{}", code); } ``` + +Breaking change: `PairingQrCode` moved from inline fields directly on the `Event::PairingQrCode { code, timeout }` variant to a dedicated sealed struct — `Event::PairingQrCode(PairingQrCode)`. Update destructuring patterns to match through the newtype, with a `..` rest since the inner struct is `#[non_exhaustive]`. + + ### PairingCode **Emitted:** When pair code is generated ```rust -Event::PairingCode { - code: String, // 8-character code - timeout: Duration, // ~180 seconds, remaining at time of emission +/// Generated pair code for phone number linking. +/// User should enter this code on their phone in WhatsApp > Linked Devices. +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingCode { + /// The 8-character pairing code to display. + pub code: String, + /// Approximate validity duration (~180 seconds). + pub timeout: std::time::Duration, } ``` @@ -441,36 +478,52 @@ Event::PairingCode { **Example:** ```rust -Event::PairingCode { code, .. } => { +Event::PairingCode(PairingCode { code, .. }) => { println!("Enter {} on your phone", code); } ``` + +Breaking change: `PairingCode` moved from inline fields on `Event::PairingCode { code, timeout }` to a dedicated sealed struct — `Event::PairingCode(PairingCode)`. + + ### PairingCodeRefresh **Emitted:** When the server asks the companion to refresh an in-progress pair code (WA Web `refreshAltLinkingCode` / `forceManualRefresh`). Only fired while a pair-code flow is outstanding and the server's ref matches it — a `refresh_code` notification for a stale or unrelated flow is silently ignored. ```rust -Event::PairingCodeRefresh { - force_manual: bool, // true when the server requires an explicit re-request rather than an auto-rotation +/// The server asked the companion to refresh an in-progress phone-number +/// pairing code. The consumer should request a fresh code via +/// `pair_with_code`; the previous code is no longer guaranteed valid. +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct PairingCodeRefresh { + /// `true` when the server set `force_manual_refresh` — the code must be + /// re-requested explicitly rather than auto-rotated. + pub force_manual: bool, } ``` **Example:** ```rust -Event::PairingCodeRefresh { force_manual } => { +Event::PairingCodeRefresh(PairingCodeRefresh { force_manual, .. }) => { // The previous code is no longer guaranteed valid — request a new one, // e.g. by calling `client.pair_with_code(options)` again. println!("Server requested a pair-code refresh (force_manual={force_manual})"); } ``` + +Breaking change: `PairingCodeRefresh` moved from an inline `Event::PairingCodeRefresh { force_manual }` field to a dedicated sealed struct — `Event::PairingCodeRefresh(PairingCodeRefresh)`. A `matches!` check on the field becomes `matches!(event, Event::PairingCodeRefresh(r) if r.force_manual)`. + + ### PairSuccess **Emitted:** When pairing completes successfully ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairSuccess { pub id: Jid, pub lid: Jid, @@ -493,7 +546,8 @@ Event::PairSuccess(info) => { **Emitted:** When pairing fails ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairError { pub id: Jid, pub lid: Jid, @@ -508,7 +562,8 @@ pub struct PairError { **Emitted:** During [passkey (SHORTCAKE_PASSKEY) linking](/concepts/authentication#passkey-linking-shortcake_passkey), when the server asks for a WebAuthn assertion to gate the link ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairPasskeyRequest { pub request_options_json: String, // verbatim PublicKeyCredentialRequestOptions JSON } @@ -529,7 +584,8 @@ Event::PairPasskeyRequest(req) => { **Emitted:** When the passkey link reaches the verification stage ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairPasskeyConfirmation { pub code: String, // 8-char "XXXX-XXXX" verification code pub skip_handoff_ux: bool, // true on a proven re-link: continuity means the code need not be shown @@ -552,7 +608,8 @@ Event::PairPasskeyConfirmation(conf) => { **Emitted:** When a passkey link attempt fails ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct PairPasskeyError { pub error: String, pub continuation: bool, // false = failed during the initial request, true = during continuation/verification @@ -564,8 +621,9 @@ pub struct PairPasskeyError { **Emitted:** When a QR code is scanned by a device that does not support multi-device ```rust -#[derive(Debug, Clone, Serialize)] -pub struct QrScannedWithoutMultidevice; +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct QrScannedWithoutMultidevice {} ``` **Usage:** @@ -581,8 +639,9 @@ Event::QrScannedWithoutMultidevice(_) => { **Emitted:** When the server rejects the connection because the client version is too old (connect failure code 405) ```rust -#[derive(Debug, Clone, Serialize)] -pub struct ClientOutdated; +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] +pub struct ClientOutdated {} ``` **Usage:** @@ -604,6 +663,8 @@ Event::ClientOutdated(_) => { ```rust Event::Messages(MessageBatch) +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct InboundMessage { pub message: Arc, pub info: Arc, @@ -614,6 +675,8 @@ pub enum BatchOrigin { OfflineDrain, // accumulated batch from the offline drain } +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct MessageBatch { pub messages: Arc<[InboundMessage]>, pub origin: BatchOrigin, @@ -624,6 +687,10 @@ pub struct MessageBatch { Live traffic dispatches a batch of one, so per-message latency is unchanged from the previous single-message event. During the offline drain the client accumulates decrypted messages and dispatches one `Event::Messages` per durable commit (size/byte/timeout triggers, matching WhatsApp Web's `MessageProcessorCache` — see [Inbound Durability](/advanced/inbound-durability#batching)). `MessageBatch` behaves as a collection: `batch.iter()`, `batch.len()`, `batch.is_empty()`, `batch.first()`, and `for msg in &batch` all work directly. `Event::as_messages()` returns `Option<&MessageBatch>`, and `Event::messages()` returns an iterator over the batch's `InboundMessage`s (empty for any other event kind) — use it to scan a mixed event stream without matching on `Event::Messages` first. + +Breaking change: `InboundMessage` and `MessageBatch` are now `#[non_exhaustive]`, sealed with a `bon` builder. A `for InboundMessage { message, info } in batch.iter()` destructuring pattern needs a `..` rest: `for InboundMessage { message, info, .. } in batch.iter()`. + + Both the message body and `MessageInfo` are `Arc`-wrapped inside `InboundMessage`. The same `Arc` slice handed to a registered [durability hook](/advanced/inbound-durability) is what this event carries — no deep clone, and a consumer never sees a message the hook did not commit (newsletter messages and PDO placeholder recoveries are the two exceptions: they dispatch event-only, bypassing the hook). Before v0.6 the body was `Box`; the public guarantee changed from "owned, freely mutable" to "shared, immutable read access" — call `Arc::make_mut` (or clone the inner `wa::Message`) only if you genuinely need to mutate. @@ -766,7 +833,7 @@ pub struct DeviceSentMeta { use waproto::whatsapp as wa; Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { println!("From: {} in {}", info.source.sender, info.source.chat); // Text message @@ -805,7 +872,8 @@ Event::Messages(batch) => { **Emitted:** For delivery/read/played receipts ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct Receipt { pub source: MessageSource, pub message_ids: Vec, @@ -864,7 +932,8 @@ Event::Receipt(receipt) => { When `is_unavailable` is `true`, the message had no encrypted content in the stanza. For `UnavailableType::Unknown`, the client sends a PDO request to your primary phone, and if the phone responds successfully, a follow-up `Event::Messages` is dispatched with the recovered content (event-only — a PDO recovery bypasses the durability hook and the offline-drain batcher, dispatching immediately with `BatchOrigin::Live`). For `ViewOnce`, `Hosted`, and `Bot`, no PDO request is sent — that content is unrecoverable by design, so no follow-up `Event::Messages` should be expected. ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct UndecryptableMessage { pub info: Arc, pub is_unavailable: bool, @@ -1877,7 +1946,8 @@ pub const MAX_DECOMPRESSED: u64 = 64 * 1024 * 1024; **Emitted:** Preview of pending offline sync data when reconnecting ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct OfflineSyncPreview { pub total: i32, pub app_data_changes: i32, @@ -1900,7 +1970,8 @@ Event::OfflineSyncPreview(preview) => { **Emitted:** When offline sync completes after reconnection ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct OfflineSyncCompleted { pub count: i32, } @@ -1926,7 +1997,8 @@ If the server does not complete offline sync within 60 seconds, the client force **Emitted:** When a user's device list changes (a companion device is added, removed, or updated) ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct DeviceListUpdate { pub user: Jid, pub lid_user: Option, @@ -1943,7 +2015,8 @@ pub enum DeviceListUpdateType { Update, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct DeviceNotificationInfo { pub device_id: u32, pub key_index: Option, @@ -1968,7 +2041,8 @@ This event is dispatched after the client has already patched its internal devic **Emitted:** When a contact reinstalls WhatsApp (their identity key changed). The event fires from two paths: an explicit server `` notification, or a locally-detected change discovered while decrypting an incoming message. The `implicit` field distinguishes them. ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct IdentityChange { /// The user whose identity changed pub user: Jid, @@ -2041,7 +2115,8 @@ Event::IdentityChange(change) => { **Emitted:** When a business account status changes ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct BusinessStatusUpdate { pub jid: Jid, pub update_type: BusinessUpdateType, @@ -2073,19 +2148,22 @@ pub enum BusinessUpdateType { **Emitted:** When reaction counts change or messages are updated on a newsletter you're subscribed to (via `subscribe_live_updates`). ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct NewsletterLiveUpdate { pub newsletter_jid: Jid, pub messages: Vec, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct NewsletterLiveUpdateMessage { pub server_id: u64, pub reactions: Vec, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct NewsletterLiveUpdateReaction { pub code: String, pub count: u64, @@ -2215,7 +2293,8 @@ Event::IncomingCall(call) => match &call.action { **Emitted:** When a contact changes their default disappearing messages setting. Sent by the server as a `` stanza. ```rust -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, bon::Builder)] +#[non_exhaustive] pub struct DisappearingModeChanged { pub from: Jid, pub duration: u32, @@ -2286,7 +2365,7 @@ let mut bot = Bot::builder() match &*event { Event::Messages(batch) => { // Handle each message in the batch - for InboundMessage { message: msg, info } in batch.iter() { /* … */ } + for InboundMessage { message: msg, info, .. } in batch.iter() { /* … */ } } Event::Connected(_) => { // Handle connection @@ -2317,7 +2396,7 @@ Struct-based handlers registered with [`with_event_handler`](/api/bot#with_event struct MessageHandler; impl EventHandler for MessageHandler { fn handle_event(&self, event: Arc) { - for InboundMessage { message: msg, info } in event.messages() { + for InboundMessage { message: msg, info, .. } in event.messages() { // Handle messages } } @@ -2466,7 +2545,7 @@ Combined with `LazyHistorySync`'s `OnceLock`, all handlers sharing the same `Arc ```rust .on_event(|event, client| async move { // Only handle events you care about - for InboundMessage { message: msg, info } in event.messages() { + for InboundMessage { message: msg, info, .. } in event.messages() { // These are independent filters, not mutually exclusive cases — // a non-self-sent group message enters both blocks. if info.source.is_group { @@ -2489,7 +2568,7 @@ Combined with `LazyHistorySync`'s `OnceLock`, all handlers sharing the same `Arc }) async fn handle_event(event: &Event, client: Arc) -> Result<()> { - for InboundMessage { message: msg, info } in event.messages() { + for InboundMessage { message: msg, info, .. } in event.messages() { process_message(msg, info, client.clone()).await? } Ok(()) @@ -2505,7 +2584,7 @@ async fn handle_event(event: &Event, client: Arc) -> Result<()> { let event = event.clone(); // Arc clone — O(1) tokio::spawn(async move { - for InboundMessage { message: msg, info } in event.messages() { + for InboundMessage { message: msg, info, .. } in event.messages() { process_message(msg, info, &client).await; } }); diff --git a/guides/communities.mdx b/guides/communities.mdx index 14701a5a..c2c30d4d 100644 --- a/guides/communities.mdx +++ b/guides/communities.mdx @@ -311,7 +311,7 @@ Incoming encrypted comments are decrypted transparently on the receive path. The ```rust Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { if let Some(parent_key) = &info.comment_target { // This is a channel comment. println!("Comment on post: {:?}", parent_key.id); diff --git a/guides/media-handling.mdx b/guides/media-handling.mdx index 2e68c38c..89be1c1a 100644 --- a/guides/media-handling.mdx +++ b/guides/media-handling.mdx @@ -20,7 +20,7 @@ use wacore::types::events::InboundMessage; match event { Event::Messages(batch) => { - for InboundMessage { message, info } in batch.iter() { + for InboundMessage { message, info, .. } in batch.iter() { // Image if let Some(img) = message.image_message.as_option() { let data = client.download(img).await?; diff --git a/guides/receiving-messages.mdx b/guides/receiving-messages.mdx index 5ef71825..f03bb154 100644 --- a/guides/receiving-messages.mdx +++ b/guides/receiving-messages.mdx @@ -79,7 +79,7 @@ pub enum Event { LoggedOut(LoggedOut), /// Pairing QR code for scanning - PairingQrCode { code: String, timeout: Duration }, + PairingQrCode(PairingQrCode), /// Receipt (delivery, read, played) Receipt(Receipt), @@ -187,7 +187,7 @@ See [WAProto API reference](/api/waproto) for the full message type hierarchy. ```rust match event { Event::Messages(batch) => { - for InboundMessage { message, info } in batch.iter() { + for InboundMessage { message, info, .. } in batch.iter() { // Simple text if let Some(text) = &message.conversation { println!("Text: {}", text); @@ -270,7 +270,7 @@ Encrypted channel comments from Community Announcement Groups are decrypted tran ```rust Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { if let Some(parent_key) = &info.comment_target { // This item is a decrypted CAG channel comment. println!("Comment on post: {:?}", parent_key.id); @@ -315,7 +315,7 @@ When you send a message from one device, other devices receive it as a `DeviceSe // The library handles this automatically - you receive the inner message directly match event { Event::Messages(batch) => { - for InboundMessage { message, info } in batch.iter() { + for InboundMessage { message, info, .. } in batch.iter() { // If this was originally a DeviceSentMessage, the library has: // 1. Extracted the inner message content // 2. Merged message_context_info from outer + inner @@ -716,7 +716,7 @@ See [Signal Protocol](/advanced/signal-protocol) for more on session management. match &*event { Event::Messages(batch) => { // Process each message in the batch - for InboundMessage { message, info } in batch.iter() { + for InboundMessage { message, info, .. } in batch.iter() { if let Err(e) = process_message(message, info, client.clone()).await { eprintln!("Error processing message {}: {:?}", info.id, e); } @@ -794,7 +794,7 @@ Spawn tasks for long-running operations: let client = client.clone(); let event = event.clone(); tokio::spawn(async move { - for InboundMessage { message, info } in event.messages() { + for InboundMessage { message, info, .. } in event.messages() { process_heavy_task(message, info, client.clone()).await; } }); diff --git a/guides/sending-messages.mdx b/guides/sending-messages.mdx index fe83fed2..37396a28 100644 --- a/guides/sending-messages.mdx +++ b/guides/sending-messages.mdx @@ -309,7 +309,7 @@ Incoming encrypted comments are decrypted transparently. The comment body is dis ```rust Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { if let Some(parent_key) = &info.comment_target { println!("Comment on post: {:?}", parent_key.id); if let Some(text) = msg.text_content() { diff --git a/pt/quickstart.mdx b/pt/quickstart.mdx index 291f3b39..59122982 100644 --- a/pt/quickstart.mdx +++ b/pt/quickstart.mdx @@ -16,7 +16,7 @@ use whatsapp_rust::TokioRuntime; use whatsapp_rust::store::SqliteStore; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; -use wacore::types::events::{Event, InboundMessage}; +use wacore::types::events::{Event, InboundMessage, PairingQrCode}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -31,11 +31,11 @@ async fn main() -> Result<(), Box> { .with_runtime(TokioRuntime) .on_event(|event, client| async move { match &*event { - Event::PairingQrCode { code, .. } => { + Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("Scan this QR code with WhatsApp:\n{}", code); } Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { println!("Message from {}: {:?}", info.source.sender, msg); } } @@ -87,11 +87,11 @@ async fn main() -> Result<(), Box> { ```rust .on_event(|event, client| async move { match &*event { - Event::PairingQrCode { code, .. } => { + Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("QR Code:\n{}", code); } Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { // Trate a mensagem recebida } } @@ -135,11 +135,11 @@ use waproto::whatsapp as wa; .on_event(|event, client| async move { match &*event { - Event::PairingQrCode { code, .. } => { + Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("QR Code:\n{}", code); } Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { // Verifica se a mensagem é um texto dizendo "ping" if let Some(text) = msg.text_content() { if text == "ping" { @@ -176,7 +176,7 @@ use waproto::whatsapp as wa; O bot gera automaticamente códigos QR quando não está autenticado. Escaneie com seu celular para vincular: ```rust -Event::PairingQrCode { code, .. } => { +Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("Scan this QR code:\n{}", code); } ``` @@ -187,6 +187,7 @@ Alternativamente, vincule usando um número de telefone e um código de 8 dígit ```rust use whatsapp_rust::pair_code::PairCodeOptions; +use wacore::types::events::{Event, PairingCode}; let mut bot = Bot::builder() .with_backend(backend) @@ -199,7 +200,7 @@ let mut bot = Bot::builder() }) .on_event(|event, client| async move { match &*event { - Event::PairingCode { code, .. } => { + Event::PairingCode(PairingCode { code, .. }) => { println!("Enter this code on your phone: {}", code); } _ => {} @@ -353,7 +354,7 @@ use chrono::{Local, Utc}; use log::{error, info}; use std::sync::Arc; use wacore::proto_helpers::MessageExt; -use wacore::types::events::{Event, InboundMessage}; +use wacore::types::events::{Event, InboundMessage, PairingQrCode}; use waproto::whatsapp as wa; use whatsapp_rust::bot::{Bot, MessageContext}; use whatsapp_rust::TokioRuntime; @@ -390,11 +391,11 @@ async fn main() -> Result<(), Box> { .with_runtime(TokioRuntime) .on_event(|event, client| async move { match &*event { - Event::PairingQrCode { code, .. } => { + Event::PairingQrCode(PairingQrCode { code, .. }) => { println!("\n{}", code); } Event::Messages(batch) => { - for InboundMessage { message: msg, info } in batch.iter() { + for InboundMessage { message: msg, info, .. } in batch.iter() { let ctx = MessageContext::from_parts(msg, info, client.clone()); handle_message(&ctx).await; }