diff --git a/src/features/chat_actions.rs b/src/features/chat_actions.rs index 9f413fb28..b1970b6ea 100644 --- a/src/features/chat_actions.rs +++ b/src/features/chat_actions.rs @@ -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::{ @@ -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, diff --git a/src/handlers/notification.rs b/src/handlers/notification.rs old mode 100644 new mode 100755 index 0ec75df55..d848cbd12 --- a/src/handlers/notification.rs +++ b/src/handlers/notification.rs @@ -849,7 +849,7 @@ async fn handle_business_notification(client: &Arc, 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, @@ -1045,8 +1045,8 @@ fn notification_timestamp(node: &NodeRef<'_>) -> chrono::DateTime { 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. @@ -1171,7 +1171,7 @@ async fn handle_contacts_notification(client: &Arc, 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)); debug!( target: "Client/Contacts", @@ -1220,8 +1220,8 @@ async fn handle_group_notification(client: &Arc, node: Arc 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 @@ -1396,7 +1396,8 @@ fn handle_disappearing_mode_notification(client: &Arc, node: &NodeRef<'_ // WA Web: `t.attrTime("t")` — required, no default. let Some(setting_timestamp) = dm_attrs .optional_string("t") - .and_then(|s| s.parse::().ok()) + .and_then(|s| s.parse::().ok()) + .and_then(wacore::time::from_secs) else { warn!( "disappearing_mode notification missing or invalid 't' attribute: {}", @@ -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 @@ -1707,7 +1708,8 @@ mod tests { .unwrap_or(0); let setting_timestamp = dm_attrs .optional_string("t") - .and_then(|s| s.parse::().ok())?; + .and_then(|s| s.parse::().ok()) + .filter(|&t| wacore::time::from_secs(t).is_some())?; Some((duration, setting_timestamp)) } diff --git a/src/handlers/presence.rs b/src/handlers/presence.rs index 65f70c6f5..71ca1ac8b 100644 --- a/src/handlers/presence.rs +++ b/src/handlers/presence.rs @@ -44,7 +44,7 @@ impl StanzaHandler for PresenceHandler { .get_attr("last") .map(|v| v.as_str()) .and_then(|s| s.parse::().ok()) - .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); + .and_then(wacore::time::from_secs); debug!( target: "PresenceHandler", diff --git a/src/pdo.rs b/src/pdo.rs index 411e29529..abccbae30 100644 --- a/src/pdo.rs +++ b/src/pdo.rs @@ -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(), diff --git a/src/version.rs b/src/version.rs index c6d629a4c..bb1ede996 100644 --- a/src/version.rs +++ b/src/version.rs @@ -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) diff --git a/wacore/src/messages.rs b/wacore/src/messages.rs index 4478c851f..d9bcd2d6f 100644 --- a/wacore/src/messages.rs +++ b/wacore/src/messages.rs @@ -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") diff --git a/wacore/src/stanza/notification.rs b/wacore/src/stanza/notification.rs index bd9fec11d..bf25bfff2 100644 --- a/wacore/src/stanza/notification.rs +++ b/wacore/src/stanza/notification.rs @@ -20,7 +20,7 @@ use wacore_binary::Node; pub fn notification_timestamp(node: &Node) -> chrono::DateTime { 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) } diff --git a/wacore/src/time.rs b/wacore/src/time.rs index 80695f904..a09986f07 100644 --- a/wacore/src/time.rs +++ b/wacore/src/time.rs @@ -52,6 +52,34 @@ pub fn now_utc() -> chrono::DateTime { .expect("time provider returned out-of-range millisecond timestamp") } +/// Convert a Unix timestamp (seconds) to `DateTime`. +/// Returns `None` for out-of-range values. +#[inline] +pub fn from_secs(ts: i64) -> Option> { + chrono::DateTime::from_timestamp(ts, 0) +} + +/// Convert a Unix timestamp (seconds) to `DateTime`, +/// falling back to `now_utc()` for out-of-range values. +#[inline] +pub fn from_secs_or_now(ts: i64) -> chrono::DateTime { + from_secs(ts).unwrap_or_else(now_utc) +} + +/// Convert a Unix timestamp (milliseconds) to `DateTime`. +/// Returns `None` for out-of-range values. +#[inline] +pub fn from_millis(ts: i64) -> Option> { + chrono::DateTime::from_timestamp_millis(ts) +} + +/// Convert a Unix timestamp (milliseconds) to `DateTime`, +/// falling back to `now_utc()` for out-of-range values. +#[inline] +pub fn from_millis_or_now(ts: i64) -> chrono::DateTime { + from_millis(ts).unwrap_or_else(now_utc) +} + /// Portable monotonic instant, replacing `std::time::Instant` which is /// unavailable on `wasm32-unknown-unknown`. /// diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs old mode 100644 new mode 100755 index e2da1dc3c..6f2fad501 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -316,9 +316,11 @@ impl From 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, #[serde(skip_serializing_if = "Option::is_none")] pub target_jid: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -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, } #[derive(Debug, Clone, Serialize)] @@ -451,6 +454,7 @@ impl Event { /// 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, } @@ -706,6 +710,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>, @@ -729,6 +734,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, @@ -744,6 +750,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, } @@ -804,6 +811,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, pub action: Box, @@ -812,6 +820,7 @@ pub struct ContactUpdate { #[derive(Debug, Clone, Serialize)] pub struct PushNameUpdate { + /// The contact who changed their push name. pub jid: Jid, pub message: Box, pub old_push_name: String, @@ -820,6 +829,7 @@ pub struct PushNameUpdate { #[derive(Debug, Clone, Serialize)] pub struct PinUpdate { + /// The chat being pinned or unpinned. pub jid: Jid, pub timestamp: DateTime, pub action: Box, @@ -828,6 +838,7 @@ pub struct PinUpdate { #[derive(Debug, Clone, Serialize)] pub struct MuteUpdate { + /// The chat being muted or unmuted. pub jid: Jid, pub timestamp: DateTime, pub action: Box, @@ -836,6 +847,7 @@ pub struct MuteUpdate { #[derive(Debug, Clone, Serialize)] pub struct ArchiveUpdate { + /// The chat being archived or unarchived. pub jid: Jid, pub timestamp: DateTime, pub action: Box, @@ -844,6 +856,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"`). @@ -857,6 +870,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, pub action: Box, @@ -865,6 +879,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, @@ -875,6 +890,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, pub message_id: String,