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    /// Returns a raw ID regardless of the chat type.
103    ///
104    /// Prefer to use getters like `chat_id.direct().unwrap().raw()` to avoid confusing ID types.
105    pub fn raw(&self) -> i64 {
106        match self {
107            Self::Direct(id) => id.raw(),
108            Self::Group { id, scope: _ } => id.raw(),
109            Self::Local(id) => id.raw(),
110        }
111    }
112
113    /// Creates a [`ChatId::Group`] scoped to a member support thread.
114    pub fn with_group_scope(id: GroupId, group_member_support_id: MemberId) -> Self {
115        Self::Group {
116            id,
117            scope: Some(group_member_support_id),
118        }
119    }
120
121    /// Converts a [`ChatRef`] from a SimpleX API response. Returns `None` for unrecognised chat types.
122    pub fn from_chat_ref(chat_ref: &ChatRef) -> Option<Self> {
123        match chat_ref.chat_type {
124            ChatType::Direct => Some(Self::Direct(unsafe {
125                ContactId::from_raw_unchecked(chat_ref.chat_id)
126            })),
127            ChatType::Group => Some(Self::Group {
128                id: unsafe { GroupId::from_raw_unchecked(chat_ref.chat_id) },
129                scope: chat_ref.chat_scope.as_ref().and_then(|scope| {
130                    scope.member_support().and_then(|id| {
131                        id.as_ref()
132                            .copied()
133                            .map(|id| unsafe { MemberId::from_raw_unchecked(id) })
134                    })
135                }),
136            }),
137            ChatType::Local => Some(Self::Local(unsafe {
138                UserId::from_raw_unchecked(chat_ref.chat_id)
139            })),
140            _ => None,
141        }
142    }
143
144    /// Converts a [`ChatInfo`] from a SimpleX API response. Returns `None` for unrecognised chat types.
145    pub fn from_chat_info(chat_info: &ChatInfo) -> Option<Self> {
146        match chat_info {
147            ChatInfo::Direct { contact, .. } => Some(Self::Direct(ContactId::from(contact))),
148            ChatInfo::Group {
149                group_info,
150                group_chat_scope,
151                ..
152            } => Some(Self::Group {
153                id: GroupId::from(group_info),
154                scope: group_chat_scope.as_ref().and_then(|scope| {
155                    scope
156                        .member_support()
157                        .and_then(|member| member.as_ref().map(MemberId::from))
158                }),
159            }),
160            ChatInfo::Local { note_folder, .. } => Some(Self::Local(unsafe {
161                UserId::from_raw_unchecked(note_folder.user_id)
162            })),
163            _ => None,
164        }
165    }
166
167    /// Converts back into a [`ChatRef`] for use in raw API calls.
168    pub fn into_chat_ref(self) -> ChatRef {
169        let (chat_type, chat_id, chat_scope) = match self {
170            Self::Direct(contact_id) => (ChatType::Direct, contact_id.raw(), None),
171            Self::Group {
172                id: group_id,
173                scope,
174            } => (
175                ChatType::Group,
176                group_id.raw(),
177                scope.map(|member_id| GroupChatScope::MemberSupport {
178                    group_member_id: Some(member_id.raw()),
179                    undocumented: Default::default(),
180                }),
181            ),
182            Self::Local(user_id) => (ChatType::Local, user_id.raw(), None),
183        };
184
185        ChatRef {
186            chat_type,
187            chat_id,
188            chat_scope,
189            undocumented: Default::default(),
190        }
191    }
192
193    pub fn direct(&self) -> Option<ContactId> {
194        if let Self::Direct(contact) = self {
195            Some(*contact)
196        } else {
197            None
198        }
199    }
200
201    pub fn group(&self) -> Option<(GroupId, Option<MemberId>)> {
202        if let Self::Group { id, scope } = self {
203            Some((*id, *scope))
204        } else {
205            None
206        }
207    }
208
209    pub fn local(&self) -> Option<UserId> {
210        if let Self::Local(id) = self {
211            Some(*id)
212        } else {
213            None
214        }
215    }
216
217    pub fn is_direct(&self) -> bool {
218        self.direct().is_some()
219    }
220
221    pub fn is_group(&self) -> bool {
222        self.group().is_some()
223    }
224
225    pub fn is_local(&self) -> bool {
226        self.local().is_some()
227    }
228}
229
230impl From<ContactId> for ChatId {
231    fn from(id: ContactId) -> Self {
232        Self::Direct(id)
233    }
234}
235
236impl From<GroupId> for ChatId {
237    fn from(id: GroupId) -> Self {
238        Self::Group { id, scope: None }
239    }
240}
241
242impl From<UserId> for ChatId {
243    fn from(id: UserId) -> Self {
244        Self::Local(id)
245    }
246}
247
248#[derive(Debug)]
249pub struct Zero(&'static str);
250
251impl std::fmt::Display for Zero {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        write!(f, "Got {} equal to zero", self.0)
254    }
255}
256
257impl std::error::Error for Zero {}
258
259macro_rules! impl_id_from_struct {
260    ($strct:ty as $id:ty, $val:ident, $conversion:expr) => {
261        impl From<$strct> for $id {
262            fn from($val: $strct) -> Self {
263                $conversion
264            }
265        }
266
267        impl<'a> From<&'a $strct> for $id {
268            fn from($val: &'a $strct) -> Self {
269                $conversion
270            }
271        }
272
273        impl<'a> From<&'a mut $strct> for $id {
274            fn from($val: &'a mut $strct) -> Self {
275                $conversion
276            }
277        }
278    };
279}
280
281impl_id_from_struct!(User as UserId, user, unsafe {
282    UserId::from_raw_unchecked(user.user_id)
283});
284impl_id_from_struct!(UserInfo as UserId, info, UserId::from(&info.user));
285
286impl_id_from_struct!(Contact as ContactId, contact, unsafe {
287    ContactId::from_raw_unchecked(contact.contact_id)
288});
289impl_id_from_struct!(Contact as ChatId, contact, ContactId::from(contact).into());
290
291impl_id_from_struct!(UserContactRequest as ContactRequestId, req, unsafe {
292    ContactRequestId::from_raw_unchecked(req.contact_request_id)
293});
294
295impl_id_from_struct!(GroupInfo as GroupId, group, unsafe {
296    GroupId::from_raw_unchecked(group.group_id)
297});
298impl_id_from_struct!(GroupInfo as ChatId, group, GroupId::from(group).into());
299
300impl_id_from_struct!(CIMeta as MessageId, meta, unsafe {
301    MessageId::from_raw_unchecked(meta.item_id)
302});
303impl_id_from_struct!(ChatItem as MessageId, item, MessageId::from(&item.meta));
304impl_id_from_struct!(AChatItem as MessageId, it, MessageId::from(&it.chat_item));
305
306impl_id_from_struct!(CIFile as FileId, file, unsafe {
307    FileId::from_raw_unchecked(file.file_id)
308});
309impl_id_from_struct!(RcvFileTransfer as FileId, ft, unsafe {
310    FileId::from_raw_unchecked(ft.file_id)
311});
312impl_id_from_struct!(FileTransferMeta as FileId, ft, unsafe {
313    FileId::from_raw_unchecked(ft.file_id)
314});
315impl_id_from_struct!(SndFileTransfer as FileId, ft, unsafe {
316    FileId::from_raw_unchecked(ft.file_id)
317});
318
319impl_id_from_struct!(GroupMember as MemberId, member, unsafe {
320    MemberId::from_raw_unchecked(member.group_member_id)
321});
322impl_id_from_struct!(GroupRelay as RelayId, relay, unsafe {
323    RelayId::from_raw_unchecked(relay.group_relay_id)
324});