Skip to main content
The Bot provides a simplified, ergonomic API for building WhatsApp bots. It handles client setup, event routing, and background sync tasks automatically.

Overview

Use the Bot builder pattern to:
  • Configure storage backend, transport, HTTP client, and async runtime
  • Register event handlers
  • Configure device properties and versions
  • Enable pair code authentication
  • Skip history sync for bot use cases
The builder uses a typestate pattern with four type parameters <B, T, H, R> (Backend, Transport, HttpClient, Runtime). The build() method is only callable when all four are Provided, making missing-component errors compile-time instead of runtime.
The Bot is the recommended way to use whatsapp-rust. It provides sensible defaults and handles boilerplate setup.

Basic Usage

use whatsapp_rust::bot::Bot;
use whatsapp_rust::TokioRuntime;
use wacore::types::events::Event;

let mut bot = Bot::builder()
    .with_backend(backend)
    .with_transport_factory(transport)
    .with_http_client(http_client)
    .with_runtime(TokioRuntime)
    .on_event(|event, client| async move {
        match &*event {
            Event::Message(msg, info) => {
                println!("Message from {}: {:?}", info.source.sender, msg);
            }
            Event::Connected(_) => {
                println!("Connected to WhatsApp!");
            }
            _ => {}
        }
    })
    .build()
    .await?;

let bot_handle = bot.run().await?;
bot_handle.await?;

Builder Methods

builder

pub fn builder() -> BotBuilder
Creates a new bot builder.

with_backend

pub fn with_backend(self, backend: Arc<dyn Backend>) -> Self
Sets the storage backend (required).
backend
Arc<dyn Backend>
required
Backend implementation providing storage operations
Example:
use whatsapp_rust::store::SqliteStore;

let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);
let bot = Bot::builder()
    .with_backend(backend)
    // ...
For multi-account scenarios, use SqliteStore::new_for_device(path, device_id) to create isolated storage per account.

with_transport_factory

pub fn with_transport_factory<F>(self, factory: F) -> Self
where
    F: TransportFactory + 'static
Sets the transport factory for creating WebSocket connections (required).
factory
F: TransportFactory
required
Transport factory implementation
Example:
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;

let bot = Bot::builder()
    .with_transport_factory(TokioWebSocketTransportFactory::new())
    // ...

with_http_client

pub fn with_http_client<C>(self, client: C) -> Self
where
    C: HttpClient + 'static
Sets the HTTP client for media operations and version fetching (required).
client
C: HttpClient
required
HTTP client implementation
Example:
use whatsapp_rust_ureq_http_client::UreqHttpClient;

let bot = Bot::builder()
    .with_http_client(UreqHttpClient::new())
    // ...

with_runtime

pub fn with_runtime<Rt>(self, runtime: Rt) -> Self
where
    Rt: Runtime + 'static
Sets the async runtime for spawning tasks, sleeping, and blocking operations (required).
runtime
Rt: Runtime
required
Runtime implementation providing spawn, sleep, and spawn_blocking
Example:
use whatsapp_rust::TokioRuntime;

let bot = Bot::builder()
    .with_runtime(TokioRuntime)
    // ...
TokioRuntime is only available when the tokio-runtime feature is enabled (it is by default). To use a different async runtime, implement the Runtime trait from wacore::runtime. See custom backends for details.

Event Handling

on_event

pub fn on_event<F, Fut>(self, handler: F) -> Self
where
    F: Fn(Arc<Event>, Arc<Client>) -> Fut + Send + Sync + 'static,
    Fut: Future<Output = ()> + Send + 'static
Registers an async event handler. The handler receives an Arc<Event> — use &*event to pattern-match on the inner event type.
handler
F
required
Async function that receives Arc<Event> and Arc<Client>
Example:
use waproto::whatsapp as wa;

Bot::builder()
    .on_event(|event, client| async move {
        match &*event {
            Event::Message(msg, info) => {
                // Reply to messages
                let reply = wa::Message {
                    conversation: Some("Hello back!".to_string()),
                    ..Default::default()
                };
                let _ = client.send_message(info.source.chat.clone(), reply).await;
            }
            Event::Connected(_) => {
                println!("Bot online!");
            }
            _ => {}
        }
    })
    // ...
See Events Reference for all event types.

on_event_for

pub fn on_event_for<F, Fut>(self, kinds: &[EventKind], handler: F) -> Self
where
    F: Fn(Arc<Event>, Arc<Client>) -> Fut + Send + Sync + 'static,
    Fut: Future<Output = ()> + Send + 'static
Like on_event, but registers the handler with a narrowed EventInterest so the event bus skips materializing kinds you don’t subscribe to. Useful when you only handle a couple of event types and want to avoid paying the Arc allocation for high-frequency events like presence or receipts.
use wacore::types::events::EventKind;

Bot::builder()
    .on_event_for(&[EventKind::Message, EventKind::Connected], |event, client| async move {
        match &*event {
            Event::Message(msg, info) => { /* … */ }
            Event::Connected(_) => println!("online"),
            _ => {}
        }
    })
    // ...

Using ChannelEventHandler

For scenarios where you need to process events outside of a closure (e.g., testing, custom event loops, or runtime-agnostic code), use ChannelEventHandler with register_handler instead of on_event:
use wacore::types::events::{ChannelEventHandler, Event};

let mut bot = Bot::builder()
    .with_backend(backend)
    .with_transport_factory(transport)
    .with_http_client(http_client)
    .with_runtime(TokioRuntime)
    .build()
    .await?;

let client = bot.client();
let (event_handler, event_rx) = ChannelEventHandler::new();
client.register_handler(event_handler);

let handle = bot.run().await?;

// Process events in your own async loop
while let Ok(event) = event_rx.recv().await {
    match &*event {
        Event::Connected(_) => println!("Connected!"),
        Event::Message(msg, info) => {
            println!("Message from {}", info.source.sender);
        }
        _ => {}
    }
}
ChannelEventHandler uses async-channel (runtime-agnostic) with an unbounded buffer, so events fired before the receiver starts listening are not lost. You can combine it with on_event — both handlers will receive all events.

with_enc_handler

pub fn with_enc_handler<H>(self, enc_type: impl Into<String>, handler: H) -> Self
where
    H: EncHandler + 'static
Registers a custom handler for specific encrypted message types.
enc_type
String
required
Encrypted message type (e.g., “frskmsg”, “skmsg”)
handler
H: EncHandler
required
Handler implementation
On wasm32 targets, EncHandler drops the Send + Sync supertrait — your handler can capture !Send JS handles. On native, Send + Sync is retained via MaybeSendSync so Arc<dyn EncHandler> remains thread-safe. This mirrors the convention used by EventHandler and SendContextResolver.

Configuration

with_version

pub fn with_version(self, version: (u32, u32, u32)) -> Self
Overrides the WhatsApp version used by the client.
version
(u32, u32, u32)
required
Tuple of (primary, secondary, tertiary) version numbers
Example:
Bot::builder()
    .with_version((2, 3000, 1027868167))
    // ...
By default, the client automatically fetches the latest WhatsApp Web version from web.whatsapp.com/sw.js on each connect and caches it for 24 hours. Use with_version to pin a specific version when you need deterministic behavior — for example, in integration tests or CI environments where external HTTP requests are undesirable.

with_device_props

pub fn with_device_props(
    self,
    os_name: Option<String>,
    version: Option<wa::device_props::AppVersion>,
    platform_type: Option<wa::device_props::PlatformType>
) -> Self
Overrides device properties sent to WhatsApp servers.
os_name
Option<String>
Operating system name (e.g., “macOS”, “Windows”, “Linux”)
version
Option<AppVersion>
App version struct
platform_type
Option<PlatformType>
Platform type (determines device name shown on phone)
Example:
use waproto::whatsapp::device_props::{AppVersion, PlatformType};

Bot::builder()
    .with_device_props(
        Some("macOS".to_string()),
        Some(AppVersion {
            primary: Some(2),
            secondary: Some(0),
            tertiary: Some(0),
            ..Default::default()
        }),
        Some(PlatformType::Chrome),
    )
    // ...
The platform_type determines what device name is shown on the phone’s “Linked Devices” list. Common values: Chrome, Firefox, Safari, Desktop.

with_push_name

pub fn with_push_name(self, name: impl Into<String>) -> Self
Sets an initial push name on the device before connecting.
name
String
required
Display name to set on the device
Example:
Bot::builder()
    .with_push_name("My Bot")
    // ...
The push name is included in the ClientPayload during registration. This is useful for testing scenarios where the server assigns phone numbers based on push name.

Authentication

with_pair_code

pub fn with_pair_code(self, options: PairCodeOptions) -> Self
Configures pair code authentication to run automatically after connecting.
options
PairCodeOptions
required
Configuration for pair code authentication
Example:
use whatsapp_rust::pair_code::PairCodeOptions;
use wacore::companion_reg::CompanionWebClientType;
use wacore::types::events::Event;

Bot::builder()
    .with_pair_code(PairCodeOptions {
        phone_number: "15551234567".to_string(),
        show_push_notification: true,
        custom_code: None,
        // `None` derives the wire id from the device's `PlatformType`.
        // Override only when you need a specific `<companion_platform_id>`.
        platform_id: Some(CompanionWebClientType::Chrome),
    })
    .on_event(|event, _client| async move {
        match &*event {
            Event::PairingCode { code, timeout } => {
                println!("Enter this code on your phone: {}", code);
                println!("Expires in: {} seconds", timeout.as_secs());
            }
            _ => {}
        }
    })
    // ...
Pair code runs concurrently with QR code pairing — whichever completes first wins.
The companion_platform_display shown on the phone is derived automatically from the resolved platform_id and the device’s os string: web variants emit <Browser> (<OS>) (Android PlatformTypes map to Chrome, so they show as Chrome (Android) by default); explicit AndroidPhone/AndroidTablet/AndroidAmbiguous overrides emit Android (<OS>). There is no separate platform_display field on PairCodeOptions.

Cache Configuration

with_cache_config

pub fn with_cache_config(self, config: CacheConfig) -> Self
Configures cache TTL and capacity settings for internal caches.
config
CacheConfig
required
Custom cache configuration
Example:
use whatsapp_rust::{CacheConfig, CacheEntryConfig};
use std::time::Duration;

Bot::builder()
    .with_cache_config(CacheConfig {
        group_cache: CacheEntryConfig::new(None, 500), // No TTL, 500 entries
        device_registry_cache: CacheEntryConfig::new(Some(Duration::from_secs(1800)), 2000),
        ..Default::default()
    })
    // ...
See Cache Configuration for available cache types.

History Sync

History sync transfers chat history from the phone to the linked device. The processing pipeline is optimized for minimal RAM usage through zero-copy streaming and lazy parsing.

How it works

When your bot receives history sync data, the pipeline:
  1. Stream-decrypts external blobs in 8KB chunks (or moves inline payloads without copying)
  2. Decompresses zlib data on a blocking thread with pre-allocated buffers capped at 8 MiB
  3. Walks protobuf fields manually instead of decoding the entire message tree — only internal data (pushname, NCT salt, TC tokens) is extracted at this stage
  4. Wraps the decompressed blob in a LazyHistorySync with cheap metadata (sync type, chunk order, progress) available without decoding
  5. Dispatches Event::HistorySync(Box<LazyHistorySync>) — full protobuf decoding is deferred until you call .get(). Use .raw_bytes() for custom partial decoding if you only need specific fields
If no event handlers are registered, the blob is not retained in memory.

skip_history_sync

pub fn skip_history_sync(self) -> Self
Skips processing of history sync notifications from the phone. When enabled:
  • Sends a receipt so the phone stops retrying uploads
  • Does not download or process historical data
  • Emits debug log for each skipped notification
  • Useful for bot use cases where message history is not needed
Example:
Bot::builder()
    .skip_history_sync()
    // ...
For bots that only need to respond to new messages, enabling this can significantly reduce startup time and bandwidth usage.

with_wanted_pre_key_count

pub fn with_wanted_pre_key_count(self, count: usize) -> Self
Sets the number of one-time pre-keys generated and uploaded per batch. Mirrors WhatsApp Web’s UPLOAD_KEYS_COUNT. Default: 812. The value is clamped at upload time to 5..=65_535. Values outside that range log a warn! and are clamped to the nearest bound.
count
usize
required
Pre-keys per upload batch. Clamped to 5..=65_535.
Example:
Bot::builder()
    .with_wanted_pre_key_count(256) // smaller batch for embedded hosts
    // ...
Leave this at the default unless you have a specific reason to change it. Embedded or memory-constrained consumers may prefer a smaller batch to shrink the working set during each upload; smaller batches also mean more frequent uploads as peers consume keys.
The floor of 5 prevents an empty-but-flagged pool and a re-upload loop (the count guard never clears below the trigger threshold). The ceiling of 65,535 is the wire-format limit — the upload IQ encodes the pre-key list length as a u16, so a larger batch would generate and store keys locally and then fail to encode.

Building and Running

build

pub async fn build(self) -> Result<Bot, BotBuilderError>
Builds the bot with the configured options. Errors:
pub enum BotBuilderError {
    Other(anyhow::Error),
}
VariantCause
OtherBackend initialization or other runtime failure
Missing required components (backend, transport, HTTP client, runtime) are caught at compile time via the typestate pattern — build() is only available when all four type parameters are Provided. You won’t see runtime errors for missing components.

client

pub fn client(&self) -> Arc<Client>
Returns the underlying Client Arc. Example:
let bot = Bot::builder()
    .with_backend(backend)
    .with_transport_factory(transport)
    .with_http_client(http_client)
    .with_runtime(TokioRuntime)
    .build()
    .await?;

let client = bot.client();
let jid = client.get_pn();

run

pub async fn run(&mut self) -> Result<BotHandle>
Starts the bot’s connection loop and background workers. Returns a BotHandle that implements Future. You can also call .abort() on it to cancel the bot. Example:
let mut bot = Bot::builder()
    // ... configuration
    .build()
    .await?;

let handle = bot.run().await?;

// Wait for bot to finish (runs until disconnect)
handle.await?;
You must call .await? on the returned handle to keep the bot running. If you drop the handle, the bot will continue running in the background.

MessageContext

A convenience helper for message handling. You can construct it from the Event::Message components:
pub struct MessageContext {
    pub message: Arc<wa::Message>,
    pub info: MessageInfo,
    pub client: Arc<Client>,
}
Since v0.6 message is Arc<wa::Message> (was Box<wa::Message>). This matches the Event::Message payload and lets from_event / from_arc reuse the bus-dispatched Arc with zero deep clones.

from_parts

pub fn from_parts(message: &wa::Message, info: &MessageInfo, client: Arc<Client>) -> Self
Constructs a MessageContext from individual message components. Internally clones the wa::Message into a new Arc.

from_arc

pub fn from_arc(message: Arc<wa::Message>, info: &MessageInfo, client: Arc<Client>) -> Self
Constructs a MessageContext from an existing Arc<wa::Message> without copying the body — pair this with the Arc you receive from Event::Message to keep dispatch zero-clone.

from_event

pub fn from_event(event: &Event, client: Arc<Client>) -> Option<Self>
Extracts a MessageContext from an Event. Returns None if the event is not an Event::Message. Reuses the existing Arc<wa::Message> rather than cloning the body.

send_message

pub async fn send_message(&self, message: wa::Message) -> Result<SendResult, anyhow::Error>
Sends a message to the same chat. Returns a SendResult containing the message_id and to JID.

build_quote_context

pub fn build_quote_context(&self) -> wa::ContextInfo
Builds a quote context for replying to this message. Handles:
  • Correct stanza_id/participant for groups and newsletters
  • Stripping nested mentions
  • Preserving bot quote chains
Example:
use waproto::whatsapp as wa;
use whatsapp_rust::bot::MessageContext;

.on_event(|event, client| async move {
    if let Some(ctx) = MessageContext::from_event(&event, client) {
        let reply = wa::Message {
            extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
                text: Some("Quoted reply!".to_string()),
                context_info: Some(Box::new(ctx.build_quote_context())),
                ..Default::default()
            })),
            ..Default::default()
        };
        let _ = ctx.send_message(reply).await;
    }
})

edit_message

pub async fn edit_message(
    &self,
    original_message_id: impl Into<String>,
    new_message: wa::Message
) -> Result<String, anyhow::Error>
Edits a message in the same chat.

revoke_message

pub async fn revoke_message(
    &self,
    message_id: String,
    revoke_type: RevokeType
) -> Result<(), anyhow::Error>
Deletes a message in the same chat.

react

pub async fn react(&self, emoji: &str) -> Result<SendResult, anyhow::Error>
Sends an emoji reaction to the incoming message. The chat JID, target message ID, and group/status participant are taken from the context — you only supply the emoji. Pass an empty string ("") to remove a previously sent reaction. Internally this calls Client::send_reaction with self.message_key() as the target. Example:
use whatsapp_rust::bot::MessageContext;

.on_event(|event, client| async move {
    if let Some(ctx) = MessageContext::from_event(&event, client) {
        // React with a thumbs-up to every incoming message.
        let _ = ctx.react("👍").await;
    }
})
Newsletter (channel) messages don’t flow through MessageContext::react. Use client.newsletter().send_reaction() for newsletter reactions.

Complete Example

use whatsapp_rust::bot::Bot;
use whatsapp_rust::TokioRuntime;
use whatsapp_rust::store::SqliteStore;
use wacore::types::events::Event;
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
use whatsapp_rust_ureq_http_client::UreqHttpClient;
use waproto::whatsapp as wa;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Set up storage
    let backend = Arc::new(SqliteStore::new("bot.db").await?);

    // Build bot
    let mut bot = Bot::builder()
        .with_backend(backend)
        .with_transport_factory(TokioWebSocketTransportFactory::new())
        .with_http_client(UreqHttpClient::new())
        .with_runtime(TokioRuntime)
        .skip_history_sync() // Bot only needs new messages
        .on_event(|event, client| async move {
            match &*event {
                Event::Message(msg, info) => {
                    // Echo messages back
                    if let Some(text) = &msg.conversation {
                        let reply = wa::Message {
                            conversation: Some(format!("You said: {}", text)),
                            ..Default::default()
                        };
                        let _ = client.send_message(info.source.chat.clone(), reply).await;
                    }
                }
                Event::Connected(_) => {
                    println!("Bot is now online!");
                    // Set status
                    let _ = client.presence().set_available().await;
                }
                Event::PairingQrCode { code, .. } => {
                    println!("Scan this QR code:");
                    println!("{}", code);
                }
                _ => {}
            }
        })
        .build()
        .await?;

    // Run bot
    let handle = bot.run().await?;
    handle.await?;

    Ok(())
}

Cache configuration reference

The CacheConfig struct controls TTL and capacity for all internal caches. All fields have sensible defaults matching WhatsApp Web behavior.

CacheEntryConfig

pub struct CacheEntryConfig {
    pub timeout: Option<Duration>,  // None = no time-based expiry
    pub capacity: u64,              // Maximum entries
}

Available Caches

Timed caches

CacheDefault TTLDefault CapacityDescription
group_cache1 hour250Group metadata
device_registry_cache1 hour5,000Device registry
lid_pn_cache1 hour (TTI)10,000LID-to-phone mapping
retried_group_messages5 minutes2,000Retry tracking
recent_messages5 minutes0 (disabled)Optional L1 in-memory cache for sent messages (retry support)
message_retry_counts5 minutes1,000Retry count tracking
pdo_pending_requests30 seconds500PDO pending requests
sender_key_devices_cache1 hour (TTI)500Per-group SKDM distribution state
The lid_pn_cache and sender_key_devices_cache use time-to-idle (TTI) semantics — entries expire after being idle for the timeout period. All other caches use time-to-live (TTL) semantics.
The recent_messages cache is disabled by default (capacity 0), meaning sent messages are stored only in the database for retry handling — matching WhatsApp Web’s behavior. Set capacity greater than 0 to enable a fast in-memory L1 cache in front of the database. See DB-backed sent message retry for details.

Coordination caches (capacity-only, no TTL)

SettingDefaultDescription
session_locks_capacity10,000Per-device Signal session lock capacity
chat_lanes_capacity5,000Per-chat lane capacity (combined enqueue lock + message queue)

Sent message DB cleanup

SettingDefaultDescription
sent_message_ttl_secs7200 (2 hours)TTL in seconds for sent messages in DB before periodic cleanup. Set to 0 to disable automatic cleanup.
The sent_message_ttl_secs default was raised from 300s to 7200s. Retry receipts can arrive well after a message is sent (e.g. after the recipient comes back online); a 5-minute TTL could expire the stored payload before its retry, silently dropping the retry. Two hours covers realistic offline gaps.

messageSecret retention

The client stores messageSecret values so it can later decrypt add-ons that reference an original message — poll votes, poll/event edits, message edits, and Meta AI / fbid bot replies. Retention is bounded by policy and a per-class event-time horizon (expires_at = parent_message_ts + horizon, not insertion time), so secrets survive offline gaps without growing unbounded.
FieldTypeDefaultDescription
msg_secret_policyMsgSecretPolicyManagedWhich retention tier to use (see below)
msg_secret_retentionMsgSecretRetention30d / 90d / 30dPer-class horizons (text / poll_event / bot)
seed_msg_secrets_from_historybooltrueSeed secrets from pairing history-sync blobs
original_message_resolverOption<Arc<dyn OriginalMessageResolver>>NoneApp-supplied fallback when the store misses
msg_secret_resolver_timeoutDuration5sTimeout for a single resolver call
pub enum MsgSecretPolicy {
    /// Bounded (default): capture live secrets, seed only the still-relevant
    /// history slice, prune by per-add-on event-time horizon.
    Managed,
    /// Pre-v0.6 behavior: capture/seed only in bot (msmsg) contexts.
    BotOnly,
    /// Unbounded: capture/seed everything, never prune.
    Full,
    /// Persist nothing in core; rely entirely on `original_message_resolver`.
    Disabled,
}

pub struct MsgSecretRetention {
    pub text: Duration,        // default 30 days — message-edit parents
    pub poll_event: Duration,  // default 90 days — poll votes / PollAddOption / EventEdit / PollEdit
    pub bot: Duration,         // default 30 days — outbound msmsg bot context
}
The OriginalMessageResolver trait lets you supply secrets from your own store (required when the policy is Disabled):
#[async_trait]
pub trait OriginalMessageResolver: Send + Sync {
    async fn resolve_msg_secret(
        &self,
        chat: &str,
        sender: &str,
        msg_id: &str,
    ) -> Option<[u8; 32]>;
}
It is consulted only after the in-core MsgSecretStore and the LID/PN alternate lookups miss.
MsgSecretPolicy, MsgSecretRetention, and OriginalMessageResolver are re-exported from the crate root (whatsapp_rust::{MsgSecretPolicy, MsgSecretRetention, OriginalMessageResolver}). The default Managed policy is bounded and needs no tuning for most apps.

Custom cache store overrides

You can replace any of the pluggable caches with a custom CacheStore backend (e.g., Redis):
FieldCacheDescription
cache_stores.group_cacheGroup metadataGroup info lookups
cache_stores.device_registry_cacheDevice registryDevice registry entries
cache_stores.lid_pn_cacheLID-PN mappingLID-to-phone bidirectional lookups
use whatsapp_rust::{CacheConfig, CacheStores};
use std::sync::Arc;

let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379"));

// Route specific caches to Redis
let config = CacheConfig {
    cache_stores: CacheStores {
        group_cache: Some(redis.clone()),
        device_registry_cache: Some(redis.clone()),
        ..Default::default()
    },
    ..Default::default()
};

// Or route all pluggable caches at once
let config = CacheConfig {
    cache_stores: CacheStores::all(redis.clone()),
    ..Default::default()
};
Fields left as None keep the default in-process moka behavior. See Custom backends — cache store for a full implementation guide.
Coordination caches (session_locks, chat_lanes), the signal write-behind cache, and pdo_pending_requests always stay in-process — they hold live Rust objects that cannot be serialized to an external store.

Custom configuration example

use whatsapp_rust::{CacheConfig, CacheEntryConfig};
use std::time::Duration;

let config = CacheConfig {
    // Disable TTL for group cache (entries only evicted by capacity)
    group_cache: CacheEntryConfig::new(None, 500),
    // Shorter TTL for device cache
    device_registry_cache: CacheEntryConfig::new(Some(Duration::from_secs(1800)), 3_000),
    // Enable L1 in-memory cache for sent messages (faster retry lookups)
    recent_messages: CacheEntryConfig::new(Some(Duration::from_secs(300)), 1_000),
    // Use defaults for everything else
    ..Default::default()
};

let bot = Bot::builder()
    .with_runtime(TokioRuntime)
    .with_cache_config(config)
    // ...

DB-backed sent message retry

Sent messages are persisted to the database for retry handling, matching WhatsApp Web’s getMessageTable pattern. When a retry receipt arrives, the client looks up the original message payload from the database, re-encrypts it, and resends. How it works:
  1. Every send_message() call stores the serialized message payload in the sent_messages table
  2. On retry receipt, the client retrieves and consumes the payload (atomic take)
  3. Expired entries are periodically cleaned up based on sent_message_ttl_secs
Optional L1 cache: By default, the recent_messages cache capacity is 0 (DB-only mode). If you set capacity greater than 0, sent messages are also cached in memory for faster retrieval. In L1 mode, the DB write is backgrounded since the cache serves reads immediately. In DB-only mode, the write is awaited to guarantee persistence.
let config = CacheConfig {
    // Enable L1 in-memory cache for faster retry lookups
    recent_messages: CacheEntryConfig::new(Some(Duration::from_secs(300)), 1_000),
    // Keep sent messages in DB for 10 minutes before cleanup
    sent_message_ttl_secs: 600,
    ..Default::default()
};

See Also