tap_msg/message/
tap_message_trait.rs

1//! Traits for TAP message conversion and validation.
2//!
3//! This module provides traits for converting between DIDComm messages
4//! and TAP-specific message bodies, as well as validation of those bodies.
5
6use crate::error::{Error, Result};
7use chrono::Utc;
8use didcomm::Message;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11
12/// A trait for TAP message body types that can be serialized to and deserialized from DIDComm messages.
13pub trait TapMessageBody: Serialize + DeserializeOwned + Send + Sync {
14    /// Get the message type string for this body type.
15    fn message_type() -> &'static str
16    where
17        Self: Sized;
18
19    /// Validate the message body.
20    fn validate(&self) -> Result<()>;
21
22    /// Convert this body to a DIDComm message.
23    fn to_didcomm(&self, from_did: Option<&str>) -> Result<Message> {
24        // Create a JSON representation of self with explicit type field
25        let mut body_json =
26            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
27
28        // Ensure the @type field is correctly set in the body
29        if let Some(body_obj) = body_json.as_object_mut() {
30            // Add or update the @type field with the message type
31            body_obj.insert(
32                "@type".to_string(),
33                serde_json::Value::String(Self::message_type().to_string()),
34            );
35        }
36
37        // Create a unique ID for the message
38        let id = uuid::Uuid::new_v4().to_string();
39
40        // Get current timestamp in milliseconds since Unix epoch
41        let now = Utc::now().timestamp_millis() as u64;
42
43        // Extract agent DIDs directly from the message body
44        let mut agent_dids = Vec::new();
45
46        // Extract from the body JSON
47        if let Some(body_obj) = body_json.as_object() {
48            // Extract from originator
49            if let Some(originator) = body_obj.get("originator") {
50                if let Some(originator_obj) = originator.as_object() {
51                    if let Some(id) = originator_obj.get("id") {
52                        if let Some(id_str) = id.as_str() {
53                            if id_str.starts_with("did:") {
54                                agent_dids.push(id_str.to_string());
55                            }
56                        }
57                    }
58                }
59            }
60
61            // Extract from beneficiary
62            if let Some(beneficiary) = body_obj.get("beneficiary") {
63                if let Some(beneficiary_obj) = beneficiary.as_object() {
64                    if let Some(id) = beneficiary_obj.get("id") {
65                        if let Some(id_str) = id.as_str() {
66                            if id_str.starts_with("did:") {
67                                agent_dids.push(id_str.to_string());
68                            }
69                        }
70                    }
71                }
72            }
73
74            // Extract from agents array
75            if let Some(agents) = body_obj.get("agents") {
76                if let Some(agents_array) = agents.as_array() {
77                    for agent in agents_array {
78                        if let Some(agent_obj) = agent.as_object() {
79                            if let Some(id) = agent_obj.get("id") {
80                                if let Some(id_str) = id.as_str() {
81                                    if id_str.starts_with("did:") {
82                                        agent_dids.push(id_str.to_string());
83                                    }
84                                }
85                            }
86                        }
87                    }
88                }
89            }
90        }
91
92        // Remove duplicates
93        agent_dids.sort();
94        agent_dids.dedup();
95
96        // If from_did is provided, remove it from the recipients list to avoid sending to self
97        if let Some(from) = from_did {
98            agent_dids.retain(|did| did != from);
99        }
100
101        // Always set the 'to' field, even if it's an empty list
102        let to = Some(agent_dids);
103
104        // Create the message
105        let message = Message {
106            id,
107            typ: "application/didcomm-plain+json".to_string(),
108            type_: Self::message_type().to_string(),
109            from: from_did.map(|s| s.to_string()),
110            to,
111            thid: None,
112            pthid: None,
113            created_time: Some(now),
114            expires_time: None,
115            extra_headers: std::collections::HashMap::new(),
116            from_prior: None,
117            body: body_json,
118            attachments: None,
119        };
120
121        Ok(message)
122    }
123
124    /// Convert this body to a DIDComm message with a sender and multiple recipients.
125    ///
126    /// According to TAP requirements:
127    /// - The `from` field should always be the creator's DID
128    /// - The `to` field should include DIDs of all agents involved except the creator
129    ///
130    /// Note: This method now directly uses the enhanced to_didcomm implementation
131    /// which automatically extracts agent DIDs. The explicit 'to' parameter allows
132    /// overriding the automatically extracted recipients when needed.
133    fn to_didcomm_with_route<'a, I>(&self, from: Option<&str>, to: I) -> Result<Message>
134    where
135        I: IntoIterator<Item = &'a str>,
136    {
137        // First create a message with the sender, automatically extracting agent DIDs
138        let mut message = self.to_didcomm(from)?;
139
140        // Override with explicitly provided recipients if any
141        let to_vec: Vec<String> = to.into_iter().map(String::from).collect();
142        if !to_vec.is_empty() {
143            message.to = Some(to_vec);
144        }
145
146        Ok(message)
147    }
148
149    /// Extract this body type from a DIDComm message.
150    fn from_didcomm(message: &Message) -> Result<Self>
151    where
152        Self: Sized,
153    {
154        // Verify that this is the correct message type
155        if message.type_ != Self::message_type() {
156            return Err(Error::InvalidMessageType(format!(
157                "Expected message type {}, but found {}",
158                Self::message_type(),
159                message.type_
160            )));
161        }
162
163        // Create a copy of the message body that we can modify
164        let mut body_json = message.body.clone();
165
166        // Ensure the @type field is present for deserialization
167        if let Some(body_obj) = body_json.as_object_mut() {
168            // Add or update the @type field to ensure it's present
169            body_obj.insert(
170                "@type".to_string(),
171                serde_json::Value::String(Self::message_type().to_string()),
172            );
173        }
174
175        // Extract and deserialize the body
176        let body = serde_json::from_value(body_json).map_err(|e| {
177            Error::SerializationError(format!("Failed to deserialize message body: {}", e))
178        })?;
179
180        Ok(body)
181    }
182}
183
184/// A trait for messages that can be connected to a prior Connect message.
185///
186/// This trait provides functionality for linking messages to a previous Connect message,
187/// enabling the building of message chains in the TAP protocol.
188pub trait Connectable {
189    /// Connect this message to a prior Connect message by setting the parent thread ID (pthid).
190    ///
191    /// # Arguments
192    ///
193    /// * `connect_id` - The ID of the Connect message to link to
194    ///
195    /// # Returns
196    ///
197    /// Self reference for fluent interface chaining
198    fn with_connection(&mut self, connect_id: &str) -> &mut Self;
199
200    /// Check if this message is connected to a prior Connect message.
201    ///
202    /// # Returns
203    ///
204    /// `true` if this message has a connection (pthid) set, `false` otherwise
205    fn has_connection(&self) -> bool;
206
207    /// Get the connection ID if present.
208    ///
209    /// # Returns
210    ///
211    /// The Connect message ID this message is connected to, or None if not connected
212    fn connection_id(&self) -> Option<&str>;
213}
214
215/// Trait for types that can be represented as TAP messages.
216///
217/// This trait provides utility methods for working with DIDComm Messages in the context of the TAP protocol.
218pub trait TapMessage {
219    /// Validates a TAP message.
220    ///
221    /// This method checks if the message meets all the requirements
222    /// for a valid TAP message, including:
223    ///
224    /// - Having a valid ID
225    /// - Having a valid created timestamp
226    /// - Having a valid TAP message type
227    /// - Having a valid body structure
228    ///
229    /// # Returns
230    ///
231    /// `Ok(())` if the message is valid, otherwise an error
232    fn validate(&self) -> Result<()>;
233
234    /// Checks if this message is a TAP message.
235    fn is_tap_message(&self) -> bool;
236
237    /// Gets the TAP message type from this message.
238    fn get_tap_type(&self) -> Option<String>;
239
240    /// Extract a specific message body type from this message.
241    ///
242    /// # Type Parameters
243    ///
244    /// * `T` - The type of the body to extract, must implement `TapMessageBody`
245    ///
246    /// # Returns
247    ///
248    /// The message body if the type matches T, otherwise an error
249    fn body_as<T: TapMessageBody>(&self) -> Result<T>;
250
251    /// Get all participant DIDs from this message.
252    ///
253    /// This includes all DIDs that are involved in the message, such as sender, recipients, and
254    /// any other participants mentioned in the message body.
255    ///
256    /// # Returns
257    ///
258    /// List of participant DIDs
259    fn get_all_participants(&self) -> Vec<String>;
260
261    /// Create a reply to this message.
262    ///
263    /// # Type Parameters
264    ///
265    /// * `T` - The TAP message body type to create
266    ///
267    /// # Arguments
268    ///
269    /// * `body` - The reply message body
270    /// * `creator_did` - DID of the creator of this reply
271    ///
272    /// # Returns
273    ///
274    /// A new DIDComm message that is properly linked to this message as a reply
275    fn create_reply<T: TapMessageBody>(&self, body: &T, creator_did: &str) -> Result<Message> {
276        // Create the base message with creator as sender
277        let mut message = body.to_didcomm(Some(creator_did))?;
278
279        // Set the thread ID to maintain the conversation thread
280        if let Some(thread_id) = self.thread_id() {
281            message.thid = Some(thread_id.to_string());
282        } else {
283            // If no thread ID exists, use the original message ID as the thread ID
284            message.thid = Some(self.message_id().to_string());
285        }
286
287        // Set the parent thread ID if this thread is part of a larger transaction
288        if let Some(parent_thread_id) = self.parent_thread_id() {
289            message.pthid = Some(parent_thread_id.to_string());
290        }
291
292        // Get all participants from the message
293        let participant_dids = self.get_all_participants();
294
295        // Set recipients to all participants except the creator
296        let recipients: Vec<String> = participant_dids
297            .into_iter()
298            .filter(|did| did != creator_did)
299            .collect();
300
301        if !recipients.is_empty() {
302            message.to = Some(recipients);
303        }
304
305        Ok(message)
306    }
307
308    /// Get the message type for this message
309    fn message_type(&self) -> &'static str;
310
311    /// Get the thread ID for this message
312    fn thread_id(&self) -> Option<&str>;
313
314    /// Get the parent thread ID for this message
315    fn parent_thread_id(&self) -> Option<&str>;
316
317    /// Get the message ID for this message
318    fn message_id(&self) -> &str;
319}
320
321// Implement TapMessage trait for didcomm::Message
322impl TapMessage for Message {
323    fn validate(&self) -> Result<()> {
324        // Check if it's a TAP message first
325        if !self.is_tap_message() {
326            return Err(Error::Validation("Not a TAP message".to_string()));
327        }
328
329        // Check ID
330        if self.id.is_empty() {
331            return Err(Error::Validation(
332                "Message must have a non-empty ID".to_string(),
333            ));
334        }
335
336        // Check created time
337        if self.created_time.is_none() {
338            return Err(Error::Validation(
339                "Message must have a created timestamp".to_string(),
340            ));
341        }
342
343        // Body validation is type-specific and handled during body extraction
344        Ok(())
345    }
346
347    fn is_tap_message(&self) -> bool {
348        self.type_.starts_with("https://tap.rsvp/schema/1.0#")
349    }
350
351    fn get_tap_type(&self) -> Option<String> {
352        if self.is_tap_message() {
353            Some(self.type_.clone())
354        } else {
355            None
356        }
357    }
358
359    fn body_as<T: TapMessageBody>(&self) -> Result<T> {
360        // Check if the message type matches the expected type
361        if self.type_ != T::message_type() {
362            return Err(Error::Validation(format!(
363                "Message type mismatch. Expected {}, got {}",
364                T::message_type(),
365                self.type_
366            )));
367        }
368
369        // Create a copy of the body that we can modify
370        let mut body_json = self.body.clone();
371
372        // Ensure the @type field is present for deserialization
373        if let Some(body_obj) = body_json.as_object_mut() {
374            // Add or update the @type field to ensure it's present
375            body_obj.insert(
376                "@type".to_string(),
377                serde_json::Value::String(T::message_type().to_string()),
378            );
379        }
380
381        // Extract and deserialize the body
382        let body = serde_json::from_value(body_json).map_err(|e| {
383            Error::SerializationError(format!("Failed to deserialize message body: {}", e))
384        })?;
385
386        Ok(body)
387    }
388
389    fn get_all_participants(&self) -> Vec<String> {
390        let mut participants = Vec::new();
391
392        // Add sender if present
393        if let Some(from) = &self.from {
394            participants.push(from.clone());
395        }
396
397        // Add recipients if present
398        if let Some(to) = &self.to {
399            participants.extend(to.clone());
400        }
401
402        participants
403    }
404
405    fn message_type(&self) -> &'static str {
406        match self.type_.as_str() {
407            "https://tap.rsvp/schema/1.0#transfer" => "https://tap.rsvp/schema/1.0#transfer",
408            "https://tap.rsvp/schema/1.0#payment-request" => {
409                "https://tap.rsvp/schema/1.0#payment-request"
410            }
411            "https://tap.rsvp/schema/1.0#connect" => "https://tap.rsvp/schema/1.0#connect",
412            "https://tap.rsvp/schema/1.0#authorize" => "https://tap.rsvp/schema/1.0#authorize",
413            "https://tap.rsvp/schema/1.0#reject" => "https://tap.rsvp/schema/1.0#reject",
414            "https://tap.rsvp/schema/1.0#settle" => "https://tap.rsvp/schema/1.0#settle",
415            "https://tap.rsvp/schema/1.0#update-party" => {
416                "https://tap.rsvp/schema/1.0#update-party"
417            }
418            "https://tap.rsvp/schema/1.0#update-policies" => {
419                "https://tap.rsvp/schema/1.0#update-policies"
420            }
421            "https://didcomm.org/present-proof/3.0/presentation" => {
422                "https://didcomm.org/present-proof/3.0/presentation"
423            }
424            _ => "unknown",
425        }
426    }
427
428    fn thread_id(&self) -> Option<&str> {
429        self.thid.as_deref()
430    }
431
432    fn parent_thread_id(&self) -> Option<&str> {
433        self.pthid.as_deref()
434    }
435
436    fn message_id(&self) -> &str {
437        &self.id
438    }
439}
440
441// Implement Connectable trait for Message
442impl Connectable for Message {
443    fn with_connection(&mut self, connect_id: &str) -> &mut Self {
444        self.pthid = Some(connect_id.to_string());
445        self
446    }
447
448    fn has_connection(&self) -> bool {
449        self.pthid.is_some()
450    }
451
452    fn connection_id(&self) -> Option<&str> {
453        self.pthid.as_deref()
454    }
455}
456
457/// Creates a new TAP message from a message body.
458///
459/// This function constructs a DIDComm message with the appropriate
460/// TAP message type and body.
461///
462/// # Arguments
463///
464/// * `body` - The message body to include
465/// * `id` - Optional message ID (will be generated if None)
466/// * `from_did` - Optional sender DID
467/// * `to_dids` - Recipient DIDs (will override automatically extracted agent DIDs)
468///
469/// # Returns
470///
471/// A DIDComm message object ready for further processing
472pub fn create_tap_message<T: TapMessageBody>(
473    body: &T,
474    id: Option<String>,
475    from_did: Option<&str>,
476    to_dids: &[&str],
477) -> Result<Message> {
478    // Create the base message from the body, passing the from_did
479    let mut message = body.to_didcomm(from_did)?;
480
481    // Set custom ID if provided
482    if let Some(custom_id) = id {
483        message.id = custom_id;
484    }
485
486    // Override with explicitly provided recipients if any
487    if !to_dids.is_empty() {
488        message.to = Some(to_dids.iter().map(|&s| s.to_string()).collect());
489    }
490
491    Ok(message)
492}