Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions advanced/inbound-durability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<wa::Message>,
pub info: Arc<MessageInfo>,
Expand Down
12 changes: 6 additions & 6 deletions api/bot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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());
}
Expand Down Expand Up @@ -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;
Expand All @@ -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)),
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1618,7 +1618,7 @@ impl EventHandler for MyHandler {
fn handle_event(&self, event: Arc<Event>) {
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);
}
}
Expand Down
5 changes: 5 additions & 0 deletions api/receipt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand All @@ -390,6 +391,10 @@ pub struct Receipt {
}
```

<Note>
`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.
</Note>

<ParamField path="source" type="MessageSource">
Source information:
- `chat` - Chat JID where the receipt originated
Expand Down
99 changes: 68 additions & 31 deletions concepts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
Expand All @@ -97,7 +97,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_runtime(TokioRuntime)
.on_event(|event, _client| async move {
match &*event {
Event::PairingQrCode { code, timeout } => {
Event::PairingQrCode(PairingQrCode { code, timeout, .. }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The updated best-practices event-handling snippet introduces three unqualified payload type names (PairingQrCode, PairingCode, PairingCodeRefresh) that now must be in scope for the pattern to compile. Before the API freeze, matching on inline enum-variant fields only required Event to be imported. Consider adding an use line (e.g., use wacore::types::events::{Event, PairingQrCode, PairingCode, PairingCodeRefresh};) above the snippet or qualifying the types, so the example remains copy-pasteable for readers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At concepts/authentication.mdx, line 100:

<comment>The updated best-practices event-handling snippet introduces three unqualified payload type names (`PairingQrCode`, `PairingCode`, `PairingCodeRefresh`) that now must be in scope for the pattern to compile. Before the API freeze, matching on inline enum-variant fields only required `Event` to be imported. Consider adding an `use` line (e.g., `use wacore::types::events::{Event, PairingQrCode, PairingCode, PairingCodeRefresh};`) above the snippet or qualifying the types, so the example remains copy-pasteable for readers.</comment>

<file context>
@@ -97,7 +97,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         .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);
</file context>

println!("Scan this QR code (valid for {}s):", timeout.as_secs());
println!("{}", code);
}
Expand All @@ -117,16 +117,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

### 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)
}
```

<Note>
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.
</Note>

**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.
Expand All @@ -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();
Expand Down Expand Up @@ -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) {
Expand All @@ -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}");

Expand Down Expand Up @@ -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
}
```

Expand All @@ -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(),
));
```

<Note>
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()`.
</Note>

### 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
}
```

Expand Down Expand Up @@ -642,6 +660,10 @@ Event::PairPasskeyError(PairPasskeyError {

Linking completes through the ordinary [`PairSuccess`/`PairError`](#success-events) events — there is no separate "passkey success" event.

<Note>
`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.
</Note>

### Client methods

| Method | Purpose |
Expand Down Expand Up @@ -873,21 +895,30 @@ 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")
pub business_name: String, // Push name / business name
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,
Expand All @@ -896,9 +927,13 @@ pub struct PairError {
pub error: String, // Error description
}

Event::PairError(PairError { /* ... */ })
Event::PairError(PairError::builder() /* .id(..).lid(..)… */ .build())
```

<Note>
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.
</Note>

## Error Handling

### QR code errors
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Loading