tap_msg/
didcomm.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5/// Wrapper for plain message. Provides helpers for message building and packing/unpacking.
6/// Adapted from https://github.com/sicpa-dlab/didcomm-rust/blob/main/src/message/message.rs
7#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
8#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")]
9pub struct PlainMessage<T = Value> {
10    /// Message id. Must be unique to the sender.
11    pub id: String,
12
13    /// Optional, if present it must be "application/didcomm-plain+json"
14    #[serde(default = "default_typ")]
15    pub typ: String,
16
17    /// Message type attribute value MUST be a valid Message Type URI,
18    /// that when resolved gives human readable information about the message.
19    /// The attribute’s value also informs the content of the message,
20    /// or example the presence of other attributes and how they should be processed.
21    #[serde(rename = "type")]
22    pub type_: String,
23
24    /// Message body - strongly typed when T is specified.
25    pub body: T,
26
27    /// Sender identifier. The from attribute MUST be a string that is a valid DID
28    /// or DID URL (without the fragment component) which identifies the sender of the message.
29    pub from: String,
30
31    /// Identifier(s) for recipients. MUST be an array of strings where each element
32    /// is a valid DID or DID URL (without the fragment component) that identifies a member
33    /// of the message’s intended audience.
34    pub to: Vec<String>,
35
36    /// Uniquely identifies the thread that the message belongs to.
37    /// If not included the id property of the message MUST be treated as the value of the `thid`.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub thid: Option<String>,
40
41    /// If the message is a child of a thread the `pthid`
42    /// will uniquely identify which thread is the parent.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub pthid: Option<String>,
45
46    /// Custom message headers.
47    #[serde(flatten)]
48    #[serde(skip_serializing_if = "HashMap::is_empty")]
49    pub extra_headers: HashMap<String, Value>,
50
51    /// The attribute is used for the sender
52    /// to express when they created the message, expressed in
53    /// UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
54    /// This attribute is informative to the recipient, and may be relied on by protocols.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub created_time: Option<u64>,
57
58    /// The expires_time attribute is used for the sender to express when they consider
59    /// the message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
60    /// This attribute signals when the message is considered no longer valid by the sender.
61    /// When omitted, the message is considered to have no expiration by the sender.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub expires_time: Option<u64>,
64
65    /// from_prior is a compactly serialized signed JWT containing FromPrior value
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub from_prior: Option<String>,
68
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub attachments: Option<Vec<Attachment>>,
71}
72
73/// Type alias for backward compatibility - PlainMessage with Value body
74pub type UntypedPlainMessage = PlainMessage<Value>;
75
76const PLAINTEXT_TYP: &str = "application/didcomm-plain+json";
77
78fn default_typ() -> String {
79    PLAINTEXT_TYP.to_string()
80}
81
82// Implementation for generic PlainMessage<T>
83impl<T> PlainMessage<T>
84where
85    T: serde::Serialize + serde::de::DeserializeOwned,
86{
87    /// Create a new PlainMessage with the given body
88    pub fn new(id: String, type_: String, body: T, from: String) -> Self {
89        Self {
90            id,
91            typ: default_typ(),
92            type_,
93            body,
94            from,
95            to: vec![],
96            thid: None,
97            pthid: None,
98            created_time: Some(chrono::Utc::now().timestamp() as u64),
99            expires_time: None,
100            from_prior: None,
101            attachments: None,
102            extra_headers: HashMap::new(),
103        }
104    }
105
106    /// Builder method to set recipients
107    pub fn with_recipients(mut self, to: Vec<String>) -> Self {
108        self.to = to;
109        self
110    }
111
112    /// Builder method to add a single recipient
113    pub fn with_recipient(mut self, recipient: &str) -> Self {
114        self.to.push(recipient.to_string());
115        self
116    }
117
118    /// Builder method to set thread ID
119    pub fn with_thread_id(mut self, thid: Option<String>) -> Self {
120        self.thid = thid;
121        self
122    }
123
124    /// Builder method to set parent thread ID
125    pub fn with_parent_thread_id(mut self, pthid: Option<String>) -> Self {
126        self.pthid = pthid;
127        self
128    }
129
130    /// Builder method to set expiration time
131    pub fn with_expires_at(mut self, expires_time: u64) -> Self {
132        self.expires_time = Some(expires_time);
133        self
134    }
135
136    /// Builder method to add attachments
137    pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
138        self.attachments = Some(attachments);
139        self
140    }
141
142    /// Builder method to add a custom header
143    pub fn with_header(mut self, key: String, value: Value) -> Self {
144        self.extra_headers.insert(key, value);
145        self
146    }
147}
148
149// Implementation specific to typed PlainMessage where T implements TapMessageBody
150impl<T> PlainMessage<T>
151where
152    T: crate::message::TapMessageBody + serde::Serialize + serde::de::DeserializeOwned,
153{
154    /// Create a new typed message
155    pub fn new_typed(body: T, from: &str) -> Self {
156        Self {
157            id: uuid::Uuid::new_v4().to_string(),
158            typ: default_typ(),
159            type_: T::message_type().to_string(),
160            body,
161            from: from.to_string(),
162            to: vec![],
163            thid: None,
164            pthid: None,
165            created_time: Some(chrono::Utc::now().timestamp() as u64),
166            expires_time: None,
167            from_prior: None,
168            attachments: None,
169            extra_headers: HashMap::new(),
170        }
171    }
172
173    /// Convert to untyped PlainMessage for serialization/transport
174    pub fn to_plain_message(self) -> crate::error::Result<PlainMessage<Value>> {
175        // First serialize the body with the @type field
176        let mut body_value = serde_json::to_value(&self.body)?;
177
178        // Ensure @type is set in the body
179        if let Some(body_obj) = body_value.as_object_mut() {
180            body_obj.insert(
181                "@type".to_string(),
182                Value::String(T::message_type().to_string()),
183            );
184        }
185
186        Ok(PlainMessage {
187            id: self.id,
188            typ: self.typ,
189            type_: self.type_,
190            body: body_value,
191            from: self.from,
192            to: self.to,
193            thid: self.thid,
194            pthid: self.pthid,
195            created_time: self.created_time,
196            expires_time: self.expires_time,
197            from_prior: self.from_prior,
198            attachments: self.attachments,
199            extra_headers: self.extra_headers,
200        })
201    }
202
203    /// Extract recipients based on the message body participants
204    pub fn extract_participants(&self) -> Vec<String> {
205        let mut participants = vec![];
206
207        // Try to extract from MessageContext first if implemented
208        if let Some(ctx_participants) = self.try_extract_from_context() {
209            participants = ctx_participants;
210        } else {
211            // Fallback to TapMessageBody::to_didcomm
212            if let Ok(plain_msg) = self.body.to_didcomm(&self.from) {
213                participants = plain_msg.to;
214            }
215        }
216
217        // Add any explicitly set recipients
218        for recipient in &self.to {
219            if !participants.contains(recipient) {
220                participants.push(recipient.clone());
221            }
222        }
223
224        participants
225    }
226
227    /// Try to extract participants using MessageContext if available
228    fn try_extract_from_context(&self) -> Option<Vec<String>> {
229        // This is a compile-time check - if T implements MessageContext,
230        // we can use it. Otherwise, this will return None.
231        //
232        // In practice, this would need to be implemented using trait objects
233        // or type erasure, but for now we'll use the TapMessageBody approach
234        // and let individual message types override this behavior.
235        None
236    }
237}
238
239// Implementation for PlainMessage<T> where T implements both TapMessageBody and MessageContext
240impl<T> PlainMessage<T>
241where
242    T: crate::message::TapMessageBody
243        + crate::message::MessageContext
244        + serde::Serialize
245        + serde::de::DeserializeOwned,
246{
247    /// Extract participants using MessageContext
248    pub fn extract_participants_with_context(&self) -> Vec<String> {
249        self.body.participant_dids()
250    }
251
252    /// Create a typed message with automatic recipient detection
253    pub fn new_typed_with_context(body: T, from: &str) -> Self {
254        let participants = body.participant_dids();
255
256        Self {
257            id: uuid::Uuid::new_v4().to_string(),
258            typ: default_typ(),
259            type_: T::message_type().to_string(),
260            body,
261            from: from.to_string(),
262            to: participants.into_iter().filter(|did| did != from).collect(),
263            thid: None,
264            pthid: None,
265            created_time: Some(chrono::Utc::now().timestamp() as u64),
266            expires_time: None,
267            from_prior: None,
268            attachments: None,
269            extra_headers: HashMap::new(),
270        }
271    }
272
273    /// Get routing hints from the message body
274    pub fn routing_hints(&self) -> crate::message::RoutingHints {
275        self.body.routing_hints()
276    }
277
278    /// Get transaction context from the message body
279    pub fn transaction_context(&self) -> Option<crate::message::TransactionContext> {
280        self.body.transaction_context()
281    }
282}
283
284// Implementation for PlainMessage<Value> (untyped)
285impl PlainMessage<Value> {
286    /// Create a typed message from an untyped PlainMessage
287    pub fn from_untyped(plain_msg: PlainMessage<Value>) -> Self {
288        plain_msg
289    }
290
291    /// Try to parse the body into a specific TAP message type
292    pub fn parse_body<T: crate::message::TapMessageBody>(
293        self,
294    ) -> crate::error::Result<PlainMessage<T>> {
295        // Check type matches
296        if self.type_ != T::message_type() {
297            return Err(crate::error::Error::Validation(format!(
298                "Type mismatch: expected {}, got {}",
299                T::message_type(),
300                self.type_
301            )));
302        }
303
304        // Parse the body
305        let typed_body: T = serde_json::from_value(self.body)?;
306
307        Ok(PlainMessage {
308            id: self.id,
309            typ: self.typ,
310            type_: self.type_,
311            body: typed_body,
312            from: self.from,
313            to: self.to,
314            thid: self.thid,
315            pthid: self.pthid,
316            created_time: self.created_time,
317            expires_time: self.expires_time,
318            from_prior: self.from_prior,
319            attachments: self.attachments,
320            extra_headers: self.extra_headers,
321        })
322    }
323
324    /// Parse into the TapMessage enum for runtime dispatch
325    pub fn parse_tap_message(
326        &self,
327    ) -> crate::error::Result<crate::message::tap_message_enum::TapMessage> {
328        crate::message::tap_message_enum::TapMessage::from_plain_message(self)
329    }
330}
331
332/// Extension trait for PlainMessage to work with typed messages
333pub trait PlainMessageExt<T> {
334    /// Convert to a typed message
335    fn into_typed(self) -> PlainMessage<T>;
336
337    /// Try to parse as a specific message type
338    fn parse_as<U: crate::message::TapMessageBody>(self) -> crate::error::Result<PlainMessage<U>>;
339}
340
341impl PlainMessageExt<Value> for PlainMessage<Value> {
342    fn into_typed(self) -> PlainMessage<Value> {
343        self
344    }
345
346    fn parse_as<U: crate::message::TapMessageBody>(self) -> crate::error::Result<PlainMessage<U>> {
347        self.parse_body()
348    }
349}
350
351/// Helper to convert between typed messages and TapMessage enum
352impl<T: crate::message::TapMessageBody> TryFrom<PlainMessage<T>>
353    for crate::message::tap_message_enum::TapMessage
354where
355    crate::message::tap_message_enum::TapMessage: From<T>,
356{
357    type Error = crate::error::Error;
358
359    fn try_from(typed: PlainMessage<T>) -> crate::error::Result<Self> {
360        // This would require implementing From<T> for TapMessage for each message type
361        // For now, we'll use the parse approach
362        typed.to_plain_message()?.parse_tap_message()
363    }
364}
365
366/// Message for out-of-band invitations (TAIP-2).
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct OutOfBand {
369    /// Goal code for the invitation.
370    #[serde(rename = "goal_code")]
371    pub goal_code: String,
372
373    /// Invitation message ID.
374    pub id: String,
375
376    /// Label for the invitation.
377    pub label: String,
378
379    /// Accept option for the invitation.
380    pub accept: Option<String>,
381
382    /// The DIDComm services to connect to.
383    pub services: Vec<serde_json::Value>,
384}
385
386/// Simple attachment data for a TAP message.
387///
388/// This structure represents a simplified version of attachment data
389/// that directly contains base64 or JSON without the complexity of the
390/// full AttachmentData enum.
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
392pub struct SimpleAttachmentData {
393    /// Base64-encoded data.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub base64: Option<String>,
396
397    /// JSON data.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub json: Option<serde_json::Value>,
400}
401
402/// Attachment for a TAP message.
403#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
404pub struct Attachment {
405    /// A JSON object that gives access to the actual content of the attachment.
406    /// Can be based on base64, json or external links.
407    pub data: AttachmentData,
408
409    /// Identifies attached content within the scope of a given message.
410    ///  Recommended on appended attachment descriptors. Possible but generally unused
411    ///  on embedded attachment descriptors. Never required if no references to the attachment
412    ///  exist; if omitted, then there is no way to refer to the attachment later in the thread,
413    ///  in error messages, and so forth. Because id is used to compose URIs, it is recommended
414    ///  that this name be brief and avoid spaces and other characters that require URI escaping.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub id: Option<String>,
417
418    /// A human-readable description of the content.
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub description: Option<String>,
421
422    /// A hint about the name that might be used if this attachment is persisted as a file.
423    /// It is not required, and need not be unique. If this field is present and mime-type is not,
424    /// the extension on the filename may be used to infer a MIME type.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub filename: Option<String>,
427
428    /// Describes the MIME type of the attached content.
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub media_type: Option<String>,
431
432    /// Describes the format of the attachment if the mime_type is not sufficient.
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub format: Option<String>,
435
436    /// A hint about when the content in this attachment was last modified
437    /// in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC).
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub lastmod_time: Option<u64>,
440
441    /// Mostly relevant when content is included by reference instead of by value.
442    /// Lets the receiver guess how expensive it will be, in time, bandwidth, and storage,
443    /// to fully fetch the attachment.
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub byte_count: Option<u64>,
446}
447
448impl Attachment {
449    pub fn base64(base64: String) -> AttachmentBuilder {
450        AttachmentBuilder::new(AttachmentData::Base64 {
451            value: Base64AttachmentData { base64, jws: None },
452        })
453    }
454
455    pub fn json(json: Value) -> AttachmentBuilder {
456        AttachmentBuilder::new(AttachmentData::Json {
457            value: JsonAttachmentData { json, jws: None },
458        })
459    }
460
461    pub fn links(links: Vec<String>, hash: String) -> AttachmentBuilder {
462        AttachmentBuilder::new(AttachmentData::Links {
463            value: LinksAttachmentData {
464                links,
465                hash,
466                jws: None,
467            },
468        })
469    }
470}
471
472pub struct AttachmentBuilder {
473    data: AttachmentData,
474    id: Option<String>,
475    description: Option<String>,
476    filename: Option<String>,
477    media_type: Option<String>,
478    format: Option<String>,
479    lastmod_time: Option<u64>,
480    byte_count: Option<u64>,
481}
482
483impl AttachmentBuilder {
484    fn new(data: AttachmentData) -> Self {
485        AttachmentBuilder {
486            data,
487            id: None,
488            description: None,
489            filename: None,
490            media_type: None,
491            format: None,
492            lastmod_time: None,
493            byte_count: None,
494        }
495    }
496
497    pub fn id(mut self, id: String) -> Self {
498        self.id = Some(id);
499        self
500    }
501
502    pub fn description(mut self, description: String) -> Self {
503        self.description = Some(description);
504        self
505    }
506
507    pub fn filename(mut self, filename: String) -> Self {
508        self.filename = Some(filename);
509        self
510    }
511
512    pub fn media_type(mut self, media_type: String) -> Self {
513        self.media_type = Some(media_type);
514        self
515    }
516
517    pub fn format(mut self, format: String) -> Self {
518        self.format = Some(format);
519        self
520    }
521
522    pub fn lastmod_time(mut self, lastmod_time: u64) -> Self {
523        self.lastmod_time = Some(lastmod_time);
524        self
525    }
526
527    pub fn byte_count(mut self, byte_count: u64) -> Self {
528        self.byte_count = Some(byte_count);
529        self
530    }
531
532    pub fn jws(mut self, jws: String) -> Self {
533        match self.data {
534            AttachmentData::Base64 { ref mut value } => value.jws = Some(jws),
535            AttachmentData::Json { ref mut value } => value.jws = Some(jws),
536            AttachmentData::Links { ref mut value } => value.jws = Some(jws),
537        }
538
539        self
540    }
541
542    pub fn finalize(self) -> Attachment {
543        Attachment {
544            data: self.data,
545            id: self.id,
546            description: self.description,
547            filename: self.filename,
548            media_type: self.media_type,
549            format: self.format,
550            lastmod_time: self.lastmod_time,
551            byte_count: self.byte_count,
552        }
553    }
554}
555
556// Attention: we are using untagged enum serialization variant.
557// Serde will try to match the data against each variant in order and the
558// first one that deserializes successfully is the one returned.
559// It should work as we always have discrimination here.
560
561/// Represents attachment data in Base64, embedded Json or Links form.
562#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
563#[serde(untagged)]
564pub enum AttachmentData {
565    Base64 {
566        #[serde(flatten)]
567        value: Base64AttachmentData,
568    },
569    Json {
570        #[serde(flatten)]
571        value: JsonAttachmentData,
572    },
573    Links {
574        #[serde(flatten)]
575        value: LinksAttachmentData,
576    },
577}
578
579#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
580pub struct Base64AttachmentData {
581    /// Base64-encoded data, when representing arbitrary content inline.
582    pub base64: String,
583
584    /// A JSON Web Signature over the content of the attachment.
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub jws: Option<String>,
587}
588
589#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
590pub struct JsonAttachmentData {
591    /// Directly embedded JSON data.
592    pub json: Value,
593
594    /// A JSON Web Signature over the content of the attachment.
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub jws: Option<String>,
597}
598
599#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
600pub struct LinksAttachmentData {
601    /// A list of one or more locations at which the content may be fetched.
602    pub links: Vec<String>,
603
604    /// The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment.
605    pub hash: String,
606
607    /// A JSON Web Signature over the content of the attachment.
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub jws: Option<String>,
610}
611
612#[cfg(test)]
613mod tests {
614    use core::panic;
615    use serde_json::json;
616
617    use super::*;
618
619    #[test]
620    fn attachment_base64_works() {
621        let attachment = Attachment::base64("ZXhhbXBsZQ==".to_owned())
622            .id("example-1".to_owned())
623            .description("example-1-description".to_owned())
624            .filename("attachment-1".to_owned())
625            .media_type("message/example".to_owned())
626            .format("json".to_owned())
627            .lastmod_time(10000)
628            .byte_count(200)
629            .jws("jws".to_owned())
630            .finalize();
631
632        let data = match attachment.data {
633            AttachmentData::Base64 { ref value } => value,
634            _ => panic!("data isn't base64."),
635        };
636
637        assert_eq!(data.base64, "ZXhhbXBsZQ==");
638        assert_eq!(data.jws, Some("jws".to_owned()));
639        assert_eq!(attachment.id, Some("example-1".to_owned()));
640
641        assert_eq!(
642            attachment.description,
643            Some("example-1-description".to_owned())
644        );
645
646        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
647        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
648        assert_eq!(attachment.format, Some("json".to_owned()));
649        assert_eq!(attachment.lastmod_time, Some(10000));
650        assert_eq!(attachment.byte_count, Some(200));
651    }
652
653    #[test]
654    fn attachment_json_works() {
655        let attachment = Attachment::json(json!("example"))
656            .id("example-1".to_owned())
657            .description("example-1-description".to_owned())
658            .filename("attachment-1".to_owned())
659            .media_type("message/example".to_owned())
660            .format("json".to_owned())
661            .lastmod_time(10000)
662            .byte_count(200)
663            .jws("jws".to_owned())
664            .finalize();
665
666        let data = match attachment.data {
667            AttachmentData::Json { ref value } => value,
668            _ => panic!("data isn't json."),
669        };
670
671        assert_eq!(data.json, json!("example"));
672        assert_eq!(data.jws, Some("jws".to_owned()));
673        assert_eq!(attachment.id, Some("example-1".to_owned()));
674
675        assert_eq!(
676            attachment.description,
677            Some("example-1-description".to_owned())
678        );
679
680        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
681        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
682        assert_eq!(attachment.format, Some("json".to_owned()));
683        assert_eq!(attachment.lastmod_time, Some(10000));
684        assert_eq!(attachment.byte_count, Some(200));
685    }
686
687    #[test]
688    fn attachment_links_works() {
689        let attachment = Attachment::links(
690            vec!["http://example1".to_owned(), "https://example2".to_owned()],
691            "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned(),
692        )
693        .id("example-1".to_owned())
694        .description("example-1-description".to_owned())
695        .filename("attachment-1".to_owned())
696        .media_type("message/example".to_owned())
697        .format("json".to_owned())
698        .lastmod_time(10000)
699        .byte_count(200)
700        .jws("jws".to_owned())
701        .finalize();
702
703        let data = match attachment.data {
704            AttachmentData::Links { ref value } => value,
705            _ => panic!("data isn't links."),
706        };
707
708        assert_eq!(
709            data.links,
710            vec!["http://example1".to_owned(), "https://example2".to_owned()]
711        );
712
713        assert_eq!(
714            data.hash,
715            "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned()
716        );
717
718        assert_eq!(data.jws, Some("jws".to_owned()));
719        assert_eq!(attachment.id, Some("example-1".to_owned()));
720
721        assert_eq!(
722            attachment.description,
723            Some("example-1-description".to_owned())
724        );
725
726        assert_eq!(attachment.filename, Some("attachment-1".to_owned()));
727        assert_eq!(attachment.media_type, Some("message/example".to_owned()));
728        assert_eq!(attachment.format, Some("json".to_owned()));
729        assert_eq!(attachment.lastmod_time, Some(10000));
730        assert_eq!(attachment.byte_count, Some(200));
731    }
732}