diff --git a/changelog/2026-06-11-bot-api-overhaul.mdx b/changelog/2026-06-11-bot-api-overhaul.mdx new file mode 100644 index 00000000..557aef2c --- /dev/null +++ b/changelog/2026-06-11-bot-api-overhaul.mdx @@ -0,0 +1,172 @@ +--- +title: "June 11, 2026 — Bot API overhaul (breaking)" +description: "Lifecycle simplification, typed event registrars, single-dependency consumption, and messaging sugar ahead of 1.0." +--- + +PR [#852](https://github.com/oxidezap/whatsapp-rust/pull/852) is a focused breaking-change pass over the public bot API while such changes are still cheap. You will need to migrate your code, but the migration is mechanical. See the [breaking changes](#breaking-changes) section below. + +## New features + +### One dependency is enough + +`whatsapp-rust` now re-exports `wacore`, `wacore_binary`, and `waproto` wholesale. A consumer needs exactly one `Cargo.toml` line: + +```toml +[dependencies] +whatsapp-rust = "0.6" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + +All sub-crate paths (`whatsapp_rust::wacore::...`, `whatsapp_rust::waproto::whatsapp`, etc.) are reachable through the main crate, including when pinning a git revision. + +`UreqHttpClient` moved from the misplaced `whatsapp_rust::transport` to `whatsapp_rust::http`. A new `whatsapp_rust::prelude` covers the common bot path in one import line (including `wa` as the protobuf alias): + +```rust +use whatsapp_rust::prelude::*; +``` + +### Simplified lifecycle + +`Bot::run(self)` now runs the bot on the current task with a **single** `await`. `Bot::spawn(self)` starts it in the background and returns a `BotHandle`: + +```rust +// Foreground — blocks until logout or disconnect +bot.run().await; + +// Background — returns immediately +let handle = bot.spawn(); +let client = handle.client(); // full Client API +handle.shutdown().await; // graceful: flushes state, then stops +handle.abort(); // escape hatch, skips flush +``` + +Awaiting a `BotHandle` resolves to `()` (was `Result<(), Canceled>`). Dropping the handle still aborts the task — the e2e harness relies on this. + +### Builder defaults + +With the default cargo features, transport, HTTP client, and runtime are pre-filled (Tokio WebSocket, ureq, Tokio), so only the backend has to be provided: + +```rust +let bot = Bot::builder() + .with_backend(SqliteStore::new("whatsapp.db").await?) + .build() + .await?; +``` + +`with_*` setters are now available in every typestate, so they also work as overrides. The typestate markers got names (`MissingBackend`, `MissingTransport`, `MissingHttpClient`, `MissingRuntime`) — a missing-field compile error now names which field is missing. + +`with_backend` accepts `impl Backend + 'static` (no caller-side `Arc::new`). `with_backend_arc` accepts an already-shared `Arc`. + +### Typed event registrars + +Dedicated builder methods cover the common event path — no `match &*event` needed: + +```rust +Bot::builder() + .on_message(|ctx| async move { + // ctx: MessageContext — has reply, reply_quoting, react, send_message + }) + .on_qr_code(|code, timeout| async move { /* ... */ }) + .on_pair_code(|code, timeout| async move { /* ... */ }) + .on_connected(|client| async move { /* ... */ }) + .on_logged_out(|info| async move { /* ... */ }) +``` + +`on_event` / `on_event_for` are unchanged as catch-alls. `with_event_handler` registers a struct-based `EventHandler` directly on the bus for stateful handlers (eliminates the clone-dance that closure captures force on consumers). + +All handlers **accumulate** — registering a second handler now runs both instead of silently replacing the first. + +The bus still skips materializing events nobody wants: the adapter registers the union of all handler interests and filters per handler at dispatch. + +### Messaging sugar + +New helpers for the common send paths: + +```rust +// On MessageContext (inside on_message) +ctx.reply("pong").await?; +ctx.reply_quoting("pong").await?; + +// On Client +client.send_text(&jid, "hello").await?; + +// Static constructors on wa::Message (via MessageBuilderExt) +use whatsapp_rust::prelude::*; +let msg = wa::Message::text("hello"); +let msg_with_quote = wa::Message::text_with_context("hello", ctx.build_quote_context()); +``` + +The raw `ctx.send_message(wa::Message { ... })` path is unchanged for advanced cases. + +### `BotBuilderError` typed + +`BotBuilderError::Other(anyhow)` is gone. The only variant is now `Store(StoreError)` — the one error `build()` can actually produce. + +## Breaking changes + +| Old | New | +|---|---| +| `bot.run().await?.await?` | `bot.run().await` (foreground) or `let handle = bot.spawn()` (background) | +| `.with_backend(Arc::new(store))` | `.with_backend(store)` — `Arc` wrapping is internal | +| `.with_backend(arc)` for a shared Arc | `.with_backend_arc(arc)` | +| `use whatsapp_rust::transport::UreqHttpClient` | `use whatsapp_rust::http::UreqHttpClient` | +| `bot::Missing` typestate marker | `bot::MissingBackend` / `MissingTransport` / `MissingHttpClient` / `MissingRuntime` | +| `BotBuilderError::Other(anyhow)` | removed; only `BotBuilderError::Store(StoreError)` | +| Registering two handlers — second replaces first | Both run; handlers accumulate | + +### Migration guide + +**Lifecycle:** +```rust +// Before +let mut bot = builder.build().await?; +let handle = bot.run().await?; +handle.await?; + +// After (foreground) +let bot = builder.build().await?; +bot.run().await; + +// After (background / ctrl-c handling) +let bot = builder.build().await?; +let handle = bot.spawn(); +tokio::signal::ctrl_c().await?; +handle.shutdown().await; +``` + +**Backend:** +```rust +// Before +let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); +Bot::builder().with_backend(backend) + +// After +Bot::builder().with_backend(SqliteStore::new("whatsapp.db").await?) + +// After (already-shared Arc) +Bot::builder().with_backend_arc(existing_arc) +``` + +**`UreqHttpClient` import:** +```rust +// Before +use whatsapp_rust::transport::UreqHttpClient; + +// After +use whatsapp_rust::http::UreqHttpClient; +``` + +**Builder dependencies — drop sibling crates from `Cargo.toml`:** +```toml +# Before (all required) +whatsapp-rust = "0.6" +whatsapp-rust-sqlite-storage = "0.6" +whatsapp-rust-tokio-transport = "0.6" +whatsapp-rust-ureq-http-client = "0.6" +wacore = "0.6" +waproto = "0.6" + +# After (one line is enough) +whatsapp-rust = "0.6" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } +``` diff --git a/docs.json b/docs.json index a5cda70b..b19459bb 100644 --- a/docs.json +++ b/docs.json @@ -138,6 +138,7 @@ "group": "Changelog", "pages": [ "changelog/overview", + "changelog/2026-06-11-bot-api-overhaul", "changelog/2026-06-11-dockerfile-share-generics", "changelog/2026-06-11-pdo-once-per-message", "changelog/2026-06-11-history-sync-secret-prescan", diff --git a/guides/receiving-messages.mdx b/guides/receiving-messages.mdx index fcb15e45..ac26042a 100644 --- a/guides/receiving-messages.mdx +++ b/guides/receiving-messages.mdx @@ -11,32 +11,51 @@ This guide covers message event handling, decryption, and receipt management in ### Subscribing to Events -Use the Bot API to handle events: +Use the typed registrars on `BotBuilder` — they extract the relevant payload before calling your handler, so you never pattern-match on `Arc` for the common cases: ```rust -use whatsapp_rust::bot::Bot; -use wacore::types::events::Event; +use whatsapp_rust::prelude::*; let bot = Bot::builder() - .with_backend(backend) - .with_transport_factory(transport_factory) - .with_http_client(http_client) - .on_event(|event, client| async move { - match &*event { - Event::Message(message, info) => { - println!("📨 Message from: {}", info.source.sender); - println!("💬 Text: {:?}", message.text_content()); - } - Event::Connected(_) => { - println!("✅ Connected!"); - } - _ => {} - } + .with_backend(SqliteStore::new("whatsapp.db").await?) + .on_qr_code(|code, _timeout| async move { + println!("Scan to pair:\n{code}"); + }) + .on_message(|ctx| async move { + println!("📨 Message from: {}", ctx.info.source.sender); + println!("💬 Text: {:?}", ctx.message.text_content()); + }) + .on_connected(|_client| async { + println!("✅ Connected!"); + }) + .on_logged_out(|_info| async { + eprintln!("Logged out!"); }) .build() .await?; ``` +Available typed registrars: `on_message`, `on_qr_code`, `on_pair_code`, `on_connected`, `on_logged_out`. All handlers accumulate — registering a second one no longer silently replaces the first. + +For events without a typed registrar, use the catch-all `on_event` / `on_event_for`: + +```rust +.on_event(|event, client| async move { + match &*event { + Event::Receipt(receipt) => { + println!("Receipt for: {:?}", receipt.message_ids); + } + _ => {} + } +}) +``` + +For stateful handlers that hold shared state in `&self`, register a struct implementing `EventHandler` directly: + +```rust +.with_event_handler(my_stateful_handler) +``` + See [Bot API reference](/api/bot#event-handling) for full details. ### Available Events @@ -565,30 +584,25 @@ See [Client API reference](/api/client) for handler registration details. ### Filtering Messages -Use the type-safe JID methods (`is_group()`, `is_broadcast_list()`, `is_status_broadcast()`) to classify messages by chat type: +Use the type-safe JID methods (`is_group()`, `is_broadcast_list()`, `is_status_broadcast()`) to classify messages by chat type. With `on_message`, the `MessageContext` is already available: ```rust -use wacore_binary::jid::JidExt; +use whatsapp_rust::prelude::*; -.on_event(|event, _client| async move { - match &*event { - Event::Message(message, info) => { - // Ignore own messages - if info.source.is_from_me { - return; - } - - // Only handle group messages - if !info.source.chat.is_group() { - return; - } - - // Only handle text messages - if let Some(text) = message.text_content() { - println!("Group text: {}", text); - } - } - _ => {} +.on_message(|ctx| async move { + // Ignore own messages + if ctx.info.source.is_from_me { + return; + } + + // Only handle group messages + if !ctx.info.source.chat.is_group() { + return; + } + + // Only handle text messages + if let Some(text) = ctx.message.text_content() { + println!("Group text: {text}"); } }) ``` diff --git a/guides/sending-messages.mdx b/guides/sending-messages.mdx index 7510a1e5..747aaf68 100644 --- a/guides/sending-messages.mdx +++ b/guides/sending-messages.mdx @@ -7,11 +7,57 @@ description: Learn how to send text messages, reactions, channel comments, quote This guide covers sending messages, including text, reactions, channel comments, quotes, album messages (grouped media), sticker packs, and message editing operations using the whatsapp-rust library. +## Text message shortcuts + +For the common case of sending or replying with plain text, prefer the high-level helpers: + +### `MessageContext` reply helpers (inside event handlers) + +When you're already inside an `on_message` handler: + +```rust +.on_message(|ctx| async move { + // Reply without quoting + if let Err(e) = ctx.reply("pong").await { + eprintln!("Failed to reply: {e}"); + } + + // Reply quoting the received message + if let Err(e) = ctx.reply_quoting("pong").await { + eprintln!("Failed to reply: {e}"); + } +}) +``` + +### `Client::send_text` (from any `Client` reference) + +```rust +client.send_text(&chat_jid, "Hello from whatsapp-rust!").await?; +``` + +### `wa::Message::text` / `wa::Message::text_with_context` + +Build a text message without hand-assembling the protobuf struct: + +```rust +use whatsapp_rust::prelude::*; // re-exports MessageBuilderExt + +// Plain text — WA Web sends this as `conversation` +let msg = wa::Message::text("Hello!"); + +// Text with a quote/context — switches to `extendedTextMessage` internally +let msg_with_quote = wa::Message::text_with_context("Replying!", ctx.build_quote_context()); +``` + +These are equivalent to building `wa::Message { conversation: Some(...) }` and `wa::Message { extended_text_message: Some(...) }` by hand, but less verbose. + +--- + ## Sending text messages ### Simple text message -Use the `conversation` field for plain text messages: +Use the `conversation` field for plain text messages, or `wa::Message::text("...")` for shorter syntax: ```rust use waproto::whatsapp as wa; diff --git a/installation.mdx b/installation.mdx index 551626dc..abefa1d3 100644 --- a/installation.mdx +++ b/installation.mdx @@ -16,18 +16,41 @@ Before installing whatsapp-rust, ensure you have: ## Add to your project -Add whatsapp-rust and its required dependencies to your `Cargo.toml`: +`whatsapp-rust` re-exports the entire stack (`wacore`, `wacore_binary`, `waproto`, and all bundled implementations), so **one dependency line is enough** for most projects: + +```toml Cargo.toml +[dependencies] +whatsapp-rust = "0.6" +tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } +``` + +Every sub-crate path is reachable through the main crate: + +- `whatsapp_rust::waproto::whatsapp` (aliased as `wa` in `prelude`) +- `whatsapp_rust::wacore`, `whatsapp_rust::wacore_binary` +- `whatsapp_rust::store::SqliteStore`, `whatsapp_rust::http::UreqHttpClient`, `whatsapp_rust::transport::TokioWebSocketTransportFactory` + +The same holds for git consumers — no need to pin every sibling crate: + +```toml Cargo.toml +[dependencies] +whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "" } +tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } +``` + +If you need to declare a sibling crate explicitly (for example to enable a crate-specific feature flag), you can still add it individually. The full multi-crate form: ```toml Nightly (default) [dependencies] whatsapp-rust = "0.6" -whatsapp-rust-sqlite-storage = "0.6" -whatsapp-rust-tokio-transport = "0.6" -whatsapp-rust-ureq-http-client = "0.6" -wacore = "0.6" -waproto = "0.6" +# Only needed for crate-specific features not exposed through whatsapp-rust: +# whatsapp-rust-sqlite-storage = "0.6" +# whatsapp-rust-tokio-transport = "0.6" +# whatsapp-rust-ureq-http-client = "0.6" +# wacore = "0.6" +# waproto = "0.6" tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } ``` @@ -42,11 +65,7 @@ whatsapp-rust = { version = "0.6", default-features = false, features = [ "signal", "moka-cache", ] } -whatsapp-rust-sqlite-storage = "0.6" -whatsapp-rust-tokio-transport = "0.6" -whatsapp-rust-ureq-http-client = "0.6" wacore = { version = "0.6", default-features = false } -waproto = "0.6" tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } ``` diff --git a/quickstart.mdx b/quickstart.mdx index bb8a3e77..20719643 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -7,44 +7,38 @@ This guide will help you create a simple WhatsApp bot that responds to messages. ## Basic example +Add the dependencies — one crate is enough; `whatsapp-rust` re-exports the entire stack: + +```toml Cargo.toml +[dependencies] +whatsapp-rust = "0.6" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + Here's a minimal bot that responds to "ping" messages: ```rust src/main.rs -use std::sync::Arc; -use whatsapp_rust::bot::Bot; -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 whatsapp_rust::prelude::*; #[tokio::main] async fn main() -> Result<(), Box> { - // Initialize storage backend - let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); - - // Build the bot - let mut bot = Bot::builder() - .with_backend(backend) - .with_transport_factory(TokioWebSocketTransportFactory::new()) - .with_http_client(UreqHttpClient::new()) - .with_runtime(TokioRuntime) - .on_event(|event, client| async move { - match &*event { - Event::PairingQrCode { code, .. } => { - println!("Scan this QR code with WhatsApp:\n{}", code); - } - Event::Message(msg, info) => { - println!("Message from {}: {:?}", info.source.sender, msg); + let bot = Bot::builder() + .with_backend(SqliteStore::new("whatsapp.db").await?) + .on_qr_code(|code, _timeout| async move { + println!("Scan this QR code with WhatsApp:\n{code}"); + }) + .on_message(|ctx| async move { + if ctx.message.text_content() == Some("ping") { + if let Err(e) = ctx.reply("pong").await { + eprintln!("Failed to reply: {e}"); } - _ => {} } }) .build() .await?; - // Start the bot - bot.run().await?.await?; + // Runs until logout or client.disconnect() from another task. + bot.run().await; Ok(()) } ``` @@ -57,51 +51,44 @@ async fn main() -> Result<(), Box> { The bot needs persistent storage for session data, keys, and state: ```rust - let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); + let backend = SqliteStore::new("whatsapp.db").await?; ``` This creates a SQLite database file named `whatsapp.db` in your current directory. The session will persist across restarts. - The `Bot::builder()` pattern lets you configure all required components: + The `Bot::builder()` pattern lets you configure all components: ```rust - let mut bot = Bot::builder() + let bot = Bot::builder() .with_backend(backend) - .with_transport_factory(TokioWebSocketTransportFactory::new()) - .with_http_client(UreqHttpClient::new()) - .with_runtime(TokioRuntime) ``` - All four components (backend, transport, HTTP client, runtime) are required. The builder uses a typestate pattern — your code won't compile if any are missing. + With the default cargo features, only the backend is required. The Tokio WebSocket transport, ureq HTTP client, and Tokio runtime are pre-wired. Use `with_transport_factory`, `with_http_client`, and `with_runtime` to override any of them (for example when targeting WASM or a custom transport). - - Use `.on_event()` to handle incoming events from WhatsApp: + + Typed registrars cover the common cases — no `match &*event` needed: ```rust - .on_event(|event, client| async move { - match &*event { - Event::PairingQrCode { code, .. } => { - println!("QR Code:\n{}", code); - } - Event::Message(msg, info) => { - // Handle incoming message - } - Event::Connected(_) => { - println!("Connected successfully!"); - } - _ => {} - } + .on_qr_code(|code, _timeout| async move { + println!("QR Code:\n{code}"); + }) + .on_message(|ctx| async move { + // ctx.message, ctx.info, ctx.reply(...), ctx.react(...) + println!("From {}: {:?}", ctx.info.source.sender, ctx.message.text_content()); + }) + .on_connected(|_client| async { + println!("Connected successfully!"); }) ``` - The event handler receives two parameters: - - `event`: An `Arc` — use `&*event` or `event.as_ref()` to pattern-match on the inner event type - - `client`: An `Arc` you can use to send messages or call API methods + Available typed registrars: `on_message`, `on_qr_code`, `on_pair_code`, `on_connected`, `on_logged_out`. Use `on_event` or `on_event_for` as catch-alls for events without a typed registrar. Multiple handlers of any kind accumulate — registering a second one no longer silently replaces the first. + + `on_message` delivers a ready [`MessageContext`](/api/bot#messagecontext) with `ctx.reply(text)`, `ctx.reply_quoting(text)`, `ctx.react(emoji)`, and `ctx.send_message(msg)` helpers. @@ -111,68 +98,81 @@ async fn main() -> Result<(), Box> { .build() .await?; - bot.run().await?.await?; + bot.run().await; ``` - The double `.await?` is intentional: - - First `.await?` starts the bot and returns a `BotHandle` - - Second `.await?` waits for the bot to finish running + `bot.run().await` runs the bot on the current task until it disconnects or logs out. To run in the background instead, use `bot.spawn()`, which returns a [`BotHandle`](/api/bot) with `client()`, `shutdown()`, and `abort()`. ## Responding to messages -Let's extend the bot to respond to "ping" with "pong": +Let's extend the bot to respond to "ping" with "pong" using the `on_message` helper and `ctx.reply`: ```rust -use wacore::proto_helpers::MessageExt; -use waproto::whatsapp as wa; +use whatsapp_rust::prelude::*; -.on_event(|event, client| async move { - match &*event { - Event::PairingQrCode { code, .. } => { - println!("QR Code:\n{}", code); - } - Event::Message(msg, info) => { - // Check if message is a text message saying "ping" - if let Some(text) = msg.text_content() { - if text == "ping" { - // Create reply message - let reply = wa::Message { - conversation: Some("pong".to_string()), - ..Default::default() - }; - - // Send the reply - if let Err(e) = client.send_message(info.source.chat.clone(), reply).await { - eprintln!("Failed to send reply: {}", e); - } - } - } +.on_qr_code(|code, _timeout| async move { + println!("QR Code:\n{code}"); +}) +.on_message(|ctx| async move { + if ctx.message.text_content() == Some("ping") { + if let Err(e) = ctx.reply("pong").await { + eprintln!("Failed to send reply: {e}"); } - _ => {} } }) ``` ### Key methods -- `msg.text_content()` - Extract text from any message type (conversation, extended text, etc.) -- `client.send_message()` - Send a message to a chat -- `info.source.chat` - The JID (identifier) of the chat where the message came from -- `info.source.sender` - The JID of the user who sent the message +- `msg.text_content()` — Extract text from any message type (conversation, extended text, etc.) +- `ctx.reply(text)` — Send a plain-text reply in the same chat without quoting +- `ctx.reply_quoting(text)` — Send a plain-text reply that quotes the received message +- `ctx.react(emoji)` — React to the received message +- `ctx.send_message(msg)` — Send an arbitrary `wa::Message` to the source chat +- `client.send_text(jid, text)` — Send a plain-text message from a `Client` reference +- `info.source.chat` — The JID (identifier) of the chat where the message came from +- `info.source.sender` — The JID of the user who sent the message + +For raw protobuf access (advanced cases), build a `wa::Message` directly: + +```rust +use whatsapp_rust::prelude::*; + +.on_message(|ctx| async move { + if ctx.message.text_content() == Some("ping") { + let reply = wa::Message { + conversation: Some("pong".to_string()), + ..Default::default() + }; + if let Err(e) = ctx.send_message(reply).await { + eprintln!("Failed to send reply: {e}"); + } + } +}) +``` + +Or with the `MessageBuilderExt` helpers: + +```rust +use whatsapp_rust::prelude::*; // re-exports MessageBuilderExt + +let msg = wa::Message::text("pong"); +let msg_with_quote = wa::Message::text_with_context("pong", ctx.build_quote_context()); +``` ## Authentication methods ### QR code pairing (default) -The bot automatically generates QR codes when not authenticated. Scan with your phone to link: +The bot automatically generates QR codes when not authenticated. Display them with `on_qr_code`: ```rust -Event::PairingQrCode { code, .. } => { - println!("Scan this QR code:\n{}", code); -} +.on_qr_code(|code, timeout| async move { + println!("Scan this QR code (valid {}s):\n{code}", timeout.as_secs()); +}) ``` ### Pair code (phone number) @@ -180,24 +180,17 @@ Event::PairingQrCode { code, .. } => { Alternatively, link using a phone number and 8-digit code: ```rust +use whatsapp_rust::prelude::*; use whatsapp_rust::pair_code::PairCodeOptions; -let mut bot = Bot::builder() - .with_backend(backend) - .with_transport_factory(TokioWebSocketTransportFactory::new()) - .with_http_client(UreqHttpClient::new()) - .with_runtime(TokioRuntime) +let bot = Bot::builder() + .with_backend(SqliteStore::new("whatsapp.db").await?) .with_pair_code(PairCodeOptions { phone_number: "15551234567".to_string(), ..Default::default() }) - .on_event(|event, client| async move { - match &*event { - Event::PairingCode { code, .. } => { - println!("Enter this code on your phone: {}", code); - } - _ => {} - } + .on_pair_code(|code, _timeout| async move { + println!("Enter this code on your phone: {code}"); }) .build() .await?; @@ -279,36 +272,30 @@ The demo bot responds to `🦀ping` with a quoted `🏓 Pong!` reply, edits the ## Using MessageContext -For cleaner message handling, use `MessageContext` to wrap the message, metadata, and client together. This provides convenience methods like `send_message` (auto-targets the source chat), `build_quote_context`, `edit_message`, and `revoke_message`: +`on_message` delivers a ready `MessageContext`, so you no longer need to extract it from `Arc` manually. Use typed handler functions for cleaner separation: ```rust -use whatsapp_rust::bot::{Bot, MessageContext}; +use whatsapp_rust::prelude::*; -.on_event(|event, client| async move { - if let Some(ctx) = MessageContext::from_event(&event, client) { - handle_message(&ctx).await; - } +.on_message(|ctx| async move { + handle_message(ctx).await; }) ``` Then define focused handler functions: ```rust -async fn handle_message(ctx: &MessageContext) { - if let Some(text) = ctx.message.text_content() { - if text == "ping" { - let reply = wa::Message { - conversation: Some("pong".to_string()), - ..Default::default() - }; - if let Err(e) = ctx.send_message(reply).await { - eprintln!("Failed to send: {}", e); - } +async fn handle_message(ctx: MessageContext) { + if ctx.message.text_content() == Some("ping") { + if let Err(e) = ctx.reply("pong").await { + eprintln!("Failed to send: {e}"); } } } ``` +`MessageContext` provides convenience methods including `send_message` (auto-targets the source chat), `reply`, `reply_quoting`, `react`, `build_quote_context`, `edit_message`, and `revoke_message`. + ### Media forwarding with CDN reuse You can also forward media instantly by reusing the original CDN fields — no download or re-upload needed: @@ -333,25 +320,45 @@ fn build_media_reply(message: &wa::Message) -> Option { See the [media forwarding guide](/guides/media-handling#forwarding-media-via-cdn-reuse) for more details. +## Background operation and graceful shutdown + +Use `spawn()` to run the bot in the background while your code continues, and `shutdown()` for a clean stop: + +```rust +use whatsapp_rust::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let bot = Bot::builder() + .with_backend(SqliteStore::new("whatsapp.db").await?) + .on_message(|ctx| async move { + let _ = ctx.reply("hello").await; + }) + .build() + .await?; + + let handle = bot.spawn(); // bot runs in the background; client is available via handle.client() + + tokio::signal::ctrl_c().await?; + handle.shutdown().await; // graceful: flushes pending state, then stops + Ok(()) +} +``` + +`BotHandle` also exposes `abort()` as an escape hatch (skips the flush). Awaiting the handle resolves to `()` once the run loop exits. + ## Complete example with logging Here's a production-ready example with proper logging, reactions, message editing, and media CDN reuse: ```rust src/main.rs -use chrono::{Local, Utc}; use log::{error, info}; -use std::sync::Arc; -use wacore::proto_helpers::MessageExt; -use wacore::types::events::Event; -use waproto::whatsapp as wa; -use whatsapp_rust::bot::{Bot, MessageContext}; -use whatsapp_rust::TokioRuntime; -use whatsapp_rust::store::SqliteStore; -use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; -use whatsapp_rust_ureq_http_client::UreqHttpClient; +use whatsapp_rust::prelude::*; +use whatsapp_rust::pair_code::PairCodeOptions; const PING_TRIGGER: &str = "🦀ping"; const PONG_TEXT: &str = "🏓 Pong!"; +const REACTION_EMOJI: &str = "🏓"; #[tokio::main] async fn main() -> Result<(), Box> { @@ -361,7 +368,7 @@ async fn main() -> Result<(), Box> { writeln!( buf, "{} [{:<5}] [{}] - {}", - Local::now().format("%H:%M:%S"), + wacore::time::now_utc().format("%H:%M:%S"), record.level(), record.target(), record.args() @@ -369,59 +376,46 @@ async fn main() -> Result<(), Box> { }) .init(); - let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); + let store = SqliteStore::new("whatsapp.db").await?; info!("SQLite backend initialized"); - let mut bot = Bot::builder() - .with_backend(backend) - .with_transport_factory(TokioWebSocketTransportFactory::new()) - .with_http_client(UreqHttpClient::new()) - .with_runtime(TokioRuntime) - .on_event(|event, client| async move { - match &*event { - Event::PairingQrCode { code, .. } => { - println!("\n{}", code); - } - Event::Message(msg, info) => { - let ctx = MessageContext::from_parts(msg, info, client); - handle_message(&ctx).await; - } - Event::Connected(_) => info!("Bot connected!"), - Event::LoggedOut(_) => error!("Bot was logged out!"), - _ => {} - } + let bot = Bot::builder() + .with_backend(store) + .on_qr_code(|code, _timeout| async move { + println!("\n{code}\n"); + }) + .on_connected(|_client| async { + info!("Bot connected!"); + }) + .on_logged_out(|_info| async { + error!("Bot was logged out!"); + }) + .on_message(|ctx| async move { + handle_message(ctx).await; }) .build() .await?; info!("Starting bot..."); - bot.run().await?.await?; + bot.run().await; Ok(()) } -async fn handle_message(ctx: &MessageContext) { +async fn handle_message(ctx: MessageContext) { // Try CDN-reuse media reply first (instant, no download needed) if let Some(reply) = build_media_pong(&ctx.message) { if let Err(e) = ctx.send_message(reply).await { - error!("Failed to send media pong: {}", e); + error!("Failed to send media pong: {e}"); } return; } - // Handle text ping if ctx.message.text_content() == Some(PING_TRIGGER) { - let context_info = ctx.build_quote_context(); - let reply = wa::Message { - extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage { - text: Some(PONG_TEXT.to_string()), - context_info: Some(Box::new(context_info)), - ..Default::default() - })), - ..Default::default() - }; - - if let Err(e) = ctx.send_message(reply).await { - error!("Failed to send pong: {}", e); + if let Err(e) = ctx.react(REACTION_EMOJI).await { + error!("Failed to send reaction: {e}"); + } + if let Err(e) = ctx.reply_quoting(PONG_TEXT).await { + error!("Failed to send pong: {e}"); } } } @@ -564,4 +558,4 @@ See the [wacore benchmarks documentation](/api/wacore#benchmarks) for details on Explore all available client methods - \ No newline at end of file +