diff --git a/whatsrust/src/actions.rs b/whatsrust/src/actions.rs index 7414ccc..5f9d2e3 100644 --- a/whatsrust/src/actions.rs +++ b/whatsrust/src/actions.rs @@ -32,8 +32,13 @@ pub fn react_to_message_in_chat( message_id: &MessageId, reaction: &str, ) -> Result<(), MessageActionFailed> { - let (target, destination, sender, id, reaction) = - reaction_to_ffi(target_jid, destination_jid, sender_jid, message_id, reaction)?; + let (target, destination, sender, id, reaction) = reaction_to_ffi( + target_jid, + destination_jid, + sender_jid, + message_id, + reaction, + )?; let result = unsafe { C_ReactToMessage( target.as_ptr(), @@ -43,7 +48,11 @@ pub fn react_to_message_in_chat( reaction.as_ptr(), ) }; - if result == 0 { Ok(()) } else { Err(MessageActionFailed) } + if result == 0 { + Ok(()) + } else { + Err(MessageActionFailed) + } } pub(crate) fn edit_to_ffi( @@ -68,7 +77,11 @@ pub fn edit_message( ) -> Result<(), MessageActionFailed> { let (chat, id, replacement) = edit_to_ffi(chat_jid, message_id, replacement)?; let result = unsafe { C_EditMessage(chat.as_ptr(), id.as_ptr(), replacement.as_ptr()) }; - if result == 0 { Ok(()) } else { Err(MessageActionFailed) } + if result == 0 { + Ok(()) + } else { + Err(MessageActionFailed) + } } pub(crate) fn revoke_to_ffi( @@ -90,5 +103,9 @@ pub fn revoke_message( ) -> Result<(), MessageActionFailed> { let (chat, sender, id) = revoke_to_ffi(chat_jid, sender_jid, message_id)?; let result = unsafe { C_RevokeMessage(chat.as_ptr(), sender.as_ptr(), id.as_ptr()) }; - if result == 0 { Ok(()) } else { Err(MessageActionFailed) } + if result == 0 { + Ok(()) + } else { + Err(MessageActionFailed) + } } diff --git a/whatsrust/src/events.rs b/whatsrust/src/events.rs index 519720e..75121f6 100644 --- a/whatsrust/src/events.rs +++ b/whatsrust/src/events.rs @@ -77,7 +77,7 @@ unsafe fn chat_event_from_ffi(event: &CChatEvent) -> Event { } } -unsafe fn reaction_event_from_ffi(event: &CReactionEvent) -> Event { +pub(crate) unsafe fn reaction_event_from_ffi(event: &CReactionEvent) -> Event { Event::Reaction { chat: (&event.chat).into(), target_message_id: unsafe { CStr::from_ptr(event.target_message_id) } @@ -93,7 +93,7 @@ unsafe fn reaction_event_from_ffi(event: &CReactionEvent) -> Event { } } -unsafe fn message_action_event_from_ffi(event: &CMessageActionEvent) -> Event { +pub(crate) unsafe fn message_action_event_from_ffi(event: &CMessageActionEvent) -> Event { let kind = match event.kind { 0 => MessageActionKind::Edit { replacement: unsafe { CStr::from_ptr(event.replacement) } diff --git a/whatsrust/src/lib.rs b/whatsrust/src/lib.rs index cd03fa8..6b5fb51 100644 --- a/whatsrust/src/lib.rs +++ b/whatsrust/src/lib.rs @@ -6,29 +6,24 @@ use std::{ #[macro_use] mod callbacks; -mod registrations; -mod lifecycle; -mod presence; -mod media; -mod actions; mod abi; +mod actions; mod caches; mod events; mod incoming; +mod lifecycle; +mod media; mod models; +mod presence; +mod read_sync; +mod registrations; use abi::*; pub use abi::{LogoutStatus, ReceiptKind}; -pub use events::set_event_handler; +pub use actions::{edit_message, react_to_message, react_to_message_in_chat, revoke_message}; pub use callbacks::CallbackTranslator; -pub use registrations::{ - set_log_handler, set_message_handler, - set_optimistic_text_sent_handler, set_presence_handler, -}; +pub use events::set_event_handler; pub use lifecycle::{connect, disconnect, logout, new_client, pair_phone}; -pub use presence::{SubscribePresenceResult, drain_raw_presence_diagnostics, subscribe_presence}; pub use media::{download_file, get_community_profile_picture, get_profile_picture}; -pub use actions::{edit_message, react_to_message, react_to_message_in_chat, revoke_message}; -pub(crate) use actions::{edit_to_ffi, reaction_to_ffi, revoke_to_ffi}; pub(crate) use models::file_kind_discriminant; pub use models::{ ChatSettings, CommunitiesError, CommunityInfo, Contact, DownloadFailed, Event, FileContent, @@ -37,6 +32,11 @@ pub use models::{ MessageId, MessageInfo, PresenceUpdate, ProfilePicture, ProfilePictureAvailability, ProfilePictureError, }; +pub use presence::{SubscribePresenceResult, drain_raw_presence_diagnostics, subscribe_presence}; +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, +}; use strum::FromRepr; #[cfg(test)] @@ -63,10 +63,9 @@ mod file_kind_tests { mod message_action_tests { use std::ffi::CString; - use super::{ - CMessageActionEvent, CReactionEvent, Event, JID, MessageActionKind, edit_to_ffi, - message_action_event_from_ffi, reaction_event_from_ffi, reaction_to_ffi, revoke_to_ffi, - }; + use super::actions::{edit_to_ffi, reaction_to_ffi, revoke_to_ffi}; + use super::events::{message_action_event_from_ffi, reaction_event_from_ffi}; + use super::{CMessageActionEvent, CReactionEvent, Event, JID, MessageActionKind}; #[test] fn reaction_mapping_preserves_every_ordinary_message_field() { @@ -299,108 +298,6 @@ impl ForwardReport { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MarkAsReadError { - Disconnected, - Transient, - Permanent, -} - -fn with_borrowed_mark_read_args( - msg_id: &MessageId, - chat_jid: &JID, - sender_jid: &JID, - send: impl FnOnce(*const c_char, CJID, CJID) -> T, -) -> Result { - let msg_id_c = CString::new(msg_id.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; - let chat_jid_c = CString::new(chat_jid.0.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; - let sender_jid_c = - CString::new(sender_jid.0.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; - Ok(send( - msg_id_c.as_ptr(), - chat_jid_c.as_ptr(), - sender_jid_c.as_ptr(), - )) -} - -pub fn mark_as_read( - msg_id: &MessageId, - chat_jid: &JID, - sender_jid: &JID, -) -> Result<(), MarkAsReadError> { - let result = - with_borrowed_mark_read_args(msg_id, chat_jid, sender_jid, |id, chat, sender| unsafe { - C_MarkAsRead(id, chat, sender) - })?; - match result { - 0 => Ok(()), - 1 => Err(MarkAsReadError::Disconnected), - 3 => Err(MarkAsReadError::Permanent), - _ => Err(MarkAsReadError::Transient), - } -} - -pub fn sync_chat_read( - chat_jid: &JID, - message_id: &MessageId, - timestamp: i64, - from_me: bool, - participant_jid: Option<&JID>, -) { - let chat = chat_jid.0.to_string(); - let message = message_id.to_string(); - let participant = participant_jid.map(|jid| jid.0.to_string()); - std::thread::spawn(move || { - let chat_c = match CString::new(chat) { - Ok(value) => value, - Err(_) => { - log::warn!("chat read sync skipped: invalid chat JID"); - return; - } - }; - let message_c = match CString::new(message) { - Ok(value) => value, - Err(_) => { - log::warn!("chat read sync skipped: invalid message ID"); - return; - } - }; - let participant_c = participant.and_then(|value| CString::new(value).ok()); - let participant_ptr = participant_c - .as_ref() - .map_or(std::ptr::null(), |value| value.as_ptr()); - let result = unsafe { - C_MarkChatReadSync( - chat_c.as_ptr(), - message_c.as_ptr(), - timestamp, - from_me, - participant_ptr, - ) - }; - if result != 0 { - log::warn!("chat read sync failed with bridge status {result}"); - } - }); -} - -#[cfg(test)] -mod read_receipt_ffi_tests { - use super::*; - #[test] - fn borrowed_ffi_arguments_can_be_reused_without_owned_pointer_leaks() { - let id: MessageId = "message".into(); - let chat = JID::from("chat@s.whatsapp.net".to_owned()); - let sender = JID::from("sender@s.whatsapp.net".to_owned()); - for _ in 0..1_000 { - with_borrowed_mark_read_args(&id, &chat, &sender, |id, chat, sender| { - assert!(!id.is_null() && !chat.is_null() && !sender.is_null()); - }) - .unwrap(); - } - } -} - impl CallbackTranslator for JID { unsafe fn to_rust(from: CJID) -> Self { (&from).into() diff --git a/whatsrust/src/media.rs b/whatsrust/src/media.rs index dcafec0..36fab22 100644 --- a/whatsrust/src/media.rs +++ b/whatsrust/src/media.rs @@ -79,7 +79,7 @@ pub fn download_file(file_id: &FileId, base_path: &Path) -> Result<(), DownloadF #[cfg(test)] mod tests { - use super::{profile_picture_from_parts, ProfilePictureAvailability, ProfilePictureError}; + use super::{ProfilePictureAvailability, ProfilePictureError, profile_picture_from_parts}; #[test] fn maps_available_payload_without_exposing_the_temporary_url() { diff --git a/whatsrust/src/read_sync.rs b/whatsrust/src/read_sync.rs new file mode 100644 index 0000000..2f6fec9 --- /dev/null +++ b/whatsrust/src/read_sync.rs @@ -0,0 +1,109 @@ +use std::ffi::{CString, c_char}; + +use crate::{ + abi::*, + models::{JID, MessageId}, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MarkAsReadError { + Disconnected, + Transient, + Permanent, +} + +fn with_borrowed_mark_read_args( + msg_id: &MessageId, + chat_jid: &JID, + sender_jid: &JID, + send: impl FnOnce(*const c_char, *const c_char, *const c_char) -> T, +) -> Result { + let msg_id_c = CString::new(msg_id.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; + let chat_jid_c = CString::new(chat_jid.0.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; + let sender_jid_c = + CString::new(sender_jid.0.as_ref()).map_err(|_| MarkAsReadError::Permanent)?; + Ok(send( + msg_id_c.as_ptr(), + chat_jid_c.as_ptr(), + sender_jid_c.as_ptr(), + )) +} + +pub fn mark_as_read( + msg_id: &MessageId, + chat_jid: &JID, + sender_jid: &JID, +) -> Result<(), MarkAsReadError> { + let result = + with_borrowed_mark_read_args(msg_id, chat_jid, sender_jid, |id, chat, sender| unsafe { + C_MarkAsRead(id, chat, sender) + })?; + match result { + 0 => Ok(()), + 1 => Err(MarkAsReadError::Disconnected), + 3 => Err(MarkAsReadError::Permanent), + _ => Err(MarkAsReadError::Transient), + } +} + +pub fn sync_chat_read( + chat_jid: &JID, + message_id: &MessageId, + timestamp: i64, + from_me: bool, + participant_jid: Option<&JID>, +) { + let chat = chat_jid.0.to_string(); + let message = message_id.to_string(); + let participant = participant_jid.map(|jid| jid.0.to_string()); + std::thread::spawn(move || { + let chat_c = match CString::new(chat) { + Ok(value) => value, + Err(_) => { + log::warn!("chat read sync skipped: invalid chat JID"); + return; + } + }; + let message_c = match CString::new(message) { + Ok(value) => value, + Err(_) => { + log::warn!("chat read sync skipped: invalid message ID"); + return; + } + }; + let participant_c = participant.and_then(|value| CString::new(value).ok()); + let participant_ptr = participant_c + .as_ref() + .map_or(std::ptr::null(), |value| value.as_ptr()); + let result = unsafe { + C_MarkChatReadSync( + chat_c.as_ptr(), + message_c.as_ptr(), + timestamp, + from_me, + participant_ptr, + ) + }; + if result != 0 { + log::warn!("chat read sync failed with bridge status {result}"); + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn borrowed_ffi_arguments_can_be_reused_without_owned_pointer_leaks() { + let id: MessageId = "message".into(); + let chat = JID::from("chat@s.whatsapp.net".to_owned()); + let sender = JID::from("sender@s.whatsapp.net".to_owned()); + for _ in 0..1_000 { + with_borrowed_mark_read_args(&id, &chat, &sender, |id, chat, sender| { + assert!(!id.is_null() && !chat.is_null() && !sender.is_null()); + }) + .unwrap(); + } + } +}