Skip to content
5 changes: 2 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,12 +1539,11 @@ impl Client {

pub async fn clean_dirty_bits(
&self,
type_: &str,
timestamp: Option<&str>,
bit: wacore::iq::dirty::DirtyBit,
) -> Result<(), crate::request::IqError> {
use wacore::iq::dirty::CleanDirtyBitsSpec;

let spec = CleanDirtyBitsSpec::single(type_, timestamp)?;
let spec = CleanDirtyBitsSpec::single(bit);
self.execute(spec).await
}

Expand Down
4 changes: 3 additions & 1 deletion src/client/context_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::client::Client;
use async_trait::async_trait;
use std::collections::HashMap;
use wacore::client::context::{GroupInfo, SendContextResolver};
use wacore::iq::prekeys::PreKeyFetchReason;
use wacore::libsignal::protocol::PreKeyBundle;
use wacore_binary::jid::Jid;

Expand All @@ -23,7 +24,8 @@ impl SendContextResolver for Client {
&self,
jids: &[Jid],
) -> Result<HashMap<Jid, PreKeyBundle>, anyhow::Error> {
self.fetch_pre_keys(jids, Some("identity")).await
self.fetch_pre_keys(jids, Some(PreKeyFetchReason::Identity))
.await
}

async fn resolve_group_info(&self, jid: &Jid) -> Result<GroupInfo, anyhow::Error> {
Expand Down
4 changes: 3 additions & 1 deletion src/client/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ impl Client {
return Ok(0);
}

let prekey_bundles = self.fetch_pre_keys(jids, Some("identity")).await?;
let prekey_bundles = self
.fetch_pre_keys(jids, Some(wacore::iq::prekeys::PreKeyFetchReason::Identity))
.await?;

let device_store = self.persistence_manager.get_device_arc().await;
let mut adapter = crate::store::signal_adapter::SignalProtocolStoreAdapter::new(
Expand Down
4 changes: 2 additions & 2 deletions src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pub use media_reupload::{MediaRetryResult, MediaReupload, MediaReuploadRequest};
pub use mex::{Mex, MexError, MexErrorExtensions, MexGraphQLError, MexRequest, MexResponse};

pub use newsletter::{
Newsletter, NewsletterMessage, NewsletterMetadata, NewsletterReactionCount, NewsletterRole,
NewsletterState, NewsletterVerification,
Newsletter, NewsletterMessage, NewsletterMessageType, NewsletterMetadata,
NewsletterReactionCount, NewsletterRole, NewsletterState, NewsletterVerification,
};

pub use polls::{PollOptionResult, Polls};
Expand Down
32 changes: 28 additions & 4 deletions src/features/newsletter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//! Uses MEX (GraphQL) for metadata/management and standard IQ for message operations.
//! Newsletter messages are plaintext (no Signal E2E encryption).

use wacore::StringEnum;

use crate::client::Client;
use crate::features::mex::{MexError, MexRequest};
use prost::Message as ProtoMessage;
Expand All @@ -17,6 +19,26 @@ use waproto::whatsapp as wa;

// Types

#[derive(Debug, Clone, PartialEq, Eq, StringEnum)]
pub enum NewsletterMessageType {
#[str = "text"]
Text,
#[str = "media"]
Media,
#[str = "reaction"]
Reaction,
#[str = "revoke"]
Revoke,
#[str = "poll_creation"]
PollCreation,
#[str = "poll_vote"]
PollVote,
#[str = "edit"]
Edit,
#[string_fallback]
Other(String),
}

/// Newsletter verification status.
#[derive(Debug, Clone)]
pub enum NewsletterVerification {
Expand Down Expand Up @@ -71,8 +93,8 @@ pub struct NewsletterMessage {
pub server_id: u64,
/// Message timestamp (Unix seconds).
pub timestamp: u64,
/// Message type ("text", "media", etc.).
pub message_type: String,
/// Message type (text, media, reaction, etc.).
pub message_type: NewsletterMessageType,
/// Whether the viewer is the sender.
pub is_sender: bool,
/// Decoded protobuf message (from `<plaintext>` bytes).
Expand Down Expand Up @@ -570,11 +592,13 @@ fn parse_newsletter_messages_response(
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);

// TODO: verify against WAWeb/Newsletter/MsgParser.js whether the server always sends
// a type attribute. If so, this default is moot.
let message_type = msg_node
.attrs
.get("type")
.map(|v| v.as_str().into_owned())
.unwrap_or_default();
.map(|v| NewsletterMessageType::from(v.as_str().as_ref()))
.unwrap_or(NewsletterMessageType::Text);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let is_sender = msg_node.attrs.get("is_sender").is_some_and(|v| v == "true");

Expand Down
41 changes: 24 additions & 17 deletions src/handlers/ib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use async_trait::async_trait;
use log::{debug, info, warn};
use std::sync::Arc;
use wacore::appstate::patch_decode::WAPatchName;
use wacore::iq::dirty::{DirtyBit, DirtyType};
use wacore_binary::node::{Node, NodeContent};

/// Handler for `<ib>` (information broadcast) stanzas.
Expand Down Expand Up @@ -35,48 +36,54 @@ async fn handle_ib_impl(client: Arc<Client>, node: &Node) {
match child.tag.as_ref() {
"dirty" => {
let mut attrs = child.attrs();
let dirty_type = match attrs.optional_string("type") {
let dirty_type_str = match attrs.optional_string("type") {
Some(t) => t.to_string(),
None => {
warn!("Dirty notification missing 'type' attribute");
continue;
}
};
let timestamp = attrs.optional_string("timestamp").map(|s| s.to_string());
let ts = attrs
.optional_string("timestamp")
.and_then(|s| s.parse::<u64>().ok());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let dirty_type = DirtyType::from(dirty_type_str.as_ref());

let needs_offline_wait = matches!(
dirty_type,
DirtyType::Groups | DirtyType::NewsletterMetadata
);
let needs_resync = dirty_type == DirtyType::SyncdAppState;

let bit = match ts {
Some(t) => DirtyBit::with_timestamp(dirty_type, t),
None => DirtyBit::new(dirty_type),
};

debug!(
"Received dirty state notification for type: '{dirty_type}'. Sending clean IQ."
"Received dirty state notification for type: '{dirty_type_str}'. Sending clean IQ."
);

let client_clone = client.clone();

// WA Web gates `groups` and `newsletter_metadata` dirty types behind
// offlineDeliveryEnd — only process them after offline sync completes.
// `account_sync` and `syncd_app_state` run immediately.
// See WAWebHandleDirtyBits in 5Yec01dI04o.js:50765-50782.
// Groups/newsletter_metadata: wait for offline sync per WAWebHandleDirtyBits.
client
.runtime
.spawn(Box::pin(async move {
if dirty_type == "groups" || dirty_type == "newsletter_metadata" {
if needs_offline_wait {
client_clone.wait_for_offline_delivery_end().await;
}
if client_clone.is_shutting_down() {
debug!("Skipping clean dirty bits: client is shutting down");
return;
}
if let Err(e) = client_clone
.clean_dirty_bits(&dirty_type, timestamp.as_deref())
.await
if let Err(e) = client_clone.clean_dirty_bits(bit).await
&& !client_clone.is_shutting_down()
{
warn!("Failed to send clean dirty bits IQ: {e:?}");
}

// Re-sync app state collections when notified they are stale.
// Real WA Web re-syncs all collections on syncd_app_state dirty.
// See WAWebHandleDirtyBits → WAWebSyncdCollectionsStateMachine.
if dirty_type == "syncd_app_state" && !client_clone.is_shutting_down() {
info!("syncd_app_state dirty — re-syncing all app state collections");
if needs_resync && !client_clone.is_shutting_down() {
info!("syncd_app_state dirty -- re-syncing all app state collections");
if let Err(e) = client_clone
.sync_collections_batched(vec![
WAPatchName::CriticalBlock,
Expand Down
11 changes: 6 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ pub use features::{
GroupSubject, GroupType, Groups, IsOnWhatsAppResult, JoinGroupResult, LinkSubgroupsResult,
MediaRetryResult, MediaReupload, MediaReuploadRequest, MemberAddMode, MemberLinkMode,
MembershipApprovalMode, MembershipRequest, Mex, MexError, MexErrorExtensions, MexRequest,
MexResponse, Newsletter, NewsletterMessage, NewsletterMetadata, NewsletterReactionCount,
NewsletterRole, NewsletterState, NewsletterVerification, ParticipantChangeResponse, Presence,
PresenceError, PresenceStatus, Profile, ProfilePicture, SetProfilePictureResponse, Status,
StatusPrivacySetting, StatusSendOptions, SyncActionMessageRange, TcToken,
UnlinkSubgroupsResult, UserInfo, group_type, message_key, message_range,
MexResponse, Newsletter, NewsletterMessage, NewsletterMessageType, NewsletterMetadata,
NewsletterReactionCount, NewsletterRole, NewsletterState, NewsletterVerification,
ParticipantChangeResponse, Presence, PresenceError, PresenceStatus, Profile, ProfilePicture,
SetProfilePictureResponse, Status, StatusPrivacySetting, StatusSendOptions,
SyncActionMessageRange, TcToken, UnlinkSubgroupsResult, UserInfo, group_type, message_key,
message_range,
};

pub mod bot;
Expand Down
4 changes: 2 additions & 2 deletions src/mediaconn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::time::Duration;
use wacore::iq::mediaconn::MediaConnSpec;
use wacore::time::Instant;

/// Re-export the host type from wacore.
pub use wacore::iq::mediaconn::MediaConnHost;
/// Re-export protocol types from wacore.
pub use wacore::iq::mediaconn::{HostType, MediaConnHost};

/// Number of retry attempts after a media auth error (401/403).
/// On auth failure, the media connection is invalidated and refreshed before retrying.
Expand Down
36 changes: 22 additions & 14 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use wacore::libsignal::protocol::{
PublicKey as SignalPublicKey, SENDERKEY_MESSAGE_CURRENT_VERSION,
};
use wacore::libsignal::store::sender_key_name::SenderKeyName;
use wacore::message_processing::EncType;
use wacore::types::jid::JidExt;
use wacore_binary::jid::Jid;
use wacore_binary::jid::JidExt as _;
Expand Down Expand Up @@ -442,9 +443,9 @@ impl Client {
}

// Fall back to built-in handlers
match enc_type.as_ref() {
"pkmsg" | "msg" => session_enc_nodes.push(enc_node),
"skmsg" => group_content_enc_nodes.push(enc_node),
match EncType::from_wire(enc_type.as_ref()) {
Some(et) if et.is_session() => session_enc_nodes.push(enc_node),
Some(EncType::SenderKey) => group_content_enc_nodes.push(enc_node),
_ => log::warn!("Unknown enc type: {enc_type}"),
}
}
Expand All @@ -454,9 +455,11 @@ impl Client {
// so the skmsg decryption would fail with NoSenderKey.
if !session_enc_nodes.is_empty()
&& !group_content_enc_nodes.is_empty()
&& all_enc_nodes
.first()
.is_some_and(|n| n.attrs.get("type").is_some_and(|v| v == "skmsg"))
&& all_enc_nodes.first().is_some_and(|n| {
n.attrs
.get("type")
.is_some_and(|v| v == EncType::SenderKey.as_wire_str())
})
{
log::error!(
"[msg:{}] Protocol violation: skmsg is first in multi-enc message from {}. \
Expand Down Expand Up @@ -697,8 +700,9 @@ impl Client {
}
};
let padding_version = enc_node.attrs().optional_u64("v").unwrap_or(2) as u8;
let enc_type_enum = EncType::from_wire(enc_type.as_ref());

let parsed_message = if enc_type.as_ref() == "pkmsg" {
let parsed_message = if enc_type_enum == Some(EncType::PreKeyMessage) {
match PreKeySignalMessage::try_from(ciphertext) {
Ok(m) => CiphertextMessage::PreKeySignalMessage(m),
Err(e) => {
Expand All @@ -716,7 +720,7 @@ impl Client {
}
};

if enc_type.as_ref() == "pkmsg" {
if enc_type_enum == Some(EncType::PreKeyMessage) {
// FLAGGED FOR DEBUGGING: "Bad Mac" Reproducibility
#[cfg(feature = "debug-snapshots")]
{
Expand Down Expand Up @@ -2086,6 +2090,7 @@ mod tests {
async fn test_parse_message_info_sender_alt_extraction() {
use crate::store::SqliteStore;
use std::sync::Arc;
use wacore::types::message::AddressingMode;
use wacore_binary::builder::NodeBuilder;

let backend = Arc::new(
Expand Down Expand Up @@ -2129,7 +2134,7 @@ mod tests {
.attr("from", "120363021033254949@g.us")
.attr("participant", "987654321000000.2:42@lid")
.attr("participant_pn", "551234567890:42@s.whatsapp.net")
.attr("addressing_mode", "lid")
.attr("addressing_mode", AddressingMode::Lid.as_str())
.attr("id", "test1")
.attr("t", "12345")
.build();
Expand All @@ -2155,7 +2160,7 @@ mod tests {
.attr("from", "120363021033254949@g.us")
.attr("participant", "100000000000001.1:75@lid")
.attr("participant_pn", "15551234567:75@s.whatsapp.net")
.attr("addressing_mode", "lid")
.attr("addressing_mode", AddressingMode::Lid.as_str())
.attr("id", "test2")
.attr("t", "12346")
.build();
Expand Down Expand Up @@ -2441,6 +2446,7 @@ mod tests {
create_sender_key_distribution_message, process_sender_key_distribution_message,
};
use wacore::libsignal::store::sender_key_name::SenderKeyName;
use wacore::types::message::AddressingMode;
use wacore_binary::builder::NodeBuilder;

let backend = Arc::new(
Expand Down Expand Up @@ -2517,7 +2523,7 @@ mod tests {
.attr("id", "SECOND_MSG_TEST")
.attr("t", "1759306493")
.attr("type", "text")
.attr("addressing_mode", "lid")
.attr("addressing_mode", AddressingMode::Lid.as_str())
.children(vec![skmsg_node])
.build(),
);
Expand Down Expand Up @@ -3183,6 +3189,8 @@ mod tests {
/// 2. This enables sending to users we've only seen as LID senders
#[tokio::test]
async fn test_lid_pn_cache_populated_for_lid_sender_with_participant_pn() {
use wacore::types::message::AddressingMode;

// Setup client
let backend = Arc::new(
SqliteStore::new("file:memdb_lid_sender_test?mode=memory&cache=shared")
Expand Down Expand Up @@ -3212,7 +3220,7 @@ mod tests {
.attr("from", "120363123456789012@g.us") // Group chat
.attr("participant", Jid::lid(lid).to_string()) // Sender is LID
.attr("participant_pn", Jid::pn(phone).to_string()) // Their phone number
.attr("addressing_mode", "lid") // Required for participant_pn to be parsed
.attr("addressing_mode", AddressingMode::Lid.as_str()) // Required for participant_pn to be parsed
.attr("id", "TEST123456789")
.attr("t", "1765482972")
.attr("type", "text")
Expand Down Expand Up @@ -3687,7 +3695,7 @@ mod tests {

/// Helper to create a test MessageInfo with customizable fields
fn create_test_message_info(chat: &str, msg_id: &str, sender: &str) -> MessageInfo {
use wacore::types::message::{EditAttribute, MessageSource, MsgMetaInfo};
use wacore::types::message::{EditAttribute, MessageCategory, MessageSource, MsgMetaInfo};

let chat_jid: Jid = chat.parse().expect("valid chat JID");
let sender_jid: Jid = sender.parse().expect("valid sender JID");
Expand All @@ -3709,7 +3717,7 @@ mod tests {
},
timestamp: wacore::time::now_utc(),
push_name: "Test User".to_string(),
category: "".to_string(),
category: MessageCategory::default(),
multicast: false,
media_type: "".to_string(),
edit: EditAttribute::default(),
Expand Down
4 changes: 2 additions & 2 deletions src/pdo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use log::{debug, info, warn};
use prost::Message;
use std::sync::Arc;
use std::time::Duration;
use wacore::types::message::{EditAttribute, MessageSource, MsgMetaInfo};
use wacore::types::message::{EditAttribute, MessageCategory, MessageSource, MsgMetaInfo};
use wacore_binary::jid::{Jid, JidExt};
use waproto::whatsapp as wa;

Expand Down Expand Up @@ -400,7 +400,7 @@ impl Client {
},
timestamp,
push_name: web_msg.push_name.clone().unwrap_or_default(),
category: String::new(),
category: MessageCategory::default(),
multicast: false,
media_type: String::new(),
edit: EditAttribute::default(),
Expand Down
4 changes: 2 additions & 2 deletions src/prekeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use log;

use std::sync::atomic::Ordering;
use wacore::iq::prekeys::{
DigestKeyBundleSpec, PreKeyCountSpec, PreKeyFetchSpec, PreKeyUploadSpec,
DigestKeyBundleSpec, PreKeyCountSpec, PreKeyFetchReason, PreKeyFetchSpec, PreKeyUploadSpec,
};
use wacore::libsignal::protocol::{KeyPair, PreKeyBundle, PublicKey};
use wacore::libsignal::store::record_helpers::new_pre_key_record;
Expand All @@ -27,7 +27,7 @@ impl Client {
pub(crate) async fn fetch_pre_keys(
&self,
jids: &[Jid],
reason: Option<&str>,
reason: Option<PreKeyFetchReason>,
) -> Result<std::collections::HashMap<Jid, PreKeyBundle>, anyhow::Error> {
let spec = match reason {
Some(r) => PreKeyFetchSpec::with_reason(jids.to_vec(), r),
Expand Down
Loading
Loading