#![allow(
unsafe_code,
reason = "Conversation operations require unsafe for FFI calls to xmtp_sys"
)]
use std::ffi::{CStr, c_char};
use std::ptr;
use crate::error::{self, Result};
use crate::ffi::{
FfiList, OwnedHandle, borrow_c_string, borrow_nullable_string, ffi_usize, identifiers_to_ffi,
read_borrowed_strings, take_c_string, take_nullable_string, to_c_string, to_c_string_array,
to_ffi_len,
};
use crate::types::{
AccountIdentifier, ConsentState, ConversationDebugInfo, ConversationMetadata, ConversationType,
Cursor, DeliveryStatus, DisappearingSettings, GroupPermissionsPreset, HmacKey, HmacKeyEntry,
LastReadTime, ListMessagesOptions, MembershipState, MessageKind, MetadataField,
PermissionLevel, PermissionPolicy, PermissionPolicySet, PermissionUpdateType, Permissions,
SendOptions, SortDirection,
};
#[repr(i32)]
enum AdminAction {
Add = 0,
Remove = 1,
AddSuper = 2,
RemoveSuper = 3,
}
macro_rules! metadata_getter {
($(#[$m:meta])* $name:ident, $ffi_fn:path) => {
$(#[$m])*
pub fn $name(&self) -> Option<String> {
unsafe { take_nullable_string($ffi_fn(self.handle.as_ptr())) }
}
};
}
macro_rules! metadata_setter {
($(#[$m:meta])* $name:ident, $ffi_fn:path) => {
$(#[$m])*
pub fn $name(&self, value: &str) -> Result<()> {
let c = to_c_string(value)?;
error::check(unsafe { $ffi_fn(self.handle.as_ptr(), c.as_ptr()) })
}
};
}
#[derive(Debug, Clone)]
pub struct Message {
pub id: String,
pub conversation_id: String,
pub sender_inbox_id: String,
pub sender_installation_id: String,
pub sent_at_ns: i64,
pub inserted_at_ns: i64,
pub kind: MessageKind,
pub delivery_status: DeliveryStatus,
pub content_type: Option<String>,
pub fallback: Option<String>,
pub content: Vec<u8>,
pub expires_at_ns: i64,
pub num_reactions: i32,
pub num_replies: i32,
}
#[derive(Debug, Clone)]
pub struct GroupMember {
pub inbox_id: String,
pub permission_level: PermissionLevel,
pub consent_state: ConsentState,
pub account_identifiers: Vec<String>,
pub installation_ids: Vec<String>,
}
#[derive(Debug)]
pub struct Conversation {
handle: OwnedHandle<xmtp_sys::XmtpFfiConversation>,
}
impl Conversation {
pub(crate) fn from_raw(ptr: *mut xmtp_sys::XmtpFfiConversation) -> Result<Self> {
OwnedHandle::new(ptr, xmtp_sys::xmtp_conversation_free).map(|h| Self { handle: h })
}
pub(crate) const fn handle_ptr(&self) -> *const xmtp_sys::XmtpFfiConversation {
self.handle.as_ptr()
}
#[must_use]
pub fn id(&self) -> String {
let ptr = unsafe { xmtp_sys::xmtp_conversation_id(self.handle.as_ptr()) };
unsafe { take_c_string(ptr) }.unwrap_or_default()
}
#[must_use]
pub fn conversation_type(&self) -> Option<ConversationType> {
ConversationType::from_ffi(unsafe {
xmtp_sys::xmtp_conversation_type(self.handle.as_ptr())
})
}
#[must_use]
pub fn created_at_ns(&self) -> i64 {
unsafe { xmtp_sys::xmtp_conversation_created_at_ns(self.handle.as_ptr()) }
}
#[must_use]
pub fn is_active(&self) -> bool {
unsafe { xmtp_sys::xmtp_conversation_is_active(self.handle.as_ptr()) == 1 }
}
#[must_use]
pub fn membership_state(&self) -> Option<MembershipState> {
MembershipState::from_ffi(unsafe {
xmtp_sys::xmtp_conversation_membership_state(self.handle.as_ptr())
})
}
metadata_getter!( dm_peer_inbox_id, xmtp_sys::xmtp_conversation_dm_peer_inbox_id);
metadata_getter!( added_by_inbox_id, xmtp_sys::xmtp_conversation_added_by_inbox_id);
metadata_getter!( name, xmtp_sys::xmtp_conversation_group_name);
metadata_setter!( set_name, xmtp_sys::xmtp_conversation_update_group_name);
metadata_getter!( description, xmtp_sys::xmtp_conversation_group_description);
metadata_setter!( set_description, xmtp_sys::xmtp_conversation_update_group_description);
metadata_getter!( image_url, xmtp_sys::xmtp_conversation_group_image_url);
metadata_setter!( set_image_url, xmtp_sys::xmtp_conversation_update_group_image_url);
metadata_getter!( app_data, xmtp_sys::xmtp_conversation_app_data);
metadata_setter!( set_app_data, xmtp_sys::xmtp_conversation_update_app_data);
pub fn paused_for_version(&self) -> Result<Option<String>> {
let mut out: *mut c_char = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_paused_for_version(self.handle.as_ptr(), &raw mut out)
};
error::check(rc)?;
if out.is_null() {
Ok(None)
} else {
unsafe { take_c_string(out) }.map(Some)
}
}
pub fn metadata(&self) -> Result<ConversationMetadata> {
let mut out: *mut xmtp_sys::XmtpFfiGroupMetadata = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_group_metadata(self.handle.as_ptr(), &raw mut out)
};
error::check(rc)?;
if out.is_null() {
return Err(crate::XmtpError::NullPointer);
}
let meta = unsafe { &*out };
let creator = unsafe { take_c_string(meta.creator_inbox_id) }.unwrap_or_default();
let conv_type = ConversationType::from_ffi(meta.conversation_type as i32)
.unwrap_or(ConversationType::Group);
unsafe { xmtp_sys::xmtp_group_metadata_free(out) };
Ok(ConversationMetadata {
creator_inbox_id: creator,
conversation_type: conv_type,
})
}
pub fn permissions(&self) -> Result<Permissions> {
let mut out: *mut xmtp_sys::XmtpFfiGroupPermissions = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_group_permissions(self.handle.as_ptr(), &raw mut out)
};
error::check(rc)?;
if out.is_null() {
return Err(crate::XmtpError::NullPointer);
}
let perms = unsafe { &*out };
let result = read_permissions(perms);
unsafe { xmtp_sys::xmtp_group_permissions_free(out) };
Ok(result)
}
pub fn sync(&self) -> Result<()> {
error::check(unsafe { xmtp_sys::xmtp_conversation_sync(self.handle.as_ptr()) })
}
pub fn send(&self, content: &[u8]) -> Result<String> {
self.send_inner(content, ptr::null(), xmtp_sys::xmtp_conversation_send)
}
pub fn send_with(&self, content: &[u8], opts: &SendOptions) -> Result<String> {
let ffi = send_opts_to_ffi(*opts);
self.send_inner(content, &raw const ffi, xmtp_sys::xmtp_conversation_send)
}
pub fn send_optimistic(&self, content: &[u8]) -> Result<String> {
self.send_inner(
content,
ptr::null(),
xmtp_sys::xmtp_conversation_send_optimistic,
)
}
pub fn send_optimistic_with(&self, content: &[u8], opts: &SendOptions) -> Result<String> {
let ffi = send_opts_to_ffi(*opts);
self.send_inner(
content,
&raw const ffi,
xmtp_sys::xmtp_conversation_send_optimistic,
)
}
fn send_inner(
&self,
content: &[u8],
opts: *const xmtp_sys::XmtpFfiSendOpts,
ffi_fn: unsafe extern "C" fn(
*const xmtp_sys::XmtpFfiConversation,
*const u8,
i32,
*const xmtp_sys::XmtpFfiSendOpts,
*mut *mut c_char,
) -> i32,
) -> Result<String> {
let len = to_ffi_len(content.len())?;
let mut out: *mut c_char = ptr::null_mut();
let rc = unsafe {
ffi_fn(
self.handle.as_ptr(),
content.as_ptr(),
len,
opts,
&raw mut out,
)
};
error::check(rc)?;
unsafe { take_c_string(out) }
}
pub fn publish_messages(&self) -> Result<()> {
error::check(unsafe { xmtp_sys::xmtp_conversation_publish_messages(self.handle.as_ptr()) })
}
pub fn messages(&self) -> Result<Vec<Message>> {
self.list_messages(&ListMessagesOptions::default())
}
pub fn list_messages(&self, options: &ListMessagesOptions) -> Result<Vec<Message>> {
let ffi_opts = msg_opts_to_ffi(options);
let mut list: *mut xmtp_sys::XmtpFfiEnrichedMessageList = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_list_enriched_messages(
self.handle.as_ptr(),
&raw const ffi_opts,
&raw mut list,
)
};
error::check(rc)?;
Ok(read_enriched_message_list(list))
}
#[must_use]
pub fn count_messages(&self, options: &ListMessagesOptions) -> i64 {
let ffi_opts = msg_opts_to_ffi(options);
unsafe {
xmtp_sys::xmtp_conversation_count_messages(self.handle.as_ptr(), &raw const ffi_opts)
}
}
pub fn members(&self) -> Result<Vec<GroupMember>> {
let mut list: *mut xmtp_sys::XmtpFfiGroupMemberList = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_list_members(self.handle.as_ptr(), &raw mut list)
};
error::check(rc)?;
read_member_list(list)
}
pub fn add_members_by_inbox_id(&self, inbox_ids: &[&str]) -> Result<()> {
let (_owned, ptrs) = to_c_string_array(inbox_ids)?;
let len = to_ffi_len(ptrs.len())?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_add_members(self.handle.as_ptr(), ptrs.as_ptr(), len)
})
}
pub fn remove_members_by_inbox_id(&self, inbox_ids: &[&str]) -> Result<()> {
let (_owned, ptrs) = to_c_string_array(inbox_ids)?;
let len = to_ffi_len(ptrs.len())?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_remove_members(self.handle.as_ptr(), ptrs.as_ptr(), len)
})
}
pub fn add_members_by_identity(&self, identifiers: &[AccountIdentifier]) -> Result<()> {
let (_owned, ptrs, kinds) = identifiers_to_ffi(identifiers)?;
let len = to_ffi_len(ptrs.len())?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_add_members_by_identity(
self.handle.as_ptr(),
ptrs.as_ptr(),
kinds.as_ptr(),
len,
)
})
}
pub fn remove_members_by_identity(&self, identifiers: &[AccountIdentifier]) -> Result<()> {
let (_owned, ptrs, kinds) = identifiers_to_ffi(identifiers)?;
let len = to_ffi_len(ptrs.len())?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_remove_members_by_identity(
self.handle.as_ptr(),
ptrs.as_ptr(),
kinds.as_ptr(),
len,
)
})
}
pub fn leave(&self) -> Result<()> {
error::check(unsafe { xmtp_sys::xmtp_conversation_leave(self.handle.as_ptr()) })
}
pub fn consent_state(&self) -> Result<ConsentState> {
let mut out = 0i32;
let rc = unsafe {
xmtp_sys::xmtp_conversation_consent_state(self.handle.as_ptr(), &raw mut out)
};
error::check(rc)?;
ConsentState::from_ffi(out)
.ok_or_else(|| crate::XmtpError::Ffi(format!("unknown consent state: {out}")))
}
pub fn set_consent(&self, state: ConsentState) -> Result<()> {
error::check(unsafe {
xmtp_sys::xmtp_conversation_update_consent_state(self.handle.as_ptr(), state as i32)
})
}
#[must_use]
pub fn disappearing_settings(&self) -> Option<DisappearingSettings> {
let mut out = xmtp_sys::XmtpFfiDisappearingSettings::default();
let rc = unsafe {
xmtp_sys::xmtp_conversation_disappearing_settings(self.handle.as_ptr(), &raw mut out)
};
if rc == 0 {
Some(DisappearingSettings {
from_ns: out.from_ns,
in_ns: out.in_ns,
})
} else {
None
}
}
pub fn set_disappearing(&self, settings: DisappearingSettings) -> Result<()> {
let ffi = xmtp_sys::XmtpFfiDisappearingSettings {
from_ns: settings.from_ns,
in_ns: settings.in_ns,
};
error::check(unsafe {
xmtp_sys::xmtp_conversation_update_disappearing_settings(
self.handle.as_ptr(),
&raw const ffi,
)
})
}
pub fn clear_disappearing(&self) -> Result<()> {
error::check(unsafe {
xmtp_sys::xmtp_conversation_remove_disappearing_settings(self.handle.as_ptr())
})
}
#[must_use]
pub fn is_disappearing_enabled(&self) -> bool {
unsafe { xmtp_sys::xmtp_conversation_is_disappearing_enabled(self.handle.as_ptr()) == 1 }
}
pub fn set_permission_policy(
&self,
update_type: PermissionUpdateType,
policy: PermissionPolicy,
metadata_field: Option<MetadataField>,
) -> Result<()> {
let c_field = metadata_field
.map(|f| to_c_string(f.as_str()))
.transpose()?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_update_permission_policy(
self.handle.as_ptr(),
update_type as i32,
policy.to_write_i32(),
c_field.as_ref().map_or(ptr::null(), |c| c.as_ptr()),
)
})
}
pub fn last_message(&self) -> Result<Option<Message>> {
let opts = ListMessagesOptions {
limit: 1,
direction: Some(SortDirection::Descending),
..Default::default()
};
Ok(self.list_messages(&opts)?.into_iter().next())
}
pub fn add_admin(&self, inbox_id: &str) -> Result<()> {
self.update_admin_list(inbox_id, AdminAction::Add)
}
pub fn remove_admin(&self, inbox_id: &str) -> Result<()> {
self.update_admin_list(inbox_id, AdminAction::Remove)
}
pub fn add_super_admin(&self, inbox_id: &str) -> Result<()> {
self.update_admin_list(inbox_id, AdminAction::AddSuper)
}
pub fn remove_super_admin(&self, inbox_id: &str) -> Result<()> {
self.update_admin_list(inbox_id, AdminAction::RemoveSuper)
}
fn update_admin_list(&self, inbox_id: &str, action: AdminAction) -> Result<()> {
let c = to_c_string(inbox_id)?;
error::check(unsafe {
xmtp_sys::xmtp_conversation_update_admin_list(
self.handle.as_ptr(),
c.as_ptr(),
action as i32,
)
})
}
#[must_use]
pub fn admins(&self) -> Vec<String> {
let mut count = 0i32;
let ptr = unsafe {
xmtp_sys::xmtp_conversation_list_admins(self.handle.as_ptr(), &raw mut count)
};
unsafe { read_borrowed_strings(ptr.cast_const(), count) }
}
#[must_use]
pub fn super_admins(&self) -> Vec<String> {
let mut count = 0i32;
let ptr = unsafe {
xmtp_sys::xmtp_conversation_list_super_admins(self.handle.as_ptr(), &raw mut count)
};
unsafe { read_borrowed_strings(ptr.cast_const(), count) }
}
#[must_use]
pub fn is_admin(&self, inbox_id: &str) -> bool {
to_c_string(inbox_id).is_ok_and(|c| unsafe {
xmtp_sys::xmtp_conversation_is_admin(self.handle.as_ptr(), c.as_ptr()) == 1
})
}
#[must_use]
pub fn is_super_admin(&self, inbox_id: &str) -> bool {
to_c_string(inbox_id).is_ok_and(|c| unsafe {
xmtp_sys::xmtp_conversation_is_super_admin(self.handle.as_ptr(), c.as_ptr()) == 1
})
}
pub fn duplicate_dms(&self) -> Result<Vec<Self>> {
let mut list: *mut xmtp_sys::XmtpFfiConversationList = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_duplicate_dms(self.handle.as_ptr(), &raw mut list)
};
error::check(rc)?;
read_conversation_list_inner(list)
}
pub fn debug_info(&self) -> Result<ConversationDebugInfo> {
let mut out = xmtp_sys::XmtpFfiConversationDebugInfo::default();
let rc =
unsafe { xmtp_sys::xmtp_conversation_debug_info(self.handle.as_ptr(), &raw mut out) };
error::check(rc)?;
let info = read_debug_info(&out);
unsafe { xmtp_sys::xmtp_conversation_debug_info_free(&raw mut out) };
Ok(info)
}
pub fn last_read_times(&self) -> Result<Vec<LastReadTime>> {
let mut list: *mut xmtp_sys::XmtpFfiLastReadTimeList = ptr::null_mut();
let rc = unsafe {
xmtp_sys::xmtp_conversation_last_read_times(self.handle.as_ptr(), &raw mut list)
};
error::check(rc)?;
Ok(read_last_read_times(list))
}
pub fn hmac_keys(&self) -> Result<Vec<HmacKeyEntry>> {
let mut map: *mut xmtp_sys::XmtpFfiHmacKeyMap = ptr::null_mut();
let rc =
unsafe { xmtp_sys::xmtp_conversation_hmac_keys(self.handle.as_ptr(), &raw mut map) };
error::check(rc)?;
Ok(read_hmac_key_map(map))
}
}
fn send_opts_to_ffi(opts: SendOptions) -> xmtp_sys::XmtpFfiSendOpts {
xmtp_sys::XmtpFfiSendOpts {
should_push: i32::from(opts.should_push),
}
}
pub(crate) fn msg_opts_to_ffi(
options: &ListMessagesOptions,
) -> xmtp_sys::XmtpFfiListMessagesOptions {
xmtp_sys::XmtpFfiListMessagesOptions {
sent_after_ns: options.sent_after_ns,
sent_before_ns: options.sent_before_ns,
limit: options.limit,
delivery_status: options.delivery_status.map_or(-1, |d| d as i32),
kind: options.kind.map_or(-1, |k| k as i32),
direction: options.direction.map_or(0, |d| d as i32),
..Default::default()
}
}
pub(crate) fn read_enriched_message_list(
ptr: *mut xmtp_sys::XmtpFfiEnrichedMessageList,
) -> Vec<Message> {
let list = FfiList::new(
ptr,
xmtp_sys::xmtp_enriched_message_list_len,
xmtp_sys::xmtp_enriched_message_list_free,
);
let mut msgs = Vec::with_capacity(ffi_usize(list.len()));
for i in 0..list.len() {
let p = unsafe { xmtp_sys::xmtp_enriched_message_list_get(list.as_ptr(), i) };
if p.is_null() {
continue;
}
let m = unsafe { &*p };
let content = if m.content_bytes.is_null() || m.content_bytes_len <= 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(m.content_bytes, ffi_usize(m.content_bytes_len)) }
.to_vec()
};
msgs.push(Message {
id: unsafe { borrow_c_string(m.id) },
conversation_id: unsafe { borrow_c_string(m.group_id) },
sender_inbox_id: unsafe { borrow_c_string(m.sender_inbox_id) },
sender_installation_id: unsafe { borrow_c_string(m.sender_installation_id) },
sent_at_ns: m.sent_at_ns,
inserted_at_ns: m.inserted_at_ns,
kind: MessageKind::from_ffi(m.kind as i32).unwrap_or(MessageKind::Application),
delivery_status: DeliveryStatus::from_ffi(m.delivery_status as i32)
.unwrap_or(DeliveryStatus::Unpublished),
content_type: unsafe { borrow_nullable_string(m.content_type) },
fallback: unsafe { borrow_nullable_string(m.fallback_text) },
content,
expires_at_ns: m.expires_at_ns,
num_reactions: m.num_reactions,
num_replies: m.num_replies,
});
}
msgs
}
fn read_member_list(ptr: *mut xmtp_sys::XmtpFfiGroupMemberList) -> Result<Vec<GroupMember>> {
let list = FfiList::new(
ptr,
xmtp_sys::xmtp_group_member_list_len,
xmtp_sys::xmtp_group_member_list_free,
);
let mut members = Vec::with_capacity(ffi_usize(list.len()));
for i in 0..list.len() {
let lp = list.as_ptr();
let p = unsafe { xmtp_sys::xmtp_group_member_inbox_id(lp, i) };
let inbox_id = unsafe { take_c_string(p) }?;
let permission_level = PermissionLevel::from_ffi(unsafe {
xmtp_sys::xmtp_group_member_permission_level(lp, i)
})
.unwrap_or(PermissionLevel::Member);
let consent_state =
ConsentState::from_ffi(unsafe { xmtp_sys::xmtp_group_member_consent_state(lp, i) })
.unwrap_or(ConsentState::Unknown);
let mut acct_count = 0i32;
let acct_ptr =
unsafe { xmtp_sys::xmtp_group_member_account_identifiers(lp, i, &raw mut acct_count) };
let account_identifiers = unsafe { read_borrowed_strings(acct_ptr, acct_count) };
let mut inst_count = 0i32;
let inst_ptr =
unsafe { xmtp_sys::xmtp_group_member_installation_ids(lp, i, &raw mut inst_count) };
let installation_ids = unsafe { read_borrowed_strings(inst_ptr, inst_count) };
members.push(GroupMember {
inbox_id,
permission_level,
consent_state,
account_identifiers,
installation_ids,
});
}
Ok(members)
}
pub(crate) fn read_conversation_list_inner(
ptr: *mut xmtp_sys::XmtpFfiConversationList,
) -> Result<Vec<Conversation>> {
let list = FfiList::new(
ptr,
xmtp_sys::xmtp_conversation_list_len,
xmtp_sys::xmtp_conversation_list_free,
);
let mut convs = Vec::with_capacity(ffi_usize(list.len()));
for i in 0..list.len() {
let mut conv: *mut xmtp_sys::XmtpFfiConversation = ptr::null_mut();
let rc = unsafe { xmtp_sys::xmtp_conversation_list_get(list.as_ptr(), i, &raw mut conv) };
if rc == 0 && !conv.is_null() {
convs.push(Conversation::from_raw(conv)?);
}
}
Ok(convs)
}
fn read_permissions(p: &xmtp_sys::XmtpFfiGroupPermissions) -> Permissions {
let ps = &p.policy_set;
let policy = |v: xmtp_sys::XmtpFfiPermissionPolicy| -> PermissionPolicy {
PermissionPolicy::from_ffi(v as i32).unwrap_or(PermissionPolicy::Deny)
};
Permissions {
preset: GroupPermissionsPreset::from_ffi(p.policy_type as i32)
.unwrap_or(GroupPermissionsPreset::Custom),
policies: PermissionPolicySet {
add_member: policy(ps.add_member_policy),
remove_member: policy(ps.remove_member_policy),
add_admin: policy(ps.add_admin_policy),
remove_admin: policy(ps.remove_admin_policy),
update_group_name: policy(ps.update_group_name_policy),
update_group_description: policy(ps.update_group_description_policy),
update_group_image_url: policy(ps.update_group_image_url_square_policy),
update_message_disappearing: policy(ps.update_message_disappearing_policy),
update_app_data: policy(ps.update_app_data_policy),
},
}
}
fn read_debug_info(d: &xmtp_sys::XmtpFfiConversationDebugInfo) -> ConversationDebugInfo {
let cursors = if d.cursors.is_null() || d.cursors_count <= 0 {
vec![]
} else {
let slice = unsafe {
std::slice::from_raw_parts(d.cursors, d.cursors_count.unsigned_abs() as usize)
};
slice
.iter()
.map(|c| Cursor {
originator_id: c.originator_id,
sequence_id: c.sequence_id,
})
.collect()
};
ConversationDebugInfo {
epoch: d.epoch,
maybe_forked: d.maybe_forked != 0,
fork_details: unsafe { borrow_nullable_string(d.fork_details) },
is_commit_log_forked: match d.is_commit_log_forked {
0 => Some(false),
1 => Some(true),
_ => None,
},
local_commit_log: unsafe { borrow_nullable_string(d.local_commit_log) },
remote_commit_log: unsafe { borrow_nullable_string(d.remote_commit_log) },
cursors,
}
}
fn read_last_read_times(ptr: *mut xmtp_sys::XmtpFfiLastReadTimeList) -> Vec<LastReadTime> {
let list = FfiList::new(
ptr,
xmtp_sys::xmtp_last_read_time_list_len,
xmtp_sys::xmtp_last_read_time_list_free,
);
let mut result = Vec::with_capacity(ffi_usize(list.len()));
for i in 0..list.len() {
let p = unsafe { xmtp_sys::xmtp_last_read_time_list_get(list.as_ptr(), i) };
if p.is_null() {
continue;
}
let entry = unsafe { &*p };
result.push(LastReadTime {
inbox_id: unsafe { borrow_c_string(entry.inbox_id) },
timestamp_ns: entry.timestamp_ns,
});
}
result
}
pub(crate) fn read_hmac_key_map(ptr: *mut xmtp_sys::XmtpFfiHmacKeyMap) -> Vec<HmacKeyEntry> {
let map = FfiList::new(
ptr,
xmtp_sys::xmtp_hmac_key_map_len,
xmtp_sys::xmtp_hmac_key_map_free,
);
let mut entries = Vec::with_capacity(ffi_usize(map.len()));
for i in 0..map.len() {
let mp = map.as_ptr();
let gid_ptr = unsafe { xmtp_sys::xmtp_hmac_key_map_group_id(mp, i) };
let group_id = if gid_ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(gid_ptr) }
.to_str()
.unwrap_or_default()
.to_owned()
};
let mut key_count = 0i32;
let keys_ptr = unsafe { xmtp_sys::xmtp_hmac_key_map_keys(mp, i, &raw mut key_count) };
let keys = read_hmac_keys_inner(keys_ptr, key_count);
entries.push(HmacKeyEntry { group_id, keys });
}
entries
}
fn read_hmac_keys_inner(keys_ptr: *const xmtp_sys::XmtpFfiHmacKey, key_count: i32) -> Vec<HmacKey> {
if keys_ptr.is_null() || key_count <= 0 {
return vec![];
}
let slice = unsafe { std::slice::from_raw_parts(keys_ptr, ffi_usize(key_count)) };
slice
.iter()
.map(|k| {
let key = if k.key.is_null() || k.key_len <= 0 {
vec![]
} else {
unsafe { std::slice::from_raw_parts(k.key, ffi_usize(k.key_len)) }.to_vec()
};
HmacKey {
key,
epoch: k.epoch,
}
})
.collect()
}