From c1a5264de1f0d2005834d6d5b63f5ab1cdd25583 Mon Sep 17 00:00:00 2001 From: andiveli Date: Wed, 26 Aug 2026 14:31:45 -0500 Subject: [PATCH 1/2] refactor(whatsrust): extract outbound message wrappers --- whatsrust/src/lib.rs | 128 +--------------------------------- whatsrust/src/message_send.rs | 128 +++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 128 deletions(-) diff --git a/whatsrust/src/lib.rs b/whatsrust/src/lib.rs index c7e450e..6fe907b 100644 --- a/whatsrust/src/lib.rs +++ b/whatsrust/src/lib.rs @@ -24,7 +24,7 @@ pub use callbacks::CallbackTranslator; pub use events::set_event_handler; pub use lifecycle::{connect, disconnect, logout, new_client, pair_phone}; pub use media::{download_file, get_community_profile_picture, get_profile_picture}; -use message_send::{build_content_for_ffi, quote_to_ffi}; +pub use message_send::{TextSendResult, forward_message, send_message, send_text_message}; pub(crate) use models::file_kind_discriminant; pub use models::{ ChatSettings, CommunitiesError, CommunityInfo, Contact, DownloadFailed, Event, FileContent, @@ -311,132 +311,6 @@ impl CallbackTranslator for i64 { } } -pub fn forward_message(source: &Message, destinations: &[JID]) -> ForwardReport { - if destinations.is_empty() { - return ForwardReport::default(); - } - let Some(forward_source) = forward_source(&source.info) else { - return ForwardReport::with_reason( - 0, - destinations.len(), - ForwardFailure::SourceUnavailable, - ); - }; - let source_id = match CString::new(source.info.id.as_ref()) { - Ok(value) => value, - Err(_) => { - return ForwardReport::with_reason( - 0, - destinations.len(), - ForwardFailure::InvalidSource, - ); - } - }; - let destination_values: Result, _> = destinations - .iter() - .map(|jid| CString::new(jid.0.as_ref())) - .collect(); - let Ok(destination_values) = destination_values else { - return ForwardReport::with_reason( - 0, - destinations.len(), - ForwardFailure::InvalidDestination, - ); - }; - let destination_pointers: Vec<_> = destination_values.iter().map(|jid| jid.as_ptr()).collect(); - let result = unsafe { - C_ForwardMessage( - source_id.as_ptr(), - CJID::from(&source.info.chat), - CJID::from(&source.info.sender), - source.info.is_from_me, - destination_pointers.as_ptr(), - destination_pointers.len(), - forward_source.as_ptr(), - forward_source.len(), - ) - }; - ForwardReport { - succeeded: result.succeeded as usize, - failed: result.failed as usize, - failure: ForwardFailure::from_repr(result.failure).unwrap_or(ForwardFailure::SendFailed), - } -} - -pub fn send_message( - jid: &JID, - content: &MessageContent, - quoted_message: Option<&Message>, - mentions: &[Mention], -) { - let jid_c = CJID::from(jid); - let (msg_type, content_ptr, _holder) = build_content_for_ffi(content, mentions); - let (_quote_id_owner, quote_id, quote_sender, quote_chat) = quote_to_ffi(quoted_message); - let quote_content = quoted_message.map(|message| build_content_for_ffi(&message.message, &[])); - let (quote_message_type, quote_message_content) = quote_content - .as_ref() - .map_or((0, std::ptr::null()), |(message_type, pointer, _)| { - (*message_type, *pointer) - }); - - unsafe { - C_SendMessage( - jid_c, - msg_type, - content_ptr, - quote_id, - quote_sender, - quote_chat, - quote_message_type, - quote_message_content, - ) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TextSendResult { - Sent, - Failed, -} - -pub fn send_text_message( - jid: &JID, - content: &MessageContent, - quoted_message: Option<&Message>, - mentions: &[Mention], - local_send_id: u64, -) -> TextSendResult { - let MessageContent::Text(_) = content else { - return TextSendResult::Failed; - }; - let jid_c = CJID::from(jid); - let (_message_type, content_ptr, _holder) = build_content_for_ffi(content, mentions); - let (_quote_id_owner, quote_id, quote_sender, quote_chat) = quote_to_ffi(quoted_message); - let quote_content = quoted_message.map(|message| build_content_for_ffi(&message.message, &[])); - let (quote_message_type, quote_message_content) = quote_content - .as_ref() - .map_or((0, std::ptr::null()), |(message_type, pointer, _)| { - (*message_type, *pointer) - }); - let status = unsafe { - C_SendTextMessage( - jid_c, - content_ptr, - quote_id, - quote_sender, - quote_chat, - quote_message_type, - quote_message_content, - local_send_id, - ) - }; - if status == 0 { - TextSendResult::Sent - } else { - TextSendResult::Failed - } -} - /// Returns all contacts and groups as (JID, display name). Includes LID aliases for contacts. pub fn get_contacts() -> Vec<(JID, Arc)> { let result = unsafe { C_GetContacts() }; diff --git a/whatsrust/src/message_send.rs b/whatsrust/src/message_send.rs index acdb1cb..f8a8cfe 100644 --- a/whatsrust/src/message_send.rs +++ b/whatsrust/src/message_send.rs @@ -1,9 +1,10 @@ use std::ffi::{CString, c_char, c_void}; use crate::{ + ForwardFailure, ForwardReport, abi::{CFileMessage, CJID, CTextMessage, MessageType}, file_kind_discriminant, - models::{Mention, Message, MessageContent}, + models::{JID, Mention, Message, MessageContent}, }; /// Keeps CStrings and C structs alive for the duration of an FFI call. @@ -118,6 +119,131 @@ pub(crate) fn quote_to_ffi(quoted: Option<&Message>) -> (CString, *const c_char, ), } } + +pub fn forward_message(source: &Message, destinations: &[JID]) -> ForwardReport { + if destinations.is_empty() { + return ForwardReport::default(); + } + let Some(forward_source) = crate::forward_source(&source.info) else { + return ForwardReport::with_reason( + 0, + destinations.len(), + ForwardFailure::SourceUnavailable, + ); + }; + let source_id = match CString::new(source.info.id.as_ref()) { + Ok(value) => value, + Err(_) => { + return ForwardReport::with_reason( + 0, + destinations.len(), + ForwardFailure::InvalidSource, + ); + } + }; + let destination_values: Result, _> = destinations + .iter() + .map(|jid| CString::new(jid.0.as_ref())) + .collect(); + let Ok(destination_values) = destination_values else { + return ForwardReport::with_reason( + 0, + destinations.len(), + ForwardFailure::InvalidDestination, + ); + }; + let destination_pointers: Vec<_> = destination_values.iter().map(|jid| jid.as_ptr()).collect(); + let result = unsafe { + crate::abi::C_ForwardMessage( + source_id.as_ptr(), + CJID::from(&source.info.chat), + CJID::from(&source.info.sender), + source.info.is_from_me, + destination_pointers.as_ptr(), + destination_pointers.len(), + forward_source.as_ptr(), + forward_source.len(), + ) + }; + ForwardReport { + succeeded: result.succeeded as usize, + failed: result.failed as usize, + failure: ForwardFailure::from_repr(result.failure).unwrap_or(ForwardFailure::SendFailed), + } +} + +pub fn send_message( + jid: &JID, + content: &MessageContent, + quoted_message: Option<&Message>, + mentions: &[Mention], +) { + let jid_c = CJID::from(jid); + let (msg_type, content_ptr, _holder) = build_content_for_ffi(content, mentions); + let (_quote_id_owner, quote_id, quote_sender, quote_chat) = quote_to_ffi(quoted_message); + let quote_content = quoted_message.map(|message| build_content_for_ffi(&message.message, &[])); + let (quote_message_type, quote_message_content) = quote_content + .as_ref() + .map_or((0, std::ptr::null()), |(message_type, pointer, _)| { + (*message_type, *pointer) + }); + unsafe { + crate::abi::C_SendMessage( + jid_c, + msg_type, + content_ptr, + quote_id, + quote_sender, + quote_chat, + quote_message_type, + quote_message_content, + ) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TextSendResult { + Sent, + Failed, +} + +pub fn send_text_message( + jid: &JID, + content: &MessageContent, + quoted_message: Option<&Message>, + mentions: &[Mention], + local_send_id: u64, +) -> TextSendResult { + let MessageContent::Text(_) = content else { + return TextSendResult::Failed; + }; + let jid_c = CJID::from(jid); + let (_message_type, content_ptr, _holder) = build_content_for_ffi(content, mentions); + let (_quote_id_owner, quote_id, quote_sender, quote_chat) = quote_to_ffi(quoted_message); + let quote_content = quoted_message.map(|message| build_content_for_ffi(&message.message, &[])); + let (quote_message_type, quote_message_content) = quote_content + .as_ref() + .map_or((0, std::ptr::null()), |(message_type, pointer, _)| { + (*message_type, *pointer) + }); + let status = unsafe { + crate::abi::C_SendTextMessage( + jid_c, + content_ptr, + quote_id, + quote_sender, + quote_chat, + quote_message_type, + quote_message_content, + local_send_id, + ) + }; + if status == 0 { + TextSendResult::Sent + } else { + TextSendResult::Failed + } +} #[cfg(test)] mod tests { use std::ffi::CStr; From da96630cdaaa58e104f769317c0ab404f49f1c41 Mon Sep 17 00:00:00 2001 From: andiveli Date: Wed, 26 Aug 2026 14:38:59 -0500 Subject: [PATCH 2/2] refactor(whatsrust): extract contacts and communities queries --- whatsrust/src/lib.rs | 110 ++------------------------------------- whatsrust/src/queries.rs | 104 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 105 deletions(-) create mode 100644 whatsrust/src/queries.rs diff --git a/whatsrust/src/lib.rs b/whatsrust/src/lib.rs index 6fe907b..b764969 100644 --- a/whatsrust/src/lib.rs +++ b/whatsrust/src/lib.rs @@ -1,7 +1,5 @@ -use std::{ - ffi::{CStr, CString}, - sync::Arc, -}; +use std::ffi::{CStr, CString}; +use std::sync::Arc; #[macro_use] mod callbacks; @@ -15,6 +13,7 @@ mod media; mod message_send; mod models; mod presence; +mod queries; mod read_sync; mod registrations; use abi::*; @@ -34,6 +33,7 @@ pub use models::{ ProfilePictureError, }; pub use presence::{SubscribePresenceResult, drain_raw_presence_diagnostics, subscribe_presence}; +pub use queries::{get_communities, get_contacts}; pub use read_sync::{MarkAsReadError, mark_as_read, sync_chat_read}; pub use registrations::{ set_log_handler, set_message_handler, set_optimistic_text_sent_handler, set_presence_handler, @@ -311,81 +311,6 @@ impl CallbackTranslator for i64 { } } -/// Returns all contacts and groups as (JID, display name). Includes LID aliases for contacts. -pub fn get_contacts() -> Vec<(JID, Arc)> { - let result = unsafe { C_GetContacts() }; - let entries = unsafe { std::slice::from_raw_parts(result.entries, result.size as usize) }; - - let contacts = entries - .iter() - .map(|e| { - let jid: JID = (&e.jid).into(); - let name = unsafe { CStr::from_ptr(e.name) } - .to_string_lossy() - .into_owned() - .into(); - (jid, name) - }) - .collect(); - unsafe { C_FreeContacts(result) }; - contacts -} - -const COMMUNITY_ANNOUNCEMENT_UNKNOWN: u8 = 0; -const COMMUNITY_ANNOUNCEMENT_NO: u8 = 1; -const COMMUNITY_ANNOUNCEMENT_YES: u8 = 2; - -fn community_announcement_from_code(code: u8) -> Option { - match code { - COMMUNITY_ANNOUNCEMENT_UNKNOWN => None, - COMMUNITY_ANNOUNCEMENT_NO => Some(false), - COMMUNITY_ANNOUNCEMENT_YES => Some(true), - _ => None, - } -} - -fn community_participant_count_from_abi(value: i64) -> Option { - (value >= 0).then(|| u32::try_from(value).ok()).flatten() -} - -/// Returns real community roots and linked groups reported by WhatsApp. -pub fn get_communities() -> Result, CommunitiesError> { - let result = unsafe { C_GetCommunities() }; - if result.status != 0 { - // C_GetCommunities transfers ownership even when the bridge reports - // an error; the current error result is empty, but freeing it here - // keeps that contract correct if it ever carries partial data. - unsafe { C_FreeCommunities(result) }; - return Err(CommunitiesError::BridgeUnavailable); - } - if result.entries.is_null() || result.size == 0 { - unsafe { C_FreeCommunities(result) }; - return Ok(Vec::new()); - } - let entries = unsafe { std::slice::from_raw_parts(result.entries, result.size as usize) }; - let communities = entries - .iter() - .map(|entry| { - let parent = unsafe { CStr::from_ptr(entry.parent_jid) }.to_string_lossy(); - CommunityInfo { - jid: (&entry.jid).into(), - name: unsafe { CStr::from_ptr(entry.name) } - .to_string_lossy() - .into_owned() - .into(), - parent_jid: (!parent.is_empty()).then(|| parent.to_string().into()), - is_parent: entry.is_parent, - is_joined: entry.is_joined, - is_default_subgroup: entry.is_default_subgroup, - is_announce: community_announcement_from_code(entry.announcement), - participant_count: community_participant_count_from_abi(entry.participant_count), - } - }) - .collect::>(); - unsafe { C_FreeCommunities(result) }; - Ok(communities) -} - pub fn get_chat_settings(jid: &JID) -> ChatSettings { let jid_c = CJID::from(jid); let settings = unsafe { C_GetChatSettings(jid_c) }; @@ -461,10 +386,7 @@ pub fn get_group_participants(jid: &JID) -> Vec { #[cfg(test)] mod group_info_tests { - use super::{ - GroupInfoError, JID, community_announcement_from_code, - community_participant_count_from_abi, group_info_from_parts, - }; + use super::{GroupInfoError, JID, group_info_from_parts}; #[test] fn maps_announce_and_admin_flags() { @@ -476,28 +398,6 @@ mod group_info_tests { assert!(!info.is_admin); } - #[test] - fn maps_community_announcement_tristate() { - assert_eq!(community_announcement_from_code(0), None); - assert_eq!(community_announcement_from_code(1), Some(false)); - assert_eq!(community_announcement_from_code(2), Some(true)); - assert_eq!(community_announcement_from_code(255), None); - } - - #[test] - fn maps_community_participant_count_without_truncation() { - assert_eq!(community_participant_count_from_abi(-1), None); - assert_eq!(community_participant_count_from_abi(0), Some(0)); - assert_eq!( - community_participant_count_from_abi(i64::from(u32::MAX)), - Some(u32::MAX) - ); - assert_eq!( - community_participant_count_from_abi(i64::from(u32::MAX) + 1), - None - ); - } - #[test] fn maps_bridge_failures_without_claiming_send_permission() { for (status, expected) in [ diff --git a/whatsrust/src/queries.rs b/whatsrust/src/queries.rs new file mode 100644 index 0000000..ea0d02c --- /dev/null +++ b/whatsrust/src/queries.rs @@ -0,0 +1,104 @@ +use std::ffi::CStr; +use std::sync::Arc; + +use super::{C_FreeCommunities, C_FreeContacts, C_GetCommunities, C_GetContacts}; +use super::{CommunitiesError, CommunityInfo, JID}; + +/// Returns all contacts and groups as (JID, display name). Includes LID aliases for contacts. +pub fn get_contacts() -> Vec<(JID, Arc)> { + let result = unsafe { C_GetContacts() }; + let entries = unsafe { std::slice::from_raw_parts(result.entries, result.size as usize) }; + + let contacts = entries + .iter() + .map(|e| { + let jid: JID = (&e.jid).into(); + let name = unsafe { CStr::from_ptr(e.name) } + .to_string_lossy() + .into_owned() + .into(); + (jid, name) + }) + .collect(); + unsafe { C_FreeContacts(result) }; + contacts +} + +const COMMUNITY_ANNOUNCEMENT_UNKNOWN: u8 = 0; +const COMMUNITY_ANNOUNCEMENT_NO: u8 = 1; +const COMMUNITY_ANNOUNCEMENT_YES: u8 = 2; + +fn community_announcement_from_code(code: u8) -> Option { + match code { + COMMUNITY_ANNOUNCEMENT_UNKNOWN => None, + COMMUNITY_ANNOUNCEMENT_NO => Some(false), + COMMUNITY_ANNOUNCEMENT_YES => Some(true), + _ => None, + } +} + +fn community_participant_count_from_abi(value: i64) -> Option { + (value >= 0).then(|| u32::try_from(value).ok()).flatten() +} + +/// Returns real community roots and linked groups reported by WhatsApp. +pub fn get_communities() -> Result, CommunitiesError> { + let result = unsafe { C_GetCommunities() }; + if result.status != 0 { + unsafe { C_FreeCommunities(result) }; + return Err(CommunitiesError::BridgeUnavailable); + } + if result.entries.is_null() || result.size == 0 { + unsafe { C_FreeCommunities(result) }; + return Ok(Vec::new()); + } + let entries = unsafe { std::slice::from_raw_parts(result.entries, result.size as usize) }; + let communities = entries + .iter() + .map(|entry| { + let parent = unsafe { CStr::from_ptr(entry.parent_jid) }.to_string_lossy(); + CommunityInfo { + jid: (&entry.jid).into(), + name: unsafe { CStr::from_ptr(entry.name) } + .to_string_lossy() + .into_owned() + .into(), + parent_jid: (!parent.is_empty()).then(|| parent.to_string().into()), + is_parent: entry.is_parent, + is_joined: entry.is_joined, + is_default_subgroup: entry.is_default_subgroup, + is_announce: community_announcement_from_code(entry.announcement), + participant_count: community_participant_count_from_abi(entry.participant_count), + } + }) + .collect::>(); + unsafe { C_FreeCommunities(result) }; + Ok(communities) +} + +#[cfg(test)] +mod tests { + use super::{community_announcement_from_code, community_participant_count_from_abi}; + + #[test] + fn maps_community_announcement_tristate() { + assert_eq!(community_announcement_from_code(0), None); + assert_eq!(community_announcement_from_code(1), Some(false)); + assert_eq!(community_announcement_from_code(2), Some(true)); + assert_eq!(community_announcement_from_code(255), None); + } + + #[test] + fn maps_community_participant_count_without_truncation() { + assert_eq!(community_participant_count_from_abi(-1), None); + assert_eq!(community_participant_count_from_abi(0), Some(0)); + assert_eq!( + community_participant_count_from_abi(i64::from(u32::MAX)), + Some(u32::MAX) + ); + assert_eq!( + community_participant_count_from_abi(i64::from(u32::MAX) + 1), + None + ); + } +}