Skip to content
Merged
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
27 changes: 22 additions & 5 deletions whatsrust/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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)
}
}
4 changes: 2 additions & 2 deletions whatsrust/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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) }
Expand Down
135 changes: 16 additions & 119 deletions whatsrust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand All @@ -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() {
Expand Down Expand Up @@ -299,108 +298,6 @@ impl ForwardReport {
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MarkAsReadError {
Disconnected,
Transient,
Permanent,
}

fn with_borrowed_mark_read_args<T>(
msg_id: &MessageId,
chat_jid: &JID,
sender_jid: &JID,
send: impl FnOnce(*const c_char, CJID, CJID) -> T,
) -> Result<T, MarkAsReadError> {
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<CJID> for JID {
unsafe fn to_rust(from: CJID) -> Self {
(&from).into()
Expand Down
2 changes: 1 addition & 1 deletion whatsrust/src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
109 changes: 109 additions & 0 deletions whatsrust/src/read_sync.rs
Original file line number Diff line number Diff line change
@@ -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<T>(
msg_id: &MessageId,
chat_jid: &JID,
sender_jid: &JID,
send: impl FnOnce(*const c_char, *const c_char, *const c_char) -> T,
) -> Result<T, MarkAsReadError> {
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();
}
}
}
Loading