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
}
});
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;
});
}
_ => {}
}
})