Skip to main content

simploxide_client/
id.rs

1//! Type-safe wrappers for SimpleX Chat integer IDs and conversions from API structs.
2//!
3//!  ID types implement `From` for their corresponding API structs(and references to them), so you can pass a
4//! `&Contact`, `GroupInfo`, `ChatItem`, etc. directly wherever a typed ID is expected.
5
6use simploxide_api_types::{
7    AChatItem, CIFile, CIMeta, ChatInfo, ChatItem, ChatRef, ChatType, Contact, FileTransferMeta,
8    GroupChatScope, GroupInfo, GroupMember, GroupRelay, RcvFileTransfer, SndFileTransfer, User,
9    UserContactRequest, UserInfo,
10};
11
12use std::num::NonZeroI64;
13
14macro_rules! typesafe_ids {
15    ($($name:ident),*) => {
16        $(
17            #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18            #[repr(transparent)]
19            pub struct $name(NonZeroI64);
20
21            impl $name {
22                /// # Safety
23                ///
24                /// All SimpleX IDs are starting from 1 which is guaranteed by Sqlite and
25                /// PostgreSQL DBs so it is generally safe to call this method when passing an
26                /// ID value received from SimpleX backend.
27                ///
28                /// In other cases of if in doubts - use [from_raw](Self::from_raw) for version that panics or
29                /// [Self::try_from] for version that returns an error.
30                pub unsafe fn from_raw_unchecked(id: i64) -> Self {
31                    unsafe {
32                        Self(NonZeroI64::new_unchecked(id))
33                    }
34                }
35
36                /// Panics when `id == 0`. Use [Self::try_from] to handle an error
37                pub fn from_raw(id: i64) -> Self {
38                    Self(NonZeroI64::try_from(id).unwrap())
39                }
40
41                pub fn raw(&self) -> i64 {
42                    self.0.get()
43                }
44            }
45
46            impl TryFrom<i64> for $name {
47                type Error = Zero;
48
49                fn try_from(id: i64) -> Result<Self, Self::Error> {
50                    let id = NonZeroI64::new(id).ok_or(Zero(stringify!($name)))?;
51                    Ok(Self(id))
52                }
53            }
54
55            impl From<NonZeroI64> for $name {
56                fn from(id: NonZeroI64) -> Self {
57                    Self(id)
58                }
59            }
60
61            impl ::std::fmt::Display for $name {
62                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> std::fmt::Result {
63                    self.0.fmt(f)
64                }
65            }
66
67            impl ::std::str::FromStr for $name {
68                type Err = ::std::num::ParseIntError;
69
70                fn from_str(s: &str) -> Result<Self, Self::Err> {
71                    Ok(Self(s.parse()?))
72                }
73            }
74        )*
75    }
76}
77
78typesafe_ids!(
79    UserId,
80    ContactId,
81    ContactRequestId,
82    GroupId,
83    FileId,
84    MessageId,
85    MemberId,
86    RelayId
87);
88
89/// Identifies a chat: direct contact, group (optionally scoped to a member support thread),
90/// or local note-to-self.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
92pub enum ChatId {
93    Direct(ContactId),
94    Group {
95        id: GroupId,
96        scope: Option<MemberId>,
97    },
98    Local(UserId),
99}
100
101impl ChatId {
102    /// Creates a [`ChatId::Group`] scoped to a member support thread.
103    pub fn with_group_scope(id: GroupId, group_member_support_id: MemberId) -> Self {
104        Self::Group {
105            id,
106            scope: Some(group_member_support_id),
107        }
108    }
109
110    /// Converts a [`ChatRef`] from a SimpleX API response. Returns `None` for unrecognised chat types.
111    pub fn from_chat_ref(chat_ref: &ChatRef) -> Option<Self> {
112        match chat_ref.chat_type {
113            ChatType::Direct => Some(Self::Direct(unsafe {
114                ContactId::from_raw_unchecked(chat_ref.chat_id)
115            })),
116            ChatType::Group => Some(Self::Group {
117                id: unsafe { GroupId::from_raw_unchecked(chat_ref.chat_id) },
118                scope: chat_ref.chat_scope.as_ref().and_then(|scope| {
119                    scope.member_support().and_then(|id| {
120                        id.as_ref()
121                            .copied()
122                            .map(|id| unsafe { MemberId::from_raw_unchecked(id) })
123                    })
124                }),
125            }),
126            ChatType::Local => Some(Self::Local(unsafe {
127                UserId::from_raw_unchecked(chat_ref.chat_id)
128            })),
129            _ => None,
130        }
131    }
132
133    /// Converts a [`ChatInfo`] from a SimpleX API response. Returns `None` for unrecognised chat types.
134    pub fn from_chat_info(chat_info: &ChatInfo) -> Option<Self> {
135        match chat_info {
136            ChatInfo::Direct { contact, .. } => Some(Self::Direct(ContactId::from(contact))),
137            ChatInfo::Group {
138                group_info,
139                group_chat_scope,
140                ..
141            } => Some(Self::Group {
142                id: GroupId::from(group_info),
143                scope: group_chat_scope.as_ref().and_then(|scope| {
144                    scope
145                        .member_support()
146                        .and_then(|member| member.as_ref().map(MemberId::from))
147                }),
148            }),
149            ChatInfo::Local { note_folder, .. } => Some(Self::Local(unsafe {
150                UserId::from_raw_unchecked(note_folder.user_id)
151            })),
152            _ => None,
153        }
154    }
155
156    /// Converts back into a [`ChatRef`] for use in raw API calls.
157    pub fn into_chat_ref(self) -> ChatRef {
158        let (chat_type, chat_id, chat_scope) = match self {
159            Self::Direct(contact_id) => (ChatType::Direct, contact_id.raw(), None),
160            Self::Group {
161                id: group_id,
162                scope,
163            } => (
164                ChatType::Group,
165                group_id.raw(),
166                scope.map(|member_id| GroupChatScope::MemberSupport {
167                    group_member_id: Some(member_id.raw()),
168                    undocumented: Default::default(),
169                }),
170            ),
171            Self::Local(user_id) => (ChatType::Local, user_id.raw(), None),
172        };
173
174        ChatRef {
175            chat_type,
176            chat_id,
177            chat_scope,
178            undocumented: Default::default(),
179        }
180    }
181
182    pub fn is_direct(&self) -> bool {
183        matches!(self, Self::Direct(_))
184    }
185
186    pub fn is_group(&self) -> bool {
187        matches!(self, Self::Group { .. })
188    }
189
190    pub fn is_local(&self) -> bool {
191        matches!(self, Self::Local(_))
192    }
193}
194
195impl From<ContactId> for ChatId {
196    fn from(id: ContactId) -> Self {
197        Self::Direct(id)
198    }
199}
200
201impl From<GroupId> for ChatId {
202    fn from(id: GroupId) -> Self {
203        Self::Group { id, scope: None }
204    }
205}
206
207impl From<UserId> for ChatId {
208    fn from(id: UserId) -> Self {
209        Self::Local(id)
210    }
211}
212
213#[derive(Debug)]
214pub struct Zero(&'static str);
215
216impl std::fmt::Display for Zero {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        write!(f, "Got {} equal to zero", self.0)
219    }
220}
221
222impl std::error::Error for Zero {}
223
224macro_rules! impl_id_from_struct {
225    ($strct:ty as $id:ty, $val:ident, $conversion:expr) => {
226        impl From<$strct> for $id {
227            fn from($val: $strct) -> Self {
228                $conversion
229            }
230        }
231
232        impl<'a> From<&'a $strct> for $id {
233            fn from($val: &'a $strct) -> Self {
234                $conversion
235            }
236        }
237
238        impl<'a> From<&'a mut $strct> for $id {
239            fn from($val: &'a mut $strct) -> Self {
240                $conversion
241            }
242        }
243    };
244}
245
246impl_id_from_struct!(User as UserId, user, unsafe {
247    UserId::from_raw_unchecked(user.user_id)
248});
249impl_id_from_struct!(UserInfo as UserId, info, UserId::from(&info.user));
250
251impl_id_from_struct!(Contact as ContactId, contact, unsafe {
252    ContactId::from_raw_unchecked(contact.contact_id)
253});
254impl_id_from_struct!(Contact as ChatId, contact, ContactId::from(contact).into());
255
256impl_id_from_struct!(UserContactRequest as ContactRequestId, req, unsafe {
257    ContactRequestId::from_raw_unchecked(req.contact_request_id)
258});
259
260impl_id_from_struct!(GroupInfo as GroupId, group, unsafe {
261    GroupId::from_raw_unchecked(group.group_id)
262});
263impl_id_from_struct!(GroupInfo as ChatId, group, GroupId::from(group).into());
264
265impl_id_from_struct!(CIMeta as MessageId, meta, unsafe {
266    MessageId::from_raw_unchecked(meta.item_id)
267});
268impl_id_from_struct!(ChatItem as MessageId, item, MessageId::from(&item.meta));
269impl_id_from_struct!(AChatItem as MessageId, it, MessageId::from(&it.chat_item));
270
271impl_id_from_struct!(CIFile as FileId, file, unsafe {
272    FileId::from_raw_unchecked(file.file_id)
273});
274impl_id_from_struct!(RcvFileTransfer as FileId, ft, unsafe {
275    FileId::from_raw_unchecked(ft.file_id)
276});
277impl_id_from_struct!(FileTransferMeta as FileId, ft, unsafe {
278    FileId::from_raw_unchecked(ft.file_id)
279});
280impl_id_from_struct!(SndFileTransfer as FileId, ft, unsafe {
281    FileId::from_raw_unchecked(ft.file_id)
282});
283
284impl_id_from_struct!(GroupMember as MemberId, member, unsafe {
285    MemberId::from_raw_unchecked(member.group_member_id)
286});
287impl_id_from_struct!(GroupRelay as RelayId, relay, unsafe {
288    RelayId::from_raw_unchecked(relay.group_relay_id)
289});