Skip to main content

Overview

WhatsApp-Rust uses an event-driven architecture where the client emits events for all WhatsApp protocol interactions. Your application subscribes to these events to handle messages, connection changes, and notifications.

Event System Architecture

CoreEventBus

Location: wacore/src/types/events.rs
#[derive(Default, Clone)]
pub struct CoreEventBus {
    handlers: Arc<RwLock<Vec<Arc<dyn EventHandler>>>>,
}

impl CoreEventBus {
    pub fn dispatch(&self, event: &Event) {
        for handler in self.handlers.read().expect("...").iter() {
            handler.handle_event(event);
        }
    }

    pub fn has_handlers(&self) -> bool {
        !self.handlers.read().expect("...").is_empty()
    }
}
Features:
  • Thread-safe event dispatching
  • Multiple handlers supported
  • Clone-cheap with Arc

EventHandler Trait

pub trait EventHandler: Send + Sync {
    fn handle_event(&self, event: &Event);
}
Implementation:
struct MyHandler;

impl EventHandler for MyHandler {
    fn handle_event(&self, event: &Event) {
        match event {
            Event::Message(msg, info) => {
                println!("Message from {}: {:?}", info.source.sender, msg);
            }
            _ => {}
        }
    }
}

client.core.event_bus.add_handler(Arc::new(MyHandler));

Event Enum

Location: wacore/src/types/events.rs:292-351
#[derive(Debug, Clone, Serialize)]
pub enum Event {
    // Connection
    Connected(Connected),
    Disconnected(Disconnected),
    StreamReplaced(StreamReplaced),
    StreamError(StreamError),
    ConnectFailure(ConnectFailure),
    TemporaryBan(TemporaryBan),

    // Pairing
    PairingQrCode { code: String, timeout: Duration },
    PairingCode { code: String, timeout: Duration },
    PairSuccess(PairSuccess),
    PairError(PairError),
    QrScannedWithoutMultidevice(QrScannedWithoutMultidevice),
    ClientOutdated(ClientOutdated),
    LoggedOut(LoggedOut),

    // Messages
    Message(Box<wa::Message>, MessageInfo),
    Receipt(Receipt),
    UndecryptableMessage(UndecryptableMessage),
    Notification(Node),

    // Presence
    ChatPresence(ChatPresenceUpdate),
    Presence(PresenceUpdate),

    // User Updates
    PictureUpdate(PictureUpdate),
    UserAboutUpdate(UserAboutUpdate),
    PushNameUpdate(PushNameUpdate),
    SelfPushNameUpdated(SelfPushNameUpdated),

    // Group Updates
    JoinedGroup(LazyConversation),
    GroupInfoUpdate { jid: Jid, update: Box<wa::SyncActionValue> },

    // Contact Updates
    ContactUpdate(ContactUpdate),

    // Chat State
    PinUpdate(PinUpdate),
    MuteUpdate(MuteUpdate),
    ArchiveUpdate(ArchiveUpdate),
    MarkChatAsReadUpdate(MarkChatAsReadUpdate),

    // History Sync
    HistorySync(HistorySync),
    OfflineSyncPreview(OfflineSyncPreview),
    OfflineSyncCompleted(OfflineSyncCompleted),

    // Device Updates
    DeviceListUpdate(DeviceListUpdate),
    BusinessStatusUpdate(BusinessStatusUpdate),
}

Connection Events

Connected

Emitted: After successful connection and authentication
#[derive(Debug, Clone, Serialize)]
pub struct Connected;

Event::Connected(Connected)
Usage:
Event::Connected(_) => {
    println!("✅ Connected to WhatsApp");
    // Safe to send messages now
}

Disconnected

Emitted: When connection is lost
#[derive(Debug, Clone, Serialize)]
pub struct Disconnected;

Event::Disconnected(Disconnected)
Behavior: Client automatically attempts reconnection

ConnectFailure

Emitted: When connection fails with a specific reason
#[derive(Debug, Clone, Serialize)]
pub struct ConnectFailure {
    pub reason: ConnectFailureReason,
    pub message: String,
    pub raw: Option<Node>,
}

#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize)]
pub enum ConnectFailureReason {
    Generic,                // 400
    LoggedOut,              // 401
    TempBanned,             // 402
    MainDeviceGone,         // 403
    UnknownLogout,          // 406
    ClientOutdated,         // 405
    BadUserAgent,           // 409
    CatExpired,             // 413
    CatInvalid,             // 414
    NotFound,               // 415
    ClientUnknown,          // 418
    InternalServerError,    // 500
    Experimental,           // 501
    ServiceUnavailable,     // 503
    Unknown(i32),
}
Helper methods:
if reason.is_logged_out() {
    // Clear session and re-pair
}

if reason.should_reconnect() {
    // Retry connection
}

TemporaryBan

Emitted: When account is temporarily banned
#[derive(Debug, Clone, Serialize)]
pub struct TemporaryBan {
    pub code: TempBanReason,
    pub expire: Duration,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum TempBanReason {
    SentToTooManyPeople,        // 101
    BlockedByUsers,             // 102
    CreatedTooManyGroups,       // 103
    SentTooManySameMessage,     // 104
    BroadcastList,              // 106
    Unknown(i32),
}
Usage:
Event::TemporaryBan(ban) => {
    eprintln!("Banned: {} (expires in {:?})", ban.code, ban.expire);
}

StreamReplaced

Emitted: When another device connects with the same credentials
Event::StreamReplaced(_) => {
    println!("⚠️ Another instance connected - disconnecting");
}

Pairing Events

PairingQrCode

Emitted: For each QR code in rotation
Event::PairingQrCode {
    code: String,           // ASCII art QR or data string
    timeout: Duration,      // 60s first, 20s subsequent
}
Example:
Event::PairingQrCode { code, timeout } => {
    println!("Scan this QR (valid {}s):", timeout.as_secs());
    println!("{}", code);
}

PairingCode

Emitted: When pair code is generated
Event::PairingCode {
    code: String,           // 8-character code
    timeout: Duration,      // ~180 seconds
}
Example:
Event::PairingCode { code, .. } => {
    println!("Enter {} on your phone", code);
}

PairSuccess

Emitted: When pairing completes successfully
#[derive(Debug, Clone, Serialize)]
pub struct PairSuccess {
    pub id: Jid,
    pub lid: Jid,
    pub business_name: String,
    pub platform: String,
}
Example:
Event::PairSuccess(info) => {
    println!("✅ Paired as {}", info.id);
    println!("LID: {}", info.lid);
    println!("Name: {}", info.business_name);
}

PairError

Emitted: When pairing fails
#[derive(Debug, Clone, Serialize)]
pub struct PairError {
    pub id: Jid,
    pub lid: Jid,
    pub business_name: String,
    pub platform: String,
    pub error: String,
}

Message Events

Message

Emitted: For all incoming messages (text, media, etc.)
Event::Message(Box<wa::Message>, MessageInfo)
MessageInfo structure:
#[derive(Debug, Clone, Serialize)]
pub struct MessageInfo {
    pub id: MessageId,
    pub source: MessageSource,
    pub timestamp: DateTime<Utc>,
    pub push_name: String,
    pub is_group: bool,
    pub category: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct MessageSource {
    pub sender: Jid,        // Who sent the message
    pub chat: Jid,          // Where it was sent (group or DM)
    pub is_from_me: bool,
    pub is_bot: bool,
}
Example:
use waproto::whatsapp as wa;

Event::Message(msg, info) => {
    println!("From: {} in {}", info.source.sender, info.source.chat);

    // Text message
    if let Some(text) = &msg.conversation {
        println!("Text: {}", text);
    }

    // Extended text (with link preview, quoted message, etc.)
    if let Some(ext) = &msg.extended_text_message {
        println!("Text: {}", ext.text.as_deref().unwrap_or(""));

        if let Some(context) = &ext.context_info {
            if let Some(quoted) = &context.quoted_message {
                println!("Quoted: {:?}", quoted);
            }
        }
    }

    // Image message
    if let Some(img) = &msg.image_message {
        println!("Image: {} ({}x{})", 
            img.caption.as_deref().unwrap_or(""),
            img.width.unwrap_or(0),
            img.height.unwrap_or(0)
        );
    }

    // Video, audio, document, sticker, etc.
    // See waproto::whatsapp::Message for all types
}

Receipt

Emitted: For delivery/read/played receipts
#[derive(Debug, Clone, Serialize)]
pub struct Receipt {
    pub source: MessageSource,
    pub message_ids: Vec<MessageId>,
    pub timestamp: DateTime<Utc>,
    pub r#type: ReceiptType,
    pub message_sender: Jid,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ReceiptType {
    Delivery,
    Read,
    ReadSelf,
    Played,
    Sender,
    Retry,
    ServerError,
    Inactive,
}
Example:
Event::Receipt(receipt) => {
    match receipt.r#type {
        ReceiptType::Read => {
            println!("✓✓ Read by {}", receipt.source.sender);
        }
        ReceiptType::Delivery => {
            println!("✓ Delivered to {}", receipt.source.sender);
        }
        _ => {}
    }
}

UndecryptableMessage

Emitted: When a message cannot be decrypted
#[derive(Debug, Clone, Serialize)]
pub struct UndecryptableMessage {
    pub info: MessageInfo,
    pub is_unavailable: bool,
    pub unavailable_type: UnavailableType,
    pub decrypt_fail_mode: DecryptFailMode,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum UnavailableType {
    Unknown,
    ViewOnce,       // View-once media already viewed
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum DecryptFailMode {
    Show,           // Show placeholder in chat
    Hide,           // Hide from chat
}
Example:
Event::UndecryptableMessage(undec) => {
    if matches!(undec.unavailable_type, UnavailableType::ViewOnce) {
        println!("View-once message already consumed");
    } else {
        eprintln!("Failed to decrypt message from {}", undec.info.source.sender);
    }
}

Presence Events

ChatPresence

Emitted: For typing indicators and recording states
#[derive(Debug, Clone, Serialize)]
pub struct ChatPresenceUpdate {
    pub source: MessageSource,
    pub state: ChatPresence,
    pub media: ChatPresenceMedia,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ChatPresence {
    Composing,
    Paused,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ChatPresenceMedia {
    Text,
    Audio,
}
Example:
Event::ChatPresence(update) => {
    match (update.state, update.media) {
        (ChatPresence::Composing, ChatPresenceMedia::Text) => {
            println!("{} is typing...", update.source.sender);
        }
        (ChatPresence::Composing, ChatPresenceMedia::Audio) => {
            println!("{} is recording audio...", update.source.sender);
        }
        (ChatPresence::Paused, _) => {
            println!("{} stopped typing", update.source.sender);
        }
    }
}

Presence

Emitted: For online/offline status and last seen
#[derive(Debug, Clone, Serialize)]
pub struct PresenceUpdate {
    pub from: Jid,
    pub unavailable: bool,
    pub last_seen: Option<DateTime<Utc>>,
}
Example:
Event::Presence(update) => {
    if update.unavailable {
        println!("{} is offline", update.from);
        if let Some(last_seen) = update.last_seen {
            println!("Last seen: {}", last_seen);
        }
    } else {
        println!("{} is online", update.from);
    }
}

User Update Events

PictureUpdate

Emitted: When a user changes their profile picture
#[derive(Debug, Clone, Serialize)]
pub struct PictureUpdate {
    pub jid: Jid,
    pub author: Jid,
    pub timestamp: DateTime<Utc>,
    pub photo_change: Option<wa::PhotoChange>,
}

UserAboutUpdate

Emitted: When a user changes their status/about
#[derive(Debug, Clone, Serialize)]
pub struct UserAboutUpdate {
    pub jid: Jid,
    pub status: String,
    pub timestamp: DateTime<Utc>,
}

PushNameUpdate

Emitted: When a contact changes their display name
#[derive(Debug, Clone, Serialize)]
pub struct PushNameUpdate {
    pub jid: Jid,
    pub message: Box<MessageInfo>,
    pub old_push_name: String,
    pub new_push_name: String,
}

SelfPushNameUpdated

Emitted: When your own push name is updated
#[derive(Debug, Clone, Serialize)]
pub struct SelfPushNameUpdated {
    pub from_server: bool,
    pub old_name: String,
    pub new_name: String,
}

Group Events

JoinedGroup

Emitted: When added to a group
Event::JoinedGroup(LazyConversation)
LazyConversation: Lazily-parsed group conversation data
Event::JoinedGroup(lazy_conv) => {
    if let Some(conv) = lazy_conv.get() {
        println!("Joined group: {}", conv.id);
    }
}

GroupInfoUpdate

Emitted: When group metadata changes (subject, participants, etc.)
Event::GroupInfoUpdate {
    jid: Jid,
    update: Box<wa::SyncActionValue>,
}
Example:
Event::GroupInfoUpdate { jid, update } => {
    if let Some(action) = &update.group_action {
        println!("Group {} updated: {:?}", jid, action);
    }
}

Chat State Events

PinUpdate

Emitted: When a chat is pinned/unpinned
#[derive(Debug, Clone, Serialize)]
pub struct PinUpdate {
    pub jid: Jid,
    pub timestamp: DateTime<Utc>,
    pub action: Box<wa::sync_action_value::PinAction>,
    pub from_full_sync: bool,
}

MuteUpdate

Emitted: When a chat is muted/unmuted
#[derive(Debug, Clone, Serialize)]
pub struct MuteUpdate {
    pub jid: Jid,
    pub timestamp: DateTime<Utc>,
    pub action: Box<wa::sync_action_value::MuteAction>,
    pub from_full_sync: bool,
}

ArchiveUpdate

Emitted: When a chat is archived/unarchived
#[derive(Debug, Clone, Serialize)]
pub struct ArchiveUpdate {
    pub jid: Jid,
    pub timestamp: DateTime<Utc>,
    pub action: Box<wa::sync_action_value::ArchiveChatAction>,
    pub from_full_sync: bool,
}

MarkChatAsReadUpdate

Emitted: When a chat is marked as read
#[derive(Debug, Clone, Serialize)]
pub struct MarkChatAsReadUpdate {
    pub jid: Jid,
    pub timestamp: DateTime<Utc>,
    pub action: Box<wa::sync_action_value::MarkChatAsReadAction>,
    pub from_full_sync: bool,
}

History Sync Events

HistorySync

Emitted: For chat history synchronization
Event::HistorySync(HistorySync)
HistorySync: Protobuf message containing chat history

OfflineSyncPreview

Emitted: Preview of pending offline sync data when reconnecting
#[derive(Debug, Clone, Serialize)]
pub struct OfflineSyncPreview {
    pub total: i32,
    pub app_data_changes: i32,
    pub messages: i32,
    pub notifications: i32,
    pub receipts: i32,
}
Example:
Event::OfflineSyncPreview(preview) => {
    println!("Syncing {} items ({} messages, {} notifications)", 
        preview.total, preview.messages, preview.notifications);
}

OfflineSyncCompleted

Emitted: When offline sync completes after reconnection
#[derive(Debug, Clone, Serialize)]
pub struct OfflineSyncCompleted {
    pub count: i32,
}
Example:
Event::OfflineSyncCompleted(sync) => {
    println!("Offline sync completed: {} items processed", sync.count);
}
Offline sync happens automatically when the client reconnects after being disconnected. The client tracks progress internally and emits these events to notify your application of sync status.

Device Events

DeviceListUpdate

Emitted: When a user’s device list changes
#[derive(Debug, Clone, Serialize)]
pub struct DeviceListUpdate {
    pub user: Jid,
    pub lid_user: Option<Jid>,
    pub update_type: DeviceListUpdateType,
    pub devices: Vec<DeviceNotificationInfo>,
    pub key_index: Option<KeyIndexInfo>,
    pub contact_hash: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum DeviceListUpdateType {
    Add,
    Remove,
    Update,
}

BusinessStatusUpdate

Emitted: When a business account status changes
#[derive(Debug, Clone, Serialize)]
pub struct BusinessStatusUpdate {
    pub jid: Jid,
    pub update_type: BusinessUpdateType,
    pub timestamp: i64,
    pub target_jid: Option<Jid>,
    pub hash: Option<String>,
    pub verified_name: Option<String>,
    pub product_ids: Vec<String>,
    pub collection_ids: Vec<String>,
    pub subscriptions: Vec<BusinessSubscription>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum BusinessUpdateType {
    RemovedAsBusiness,
    VerifiedNameChanged,
    ProfileUpdated,
    ProductsUpdated,
    CollectionsUpdated,
    SubscriptionsUpdated,
    Unknown,
}

Event Handler Patterns

Bot Builder Pattern

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

let mut bot = Bot::builder()
    .with_backend(backend)
    .on_event(|event, client| async move {
        match event {
            Event::Message(msg, info) => {
                // Handle message
            }
            Event::Connected(_) => {
                // Handle connection
            }
            _ => {}
        }
    })
    .build()
    .await?;

Multiple Handlers

struct MessageHandler;
impl EventHandler for MessageHandler {
    fn handle_event(&self, event: &Event) {
        if let Event::Message(msg, info) = event {
            // Handle messages
        }
    }
}

struct ConnectionHandler;
impl EventHandler for ConnectionHandler {
    fn handle_event(&self, event: &Event) {
        match event {
            Event::Connected(_) => { /* ... */ }
            Event::Disconnected(_) => { /* ... */ }
            _ => {}
        }
    }
}

client.core.event_bus.add_handler(Arc::new(MessageHandler));
client.core.event_bus.add_handler(Arc::new(ConnectionHandler));

Async Event Handlers

use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::unbounded_channel();

struct AsyncHandler {
    tx: mpsc::UnboundedSender<Event>,
}

impl EventHandler for AsyncHandler {
    fn handle_event(&self, event: &Event) {
        let _ = self.tx.send(event.clone());
    }
}

client.core.event_bus.add_handler(Arc::new(AsyncHandler { tx }));

// Process events asynchronously
tokio::spawn(async move {
    while let Some(event) = rx.recv().await {
        // Async processing
    }
});

Performance Optimization

LazyConversation

Purpose: Avoid parsing large protobuf messages unless needed
// wacore/src/types/events.rs:42-97
pub struct LazyConversation {
    raw_bytes: Bytes,                    // Zero-copy bytes
    parsed: Arc<OnceLock<wa::Conversation>>,  // Parse once
}

impl LazyConversation {
    pub fn get(&self) -> Option<&wa::Conversation> {
        // Parse on first access
        let conv = self.parsed.get_or_init(|| 
            wa::Conversation::decode(&self.raw_bytes[..]).unwrap_or_default()
        );
        if conv.id.is_empty() { None } else { Some(conv) }
    }
}
Usage:
Event::JoinedGroup(lazy_conv) => {
    // No parsing cost unless you access it
    if interested_in_group() {
        if let Some(conv) = lazy_conv.get() {
            // Parse happens here
            process_group(conv);
        }
    }
}

SharedData

Purpose: Cheap cloning of large event data
// wacore/src/types/events.rs:14-39
pub struct SharedData<T>(pub Arc<T>);

impl<T> std::ops::Deref for SharedData<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
Usage:
let shared = SharedData::new(expensive_data);
let clone1 = shared.clone(); // O(1) - just increments Arc counter
let clone2 = shared.clone(); // O(1)

Best Practices

Event Filtering

.on_event(|event, client| async move {
    // Only handle events you care about
    match event {
        Event::Message(msg, info) if !info.source.is_from_me => {
            // Only handle messages from others
        }
        Event::Message(msg, info) if info.is_group => {
            // Only handle group messages
        }
        _ => {}
    }
})

Error Handling

.on_event(|event, client| async move {
    if let Err(e) = handle_event(event, client).await {
        eprintln!("Event handler error: {}", e);
    }
})

async fn handle_event(event: &Event, client: Arc<Client>) -> Result<()> {
    match event {
        Event::Message(msg, info) => {
            process_message(msg, info, client).await?
        }
        _ => {}
    }
    Ok(())
}

Spawning Tasks

.on_event(|event, client| async move {
    match event {
        Event::Message(msg, info) => {
            let client = client.clone();
            let msg = msg.clone();
            let info = info.clone();

            tokio::spawn(async move {
                // Process in background
                process_message(&msg, &info, &client).await;
            });
        }
        _ => {}
    }
})