reach_core/memory/
message.rs

1// SPDX-FileCopyrightText: 2023—2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4use std::ops::Deref;
5
6use ml_kem::EncodedSizeUser;
7use time::OffsetDateTime;
8
9use reach_proc_macros::prosted;
10
11use super::*;
12use crate::{traits::*, wire::EnvelopeId as WireEnvelopeId};
13
14#[derive(Debug)]
15#[prosted(proto::HintedEnvelopeId, Decode, Encode, ProstTraits)]
16pub struct HintedEnvelopeId {
17    pub envelope_id: WireEnvelopeId,
18    pub hint_removal_token: OneSix,
19}
20
21#[cfg(any(feature = "reaching", feature = "reachable"))]
22mod reaching_reachable {
23    use std::{fmt, str};
24
25    use reach_proc_macros::ParticipantPublicKeys;
26    use reach_signatures::Verifies;
27
28    use super::*;
29    use crate::{
30        ParticipantType, error,
31        macros::nested_participant_keys,
32        wire::{
33            EnvelopeId as WireEnvelopeId, RemoveEnvelopeIdHint as WireRemoveEnvelopeIdHint,
34            SealedEnvelopeId as WireSealedEnvelopeId,
35        },
36    };
37
38    impl From<&HintedEnvelopeId> for WireRemoveEnvelopeIdHint {
39        fn from(from: &HintedEnvelopeId) -> Self {
40            Self {
41                envelope_id: from.envelope_id,
42                hint_removal_token: from.hint_removal_token,
43            }
44        }
45    }
46
47    #[prosted(proto::Message, Decode, EncodeOwned, ProstTraitsOwned)]
48    pub struct Message {
49        pub metadata: MessageMetadata,
50        pub chunk: MessageChunk,
51        pub additional_chunks: Vec<MessageChunk>,
52    }
53
54    impl Message {
55        pub fn text(&self) -> Option<Result<&str, str::Utf8Error>> {
56            match &self.chunk.metadata {
57                MessageChunkMetadata::Language(_) => Some(str::from_utf8(&self.chunk.content)),
58                _ => None,
59            }
60        }
61
62        pub fn order(&self) -> &MessageOrder {
63            &self.metadata.order
64        }
65
66        pub fn thread_reference(&self) -> Option<&SixFour> {
67            match &self.metadata.reply_data {
68                ReplyData::ThreadReference(thread_reference) => Some(thread_reference),
69                _ => None,
70            }
71        }
72
73        pub fn reaching_public_keys(&self) -> Option<&ReachingPublicKeys> {
74            match &self.metadata.reply_data {
75                ReplyData::ReachingPublicKeys(reaching_public_keys) => Some(reaching_public_keys),
76                _ => None,
77            }
78        }
79    }
80
81    #[cfg(feature = "reachable")]
82    impl Digestible for Message {
83        fn hashable_bytes(&self) -> Vec<Box<dyn AsRef<[u8]> + '_>> {
84            let mut hashable_bytes: Vec<Box<dyn AsRef<[u8]> + '_>> =
85                Vec::with_capacity(self.additional_chunks.len() + 1);
86            hashable_bytes.push(Box::new(self.chunk.content.as_slice()));
87
88            // TODO: Figure out to get rid of this, any way I've tried results in lifetime errors
89            #[allow(clippy::type_complexity)]
90            let additional_bytes: std::iter::Map<
91                _,
92                fn(&MessageChunk) -> Box<dyn AsRef<[u8]> + '_>,
93            > = self
94                .additional_chunks
95                .iter()
96                .map(|c| Box::new(c.content.as_slice()));
97
98            hashable_bytes.extend(additional_bytes);
99
100            hashable_bytes
101        }
102    }
103
104    impl fmt::Display for Message {
105        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106            match self.order() {
107                MessageOrder::Datetime(datetime) => writeln!(f, "At {datetime}:")?,
108                MessageOrder::Relative(relative) => {
109                    writeln!(f, "Relatively to the first contact {relative}:")?
110                }
111            }
112            for chunk in &self.additional_chunks {
113                match chunk.metadata {
114                    MessageChunkMetadata::Language(_) => {
115                        let content =
116                            str::from_utf8(chunk.content.as_slice()).map_err(|_| fmt::Error)?;
117                        write!(f, "{content}")?;
118                    }
119                    _ => return Err(fmt::Error),
120                }
121            }
122
123            Ok(())
124        }
125    }
126
127    #[derive(Debug)]
128    #[prosted(proto::MessageMetadata, Decode, EncodeOwned)]
129    pub struct MessageMetadata {
130        pub order: MessageOrder,
131        pub reply_data: ReplyData,
132        pub envelope_link: Option<EnvelopeLink>,
133    }
134
135    #[derive(Debug)]
136    pub enum ReplyData {
137        ReachingPublicKeys(Box<ReachingPublicKeys>),
138        ThreadReference(SixFour),
139    }
140
141    impl TryFrom<proto::ReplyData> for ReplyData {
142        type Error = error::DecodeError;
143
144        fn try_from(value: proto::ReplyData) -> Result<Self, Self::Error> {
145            Ok(match value {
146                proto::ReplyData::ReachingPublicKeys(reaching_public_keys) => {
147                    ReplyData::ReachingPublicKeys(Box::new(ReachingPublicKeys::try_from(
148                        reaching_public_keys,
149                    )?))
150                }
151                proto::ReplyData::ThreadReference(bytes) => {
152                    ReplyData::ThreadReference(SixFour::try_from(bytes.as_slice())?)
153                }
154            })
155        }
156    }
157
158    impl From<ReplyData> for proto::ReplyData {
159        fn from(from: ReplyData) -> Self {
160            match from {
161                ReplyData::ReachingPublicKeys(reaching_public_keys) => {
162                    proto::ReplyData::ReachingPublicKeys(proto::ReachingPublicKeys::from(
163                        reaching_public_keys.deref(),
164                    ))
165                }
166                ReplyData::ThreadReference(thread_reference) => {
167                    proto::ReplyData::ThreadReference(thread_reference.to_vec())
168                }
169            }
170        }
171    }
172
173    #[derive(Debug)]
174    pub enum MessageOrder {
175        Datetime(OffsetDateTime),
176        Relative(u64),
177    }
178
179    impl MessageOrder {
180        pub fn datetime(&self) -> Option<&OffsetDateTime> {
181            match self {
182                MessageOrder::Datetime(datetime) => Some(datetime),
183                _ => None,
184            }
185        }
186
187        pub fn relative(&self) -> Option<u64> {
188            match self {
189                MessageOrder::Relative(relative) => Some(*relative),
190                _ => None,
191            }
192        }
193    }
194
195    impl TryFrom<proto::MessageOrder> for MessageOrder {
196        type Error = error::DecodeError;
197
198        fn try_from(from: proto::MessageOrder) -> Result<Self, Self::Error> {
199            use proto::MessageOrder as Order;
200
201            Ok(match from {
202                Order::Datetime(epoch) => MessageOrder::Datetime(
203                    OffsetDateTime::from_unix_timestamp(epoch).map_err(|_| error::DecodeError)?,
204                ),
205                Order::Relative(inner) => MessageOrder::Relative(inner),
206            })
207        }
208    }
209
210    impl From<MessageOrder> for proto::MessageOrder {
211        fn from(from: MessageOrder) -> Self {
212            use proto::MessageOrder as Order;
213
214            match from {
215                MessageOrder::Datetime(inner) => Order::Datetime(inner.unix_timestamp()),
216                MessageOrder::Relative(inner) => Order::Relative(inner),
217            }
218        }
219    }
220
221    impl From<OffsetDateTime> for MessageOrder {
222        fn from(from: OffsetDateTime) -> Self {
223            MessageOrder::Datetime(from)
224        }
225    }
226
227    impl From<u64> for MessageOrder {
228        fn from(from: u64) -> Self {
229            MessageOrder::Relative(from)
230        }
231    }
232
233    #[derive(Debug)]
234    pub enum EnvelopeLink {
235        SealedLink(Box<WireSealedEnvelopeId>),
236        Link(WireEnvelopeId),
237    }
238
239    impl TryFrom<proto::EnvelopeLink> for EnvelopeLink {
240        type Error = error::DecodeError;
241
242        fn try_from(from: proto::EnvelopeLink) -> Result<Self, Self::Error> {
243            use proto::envelope_link as pel;
244
245            match from.envelope_link {
246                Some(pel::EnvelopeLink::SealedLink(inner)) => Ok(EnvelopeLink::SealedLink(
247                    Box::new(WireSealedEnvelopeId::decode(inner)?),
248                )),
249                Some(pel::EnvelopeLink::Link(inner)) => {
250                    Ok(EnvelopeLink::Link(WireEnvelopeId::decode(inner)?))
251                }
252                _ => Err(error::DecodeError),
253            }
254        }
255    }
256
257    impl From<&EnvelopeLink> for proto::EnvelopeLink {
258        fn from(from: &EnvelopeLink) -> Self {
259            use proto::envelope_link as pel;
260
261            let envelope_link = Some(match from {
262                EnvelopeLink::SealedLink(inner) => {
263                    pel::EnvelopeLink::SealedLink(inner.encode_to_vec())
264                }
265                EnvelopeLink::Link(inner) => pel::EnvelopeLink::Link(inner.encode_to_vec()),
266            });
267
268            Self { envelope_link }
269        }
270    }
271
272    #[prosted(proto::MessageChunk, Decode, EncodeOwned)]
273    pub struct MessageChunk {
274        pub metadata: MessageChunkMetadata,
275        pub content: Vec<u8>,
276    }
277
278    impl MessageChunk {
279        pub fn text_only(content: String, language: String) -> MessageChunk {
280            MessageChunk {
281                metadata: MessageChunkMetadata::Language(language),
282                content: content.into_bytes(),
283            }
284        }
285    }
286
287    pub enum MessageChunkMetadata {
288        Language(String),
289        FileChunkMetadata(FileChunkMetadata),
290    }
291
292    impl TryFrom<proto::MessageChunkMetadata> for MessageChunkMetadata {
293        type Error = error::DecodeError;
294
295        fn try_from(from: proto::MessageChunkMetadata) -> Result<Self, Self::Error> {
296            use proto::message_chunk_metadata::Metadata as pmcm;
297
298            Ok(match from.metadata {
299                Some(pmcm::Language(inner)) => MessageChunkMetadata::Language(inner),
300                Some(pmcm::FileChunkMetadata(inner)) => {
301                    MessageChunkMetadata::FileChunkMetadata(inner.try_into()?)
302                }
303                _ => return Err(error::DecodeError),
304            })
305        }
306    }
307
308    impl From<MessageChunkMetadata> for proto::MessageChunkMetadata {
309        fn from(from: MessageChunkMetadata) -> Self {
310            use proto::message_chunk_metadata::Metadata as pmcm;
311
312            Self {
313                metadata: Some(match from {
314                    MessageChunkMetadata::Language(inner) => pmcm::Language(inner.clone()),
315                    MessageChunkMetadata::FileChunkMetadata(inner) => {
316                        pmcm::FileChunkMetadata(inner.into())
317                    }
318                }),
319            }
320        }
321    }
322
323    #[prosted(proto::FileChunkMetadata, Decode, EncodeOwned)]
324    pub struct FileChunkMetadata {
325        pub complete_file_hash: ThreeTwo,
326        pub file_name: Option<String>,
327        pub mime_type: Option<String>,
328        pub description: Option<String>,
329        pub chunks: Option<u32>,
330    }
331
332    #[prosted(proto::ReachingKeys, Decode, Encode)]
333    pub struct ReachingKeys {
334        pub verifying_keys: ReachingVerifyingKeys,
335        pub public_keys: ReachingPublicKeys,
336    }
337
338    nested_participant_keys!(ReachingKeys);
339
340    #[prosted(proto::ReachingPublicKeys, Decode, Encode)]
341    #[derive(ParticipantPublicKeys, Clone, Debug)]
342    pub struct ReachingPublicKeys {
343        pub ec_public_key: X25519Public,
344        pub pq_public_key: MlKemPublic,
345        pub ec_signature: Ed25519Signature,
346        pub pq_signature: FnDsaSignature,
347    }
348
349    impl Signatures for ReachingPublicKeys {
350        fn ec_signature(&self) -> &Ed25519Signature {
351            &self.ec_signature
352        }
353
354        fn pq_signature(&self) -> &FnDsaSignature {
355            &self.pq_signature
356        }
357    }
358
359    impl Verifies<ReachingPublicKeys> for ReachingVerifyingKeys {}
360}
361
362#[cfg(any(feature = "reaching", feature = "reachable"))]
363pub use reaching_reachable::*;