Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a018eea
feat(store): add MsgSecretStore for outbound messageSecret persistence
jlucaso1 May 28, 2026
117de18
feat(crypto): add bot_message::decrypt_bot_message for msmsg envelopes
jlucaso1 May 28, 2026
46ec145
feat(enc-type): add EncType::MessageSecret for msmsg
jlucaso1 May 28, 2026
f0d9124
feat(send): persist outbound MessageContextInfo.message_secret after …
jlucaso1 May 28, 2026
4d6cbda
feat(msmsg): inbound decrypt + dispatch pipeline
jlucaso1 May 28, 2026
d3768da
feat(msmsg): honour bot edit chain (edit_target_id for HKDF msg_id)
jlucaso1 May 28, 2026
f2e099b
review: address 5 findings (bot AD_JID, P1 secret capture, ordering, …
jlucaso1 May 28, 2026
1e601af
feat(msmsg): try-then-fallback decrypt covering regular bot path
jlucaso1 May 28, 2026
e8bdfd0
review: propagate group sender_identity, parse edit_target_id uncondi…
jlucaso1 May 28, 2026
cadb0d9
review: encoder typed path, 32-byte secret type, primary/fallback dedup
jlucaso1 May 28, 2026
91b61a2
review: msg_secrets retention, msmsg+unknown sibling, fanout capture
jlucaso1 May 28, 2026
c849776
review: cover invokedBotWid + isForwarded, make msg_secret TTL opt-in
jlucaso1 May 28, 2026
23a4766
test(msmsg): cover proto helpers + LID↔PN alternate secret lookup
jlucaso1 May 28, 2026
81ebe6d
perf(jid): add to_non_ad_string(), drop throwaway Jid in 23 call sites
jlucaso1 May 28, 2026
b8dc241
fix(keepalive): decouple msg_secret cleanup from sent-message TTL gate
jlucaso1 May 28, 2026
2380433
fix(msmsg): order fanout secret capture before next stanza; keepalive…
jlucaso1 May 28, 2026
4e81233
review: Debug msg_secret_ttl, canonical is_bot() in mentions_any_bot
jlucaso1 May 28, 2026
184719a
fix(ci): fetch bench baseline via raw media type (>1MB data.js)
jlucaso1 May 28, 2026
ac0e1c0
fix(msmsg): group bot support — participant-keyed secret + bare ack
jlucaso1 May 28, 2026
97c8ed7
chore(msmsg): log secret capture + decrypt success for prod validation
jlucaso1 May 28, 2026
dec0edd
chore(msmsg): downgrade expected group-companion secret miss to debug
jlucaso1 May 28, 2026
48033a3
debug(msmsg): log capture-gate inputs for group bot diagnosis
jlucaso1 May 28, 2026
f4a744d
fix(msmsg): cache group bot secret via bot_metadata + author identity
jlucaso1 May 28, 2026
106f90a
chore(msmsg): drop temporary group-flow diagnostic log
jlucaso1 May 28, 2026
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
29 changes: 28 additions & 1 deletion src/appstate_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mod tests {
use wacore::store::error::Result as StoreResult;
use wacore::store::traits::{
AppStateSyncKey, AppSyncStore, DeviceListRecord, DeviceStore, LidPnMappingEntry,
ProtocolStore, SignalStore,
MsgSecretStore, ProtocolStore, SignalStore,
};
use waproto::whatsapp as wa;

Expand Down Expand Up @@ -230,6 +230,33 @@ mod tests {
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl MsgSecretStore for MockBackend {
async fn put_msg_secret(
&self,
_chat: &str,
_sender: &str,
_msg_id: &str,
_secret: &[u8],
) -> StoreResult<()> {
Ok(())
}

async fn get_msg_secret(
&self,
_chat: &str,
_sender: &str,
_msg_id: &str,
) -> StoreResult<Option<Vec<u8>>> {
Ok(None)
}

async fn delete_expired_msg_secrets(&self, _cutoff: i64) -> StoreResult<u32> {
Ok(0)
}
}

// Implement DeviceStore - Device persistence
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand Down
12 changes: 12 additions & 0 deletions src/cache_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ pub struct CacheConfig {
/// 0 = no automatic cleanup. Default: 300 (5 minutes).
pub sent_message_ttl_secs: u64,

// --- MsgSecret retention ---
/// TTL in seconds for stored `messageSecret` rows before periodic
/// cleanup. `0` (default) disables automatic pruning, matching
/// whatsmeow and WA Web. Set to a positive value (e.g. `30 * 86_400`
/// for 30 days) to bound DB growth on long-running deployments.
pub msg_secret_ttl_secs: u64,

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// --- Custom store overrides ---
/// Per-cache custom store overrides.
///
Expand Down Expand Up @@ -222,6 +229,7 @@ impl std::fmt::Debug for CacheConfig {
.field("session_locks_capacity", &self.session_locks_capacity)
.field("chat_lanes_capacity", &self.chat_lanes_capacity)
.field("sent_message_ttl_secs", &self.sent_message_ttl_secs)
.field("msg_secret_ttl_secs", &self.msg_secret_ttl_secs)
.field(
"cache_stores.group_cache",
&self.cache_stores.group_cache.is_some(),
Expand Down Expand Up @@ -260,6 +268,10 @@ impl Default for CacheConfig {
session_locks_capacity: 10_000,
chat_lanes_capacity: 5_000,
sent_message_ttl_secs: 300,
// Disabled by default — match whatsmeow and WA Web (neither
// expires stored secrets). Callers expecting long-lived bot
// conversations, polls, or reactions can opt in.
msg_secret_ttl_secs: 0,
cache_stores: CacheStores::default(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3992,7 +3992,7 @@ impl Client {
.attrs([
("id", id),
("type", type_str.to_string()),
("to", own_jid.to_non_ad().to_string()),
("to", own_jid.to_non_ad_string()),
])
.build();

Expand Down
16 changes: 8 additions & 8 deletions src/features/message_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ pub fn decrypt(
original_sender_jid: &Jid,
editor_jid: &Jid,
) -> Result<wa::Message> {
let primary_orig = original_sender_jid.to_non_ad().to_string();
let primary_editor = editor_jid.to_non_ad().to_string();
let primary_orig = original_sender_jid.to_non_ad_string();
let primary_editor = editor_jid.to_non_ad_string();
let primary = MessageEditContext {
original_msg_id,
original_sender_jid: &primary_orig,
Expand Down Expand Up @@ -291,8 +291,8 @@ pub fn decrypt_secret_encrypted(
original_sender_jid: &Jid,
modification_sender_jid: &Jid,
) -> Result<wa::Message> {
let orig = original_sender_jid.to_non_ad().to_string();
let sender = modification_sender_jid.to_non_ad().to_string();
let orig = original_sender_jid.to_non_ad_string();
let sender = modification_sender_jid.to_non_ad_string();
let ctx = MessageEditContext {
original_msg_id,
original_sender_jid: &orig,
Expand Down Expand Up @@ -321,16 +321,16 @@ pub fn decrypt_secret_encrypted_with_fallback(
fallback_original_sender: Option<&Jid>,
fallback_modification_sender: Option<&Jid>,
) -> Result<wa::Message> {
let orig = original_sender_jid.to_non_ad().to_string();
let sender = modification_sender_jid.to_non_ad().to_string();
let orig = original_sender_jid.to_non_ad_string();
let sender = modification_sender_jid.to_non_ad_string();
let primary = MessageEditContext {
original_msg_id,
original_sender_jid: &orig,
editor_jid: &sender,
};

let fb_orig = fallback_original_sender.map(|j| j.to_non_ad().to_string());
let fb_sender = fallback_modification_sender.map(|j| j.to_non_ad().to_string());
let fb_orig = fallback_original_sender.map(|j| j.to_non_ad_string());
let fb_sender = fallback_modification_sender.map(|j| j.to_non_ad_string());
let fb_orig_resolved = fb_orig.as_deref().unwrap_or(primary.original_sender_jid);
let fb_sender_resolved = fb_sender.as_deref().unwrap_or(primary.editor_jid);
let fallback_ctx = if fb_orig_resolved == primary.original_sender_jid
Expand Down
4 changes: 2 additions & 2 deletions src/features/polls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl<'a> Polls<'a> {
.resolve_voter_jid(poll_creator_jid, &my_base, poll_msg_id)
.await;
let voter_jid_str = voter_jid.to_string();
let creator_jid_str = poll_creator_jid.to_non_ad().to_string();
let creator_jid_str = poll_creator_jid.to_non_ad_string();

let selected_hashes: Vec<Vec<u8>> = option_names
.iter()
Expand Down Expand Up @@ -232,7 +232,7 @@ impl<'a> Polls<'a> {
self.client
.swap_pn_lid_namespace(jid)
.await
.map(|j| j.to_non_ad().to_string())
.map(|j| j.to_non_ad_string())
}

/// Fallback pair only when both JIDs have a counterpart, keeping it
Expand Down
5 changes: 5 additions & 0 deletions src/features/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ impl<'a> Signal<'a> {
EncType::SenderKey => {
return Err(anyhow!("use decrypt_group_message for sender-key messages"));
}
EncType::MessageSecret => {
return Err(anyhow!(
"msmsg envelopes are not Signal messages; use the bot_message path"
));
}
};

let encryption_jid = self.client.resolve_encryption_jid(jid).await;
Expand Down
59 changes: 45 additions & 14 deletions src/keepalive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ impl Client {
return;
}

// Periodic DB retention (~every 12 ticks ≈ 5 min). Driven by
// the interval tick itself, BEFORE the idle-ping early-return,
// so busy connections (which skip the ping) still prune.
cleanup_counter += 1;
if cleanup_counter >= 12 {
cleanup_counter = 0;
self.spawn_retention_cleanup(sent_msg_ttl);
}

let last_recv = self.last_data_received_ms.load(Ordering::Relaxed);

// WA Web: maybeScheduleHealthCheck — only send ping when idle.
Expand Down Expand Up @@ -147,20 +156,6 @@ impl Client {
debug!(target: "Client/Keepalive", "Keepalive restored after {error_count} failure(s).");
}
error_count = 0;

// Periodic cleanup of expired sent messages (~every 12 ticks ≈ 5 min)
cleanup_counter += 1;
if sent_msg_ttl > 0 && cleanup_counter >= 12 {
cleanup_counter = 0;
let backend = self.persistence_manager.backend();
let cutoff = wacore::time::now_secs()
- sent_msg_ttl as i64;
self.runtime.spawn(Box::pin(async move {
if let Err(e) = backend.delete_expired_sent_messages(cutoff).await {
log::debug!(target: "Client/Keepalive", "Sent message cleanup error: {e}");
}
})).detach();
}
}
KeepaliveResult::FatalFailure => {
debug!(target: "Client/Keepalive", "Fatal keepalive failure, exiting loop.");
Expand Down Expand Up @@ -198,6 +193,42 @@ impl Client {
}
}
}

/// Fire-and-forget DB retention sweeps. Each TTL gates its own delete so
/// they enable/disable independently. `0` disables a sweep. TTLs are
/// converted with a checked cast (absurd values clamp instead of wrapping
/// the cutoff negative).
fn spawn_retention_cleanup(&self, sent_msg_ttl: u64) {
let now = wacore::time::now_secs();
let cutoff_for = |ttl: u64| now.saturating_sub(i64::try_from(ttl).unwrap_or(i64::MAX));

if sent_msg_ttl > 0 {
let backend = self.persistence_manager.backend();
let cutoff = cutoff_for(sent_msg_ttl);
self.runtime
.spawn(Box::pin(async move {
if let Err(e) = backend.delete_expired_sent_messages(cutoff).await {
log::debug!(target: "Client/Keepalive", "Sent message cleanup error: {e}");
}
}))
.detach();
}

// msg_secrets retention: disabled by default (matches whatsmeow + WA
// Web). Caller opts in via CacheConfig.msg_secret_ttl_secs.
let secret_ttl = self.cache_config.msg_secret_ttl_secs;
if secret_ttl > 0 {
let backend = self.persistence_manager.backend();
let cutoff = cutoff_for(secret_ttl);
self.runtime
.spawn(Box::pin(async move {
if let Err(e) = backend.delete_expired_msg_secrets(cutoff).await {
log::debug!(target: "Client/Keepalive", "msg_secrets cleanup error: {e}");
}
}))
.detach();
}
}
}

#[cfg(test)]
Expand Down
Loading
Loading