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
82 changes: 57 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,76 @@ A high-performance, async Rust library for the WhatsApp Web API. Inspired by [wh
- **Chat Actions** — Archive, pin, mute, star messages
- **Profile** — Set push name, status text, profile picture
- **Privacy** — Fetch/set privacy settings, disappearing messages
- **Modular** — Pluggable storage, transport, HTTP client, and async runtime
- **Modular** — Pluggable storage, transport, HTTP client, and async runtime; SQLite, Tokio WebSocket, and ureq ship as the defaults, swap any of them with `default-features = false`
- **Runtime agnostic** — Bring your own async runtime via the `Runtime` trait (Tokio included by default)

For the full API reference and guides, see the **[documentation](https://whatsapp-rust.jlucaso.com)**.

## Quick Start

```rust
use std::sync::Arc;
use whatsapp_rust::bot::Bot;
use whatsapp_rust::store::SqliteStore;
use whatsapp_rust::TokioRuntime;
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
use whatsapp_rust_ureq_http_client::UreqHttpClient;
use wacore::types::events::Event;
```toml
[dependencies]
whatsapp-rust = "0.6"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
```

```rust,no_run
use whatsapp_rust::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);

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!("QR:\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 to pair:\n{code}");
})
.on_message(|ctx| async move {
if ctx.message.text_content() == Some("ping") {
let _ = ctx.reply("pong").await;
}
})
.build()
.await?;

bot.run().await?.await?;
// Runs until logout or shutdown; a single await.
bot.run().await;
Ok(())
}
```

The default cargo features wire up the Tokio WebSocket transport, the ureq HTTP client, the SQLite store, and the Tokio runtime; only the storage backend has to be chosen explicitly. Every piece is replaceable through the builder (`with_transport_factory`, `with_http_client`, `with_runtime`) for custom environments such as wasm or embedded targets.

### One dependency is enough

`whatsapp-rust` re-exports the whole stack, so you never need to declare the sibling crates (`wacore`, `wacore-binary`, `waproto`, `whatsapp-rust-tokio-transport`, `whatsapp-rust-ureq-http-client`, `whatsapp-rust-sqlite-storage`) yourself, including when pinning a git revision:

```toml
[dependencies]
whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "<commit>" }
```

- Protobuf types: `whatsapp_rust::waproto::whatsapp` (aliased as `wa` in the prelude)
- Core protocol/types: `whatsapp_rust::wacore`, `whatsapp_rust::wacore_binary` (`Jid` is also at the crate root)
- Bundled implementations: `whatsapp_rust::transport::TokioWebSocketTransportFactory`, `whatsapp_rust::http::UreqHttpClient`, `whatsapp_rust::store::SqliteStore`, each behind its default-on cargo feature (`tokio-transport`, `ureq-client`, `sqlite-storage`)

With `default-features = false`, pick only what you need (e.g. `features = ["tokio-runtime", "tokio-transport", "ureq-client"]` for a custom store while keeping the bundled networking).

To run the bot in the background instead of blocking, use `spawn()` and keep the handle:

```rust,no_run
use whatsapp_rust::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bot = Bot::builder()
.with_backend(SqliteStore::new("whatsapp.db").await?)
.build()
.await?;

let handle = bot.spawn(); // full Client API stays available via handle.client()

tokio::signal::ctrl_c().await?;
handle.shutdown().await; // graceful: flushes pending state, then stops
Ok(())
}
```
Expand All @@ -71,7 +103,7 @@ cargo run -- -p 15551234567 -c MYCODE # Custom pair code

## Project Structure

```
```text
whatsapp-rust/
├── src/ # Main client library
├── wacore/ # Platform-agnostic core (no runtime deps)
Expand Down
32 changes: 7 additions & 25 deletions examples/benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use chrono::Local;
use log::{error, info, warn};
use std::collections::HashMap;
use std::sync::Arc;
use wacore::net::{HttpClient, HttpRequest};
use wacore::proto_helpers::MessageExt;
use wacore::store::InMemoryBackend;
use wacore::types::events::{Event, EventKind};
use waproto::whatsapp as wa;
use whatsapp_rust::TokioRuntime;
use whatsapp_rust::bot::{Bot, MessageContext};
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
Expand Down Expand Up @@ -37,7 +35,7 @@ fn main() {
writeln!(
buf,
"{} [{:<5}] [{}] - {}",
Local::now().format("%H:%M:%S"),
wacore::time::now_utc().format("%H:%M:%S"),
record.level(),
record.target(),
record.args()
Expand All @@ -51,8 +49,6 @@ fn main() {
.expect("Failed to build tokio runtime");

rt.block_on(async {
let backend = Arc::new(InMemoryBackend::new());

// Accept either WHATSAPP_WS_URL or MOCK_SERVER_URL — the latter
// matches the convention the e2e suite uses.
let configured_ws_url = std::env::var("WHATSAPP_WS_URL")
Expand All @@ -71,12 +67,12 @@ fn main() {
let http_client = UreqHttpClient::new();

let builder = Bot::builder()
.with_backend(backend)
.with_backend(InMemoryBackend::new())
.with_transport_factory(transport_factory)
.with_http_client(http_client)
.with_runtime(TokioRuntime);

let mut bot = builder
let bot = builder
.on_event_for(
&[
EventKind::Message,
Expand All @@ -92,16 +88,12 @@ fn main() {
if let Some(text) = msg.text_content()
&& text == "ping"
{
let ctx = MessageContext::from_parts(msg, info, client);
let ctx =
MessageContext::from_arc(Arc::clone(msg), info, client);
info!("Received text ping, sending pong...");

let pong_text = format!("pong {}", ctx.info.id);
let reply_message = wa::Message {
conversation: Some(pong_text),
..Default::default()
};

if let Err(e) = ctx.send_message(reply_message).await {
if let Err(e) = ctx.reply(pong_text).await {
error!("Failed to send pong reply: {}", e);
}
}
Expand Down Expand Up @@ -154,16 +146,6 @@ fn main() {
.await
.expect("Failed to build bot");

let bot_handle = match bot.run().await {
Ok(handle) => handle,
Err(e) => {
error!("Bot failed to start: {}", e);
return;
}
};

bot_handle
.await
.expect("Bot task should complete without panicking");
bot.run().await;
});
}
Loading
Loading