Skip to main content
PR #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 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:
[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):
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:
// 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:
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<dyn Backend>.

Typed event registrars

Dedicated builder methods cover the common event path — no match &*event needed:
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:
// 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

OldNew
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::UreqHttpClientuse whatsapp_rust::http::UreqHttpClient
bot::Missing typestate markerbot::MissingBackend / MissingTransport / MissingHttpClient / MissingRuntime
BotBuilderError::Other(anyhow)removed; only BotBuilderError::Store(StoreError)
Registering two handlers — second replaces firstBoth run; handlers accumulate

Migration guide

Lifecycle:
// 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:
// 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:
// Before
use whatsapp_rust::transport::UreqHttpClient;

// After
use whatsapp_rust::http::UreqHttpClient;
Builder dependencies — drop sibling crates from Cargo.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"] }