Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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