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