Skip to content
3 changes: 1 addition & 2 deletions src/features/chat_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use crate::appstate_sync::Mutation;
use crate::client::Client;
use anyhow::Result;
use chrono::DateTime;
use log::debug;
use wacore::appstate::patch_decode::WAPatchName;
use wacore::types::events::{
Expand Down Expand Up @@ -89,7 +88,7 @@ pub(crate) fn dispatch_chat_mutation(
.as_ref()
.and_then(|v| v.timestamp)
.unwrap_or(0);
let time = DateTime::from_timestamp_millis(ts).unwrap_or_else(wacore::time::now_utc);
let time = wacore::time::from_millis_or_now(ts);
let jid: Jid = if m.index.len() > 1 {
match m.index[1].parse() {
Ok(j) => j,
Expand Down
20 changes: 11 additions & 9 deletions src/handlers/notification.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ async fn handle_business_notification(client: &Arc<Client>, node: &NodeRef<'_>)
let event = Event::BusinessStatusUpdate(BusinessStatusUpdate {
jid: notification.from.clone(),
update_type,
timestamp: notification.timestamp,
timestamp: wacore::time::from_secs_or_now(notification.timestamp),
target_jid: notification.jid.clone(),
hash: notification.hash.clone(),
verified_name,
Expand Down Expand Up @@ -1045,8 +1045,8 @@ fn notification_timestamp(node: &NodeRef<'_>) -> chrono::DateTime<chrono::Utc> {
node.attrs()
.optional_u64("t")
.and_then(|t| i64::try_from(t).ok())
.and_then(|t| chrono::DateTime::from_timestamp(t, 0))
.unwrap_or_else(chrono::Utc::now)
.and_then(wacore::time::from_secs)
.unwrap_or_else(wacore::time::now_utc)
}

/// Learn LID-PN mappings from a contacts modify notification.
Expand Down Expand Up @@ -1171,7 +1171,7 @@ async fn handle_contacts_notification(client: &Arc<Client>, node: &NodeRef<'_>)
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| chrono::DateTime::from_timestamp(after as i64, 0));
.and_then(|after| wacore::time::from_secs(after as i64));
Comment on lines 1171 to +1174

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Read the file around lines 1171-1174 to see the actual code context
head -n 1180 src/handlers/notification.rs | tail -n 20

Repository: jlucaso1/whatsapp-rust

Length of output: 689


🏁 Script executed:

# Search for the wacore::time module to understand from_secs
rg "from_secs" -A 3 --type rs

Repository: jlucaso1/whatsapp-rust

Length of output: 92


🏁 Script executed:

# Check if there's a wacore crate in the project and inspect time module
fd -e rs -x grep -l "wacore" | head -5

Repository: jlucaso1/whatsapp-rust

Length of output: 257


🏁 Script executed:

# Look for the definition of from_secs to understand what it accepts
rg "fn from_secs" -A 5 --type rs

Repository: jlucaso1/whatsapp-rust

Length of output: 92


🏁 Script executed:

# Search for from_secs definition without type flag
grep -r "fn from_secs" --include="*.rs"

Repository: jlucaso1/whatsapp-rust

Length of output: 240


🏁 Script executed:

# Check the wacore module structure
find wacore -name "*.rs" -type f | head -20

Repository: jlucaso1/whatsapp-rust

Length of output: 681


🏁 Script executed:

# Look at the time module in wacore
grep -r "from_secs" wacore --include="*.rs" -B 2 -A 10

Repository: jlucaso1/whatsapp-rust

Length of output: 7301


🏁 Script executed:

# Check if there are any tests or usages that show from_secs behavior
grep -r "from_secs" --include="*.rs" -B 2 -A 5 | head -60

Repository: jlucaso1/whatsapp-rust

Length of output: 3823


Avoid u64 as i64 for after to reject oversized timestamps.

Rust wraps on overflow, so a malformed after above i64::MAX becomes a negative timestamp (before 1970) instead of None, likely unintended for a sync request attribute.

💡 Suggested fix
             let after = child
                 .attrs()
                 .optional_u64("after")
-                .and_then(|after| wacore::time::from_secs(after as i64));
+                .and_then(|after| i64::try_from(after).ok())
+                .and_then(wacore::time::from_secs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| chrono::DateTime::from_timestamp(after as i64, 0));
.and_then(|after| wacore::time::from_secs(after as i64));
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| i64::try_from(after).ok())
.and_then(wacore::time::from_secs);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 1171 - 1174, The current
conversion uses `u64 as i64` which wraps on overflow; change the `after`
handling (starting from `child.attrs().optional_u64("after")`) to safely reject
values > i64::MAX by using a fallible conversion (e.g., `try_into()`/`checked_*`
or an explicit `if after <= i64::MAX as u64`) and only call
`wacore::time::from_secs` with the safely converted `i64`; return `None` for
oversized `after` values so malformed timestamps are rejected instead of
wrapping.


debug!(
target: "Client/Contacts",
Expand Down Expand Up @@ -1220,8 +1220,8 @@ async fn handle_group_notification(client: &Arc<Client>, node: Arc<OwnedNodeRef>

let timestamp = i64::try_from(notification.timestamp)
.ok()
.and_then(|t| chrono::DateTime::from_timestamp(t, 0))
.unwrap_or_else(chrono::Utc::now);
.and_then(wacore::time::from_secs)
.unwrap_or_else(wacore::time::now_utc);

for action in notification.actions {
// Granularly patch group cache instead of invalidating — matches WA Web's
Expand Down Expand Up @@ -1396,7 +1396,8 @@ fn handle_disappearing_mode_notification(client: &Arc<Client>, node: &NodeRef<'_
// WA Web: `t.attrTime("t")` — required, no default.
let Some(setting_timestamp) = dm_attrs
.optional_string("t")
.and_then(|s| s.parse::<u64>().ok())
.and_then(|s| s.parse::<i64>().ok())
.and_then(wacore::time::from_secs)
else {
warn!(
"disappearing_mode notification missing or invalid 't' attribute: {}",
Expand Down Expand Up @@ -1698,7 +1699,7 @@ mod tests {
/// Helper: parse a disappearing_mode notification node the same way
/// the handler does, returning `(duration, setting_timestamp)` or `None`
/// on validation failure.
fn parse_disappearing_mode(node: &Node) -> Option<(u32, u64)> {
fn parse_disappearing_mode(node: &Node) -> Option<(u32, i64)> {
let dm_node = node.get_optional_child("disappearing_mode")?;
let mut dm_attrs = dm_node.attrs();
let duration = dm_attrs
Expand All @@ -1707,7 +1708,8 @@ mod tests {
.unwrap_or(0);
let setting_timestamp = dm_attrs
.optional_string("t")
.and_then(|s| s.parse::<u64>().ok())?;
.and_then(|s| s.parse::<i64>().ok())
.filter(|&t| wacore::time::from_secs(t).is_some())?;
Some((duration, setting_timestamp))
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl StanzaHandler for PresenceHandler {
.get_attr("last")
.map(|v| v.as_str())
.and_then(|s| s.parse::<i64>().ok())
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0));
.and_then(wacore::time::from_secs);

debug!(
target: "PresenceHandler",
Expand Down
11 changes: 8 additions & 3 deletions src/history_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,15 @@ impl Client {
self.store_tc_token_from_conversation_bytes(&raw_bytes)
.await;

// Wrap Bytes in LazyConversation using from_bytes (true zero-copy)
// Parsing only happens if the event handler calls .conversation() or .get()
let lazy_conv = LazyConversation::from_bytes(raw_bytes);
self.core.event_bus.dispatch(Event::JoinedGroup(lazy_conv));
if let Some(conv) = lazy_conv.get()
&& let Ok(group_jid) = conv.id.parse()
{
self.core.event_bus.dispatch(Event::JoinedGroup {
group_jid,
conversation: lazy_conv,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JoinedGroup still accepts non-group chats and forces eager decode.

conv.id.parse() only proves the ID is syntactically valid, so 1:1/newsletter conversations will still be emitted as Event::JoinedGroup. That also makes the JID extractor in wacore/src/types/events.rs:469-476 return a non-group primary JID for this variant. On top of that, lazy_conv.get() fully decodes every conversation here, which defeats the lazy path documented in this file. Reuse a partial decode for id and gate on group_jid.is_group() before dispatching.

💡 Suggested fix
-                let lazy_conv = LazyConversation::from_bytes(raw_bytes);
-                if let Some(conv) = lazy_conv.get()
-                    && let Ok(group_jid) = conv.id.parse()
-                {
+                if let Ok(conv) =
+                    <ConversationTcTokenFields as prost::Message>::decode(raw_bytes.clone())
+                    && let Ok(group_jid) = conv.id.parse::<wacore_binary::Jid>()
+                    && group_jid.is_group()
+                {
+                    let lazy_conv = LazyConversation::from_bytes(raw_bytes);
                     self.core.event_bus.dispatch(Event::JoinedGroup {
                         group_jid,
                         conversation: lazy_conv,
                     });
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/history_sync.rs` around lines 223 - 229, The code dispatches
Event::JoinedGroup based only on conv.id.parse() and eagerly calls
lazy_conv.get(), which can emit non-group chats and defeats the lazy decode;
change to parse the ID from the partially-decoded conv (reuse the existing
partial decode used to access conv.id) into group_jid, check
group_jid.is_group() and only then call lazy_conv.get() to fully decode the
conversation and dispatch Event::JoinedGroup ({ group_jid, conversation:
lazy_conv }), ensuring non-group JIDs are skipped and lazy decoding is
preserved; use the same symbols conv.id.parse(), group_jid.is_group(),
lazy_conv.get(), and Event::JoinedGroup to locate where to apply this gating.

}
}

// Drop receiver before awaiting the blocking task. If we broke out
Expand Down
6 changes: 2 additions & 4 deletions src/pdo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,10 +413,8 @@ impl Client {

let timestamp = web_msg
.message_timestamp
.map(|ts| {
chrono::DateTime::from_timestamp(ts as i64, 0).unwrap_or_else(chrono::Utc::now)
})
.unwrap_or_else(chrono::Utc::now);
.map(|ts| wacore::time::from_secs_or_now(ts as i64))
.unwrap_or_else(wacore::time::now_utc);

Ok(MessageInfo {
id: key.id.clone().unwrap_or_default(),
Expand Down
2 changes: 1 addition & 1 deletion src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub async fn resolve_and_update_version(
let needs_fetch = if last_fetched_ms == 0 {
true
} else {
match chrono::DateTime::from_timestamp_millis(last_fetched_ms) {
match wacore::time::from_millis(last_fetched_ms) {
Some(last_fetched_dt) => {
wacore::time::now_utc().signed_duration_since(last_fetched_dt)
> chrono::Duration::hours(24)
Expand Down
3 changes: 1 addition & 2 deletions wacore/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,7 @@ pub fn parse_message_info(
.optional_string("notify")
.map(|s| s.to_string())
.unwrap_or_default(),
timestamp: chrono::DateTime::from_timestamp(attrs.unix_time("t"), 0)
.unwrap_or_else(chrono::Utc::now),
timestamp: crate::time::from_secs_or_now(attrs.unix_time("t")),
category,
edit: attrs
.optional_string("edit")
Expand Down
2 changes: 1 addition & 1 deletion wacore/src/stanza/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use wacore_binary::Node;
pub fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
node.attrs()
.optional_u64("t")
.and_then(|t| chrono::DateTime::from_timestamp(t as i64, 0))
.and_then(|t| crate::time::from_secs(t as i64))
.unwrap_or_else(crate::time::now_utc)
Comment on lines 21 to 24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Rust, what happens when you convert u64toi64withasand the value is greater thani64::MAX? Does it wrap, saturate, or error?

💡 Result:

When converting a u64 to i64 using the as cast where the u64 value exceeds i64::MAX (9223372036854775807), it performs a reinterpretation of the bit pattern. Since both types are 64 bits and Rust uses two's complement for signed integers, casting u64 to i64 is a no-op on the bits—it reinterprets the unsigned bits as a signed value. For values <= i64::MAX, the numerical value is preserved. For values > i64::MAX, the result is a negative i64. Specifically, u64::MAX (all bits 1) becomes -1i64, as confirmed by the official documentation: u64::cast_signed example shows u64::MAX.cast_signed == -1i64, and this produces the same result as as. This is neither saturation (clamping to i64::MAX), nor does it error or panic. It "wraps" in the sense of bit reinterpretation, producing a negative number equivalent to the two's complement interpretation. Official Rust Reference confirms: "Casting between two integers of the same size (e.g. i32 -> u32) is a no-op (Rust uses 2's complement for negative numbers)." Rust By Example notes: "When casting to a signed type, the (bitwise) result is the same as first casting to the corresponding unsigned type." It does not panic on overflow like arithmetic operations in debug mode; casts are always defined and do not trigger overflow checks.

Citations:


🏁 Script executed:

cat -n wacore/src/stanza/notification.rs | head -50

Repository: jlucaso1/whatsapp-rust

Length of output: 2503


🏁 Script executed:

rg "parse_disappearing_mode" wacore/src/stanza/notification.rs -A 10

Repository: jlucaso1/whatsapp-rust

Length of output: 2290


🏁 Script executed:

rg "fn from_secs" wacore/src/time.rs -A 5

Repository: jlucaso1/whatsapp-rust

Length of output: 521


🏁 Script executed:

rg "notification_timestamp" wacore/src -B 2 -A 5

Repository: jlucaso1/whatsapp-rust

Length of output: 1924


🏁 Script executed:

rg "from_timestamp" wacore/src -B 2 -A 2

Repository: jlucaso1/whatsapp-rust

Length of output: 813


🏁 Script executed:

# Check if there are any tests for notification_timestamp that show the wrapping issue
rg "test.*notification_timestamp" wacore/src/stanza/notification.rs -A 15

Repository: jlucaso1/whatsapp-rust

Length of output: 48


Use signed parsing or checked conversion for the timestamp.

The current code uses optional_u64("t") followed by as i64. When the attribute contains a value larger than i64::MAX (e.g., 18446744073709551615), the cast reinterprets the bit pattern as a negative i64 (e.g., -1). Since chrono::DateTime::from_timestamp() accepts negative timestamps (valid pre-1970 dates), the conversion succeeds instead of returning None, and the invalid input produces a 1969 timestamp rather than falling back to now_utc().

Suggested change
 pub fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
     node.attrs()
-        .optional_u64("t")
-        .and_then(|t| crate::time::from_secs(t as i64))
+        .optional_string("t")
+        .and_then(|t| t.parse::<i64>().ok())
+        .and_then(crate::time::from_secs)
         .unwrap_or_else(crate::time::now_utc)
 }

Parsing as i64 will reject out-of-range values, triggering the fallback as intended. No test currently covers this edge case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/stanza/notification.rs` around lines 21 - 24, Replace the
unsigned-to-signed unchecked cast that causes wraparound: instead of calling
node.attrs().optional_u64("t").and_then(|t| crate::time::from_secs(t as
i64)).unwrap_or_else(crate::time::now_utc), parse or convert the attribute into
a signed i64 safely (e.g., use an optional_i64 accessor or try_from/checked
conversion on the u64) so out-of-range u64 values fail the conversion and
trigger the fallback to crate::time::now_utc; locate the code around
node.attrs(), optional_u64("t"), crate::time::from_secs and crate::time::now_utc
and change the conversion to a checked/validated i64 path.

}

Expand Down
28 changes: 28 additions & 0 deletions wacore/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,34 @@ pub fn now_utc() -> chrono::DateTime<chrono::Utc> {
.expect("time provider returned out-of-range millisecond timestamp")
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_secs(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp(ts, 0)
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_secs_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_secs(ts).unwrap_or_else(now_utc)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_millis(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp_millis(ts)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_millis_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_millis(ts).unwrap_or_else(now_utc)
}

/// Portable monotonic instant, replacing `std::time::Instant` which is
/// unavailable on `wasm32-unknown-unknown`.
///
Expand Down
86 changes: 80 additions & 6 deletions wacore/src/types/events.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,11 @@ impl From<crate::stanza::business::BusinessNotificationType> for BusinessUpdateT
/// Business status update notification.
#[derive(Debug, Clone, Serialize)]
pub struct BusinessStatusUpdate {
/// The business account whose status changed.
pub jid: Jid,
pub update_type: BusinessUpdateType,
pub timestamp: i64,
#[serde(with = "chrono::serde::ts_seconds")]
pub timestamp: DateTime<Utc>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#[serde(skip_serializing_if = "Option::is_none")]
pub target_jid: Option<Jid>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -344,9 +346,10 @@ pub struct DisappearingModeChanged {
pub from: Jid,
/// New duration in seconds (0 = disabled, 86400 = 24h, etc.).
pub duration: u32,
/// Unix timestamp (seconds) when the setting was changed.
/// Consumers should only apply this if it's newer than their stored timestamp.
pub setting_timestamp: u64,
/// When the setting was changed.
/// Consumers should only apply this if it's newer than their stored value.
#[serde(with = "chrono::serde::ts_seconds")]
pub setting_timestamp: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize)]
Expand Down Expand Up @@ -386,7 +389,10 @@ pub enum Event {
ContactNumberChanged(ContactNumberChanged),
ContactSyncRequested(ContactSyncRequested),

JoinedGroup(LazyConversation),
JoinedGroup {
group_jid: Jid,
conversation: LazyConversation,
},
/// Group metadata/settings/participant change from w:gp2 notification.
GroupUpdate(GroupUpdate),
ContactUpdate(ContactUpdate),
Expand Down Expand Up @@ -445,12 +451,67 @@ impl Event {
let (msg, _) = self.as_message()?;
msg.conversation.as_deref()
}

/// Returns the primary JID associated with this event, if any.
///
/// Useful for routing or filtering events without exhaustively matching every variant.
/// Returns `None` for connection-lifecycle and sync events that have no associated JID.
pub fn primary_jid(&self) -> Option<&Jid> {
match self {
Event::Message(_, info) => Some(&info.source.chat),
Event::Receipt(r) => Some(&r.source.chat),
Event::UndecryptableMessage(u) => Some(&u.info.source.chat),
Event::ChatPresence(c) => Some(&c.source.chat),
Event::Presence(p) => Some(&p.from),
Event::PictureUpdate(p) => Some(&p.jid),
Event::UserAboutUpdate(u) => Some(&u.jid),
Event::ContactUpdated(c) => Some(&c.jid),
Event::ContactNumberChanged(c) => Some(&c.new_jid),
Event::GroupUpdate(g) => Some(&g.group_jid),
Event::JoinedGroup { group_jid, .. } => Some(group_jid),
Event::ContactUpdate(c) => Some(&c.jid),
Event::PushNameUpdate(p) => Some(&p.jid),
Event::PinUpdate(p) => Some(&p.jid),
Event::MuteUpdate(m) => Some(&m.jid),
Event::ArchiveUpdate(a) => Some(&a.jid),
Event::StarUpdate(s) => Some(&s.chat_jid),
Event::MarkChatAsReadUpdate(m) => Some(&m.jid),
Event::DeleteChatUpdate(d) => Some(&d.jid),
Event::DeleteMessageForMeUpdate(d) => Some(&d.chat_jid),
Event::BusinessStatusUpdate(b) => Some(&b.jid),
Event::DisappearingModeChanged(d) => Some(&d.from),
Event::NewsletterLiveUpdate(n) => Some(&n.newsletter_jid),
Event::DeviceListUpdate(d) => Some(&d.user),
Event::IdentityChange(i) => Some(&i.user),
Event::Connected(_)
| Event::Disconnected(_)
| Event::PairSuccess(_)
| Event::PairError(_)
| Event::LoggedOut(_)
| Event::PairingQrCode { .. }
| Event::PairingCode { .. }
| Event::QrScannedWithoutMultidevice(_)
| Event::ClientOutdated(_)
| Event::SelfPushNameUpdated(_)
| Event::HistorySync(_)
| Event::OfflineSyncPreview(_)
| Event::OfflineSyncCompleted(_)
| Event::StreamReplaced(_)
| Event::TemporaryBan(_)
| Event::ConnectFailure(_)
| Event::StreamError(_)
| Event::ContactSyncRequested(_)
| Event::Notification(_)
| Event::RawNode(_) => None,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

primary_jid() skips pairing-result events that already carry account JIDs.

PairSuccess and PairError both contain id/lid, but they currently fall through to None. That makes generic JID-based routing miss two public variants even though this helper is documented as the primary JID extractor. Either surface one of those identifiers here or narrow the doc comment so the contract matches the behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/types/events.rs` around lines 455 - 506, The primary_jid() helper
currently returns None for PairSuccess and PairError even though those variants
carry account JIDs; update the match in primary_jid() to return those IDs (e.g.,
match Event::PairSuccess(p) => Some(&p.id) and Event::PairError(e) =>
Some(&e.lid)) so JID-based routing includes these variants, or alternatively
update the doc comment to explicitly exclude pairing-result events if you prefer
not to expose those fields.

}
}

/// A newsletter live update notification, typically containing updated
/// reaction counts for one or more messages.
#[derive(Debug, Clone, Serialize)]
pub struct NewsletterLiveUpdate {
/// The newsletter channel this update belongs to.
pub newsletter_jid: Jid,
pub messages: Vec<NewsletterLiveUpdateMessage>,
}
Expand Down Expand Up @@ -706,6 +767,7 @@ pub struct ChatPresenceUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct PresenceUpdate {
/// The contact whose presence changed.
pub from: Jid,
pub unavailable: bool,
pub last_seen: Option<DateTime<Utc>>,
Expand All @@ -729,6 +791,7 @@ pub struct PictureUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct UserAboutUpdate {
/// The contact whose about text changed.
pub jid: Jid,
pub status: String,
pub timestamp: DateTime<Utc>,
Expand All @@ -744,6 +807,7 @@ pub struct UserAboutUpdate {
/// sync mutations (different source, different payload).
#[derive(Debug, Clone, Serialize)]
pub struct ContactUpdated {
/// The contact whose profile was updated.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
}
Expand Down Expand Up @@ -804,6 +868,7 @@ pub struct GroupUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct ContactUpdate {
/// The chat/contact this sync action applies to.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::ContactAction>,
Expand All @@ -812,14 +877,17 @@ pub struct ContactUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct PushNameUpdate {
/// The contact who changed their push name.
pub jid: Jid,
pub message: Box<MessageInfo>,
pub message: Arc<MessageInfo>,
pub old_push_name: String,
pub new_push_name: String,
pub from_full_sync: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct PinUpdate {
/// The chat being pinned or unpinned.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::PinAction>,
Expand All @@ -828,6 +896,7 @@ pub struct PinUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct MuteUpdate {
/// The chat being muted or unmuted.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::MuteAction>,
Expand All @@ -836,6 +905,7 @@ pub struct MuteUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct ArchiveUpdate {
/// The chat being archived or unarchived.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::ArchiveChatAction>,
Expand All @@ -844,6 +914,7 @@ pub struct ArchiveUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct StarUpdate {
/// The chat containing the starred or unstarred message.
pub chat_jid: Jid,
/// The participant who sent the message. `Some` for group messages from
/// others, `None` for self-authored or 1-on-1 messages (wire value `"0"`).
Expand All @@ -857,6 +928,7 @@ pub struct StarUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct MarkChatAsReadUpdate {
/// The chat being marked as read or unread.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::MarkChatAsReadAction>,
Expand All @@ -865,6 +937,7 @@ pub struct MarkChatAsReadUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct DeleteChatUpdate {
/// The chat being deleted.
pub jid: Jid,
/// From the index, not the proto — DeleteChatAction only has messageRange.
pub delete_media: bool,
Expand All @@ -875,6 +948,7 @@ pub struct DeleteChatUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct DeleteMessageForMeUpdate {
/// The chat containing the deleted message.
pub chat_jid: Jid,
pub participant_jid: Option<Jid>,
pub message_id: String,
Expand Down
Loading