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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ cargo test -p e2e-tests # requires mock server running
- **Protocol**: Cross-reference **whatsmeow**, **Baileys**, and captured WhatsApp Web JS (`docs/captured-js/`) to verify implementations.
- **IQ Requests**: Use `client.execute(Spec::new(&jid)).await?` pattern. IqSpec constructors take `&Jid` not `Jid`.
- **New features**: Expose via `src/features/mod.rs`, re-export in `src/lib.rs`.
- **Event payloads**: Seal each payload struct with `#[non_exhaustive]` + `#[derive(bon::Builder)]` so fields can be added without breaking consumers; construct via the generated builder (`Type::builder()…build()`), not a struct literal. Model a maybe-absent field as `Option<T>` (gets a `maybe_*` setter), never an empty-string/zero sentinel. The seal is rolling out per struct, so not every payload carries the attribute yet; see the `Event` doc in `wacore/src/types/events.rs` for the full stability policy.
- **Event payloads**: Seal each payload struct with `#[non_exhaustive]` + `#[derive(bon::Builder)]` so fields can be added without breaking consumers; construct via the generated builder (`Type::builder()…build()`), not a struct literal. Model a maybe-absent field as `Option<T>` (gets a `maybe_*` setter), never an empty-string/zero sentinel. Every event payload is sealed this way (unit-marker events too, as empty sealed structs built via `X::builder().build()`); see the `Event` doc in `wacore/src/types/events.rs` for the full stability policy.
- **Wire-tagged enums**: Every protocol enum uses `#[derive(WireEnum)]`. The `#[wire = "..."]` (or `#[wire = NUM]` for int mode) attribute is the SINGLE source of truth for each variant's wire value. Do NOT also derive `serde::Serialize`/`Deserialize` or add `#[serde(rename_all)]` — the derive owns both. Three modes: unit-string (default), tagged-with-payload (`#[wire(tag = "type")]` on the enum, optional `#[wire_alias = "..."]` and `#[wire(skip)]` on fields, `#[wire_fallback]` for catch-all), and int (`#[wire(kind = "int")]`). In tagged mode the derive auto-generates a sibling `<Name>Tag` enum; parsers must dispatch via `<Name>Tag::try_from(node.tag.as_ref())` instead of matching string literals, so renaming a wire tag stays a single-attribute change.

## Detailed Docs
Expand Down
3 changes: 2 additions & 1 deletion examples/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ fn main() {
}
}
}
Event::PairingQrCode { code, .. } => {
Event::PairingQrCode(qr) => {
let code = &qr.code;
// Mirrors tests/e2e/src/lib.rs::spawn_qr_autoresponder_http.
// Auto-pair against the mock server's admin endpoint
// when the configured WS URL looks like a mock
Expand Down
32 changes: 17 additions & 15 deletions src/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ impl<B, T, H, R> BotBuilder<B, T, H, R> {
{
self.on_event_for(&[EventKind::PairingQrCode], move |event, _client| {
let fut = match &*event {
Event::PairingQrCode { code, timeout } => Some(handler(code.clone(), *timeout)),
Event::PairingQrCode(qr) => Some(handler(qr.code.clone(), qr.timeout)),
_ => None,
};
async move {
Expand All @@ -972,7 +972,7 @@ impl<B, T, H, R> BotBuilder<B, T, H, R> {
{
self.on_event_for(&[EventKind::PairingCode], move |event, _client| {
let fut = match &*event {
Event::PairingCode { code, timeout } => Some(handler(code.clone(), *timeout)),
Event::PairingCode(pc) => Some(handler(pc.code.clone(), pc.timeout)),
_ => None,
};
async move {
Expand All @@ -994,7 +994,7 @@ impl<B, T, H, R> BotBuilder<B, T, H, R> {
{
self.on_event_for(&[EventKind::PairingCodeRefresh], move |event, client| {
let fut = match &*event {
Event::PairingCodeRefresh { force_manual } => Some(handler(*force_manual, client)),
Event::PairingCodeRefresh(r) => Some(handler(r.force_manual, client)),
_ => None,
};
async move {
Expand Down Expand Up @@ -1437,10 +1437,12 @@ mod tests {
}

fn pairing_code_event(code: &str) -> Arc<Event> {
Arc::new(Event::PairingCode {
code: code.to_string(),
timeout: std::time::Duration::ZERO,
})
Arc::new(Event::PairingCode(
crate::types::events::PairingCode::builder()
.code(code.to_string())
.timeout(std::time::Duration::ZERO)
.build(),
))
}

/// `EventDelivery::Ordered` delivers events to a callback in arrival order —
Expand All @@ -1454,8 +1456,8 @@ mod tests {
callback: Arc::new(move |event, _client| {
let order_tx = order_tx.clone();
Box::pin(async move {
if let Event::PairingCode { code, .. } = &*event {
let _ = order_tx.send(code.clone()).await;
if let Event::PairingCode(pc) = &*event {
let _ = order_tx.send(pc.code.clone()).await;
}
})
}),
Expand Down Expand Up @@ -1502,8 +1504,8 @@ mod tests {
Box::pin(async move {
// Only the first event parks the single drainer, so the
// capacity-1 mailbox is deterministically full for the rest.
if let Event::PairingCode { code, .. } = &*event
&& code == "0"
if let Event::PairingCode(pc) = &*event
&& pc.code == "0"
{
let _ = started_tx.send(()).await;
let _ = release_rx.recv().await;
Expand Down Expand Up @@ -1549,12 +1551,12 @@ mod tests {
let handler = RegisteredHandler {
callback: Arc::new(move |event: Arc<Event>, _client| {
let tx = tx.clone();
if let Event::PairingCode { code, .. } = &*event {
assert_ne!(code, "boom", "deliberate test panic");
if let Event::PairingCode(pc) = &*event {
assert_ne!(&pc.code, "boom", "deliberate test panic");
}
Box::pin(async move {
if let Event::PairingCode { code, .. } = &*event {
let _ = tx.send(code.clone()).await;
if let Event::PairingCode(pc) = &*event {
let _ = tx.send(pc.code.clone()).await;
}
})
}),
Expand Down
22 changes: 12 additions & 10 deletions src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ impl Client {
pub(crate) fn dispatch_connected(&self) {
self.is_ready.store(true, Ordering::Relaxed);
wacore::telemetry::set_connected(true);
self.core
.event_bus
.dispatch(Event::Connected(crate::types::events::Connected));
self.core.event_bus.dispatch(Event::Connected(
crate::types::events::Connected::builder().build(),
));
self.connected_notifier.notify(usize::MAX);
}

Expand Down Expand Up @@ -391,7 +391,9 @@ impl Client {
// Dispatch after cleanup so handlers see cleared connection state.
if let Some(reason) = unexpected_disconnect {
self.core.event_bus.dispatch(Event::Disconnected(
crate::types::events::Disconnected { reason },
crate::types::events::Disconnected::builder()
.reason(reason)
.build(),
));
}
}
Expand Down Expand Up @@ -597,12 +599,12 @@ impl Client {

self.disconnect().await;

self.core
.event_bus
.dispatch(Event::LoggedOut(crate::types::events::LoggedOut {
on_connect: false,
reason: ConnectFailureReason::LoggedOut,
}));
self.core.event_bus.dispatch(Event::LoggedOut(
crate::types::events::LoggedOut::builder()
.on_connect(false)
.reason(ConnectFailureReason::LoggedOut)
.build(),
));

Ok(())
}
Expand Down
82 changes: 40 additions & 42 deletions src/client/node_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,12 +1224,14 @@ impl Client {
self.enable_auto_reconnect.store(false, Ordering::Relaxed);

let event = if conflict_type == "replaced" {
Event::StreamReplaced(crate::types::events::StreamReplaced)
Event::StreamReplaced(crate::types::events::StreamReplaced::builder().build())
} else {
Event::LoggedOut(crate::types::events::LoggedOut {
on_connect: false,
reason: ConnectFailureReason::LoggedOut,
})
Event::LoggedOut(
crate::types::events::LoggedOut::builder()
.on_connect(false)
.reason(ConnectFailureReason::LoggedOut)
.build(),
)
};
self.core.event_bus.dispatch(event);
should_disconnect = true;
Expand All @@ -1247,10 +1249,10 @@ impl Client {
self.expected_disconnect.store(true, Ordering::Relaxed);
self.enable_auto_reconnect.store(false, Ordering::Relaxed);
self.core.event_bus.dispatch(Event::LoggedOut(
crate::types::events::LoggedOut {
on_connect: false,
reason: ConnectFailureReason::LoggedOut,
},
crate::types::events::LoggedOut::builder()
.on_connect(false)
.reason(ConnectFailureReason::LoggedOut)
.build(),
));
should_disconnect = true;
}
Expand All @@ -1259,20 +1261,20 @@ impl Client {
self.expected_disconnect.store(true, Ordering::Relaxed);
self.enable_auto_reconnect.store(false, Ordering::Relaxed);
self.core.event_bus.dispatch(Event::LoggedOut(
crate::types::events::LoggedOut {
on_connect: false,
reason: ConnectFailureReason::LoggedOut,
},
crate::types::events::LoggedOut::builder()
.on_connect(false)
.reason(ConnectFailureReason::LoggedOut)
.build(),
));
should_disconnect = true;
}
"409" => {
info!("Got 409 stream error (conflict). Another session replaced this one.");
self.expected_disconnect.store(true, Ordering::Relaxed);
self.enable_auto_reconnect.store(false, Ordering::Relaxed);
self.core
.event_bus
.dispatch(Event::StreamReplaced(crate::types::events::StreamReplaced));
self.core.event_bus.dispatch(Event::StreamReplaced(
crate::types::events::StreamReplaced::builder().build(),
));
should_disconnect = true;
}
"429" => {
Expand Down Expand Up @@ -1332,10 +1334,10 @@ impl Client {
warn!("Unknown stream error: {}", DisplayableNodeRef(node));
}
self.core.event_bus.dispatch(Event::StreamError(
crate::types::events::StreamError {
code: code.to_string(),
raw: Some(node.to_owned()),
},
crate::types::events::StreamError::builder()
.code(code.to_string())
.raw(node.to_owned())
.build(),
));
}
}
Expand Down Expand Up @@ -1387,10 +1389,10 @@ impl Client {
self.core
.event_bus
.dispatch(wacore::types::events::Event::LoggedOut(
crate::types::events::LoggedOut {
on_connect: true,
reason,
},
crate::types::events::LoggedOut::builder()
.on_connect(true)
.reason(reason)
.build(),
));
} else if let ConnectFailureReason::TempBanned = reason {
let ban_code = attrs.optional_u64("code").unwrap_or(0) as i32;
Expand All @@ -1401,29 +1403,25 @@ impl Client {
"Temporary ban connect failure: {}",
DisplayableNodeRef(node)
);
self.core
.event_bus
.dispatch(Event::TemporaryBan(crate::types::events::TemporaryBan {
code: crate::types::events::TempBanReason::from(ban_code),
expire: expire_duration,
}));
self.core.event_bus.dispatch(Event::TemporaryBan(
crate::types::events::TemporaryBan::builder()
.code(crate::types::events::TempBanReason::from(ban_code))
.expire(expire_duration)
.build(),
));
} else if let ConnectFailureReason::ClientOutdated = reason {
error!("Client is outdated and was rejected by server.");
self.core
.event_bus
.dispatch(Event::ClientOutdated(crate::types::events::ClientOutdated));
self.core.event_bus.dispatch(Event::ClientOutdated(
crate::types::events::ClientOutdated::builder().build(),
));
} else {
warn!("Unknown connect failure: {}", DisplayableNodeRef(node));
self.core.event_bus.dispatch(Event::ConnectFailure(
crate::types::events::ConnectFailure {
reason,
message: attrs
.optional_string("message")
.as_deref()
.unwrap_or("")
.to_string(),
raw: Some(node.to_owned()),
},
crate::types::events::ConnectFailure::builder()
.reason(reason)
.maybe_message(attrs.optional_string("message").map(|m| m.into_owned()))
.raw(node.to_owned())
.build(),
));
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/client/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ impl Client {
None => {}
}
self.offline_sync_notifier.notify(usize::MAX);
self.core
.event_bus
.dispatch(Event::OfflineSyncCompleted(OfflineSyncCompleted { count }));
self.core.event_bus.dispatch(Event::OfflineSyncCompleted(
OfflineSyncCompleted::builder().count(count).build(),
));
}

/// Wait for offline message delivery to complete (with timeout).
Expand Down
19 changes: 9 additions & 10 deletions src/handlers/ib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,15 @@ async fn handle_ib_impl(client: Arc<Client>, node: &wacore_binary::NodeRef<'_>)
total, messages, notifications, receipts, app_data_changes,
);

client
.core
.event_bus
.dispatch(Event::OfflineSyncPreview(OfflineSyncPreview {
total,
app_data_changes,
messages,
notifications,
receipts,
}));
client.core.event_bus.dispatch(Event::OfflineSyncPreview(
OfflineSyncPreview::builder()
.total(total)
.app_data_changes(app_data_changes)
.messages(messages)
.notifications(notifications)
.receipts(receipts)
.build(),
));

// Drive pull-based delivery: without this the server stops
// after the ~5-stanza primer and the rest of the backlog is
Expand Down
55 changes: 30 additions & 25 deletions src/handlers/notification/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,11 @@ pub(crate) async fn handle_identity_change(client: &Arc<Client>, node: &NodeRef<

// = addSecurityCodeChangedNotifications, which WA Web fires inside the gate.
client.core.event_bus.dispatch(Event::IdentityChange(
crate::types::events::IdentityChange {
user: from_jid.clone(),
lid_user: stanza_lid,
implicit: false,
},
crate::types::events::IdentityChange::builder()
.user(from_jid.clone())
.maybe_lid_user(stanza_lid)
.implicit(false)
.build(),
));

// Re-establish the session eagerly so the next send is fast (WA Web does this
Expand Down Expand Up @@ -397,11 +397,11 @@ pub(crate) async fn handle_local_identity_change(client: &Arc<Client>, sender: J
}

client.core.event_bus.dispatch(Event::IdentityChange(
crate::types::events::IdentityChange {
user: sender,
lid_user: None,
implicit: true,
},
crate::types::events::IdentityChange::builder()
.user(sender)
.maybe_lid_user(None)
.implicit(true)
.build(),
));
}

Expand Down Expand Up @@ -481,21 +481,26 @@ pub(crate) async fn handle_devices_notification(client: &Arc<Client>, node: &Nod
}

// Dispatch event to notify application layer
let event = Event::DeviceListUpdate(DeviceListUpdate {
user: notification.from.clone(),
lid_user: notification.lid_user.clone(),
update_type: op.operation_type.into(),
devices: op
.devices
.iter()
.map(|d| DeviceNotificationInfo {
device_id: d.device_id(),
key_index: d.key_index,
})
.collect(),
key_index: op.key_index.clone(),
contact_hash: op.contact_hash.clone(),
});
let event = Event::DeviceListUpdate(
DeviceListUpdate::builder()
.user(notification.from.clone())
.maybe_lid_user(notification.lid_user.clone())
.update_type(op.operation_type.into())
.devices(
op.devices
.iter()
.map(|d| {
DeviceNotificationInfo::builder()
.device_id(d.device_id())
.maybe_key_index(d.key_index)
.build()
})
.collect(),
)
.maybe_key_index(op.key_index.clone())
.maybe_contact_hash(op.contact_hash.clone())
.build(),
);
client.core.event_bus.dispatch(event);
}

Expand Down
Loading
Loading