diff --git a/src/cache_config.rs b/src/cache_config.rs index 2b73c535c..49eb186ce 100644 --- a/src/cache_config.rs +++ b/src/cache_config.rs @@ -1,6 +1,13 @@ -use moka::future::Cache; +use std::fmt::Display; +use std::sync::Arc; use std::time::Duration; +use moka::future::Cache; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::cache_store::TypedCache; +pub use wacore::store::cache::CacheStore; + /// Configuration for a single cache instance. /// /// Controls the expiry timeout and maximum capacity of a moka cache. @@ -35,6 +42,23 @@ impl CacheEntryConfig { builder.build() } + /// Build a [`TypedCache`] with TTL semantics, using the custom store if + /// provided or falling back to an in-process moka cache. + pub(crate) fn build_typed_ttl( + &self, + store: Option>, + namespace: &'static str, + ) -> TypedCache + where + K: std::hash::Hash + Eq + Display + Send + Sync + 'static, + V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, + { + match store { + Some(s) => TypedCache::from_store(s, namespace, self.timeout), + None => TypedCache::from_moka(self.build_with_ttl()), + } + } + /// Build a moka Cache using time_to_idle semantics. pub(crate) fn build_with_tti(&self) -> Cache where @@ -49,12 +73,64 @@ impl CacheEntryConfig { } } +/// Per-cache custom store overrides. +/// +/// Each field is an optional [`CacheStore`] for that specific cache. When +/// `None`, the default in-process moka cache is used. +/// +/// # Example — only group and device on Redis +/// +/// ```rust,ignore +/// let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379")); +/// let config = CacheConfig { +/// cache_stores: CacheStores { +/// group_cache: Some(redis.clone()), +/// device_cache: Some(redis.clone()), +/// ..Default::default() +/// }, +/// ..Default::default() +/// }; +/// ``` +#[derive(Default, Clone)] +pub struct CacheStores { + /// Custom store for group metadata cache. + pub group_cache: Option>, + /// Custom store for device list cache. + pub device_cache: Option>, + /// Custom store for device registry cache. + pub device_registry_cache: Option>, + /// Custom store for LID-PN bidirectional mapping cache. + pub lid_pn_cache: Option>, +} + +impl CacheStores { + /// Set the same [`CacheStore`] for all pluggable caches at once. + /// + /// Coordination caches (`session_locks`, `message_queues`, etc.) and the + /// signal write-behind cache always remain in-process regardless of this + /// setting. + /// + /// # Example + /// + /// ```rust,ignore + /// let stores = CacheStores::all(Arc::new(MyRedisCacheStore::new("redis://localhost:6379"))); + /// ``` + pub fn all(store: Arc) -> Self { + Self { + group_cache: Some(store.clone()), + device_cache: Some(store.clone()), + device_registry_cache: Some(store.clone()), + lid_pn_cache: Some(store), + } + } +} + /// Configuration for all client caches and resource pools. /// /// All fields default to WhatsApp Web behavior. Use `..Default::default()` to /// override only specific settings. /// -/// # Example +/// # Example — tune TTL/capacity /// /// ```rust,ignore /// use whatsapp_rust::{CacheConfig, CacheEntryConfig}; @@ -65,7 +141,24 @@ impl CacheEntryConfig { /// ..Default::default() /// }; /// ``` -#[derive(Debug, Clone)] +/// +/// # Example — Redis for group and device caches only +/// +/// ```rust,ignore +/// use std::sync::Arc; +/// use whatsapp_rust::{CacheConfig, CacheStores}; +/// +/// let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379")); +/// let config = CacheConfig { +/// cache_stores: CacheStores { +/// group_cache: Some(redis.clone()), +/// device_cache: Some(redis.clone()), +/// ..Default::default() +/// }, +/// ..Default::default() +/// }; +/// ``` +#[derive(Clone)] pub struct CacheConfig { /// Group metadata cache (time_to_live). Default: 1h TTL, 250 entries. pub group_cache: CacheEntryConfig, @@ -104,6 +197,63 @@ pub struct CacheConfig { /// TTL in seconds for sent messages in DB before periodic cleanup. /// 0 = no automatic cleanup. Default: 300 (5 minutes). pub sent_message_ttl_secs: u64, + + // --- Custom store overrides --- + /// Per-cache custom store overrides. + /// + /// For each field set to `Some(store)`, the corresponding cache uses that + /// backend instead of the default in-process moka cache. Fields left as + /// `None` keep the default moka behaviour. + /// + /// Coordination caches (`session_locks`, `message_queues`, + /// `message_enqueue_locks`), the signal write-behind cache, and + /// `pdo_pending_requests` always stay in-process — they hold live Rust + /// objects (mutexes, channel senders, oneshot senders) that cannot be + /// serialised to an external store. + pub cache_stores: CacheStores, +} + +impl std::fmt::Debug for CacheConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CacheConfig") + .field("group_cache", &self.group_cache) + .field("device_cache", &self.device_cache) + .field("device_registry_cache", &self.device_registry_cache) + .field("lid_pn_cache", &self.lid_pn_cache) + .field("retried_group_messages", &self.retried_group_messages) + .field("recent_messages", &self.recent_messages) + .field("message_retry_counts", &self.message_retry_counts) + .field("pdo_pending_requests", &self.pdo_pending_requests) + .field("session_locks_capacity", &self.session_locks_capacity) + .field("message_queues_capacity", &self.message_queues_capacity) + .field( + "message_enqueue_locks_capacity", + &self.message_enqueue_locks_capacity, + ) + .field("max_pooled_buffers", &self.max_pooled_buffers) + .field( + "max_pooled_buffer_capacity", + &self.max_pooled_buffer_capacity, + ) + .field("sent_message_ttl_secs", &self.sent_message_ttl_secs) + .field( + "cache_stores.group_cache", + &self.cache_stores.group_cache.is_some(), + ) + .field( + "cache_stores.device_cache", + &self.cache_stores.device_cache.is_some(), + ) + .field( + "cache_stores.device_registry_cache", + &self.cache_stores.device_registry_cache.is_some(), + ) + .field( + "cache_stores.lid_pn_cache", + &self.cache_stores.lid_pn_cache.is_some(), + ) + .finish() + } } impl Default for CacheConfig { @@ -126,6 +276,7 @@ impl Default for CacheConfig { max_pooled_buffers: 8, max_pooled_buffer_capacity: 256 * 1024, sent_message_ttl_secs: 300, + cache_stores: CacheStores::default(), } } } diff --git a/src/cache_store.rs b/src/cache_store.rs new file mode 100644 index 000000000..5855281ab --- /dev/null +++ b/src/cache_store.rs @@ -0,0 +1,238 @@ +//! Typed cache wrapper that dispatches to either moka (in-process) or a custom +//! [`CacheStore`] backend (e.g., Redis). +//! +//! [`TypedCache`] presents the same interface regardless of the backing store. +//! Keys are serialised via [`Display`]; values are serialised with `serde_json` +//! only on the custom-store path — the moka path has zero extra overhead. + +use std::borrow::Borrow; +use std::fmt::Display; +use std::marker::PhantomData; +use std::sync::Arc; +use std::time::Duration; + +use moka::future::Cache; +use serde::{Serialize, de::DeserializeOwned}; + +pub use wacore::store::cache::CacheStore; + +// ── Internal storage variant ────────────────────────────────────────────────── + +enum Inner { + Moka(Cache), + Custom { + store: Arc, + namespace: &'static str, + ttl: Option, + _marker: PhantomData, + }, +} + +// ── TypedCache ───────────────────────────────────────────────────────────────── + +/// A cache over `K → V` backed by either moka or any [`CacheStore`]. +/// +/// The moka path has **zero extra overhead** — values are stored in-process +/// without any serialisation. The custom-store path serialises values with +/// `serde_json` and keys via [`Display`]. +/// +/// Methods mirror moka's API so call sites need no changes. +pub struct TypedCache { + inner: Inner, +} + +impl TypedCache +where + K: std::hash::Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + /// Wrap an existing moka cache (zero overhead vs. using moka directly). + pub fn from_moka(cache: Cache) -> Self { + Self { + inner: Inner::Moka(cache), + } + } +} + +impl TypedCache +where + K: std::hash::Hash + Eq + Display + Send + Sync + 'static, + V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, +{ + /// Create a cache backed by a custom store. + /// + /// - `namespace` — unique string for this cache (e.g., `"group"`) + /// - `ttl` — forwarded to [`CacheStore::set`]; `None` means no expiry + pub fn from_store( + store: Arc, + namespace: &'static str, + ttl: Option, + ) -> Self { + Self { + inner: Inner::Custom { + store, + namespace, + ttl, + _marker: PhantomData, + }, + } + } + + /// Look up a value. + /// + /// Accepts borrowed keys (`&str` for `String`, `&Jid` for `Jid`, etc.) + /// following the same pattern as [`std::collections::HashMap::get`]. + /// + /// Cache misses and deserialisation failures both return `None`; the + /// caller re-fetches from the authoritative source. + pub async fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: std::hash::Hash + Eq + Display + ?Sized, + { + match &self.inner { + Inner::Moka(cache) => cache.get(key).await, + Inner::Custom { + store, namespace, .. + } => { + let key_str = key.to_string(); + match store.get(namespace, &key_str).await { + Ok(Some(bytes)) => serde_json::from_slice(&bytes) + .inspect_err(|e| { + log::warn!( + "TypedCache[{namespace}]: deserialise failed for {key_str}: {e}" + ); + }) + .ok(), + Ok(None) => None, + Err(e) => { + log::warn!("TypedCache[{namespace}]: get({key_str}) error: {e}"); + None + } + } + } + } + } + + /// Insert or update a value (takes ownership of key and value). + pub async fn insert(&self, key: K, value: V) { + match &self.inner { + Inner::Moka(cache) => cache.insert(key, value).await, + Inner::Custom { + store, + namespace, + ttl, + .. + } => { + let key_str = key.to_string(); + match serde_json::to_vec(&value) { + Ok(bytes) => { + if let Err(e) = store.set(namespace, &key_str, &bytes, *ttl).await { + log::warn!("TypedCache[{namespace}]: set({key_str}) error: {e}"); + } + } + Err(e) => { + log::warn!("TypedCache[{namespace}]: serialise failed for {key_str}: {e}"); + } + } + } + } + } + + /// Remove a single key. + /// + /// Accepts borrowed keys following the same pattern as `get`. + pub async fn invalidate(&self, key: &Q) + where + K: Borrow, + Q: std::hash::Hash + Eq + Display + ?Sized, + { + match &self.inner { + Inner::Moka(cache) => cache.invalidate(key).await, + Inner::Custom { + store, namespace, .. + } => { + let key_str = key.to_string(); + if let Err(e) = store.delete(namespace, &key_str).await { + log::warn!("TypedCache[{namespace}]: delete({key_str}) error: {e}"); + } + } + } + } + + /// Remove all entries. + /// + /// For the moka backend this is synchronous (matching moka's API). + /// For the custom backend this spawns a fire-and-forget task via + /// [`tokio::runtime::Handle::try_current`] to avoid panicking if + /// called outside a Tokio runtime. + pub fn invalidate_all(&self) { + match &self.inner { + Inner::Moka(cache) => cache.invalidate_all(), + Inner::Custom { + store, namespace, .. + } => { + let store = store.clone(); + let ns = *namespace; + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + handle.spawn(async move { + if let Err(e) = store.clear(ns).await { + log::warn!("TypedCache[{ns}]: clear() error: {e}"); + } + }); + } + Err(_) => { + log::warn!("TypedCache[{ns}]: clear() skipped: no Tokio runtime"); + } + } + } + } + } + + /// Remove all entries, awaiting completion for custom backends. + pub async fn clear(&self) { + match &self.inner { + Inner::Moka(cache) => cache.invalidate_all(), + Inner::Custom { + store, namespace, .. + } => { + if let Err(e) = store.clear(namespace).await { + log::warn!("TypedCache[{namespace}]: clear() error: {e}"); + } + } + } + } + + /// Run any pending internal housekeeping tasks (moka only). + /// + /// For the moka backend this ensures all writes have been applied before + /// calling [`entry_count`](Self::entry_count), which can otherwise lag. + /// For custom backends this is a no-op. + pub async fn run_pending_tasks(&self) { + if let Inner::Moka(cache) = &self.inner { + cache.run_pending_tasks().await; + } + } + + /// Approximate entry count (sync). Returns `0` for custom backends. + /// + /// For diagnostics that need custom backend counts, use + /// [`entry_count_async`](Self::entry_count_async) instead. + pub fn entry_count(&self) -> u64 { + match &self.inner { + Inner::Moka(cache) => cache.entry_count(), + Inner::Custom { .. } => 0, + } + } + + /// Approximate entry count, delegating to the custom backend if available. + pub async fn entry_count_async(&self) -> u64 { + match &self.inner { + Inner::Moka(cache) => cache.entry_count(), + Inner::Custom { + store, namespace, .. + } => store.entry_count(namespace).await.unwrap_or(0), + } + } +} diff --git a/src/client.rs b/src/client.rs index b9dc25875..26ae8c9b2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ mod lid_pn; mod sender_keys; mod sessions; +use crate::cache_store::TypedCache; use crate::handshake; use crate::lid_pn_cache::LidPnCache; use crate::pair; @@ -319,8 +320,8 @@ pub struct Client { /// preventing race conditions during queue initialization. pub(crate) message_enqueue_locks: Cache>>, - pub group_cache: OnceCell>, - pub device_cache: OnceCell>>, + pub group_cache: OnceCell>, + pub device_cache: OnceCell>>, pub(crate) retried_group_messages: Cache, pub(crate) expected_disconnect: Arc, @@ -408,7 +409,7 @@ pub struct Client { /// LRU cache for device registry (matches WhatsApp Web's 5000 entry limit). /// Maps user ID to DeviceListRecord for fast device existence checks. /// Backed by persistent storage. - pub(crate) device_registry_cache: Cache, + pub(crate) device_registry_cache: TypedCache, /// Router for dispatching stanzas to their appropriate handlers pub(crate) stanza_router: crate::handlers::router::StanzaRouter, @@ -560,7 +561,10 @@ impl Client { message_queues: Cache::builder() .max_capacity(cache_config.message_queues_capacity.max(1)) .build(), - lid_pn_cache: Arc::new(LidPnCache::with_config(&cache_config.lid_pn_cache)), + lid_pn_cache: Arc::new(LidPnCache::with_config( + &cache_config.lid_pn_cache, + cache_config.cache_stores.lid_pn_cache.clone(), + )), message_enqueue_locks: Cache::builder() .max_capacity(cache_config.message_enqueue_locks_capacity.max(1)) .build(), @@ -614,7 +618,10 @@ impl Client { custom_enc_handlers: Arc::new(DashMap::new()), chatstate_handlers: Arc::new(RwLock::new(Vec::new())), pdo_pending_requests: cache_config.pdo_pending_requests.build_with_ttl(), - device_registry_cache: cache_config.device_registry_cache.build_with_ttl(), + device_registry_cache: cache_config.device_registry_cache.build_typed_ttl( + cache_config.cache_stores.device_registry_cache.clone(), + "device_registry", + ), stanza_router: Self::create_stanza_router(), synchronous_ack: false, http_client, @@ -642,20 +649,25 @@ impl Client { (arc, rx) } - pub(crate) async fn get_group_cache(&self) -> &Cache { + pub(crate) async fn get_group_cache(&self) -> &TypedCache { self.group_cache .get_or_init(|| async { debug!("Initializing Group Cache for the first time."); - self.cache_config.group_cache.build_with_ttl() + self.cache_config + .group_cache + .build_typed_ttl(self.cache_config.cache_stores.group_cache.clone(), "group") }) .await } - pub(crate) async fn get_device_cache(&self) -> &Cache> { + pub(crate) async fn get_device_cache(&self) -> &TypedCache> { self.device_cache .get_or_init(|| async { debug!("Initializing Device Cache for the first time."); - self.cache_config.device_cache.build_with_ttl() + self.cache_config.device_cache.build_typed_ttl( + self.cache_config.cache_stores.device_cache.clone(), + "device", + ) }) .await } diff --git a/src/lib.rs b/src/lib.rs index ac4bc09f3..75ffff496 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,9 @@ pub use wacore_binary::jid::Jid; pub use waproto; pub mod cache_config; -pub use cache_config::{CacheConfig, CacheEntryConfig}; +pub use cache_config::{CacheConfig, CacheEntryConfig, CacheStores}; +pub mod cache_store; +pub use cache_store::CacheStore; pub mod http; pub mod types; diff --git a/src/lid_pn_cache.rs b/src/lid_pn_cache.rs index a5c376d16..9fe6191c2 100644 --- a/src/lid_pn_cache.rs +++ b/src/lid_pn_cache.rs @@ -14,11 +14,16 @@ //! Both maps are bounded (max 10 000 entries, 1 h idle TTL) to prevent unbounded //! memory growth in long-running sessions. -use moka::future::Cache; +use std::sync::Arc; use crate::cache_config::{CacheConfig, CacheEntryConfig}; +use crate::cache_store::TypedCache; pub use wacore::types::{LearningSource, LidPnEntry}; +/// Namespaces used in the custom store. +const NS_LID: &str = "lid_pn_by_lid"; +const NS_PN: &str = "lid_pn_by_pn"; + /// Cache for LID to Phone Number mappings. /// /// This cache maintains bidirectional mappings between LIDs and phone numbers, @@ -28,9 +33,9 @@ pub use wacore::types::{LearningSource, LidPnEntry}; /// The cache is thread-safe and can be shared across async tasks. pub struct LidPnCache { /// LID -> Entry mapping - lid_to_entry: Cache, + lid_to_entry: TypedCache, /// Phone number -> Entry mapping (stores the most recent LID for that PN) - pn_to_entry: Cache, + pn_to_entry: TypedCache, } impl Default for LidPnCache { @@ -42,14 +47,26 @@ impl Default for LidPnCache { impl LidPnCache { /// Create a new empty cache with default settings (1h idle TTL, 10000 entries). pub fn new() -> Self { - Self::with_config(&CacheConfig::default().lid_pn_cache) + Self::with_config(&CacheConfig::default().lid_pn_cache, None) } /// Create a new cache with custom configuration (uses time_to_idle semantics). - pub fn with_config(config: &CacheEntryConfig) -> Self { - Self { - lid_to_entry: config.build_with_tti(), - pn_to_entry: config.build_with_tti(), + /// + /// When `store` is `Some`, both internal maps use the custom backend. + /// When `store` is `None`, both maps use in-process moka caches. + pub fn with_config( + config: &CacheEntryConfig, + store: Option>, + ) -> Self { + match store { + Some(s) => Self { + lid_to_entry: TypedCache::from_store(s.clone(), NS_LID, config.timeout), + pn_to_entry: TypedCache::from_store(s, NS_PN, config.timeout), + }, + None => Self { + lid_to_entry: TypedCache::from_moka(config.build_with_tti()), + pn_to_entry: TypedCache::from_moka(config.build_with_tti()), + }, } } @@ -94,9 +111,14 @@ impl LidPnCache { /// For the LID -> Entry map, this always updates. /// For the PN -> Entry map, this only updates if the new entry has a /// newer or equal `created_at` timestamp (matching WhatsApp Web behavior). + /// + /// Note: the get-then-insert on the PN map is not atomic. With external + /// backends (e.g., Redis), concurrent `add()` calls for the same phone + /// number can race. This is acceptable because the cache is best-effort + /// and backed by persistent storage for correctness. pub async fn add(&self, entry: LidPnEntry) { // Check if PN map needs update first - let should_update_pn = match self.pn_to_entry.get(&entry.phone_number).await { + let should_update_pn = match self.pn_to_entry.get(entry.phone_number.as_str()).await { Some(existing) => existing.created_at <= entry.created_at, None => true, }; @@ -135,9 +157,12 @@ impl LidPnCache { } /// Clear all entries from the cache. + /// + /// Awaits the actual clear operation on custom backends (unlike + /// `invalidate_all` which is fire-and-forget). pub async fn clear(&self) { - self.lid_to_entry.invalidate_all(); - self.pn_to_entry.invalidate_all(); + self.lid_to_entry.clear().await; + self.pn_to_entry.clear().await; } /// Get the number of LID entries in the cache. @@ -307,7 +332,6 @@ mod tests { assert_eq!(cache.pn_count().await, 1); cache.clear().await; - // run_pending_tasks is called inside lid_count/pn_count assert_eq!(cache.lid_count().await, 0); assert_eq!(cache.pn_count().await, 0); assert!(cache.get_current_lid("559980000001").await.is_none()); diff --git a/wacore/src/client/context.rs b/wacore/src/client/context.rs index 19f8c101a..5e9017f8d 100644 --- a/wacore/src/client/context.rs +++ b/wacore/src/client/context.rs @@ -14,7 +14,7 @@ fn build_pn_to_lid_map(lid_to_pn_map: &HashMap) -> HashMap, pub addressing_mode: AddressingMode, diff --git a/wacore/src/store/cache.rs b/wacore/src/store/cache.rs new file mode 100644 index 000000000..ec81053d3 --- /dev/null +++ b/wacore/src/store/cache.rs @@ -0,0 +1,58 @@ +//! Pluggable cache storage trait. +//! +//! Implementations of [`CacheStore`] can back any of the client's data caches +//! (group metadata, device lists, LID-PN mappings, etc.). The default behaviour +//! uses in-process moka caches; a Redis, Memcached, or any other implementation +//! can be plugged in via [`CacheConfig`](crate::CacheConfig). + +use async_trait::async_trait; +use std::time::Duration; + +/// Backend trait for pluggable cache storage. +/// +/// Keys and values are opaque strings / bytes — the typed cache wrapper in +/// `whatsapp-rust` handles serialization via serde. +/// +/// # Namespaces +/// +/// Each logical cache uses a unique namespace string (e.g., `"group"`, +/// `"device"`, `"lid_pn_by_lid"`). Implementations should use this to +/// partition keys — for example, a Redis implementation might prefix keys +/// as `{namespace}:{key}`. +/// +/// # Error handling +/// +/// Cache operations are best-effort. The client falls back gracefully when +/// cache reads fail (treats as miss) and logs warnings on write failures. +/// Implementations should still return errors for observability. +#[async_trait] +pub trait CacheStore: Send + Sync + 'static { + /// Retrieve a cached value by namespace and key. + async fn get(&self, namespace: &str, key: &str) -> anyhow::Result>>; + + /// Store a value with an optional TTL. + /// + /// When `ttl` is `None`, the entry should persist until explicitly deleted + /// or evicted by the implementation's own policy. + async fn set( + &self, + namespace: &str, + key: &str, + value: &[u8], + ttl: Option, + ) -> anyhow::Result<()>; + + /// Delete a single key from the given namespace. + async fn delete(&self, namespace: &str, key: &str) -> anyhow::Result<()>; + + /// Delete all keys in a namespace. + async fn clear(&self, namespace: &str) -> anyhow::Result<()>; + + /// Return the approximate number of entries in a namespace. + /// + /// Used only for diagnostics. Implementations that cannot cheaply + /// report counts should return `Ok(0)`. + async fn entry_count(&self, _namespace: &str) -> anyhow::Result { + Ok(0) + } +} diff --git a/wacore/src/store/mod.rs b/wacore/src/store/mod.rs index 2661775d0..bd6754159 100644 --- a/wacore/src/store/mod.rs +++ b/wacore/src/store/mod.rs @@ -1,7 +1,9 @@ +pub mod cache; pub mod commands; pub mod device; pub mod error; pub mod traits; +pub use cache::CacheStore; pub use commands::*; pub use device::Device; diff --git a/wacore/src/types/lid_pn.rs b/wacore/src/types/lid_pn.rs index d462e72c3..23bd6c4a3 100644 --- a/wacore/src/types/lid_pn.rs +++ b/wacore/src/types/lid_pn.rs @@ -13,7 +13,10 @@ /// The source from which a LID-PN mapping was learned. /// Different sources have different trust levels and handling for identity changes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, crate::StringEnum)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, crate::StringEnum, +)] +#[serde(rename_all = "snake_case")] pub enum LearningSource { /// Mapping learned from usync (device sync) query response #[str = "usync"] @@ -59,7 +62,7 @@ impl LearningSource { } /// An entry in the LID-PN cache containing the full mapping information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LidPnEntry { /// The LID user part (e.g., "100000012345678") pub lid: String, diff --git a/wacore/src/types/message.rs b/wacore/src/types/message.rs index 6f47f941e..b628de122 100644 --- a/wacore/src/types/message.rs +++ b/wacore/src/types/message.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use wacore_binary::jid::{Jid, JidExt, MessageId, MessageServerId}; use waproto::whatsapp as wa; @@ -19,7 +18,7 @@ impl StanzaKey { } /// Addressing mode for a group (phone number vs LID). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, crate::StringEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, crate::StringEnum)] #[serde(rename_all = "lowercase")] pub enum AddressingMode { #[string_default]