Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
238 changes: 6 additions & 232 deletions whatsrust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::{
ffi::{CStr, CString},
sync::Arc,
};
use std::ffi::{CStr, CString};
use std::sync::Arc;

#[macro_use]
mod callbacks;
Expand All @@ -15,6 +13,7 @@ mod media;
mod message_send;
mod models;
mod presence;
mod queries;
mod read_sync;
mod registrations;
use abi::*;
Expand All @@ -24,7 +23,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,
Expand All @@ -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,
Expand Down Expand Up @@ -311,207 +311,6 @@ impl CallbackTranslator<i64> 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<Vec<_>, _> = 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<str>)> {
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<bool> {
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<u32> {
(value >= 0).then(|| u32::try_from(value).ok()).flatten()
}

/// Returns real community roots and linked groups reported by WhatsApp.
pub fn get_communities() -> Result<Vec<CommunityInfo>, 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::<Vec<_>>();
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) };
Expand Down Expand Up @@ -587,10 +386,7 @@ pub fn get_group_participants(jid: &JID) -> Vec<GroupParticipant> {

#[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() {
Expand All @@ -602,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 [
Expand Down
Loading
Loading