nrc_mls/
messages.rs

1//! Nostr MLS Messages
2//!
3//! This module provides functionality for creating, processing, and managing encrypted
4//! messages in MLS groups. It handles:
5//! - Message creation and encryption
6//! - Message processing and decryption
7//! - Message state tracking
8//! - Integration with Nostr events
9//!
10//! Messages in Nostr MLS are wrapped in Nostr events (kind:445) for relay transmission.
11//! The message content is encrypted using both MLS group keys and NIP-44 encryption.
12//! Message state is tracked to handle processing status and failure scenarios.
13
14use nostr::{EventId, UnsignedEvent};
15use nrc_mls_storage::NostrMlsStorageProvider;
16use openmls::group::{GroupId, MlsGroupStateError, ProcessMessageError, ValidationError};
17use openmls_basic_credential::SignatureKeyPair;
18use tls_codec::{Deserialize as TlsDeserialize, Serialize as TlsSerialize};
19
20use crate::error::Error;
21use crate::prelude::*;
22use crate::{util, NostrMls};
23
24/// Default number of epochs to look back when trying to decrypt messages with older exporter secrets
25const DEFAULT_EPOCH_LOOKBACK: u64 = 5;
26
27/// MessageProcessingResult covers the full spectrum of responses that we can get back from attempting to process a message
28#[derive(Debug)]
29pub enum MessageProcessingResult {
30    /// An application message (this is usually a message in a chat)
31    ApplicationMessage(message_types::Message),
32    /// Proposal message
33    Proposal(UpdateGroupResult),
34    /// External Join Proposal
35    ExternalJoinProposal,
36    /// Commit message
37    Commit,
38    /// Unprocessable message
39    Unprocessable,
40}
41
42impl<Storage> NostrMls<Storage>
43where
44    Storage: NostrMlsStorageProvider,
45{
46    /// Retrieves a message by its Nostr event ID
47    ///
48    /// This function looks up a message in storage using its associated Nostr event ID.
49    /// The message must have been previously processed and stored.
50    ///
51    /// # Arguments
52    ///
53    /// * `event_id` - The Nostr event ID to look up
54    ///
55    /// # Returns
56    ///
57    /// * `Ok(Some(Message))` - The message if found
58    /// * `Ok(None)` - If no message exists with the given event ID
59    /// * `Err(Error)` - If there is an error accessing storage
60    pub fn get_message(&self, event_id: &EventId) -> Result<Option<message_types::Message>, Error> {
61        self.storage()
62            .find_message_by_event_id(event_id)
63            .map_err(|e| Error::Message(e.to_string()))
64    }
65
66    /// Retrieves all messages for a specific MLS group
67    ///
68    /// This function returns all messages that have been processed and stored for a group,
69    /// ordered by creation time.
70    ///
71    /// # Arguments
72    ///
73    /// * `mls_group_id` - The MLS group ID to get messages for
74    ///
75    /// # Returns
76    ///
77    /// * `Ok(Vec<Message>)` - List of all messages for the group
78    /// * `Err(Error)` - If there is an error accessing storage
79    pub fn get_messages(
80        &self,
81        mls_group_id: &GroupId,
82    ) -> Result<Vec<message_types::Message>, Error> {
83        self.storage()
84            .messages(mls_group_id)
85            .map_err(|e| Error::Message(e.to_string()))
86    }
87
88    /// Creates an MLS-encrypted message from an unsigned Nostr event
89    ///
90    /// This internal function handles the MLS-level encryption of a message:
91    /// 1. Loads the member's signing keys
92    /// 2. Ensures the message has a unique ID
93    /// 3. Serializes the message content
94    /// 4. Creates and signs the MLS message
95    ///
96    /// # Arguments
97    ///
98    /// * `group` - The MLS group to create the message in
99    /// * `rumor` - The unsigned Nostr event to encrypt
100    ///
101    /// # Returns
102    ///
103    /// * `Ok(Vec<u8>)` - The serialized encrypted MLS message
104    /// * `Err(Error)` - If message creation or encryption fails
105    fn create_message_for_event(
106        &self,
107        group: &mut MlsGroup,
108        rumor: &mut UnsignedEvent,
109    ) -> Result<Vec<u8>, Error> {
110        // Load signer
111        let signer: SignatureKeyPair = self.load_mls_signer(group)?;
112
113        // Ensure rumor ID
114        rumor.ensure_id();
115
116        // Serialize as JSON
117        let json: String = rumor.as_json();
118
119        // Create message
120        let message_out = group.create_message(&self.provider, &signer, json.as_bytes())?;
121
122        let serialized_message = message_out.tls_serialize_detached()?;
123
124        Ok(serialized_message)
125    }
126
127    /// Creates a complete encrypted Nostr event for an MLS group message
128    ///
129    /// This is the main entry point for creating group messages. The function:
130    /// 1. Loads the MLS group and its metadata
131    /// 2. Creates and encrypts the MLS message
132    /// 3. Derives NIP-44 encryption keys from the group's secret
133    /// 4. Creates a Nostr event wrapping the encrypted message
134    /// 5. Stores the message state for tracking
135    ///
136    /// # Arguments
137    ///
138    /// * `mls_group_id` - The MLS group ID
139    /// * `rumor` - The unsigned Nostr event to encrypt and send
140    ///
141    /// # Returns
142    ///
143    /// * `Ok(Event)` - The signed Nostr event ready for relay publication
144    /// * `Err(Error)` - If message creation or encryption fails
145    pub fn create_message(
146        &self,
147        mls_group_id: &GroupId,
148        mut rumor: UnsignedEvent,
149    ) -> Result<Event, Error> {
150        // Load mls group
151        let mut mls_group = self
152            .load_mls_group(mls_group_id)?
153            .ok_or(Error::GroupNotFound)?;
154
155        // Load stored group
156        let mut group: group_types::Group = self
157            .get_group(mls_group_id)
158            .map_err(|e| Error::Group(e.to_string()))?
159            .ok_or(Error::GroupNotFound)?;
160
161        // Create message
162        let message: Vec<u8> = self.create_message_for_event(&mut mls_group, &mut rumor)?;
163
164        // Get the rumor ID
165        let rumor_id: EventId = rumor.id();
166
167        let event = self.build_encrypted_message_event(mls_group.group_id(), message)?;
168
169        // Create message to save to storage
170        let message: message_types::Message = message_types::Message {
171            id: rumor_id,
172            pubkey: rumor.pubkey,
173            kind: rumor.kind,
174            mls_group_id: mls_group_id.clone(),
175            created_at: rumor.created_at,
176            content: rumor.content.clone(),
177            tags: rumor.tags.clone(),
178            event: rumor.clone(),
179            wrapper_event_id: event.id,
180            state: message_types::MessageState::Created,
181        };
182
183        // Create processed_message to track state of message
184        let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
185            wrapper_event_id: event.id,
186            message_event_id: Some(rumor_id),
187            processed_at: Timestamp::now(),
188            state: message_types::ProcessedMessageState::Created,
189            failure_reason: None,
190        };
191
192        // Save message to storage
193        self.storage()
194            .save_message(message.clone())
195            .map_err(|e| Error::Message(e.to_string()))?;
196
197        // Save processed message to storage
198        self.storage()
199            .save_processed_message(processed_message)
200            .map_err(|e| Error::Message(e.to_string()))?;
201
202        // Update last_message_at and last_message_id
203        group.last_message_at = Some(rumor.created_at);
204        group.last_message_id = Some(message.id);
205        self.storage()
206            .save_group(group)
207            .map_err(|e| Error::Group(e.to_string()))?;
208
209        Ok(event)
210    }
211
212    /// Processes an incoming MLS message
213    ///
214    /// This internal function handles the MLS protocol-level message processing:
215    /// 1. Deserializes the MLS message
216    /// 2. Validates the message's group ID
217    /// 3. Processes the message according to its type
218    /// 4. Handles any resulting group state changes
219    ///
220    /// # Arguments
221    ///
222    /// * `group` - The MLS group the message belongs to
223    /// * `message_bytes` - The serialized MLS message to process
224    ///
225    /// # Returns
226    ///
227    /// * `Ok(ProcessedMessageContent)` - The processed message content based on message type
228    /// * `Err(Error)` - If message processing fails
229    fn process_message_for_group(
230        &self,
231        group: &mut MlsGroup,
232        message_bytes: &[u8],
233    ) -> Result<ProcessedMessageContent, Error> {
234        let mls_message = MlsMessageIn::tls_deserialize_exact(message_bytes)?;
235
236        tracing::debug!(target: "nostr_mls::messages::process_message_for_group", "Received message: {:?}", mls_message);
237        let protocol_message = mls_message.try_into_protocol_message()?;
238
239        // Return error if group ID doesn't match
240        if protocol_message.group_id() != group.group_id() {
241            return Err(Error::ProtocolGroupIdMismatch);
242        }
243
244        let processed_message = match group.process_message(&self.provider, protocol_message) {
245            Ok(processed_message) => processed_message,
246            Err(ProcessMessageError::ValidationError(ValidationError::CannotDecryptOwnMessage)) => {
247                return Err(Error::CannotDecryptOwnMessage);
248            }
249            Err(e) => {
250                tracing::error!(target: "nostr_mls::messages::process_message_for_group", "Error processing message: {:?}", e);
251                return Err(Error::ProcessMessage(e));
252            }
253        };
254
255        tracing::debug!(
256            target: "nostr_mls::messages::process_message_for_group",
257            "Processed message: {:?}",
258            processed_message
259        );
260
261        Ok(processed_message.into_content())
262    }
263
264    /// Processes an application message from a group member
265    ///
266    /// This internal function handles application messages (chat messages) that have been
267    /// successfully decrypted. It:
268    /// 1. Deserializes the message content as a Nostr event
269    /// 2. Creates tracking records for the message and processing state
270    /// 3. Updates the group's last message metadata
271    /// 4. Stores all data in the storage provider
272    ///
273    /// # Arguments
274    ///
275    /// * `group` - The group metadata from storage
276    /// * `event` - The wrapper Nostr event containing the encrypted message
277    /// * `application_message` - The decrypted MLS application message
278    ///
279    /// # Returns
280    ///
281    /// * `Ok(Message)` - The processed and stored message
282    /// * `Err(Error)` - If message processing or storage fails
283    fn process_application_message_for_group(
284        &self,
285        mut group: group_types::Group,
286        event: &Event,
287        application_message: ApplicationMessage,
288    ) -> Result<message_types::Message, Error> {
289        // This is a message from a group member
290        let bytes = application_message.into_bytes();
291        let mut rumor: UnsignedEvent = UnsignedEvent::from_json(bytes)?;
292
293        let rumor_id: EventId = rumor.id();
294
295        let processed_message = message_types::ProcessedMessage {
296            wrapper_event_id: event.id,
297            message_event_id: Some(rumor_id),
298            processed_at: Timestamp::now(),
299            state: message_types::ProcessedMessageState::Processed,
300            failure_reason: None,
301        };
302
303        let message = message_types::Message {
304            id: rumor_id,
305            pubkey: rumor.pubkey,
306            kind: rumor.kind,
307            mls_group_id: group.mls_group_id.clone(),
308            created_at: rumor.created_at,
309            content: rumor.content.clone(),
310            tags: rumor.tags.clone(),
311            event: rumor.clone(),
312            wrapper_event_id: event.id,
313            state: message_types::MessageState::Processed,
314        };
315
316        self.storage()
317            .save_message(message.clone())
318            .map_err(|e| Error::Message(e.to_string()))?;
319
320        self.storage()
321            .save_processed_message(processed_message.clone())
322            .map_err(|e| Error::Message(e.to_string()))?;
323
324        // Update last_message_at and last_message_id
325        group.last_message_at = Some(rumor.created_at);
326        group.last_message_id = Some(message.id);
327        self.storage()
328            .save_group(group)
329            .map_err(|e| Error::Group(e.to_string()))?;
330
331        tracing::debug!(target: "nostr_mls::messages::process_message", "Processed message: {:?}", processed_message);
332        tracing::debug!(target: "nostr_mls::messages::process_message", "Message: {:?}", message);
333        Ok(message)
334    }
335
336    /// Processes a proposal message from a group member
337    ///
338    /// This internal function handles MLS proposal messages (add/remove member proposals).
339    /// Only admin members are allowed to submit proposals. The function:
340    /// 1. Validates the sender is a group member and has admin privileges
341    /// 2. Stores the pending proposal in the MLS group state
342    /// 3. Automatically commits the proposal to the group
343    /// 4. Creates a new encrypted event for the commit message
344    /// 5. Updates processing state to prevent reprocessing
345    ///
346    /// # Arguments
347    ///
348    /// * `mls_group` - The MLS group to process the proposal for
349    /// * `event` - The wrapper Nostr event containing the encrypted proposal
350    /// * `staged_proposal` - The validated MLS proposal to process
351    ///
352    /// # Returns
353    ///
354    /// * `Ok(UpdateGroupResult)` - Contains the commit event and any welcome messages
355    /// * `Err(Error)` - If proposal processing fails or sender lacks permissions
356    fn process_proposal_message_for_group(
357        &self,
358        mls_group: &mut MlsGroup,
359        event: &Event,
360        staged_proposal: QueuedProposal,
361    ) -> Result<UpdateGroupResult, Error> {
362        match staged_proposal.sender() {
363            Sender::Member(leaf_index) => {
364                let member = mls_group.member_at(*leaf_index);
365
366                match member {
367                    Some(member) => {
368                        // Only process proposals from admins for now
369                        if self.is_member_admin(mls_group.group_id(), &member)? {
370                            mls_group
371                                .store_pending_proposal(self.provider.storage(), staged_proposal)
372                                .map_err(|e| Error::Message(e.to_string()))?;
373
374                            let _added_members =
375                                self.pending_added_members_pubkeys(mls_group.group_id())?;
376
377                            let mls_signer = self.load_mls_signer(mls_group)?;
378
379                            let (commit_message, welcomes_option, _group_info) = mls_group
380                                .commit_to_pending_proposals(&self.provider, &mls_signer)?;
381
382                            let serialized_commit_message = commit_message
383                                .tls_serialize_detached()
384                                .map_err(|e| Error::Group(e.to_string()))?;
385
386                            let commit_event = self.build_encrypted_message_event(
387                                mls_group.group_id(),
388                                serialized_commit_message,
389                            )?;
390
391                            // TODO: FUTURE Handle welcome rumors from proposals
392                            // The issue is that we don't have the key_package events to get the event id to
393                            // include in the welcome rumor to allow users to clean up those key packages on relays
394                            let welcome_rumors: Option<Vec<UnsignedEvent>> = None;
395                            if welcomes_option.is_some() {
396                                return Err(Error::NotImplemented(
397                                    "Processing welcome rumors from proposals is not supported"
398                                        .to_string(),
399                                ));
400                            }
401
402                            // Save a processed message so we don't reprocess
403                            let processed_message = message_types::ProcessedMessage {
404                                wrapper_event_id: event.id,
405                                message_event_id: None,
406                                processed_at: Timestamp::now(),
407                                state: message_types::ProcessedMessageState::Processed,
408                                failure_reason: None,
409                            };
410
411                            self.storage()
412                                .save_processed_message(processed_message)
413                                .map_err(|e| Error::Message(e.to_string()))?;
414
415                            Ok(UpdateGroupResult {
416                                evolution_event: commit_event,
417                                welcome_rumors,
418                            })
419                        } else {
420                            Err(Error::ProposalFromNonAdmin)
421                        }
422                    }
423                    None => {
424                        tracing::warn!(target: "nostr_mls::messages::process_message_for_group", "Received proposal from non-member.");
425                        Err(Error::MessageFromNonMember)
426                    }
427                }
428            }
429            Sender::External(_) => {
430                // TODO: FUTURE Handle external proposals from external proposal extensions
431                Err(Error::NotImplemented("Processing external proposals from external proposal extensions is not supported".to_string()))
432            }
433            Sender::NewMemberCommit => {
434                // TODO: FUTURE Handle new member from external member commits.
435                Err(Error::NotImplemented(
436                    "Processing external proposals for new member commits is not supported"
437                        .to_string(),
438                ))
439            }
440            Sender::NewMemberProposal => {
441                // TODO: FUTURE Handle new member from external member proposals.
442                Err(Error::NotImplemented(
443                    "Processing external proposals for new member proposals is not supported"
444                        .to_string(),
445                ))
446            }
447        }
448    }
449
450    /// Processes a commit message from a group member
451    ///
452    /// This internal function handles MLS commit messages that finalize pending proposals.
453    /// The function:
454    /// 1. Merges the staged commit into the group state
455    /// 2. Updates the group to the new epoch with new cryptographic keys
456    /// 3. Saves the new exporter secret for NIP-44 encryption
457    /// 4. Updates processing state to prevent reprocessing
458    ///
459    /// # Arguments
460    ///
461    /// * `mls_group` - The MLS group to merge the commit into
462    /// * `event` - The wrapper Nostr event containing the encrypted commit
463    /// * `staged_commit` - The validated MLS commit to merge
464    ///
465    /// # Returns
466    ///
467    /// * `Ok(())` - If commit processing succeeds
468    /// * `Err(Error)` - If commit merging or storage operations fail
469    fn process_commit_message_for_group(
470        &self,
471        mls_group: &mut MlsGroup,
472        event: &Event,
473        staged_commit: StagedCommit,
474    ) -> Result<(), Error> {
475        mls_group
476            .merge_staged_commit(&self.provider, staged_commit)
477            .map_err(|e| Error::Message(e.to_string()))?;
478
479        // Save exporter secret for the new epoch
480        self.exporter_secret(mls_group.group_id())?;
481
482        // Sync the stored group metadata with the updated MLS group state
483        // This ensures any group context extension changes are reflected in storage
484        self.sync_group_metadata_from_mls(mls_group.group_id())?;
485
486        // Save a processed message so we don't reprocess
487        let processed_message = message_types::ProcessedMessage {
488            wrapper_event_id: event.id,
489            message_event_id: None,
490            processed_at: Timestamp::now(),
491            state: message_types::ProcessedMessageState::Processed,
492            failure_reason: None,
493        };
494
495        self.storage()
496            .save_processed_message(processed_message)
497            .map_err(|e| Error::Message(e.to_string()))?;
498        Ok(())
499    }
500
501    /// Validates the incoming event and extracts the group ID
502    ///
503    /// This private method validates that the event has the correct kind and extracts
504    /// the group ID from the event tags.
505    ///
506    /// # Arguments
507    ///
508    /// * `event` - The Nostr event to validate
509    ///
510    /// # Returns
511    ///
512    /// * `Ok([u8; 32])` - The extracted Nostr group ID
513    /// * `Err(Error)` - If validation fails or group ID cannot be extracted
514    fn validate_event_and_extract_group_id(&self, event: &Event) -> Result<[u8; 32], Error> {
515        if event.kind != Kind::MlsGroupMessage {
516            return Err(Error::UnexpectedEvent {
517                expected: Kind::MlsGroupMessage,
518                received: event.kind,
519            });
520        }
521
522        let nostr_group_id_tag = event
523            .tags
524            .iter()
525            .find(|tag| tag.kind() == TagKind::h())
526            .ok_or(Error::Message("Group ID Tag not found".to_string()))?;
527
528        let nostr_group_id: [u8; 32] = hex::decode(
529            nostr_group_id_tag
530                .content()
531                .ok_or(Error::Message("Group ID Tag content not found".to_string()))?,
532        )
533        .map_err(|e| Error::Message(e.to_string()))?
534        .try_into()
535        .map_err(|_e| Error::Message("Failed to convert nostr group id to [u8; 32]".to_string()))?;
536
537        Ok(nostr_group_id)
538    }
539
540    /// Loads the group and decrypts the message content
541    ///
542    /// This private method loads the group from storage using the Nostr group ID,
543    /// loads the corresponding MLS group, and decrypts the message content using
544    /// the group's exporter secrets.
545    ///
546    /// # Arguments
547    ///
548    /// * `nostr_group_id` - The Nostr group ID extracted from the event
549    /// * `event` - The Nostr event containing the encrypted message
550    ///
551    /// # Returns
552    ///
553    /// * `Ok((group_types::Group, MlsGroup, Vec<u8>))` - The loaded group, MLS group, and decrypted message bytes
554    /// * `Err(Error)` - If group loading or message decryption fails
555    fn load_group_and_decrypt_message(
556        &self,
557        nostr_group_id: [u8; 32],
558        event: &Event,
559    ) -> Result<(group_types::Group, MlsGroup, Vec<u8>), Error> {
560        let group = self
561            .storage()
562            .find_group_by_nostr_group_id(&nostr_group_id)
563            .map_err(|e| Error::Group(e.to_string()))?
564            .ok_or(Error::GroupNotFound)?;
565
566        // Load the MLS group to get the current epoch
567        let mls_group: MlsGroup = self
568            .load_mls_group(&group.mls_group_id)
569            .map_err(|e| Error::Group(e.to_string()))?
570            .ok_or(Error::GroupNotFound)?;
571
572        // Try to decrypt message with recent exporter secrets (fallback across epochs)
573        let message_bytes: Vec<u8> =
574            self.try_decrypt_with_recent_epochs(&mls_group, &event.content)?;
575
576        Ok((group, mls_group, message_bytes))
577    }
578
579    /// Processes the decrypted message content based on its type
580    ///
581    /// This private method processes the decrypted MLS message and handles the
582    /// different message types (application messages, proposals, commits, etc.).
583    ///
584    /// # Arguments
585    ///
586    /// * `group` - The group metadata from storage
587    /// * `mls_group` - The MLS group instance (mutable for potential state changes)
588    /// * `message_bytes` - The decrypted message bytes
589    /// * `event` - The wrapper Nostr event
590    ///
591    /// # Returns
592    ///
593    /// * `Ok(MessageProcessingResult)` - The result based on message type
594    /// * `Err(Error)` - If message processing fails
595    fn process_decrypted_message(
596        &self,
597        group: group_types::Group,
598        mls_group: &mut MlsGroup,
599        message_bytes: &[u8],
600        event: &Event,
601    ) -> Result<MessageProcessingResult, Error> {
602        match self.process_message_for_group(mls_group, message_bytes) {
603            Ok(ProcessedMessageContent::ApplicationMessage(application_message)) => {
604                Ok(MessageProcessingResult::ApplicationMessage(
605                    self.process_application_message_for_group(group, event, application_message)?,
606                ))
607            }
608            Ok(ProcessedMessageContent::ProposalMessage(staged_proposal)) => {
609                Ok(MessageProcessingResult::Proposal(
610                    self.process_proposal_message_for_group(mls_group, event, *staged_proposal)?,
611                ))
612            }
613            Ok(ProcessedMessageContent::StagedCommitMessage(staged_commit)) => {
614                self.process_commit_message_for_group(mls_group, event, *staged_commit)?;
615                Ok(MessageProcessingResult::Commit)
616            }
617            Ok(ProcessedMessageContent::ExternalJoinProposalMessage(_external_join_proposal)) => {
618                // Save a processed message so we don't reprocess
619                let processed_message = message_types::ProcessedMessage {
620                    wrapper_event_id: event.id,
621                    message_event_id: None,
622                    processed_at: Timestamp::now(),
623                    state: message_types::ProcessedMessageState::Processed,
624                    failure_reason: None,
625                };
626
627                self.storage()
628                    .save_processed_message(processed_message)
629                    .map_err(|e| Error::Message(e.to_string()))?;
630
631                Ok(MessageProcessingResult::ExternalJoinProposal)
632            }
633            Err(e) => Err(e),
634        }
635    }
636
637    /// Handles message processing errors with specific error recovery logic
638    ///
639    /// This private method handles complex error scenarios when message processing fails,
640    /// including special cases like processing own messages, epoch mismatches, and
641    /// other MLS-specific validation errors.
642    ///
643    /// # Arguments
644    ///
645    /// * `error` - The error that occurred during message processing
646    /// * `event` - The wrapper Nostr event that caused the error
647    /// * `group` - The group metadata from storage
648    ///
649    /// # Returns
650    ///
651    /// * `Ok(MessageProcessingResult)` - Recovery result or unprocessable status
652    /// * `Err(Error)` - If error handling itself fails
653    fn handle_message_processing_error(
654        &self,
655        error: Error,
656        event: &Event,
657        group: &group_types::Group,
658    ) -> Result<MessageProcessingResult, Error> {
659        match error {
660            Error::CannotDecryptOwnMessage => {
661                tracing::debug!(target: "nostr_mls::messages::process_message", "Cannot decrypt own message, checking for cached message");
662
663                let mut processed_message = self
664                    .storage()
665                    .find_processed_message_by_event_id(&event.id)
666                    .map_err(|e| Error::Message(e.to_string()))?
667                    .ok_or(Error::Message("Processed message not found".to_string()))?;
668
669                // If the message is created, we need to update the state of the message and processed message
670                // If it's already processed, we don't need to do anything
671                match processed_message.state {
672                    message_types::ProcessedMessageState::Created => {
673                        let message_event_id: EventId = processed_message
674                            .message_event_id
675                            .ok_or(Error::Message("Message event ID not found".to_string()))?;
676
677                        let mut message = self
678                            .get_message(&message_event_id)?
679                            .ok_or(Error::Message("Message not found".to_string()))?;
680
681                        message.state = message_types::MessageState::Processed;
682                        self.storage()
683                            .save_message(message)
684                            .map_err(|e| Error::Message(e.to_string()))?;
685
686                        processed_message.state = message_types::ProcessedMessageState::Processed;
687                        self.storage()
688                            .save_processed_message(processed_message.clone())
689                            .map_err(|e| Error::Message(e.to_string()))?;
690
691                        tracing::debug!(target: "nostr_mls::messages::process_message", "Updated state of own cached message");
692                        let message = self
693                            .get_message(&message_event_id)?
694                            .ok_or(Error::MessageNotFound)?;
695                        Ok(MessageProcessingResult::ApplicationMessage(message))
696                    }
697                    message_types::ProcessedMessageState::ProcessedCommit => {
698                        tracing::debug!(target: "nostr_mls::messages::process_message", "Message already processed as a commit");
699
700                        // Even though this is our own commit that we can't decrypt, we still need to
701                        // sync the stored group metadata with the current MLS group state in case
702                        // the group has been updated since the commit was created
703                        self.sync_group_metadata_from_mls(&group.mls_group_id)
704                            .map_err(|e| {
705                                Error::Message(format!("Failed to sync group metadata: {}", e))
706                            })?;
707
708                        Ok(MessageProcessingResult::Commit)
709                    }
710                    message_types::ProcessedMessageState::Processed
711                    | message_types::ProcessedMessageState::Failed => {
712                        tracing::debug!(target: "nostr_mls::messages::process_message", "Message cannot be processed (already processed or failed)");
713                        Ok(MessageProcessingResult::Unprocessable)
714                    }
715                }
716            }
717            Error::ProcessMessage(ProcessMessageError::ValidationError(
718                ValidationError::WrongEpoch,
719            )) => {
720                // Epoch mismatch - check if this is our own commit that we've already processed
721                tracing::debug!(target: "nostr_mls::messages::process_message", "Epoch mismatch error, checking if this is our own commit");
722
723                if let Ok(Some(processed_message)) = self
724                    .storage()
725                    .find_processed_message_by_event_id(&event.id)
726                    .map_err(|e| Error::Message(e.to_string()))
727                {
728                    if processed_message.state
729                        == message_types::ProcessedMessageState::ProcessedCommit
730                    {
731                        tracing::debug!(target: "nostr_mls::messages::process_message", "Found own commit with epoch mismatch, syncing group metadata");
732
733                        // Sync the stored group metadata even though processing failed
734                        self.sync_group_metadata_from_mls(&group.mls_group_id)
735                            .map_err(|e| {
736                                Error::Message(format!("Failed to sync group metadata: {}", e))
737                            })?;
738
739                        return Ok(MessageProcessingResult::Commit);
740                    }
741                }
742
743                // Not our own commit - this is a genuine error
744                tracing::error!(target: "nostr_mls::messages::process_message", "Epoch mismatch for message that is not our own commit: {:?}", error);
745                let processed_message = message_types::ProcessedMessage {
746                    wrapper_event_id: event.id,
747                    message_event_id: None,
748                    processed_at: Timestamp::now(),
749                    state: message_types::ProcessedMessageState::Failed,
750                    failure_reason: Some("Epoch mismatch".to_string()),
751                };
752                self.storage()
753                    .save_processed_message(processed_message)
754                    .map_err(|e| Error::Message(e.to_string()))?;
755
756                Ok(MessageProcessingResult::Unprocessable)
757            }
758            Error::ProcessMessage(ProcessMessageError::ValidationError(
759                ValidationError::WrongGroupId,
760            )) => {
761                tracing::error!(target: "nostr_mls::messages::process_message", "Group ID mismatch: {:?}", error);
762                let processed_message = message_types::ProcessedMessage {
763                    wrapper_event_id: event.id,
764                    message_event_id: None,
765                    processed_at: Timestamp::now(),
766                    state: message_types::ProcessedMessageState::Failed,
767                    failure_reason: Some("Group ID mismatch".to_string()),
768                };
769                self.storage()
770                    .save_processed_message(processed_message)
771                    .map_err(|e| Error::Message(e.to_string()))?;
772
773                Ok(MessageProcessingResult::Unprocessable)
774            }
775            Error::ProcessMessage(ProcessMessageError::GroupStateError(
776                MlsGroupStateError::UseAfterEviction,
777            )) => {
778                tracing::error!(target: "nostr_mls::messages::process_message", "Attempted to use group after eviction: {:?}", error);
779                let processed_message = message_types::ProcessedMessage {
780                    wrapper_event_id: event.id,
781                    message_event_id: None,
782                    processed_at: Timestamp::now(),
783                    state: message_types::ProcessedMessageState::Failed,
784                    failure_reason: Some("Use after eviction".to_string()),
785                };
786                self.storage()
787                    .save_processed_message(processed_message)
788                    .map_err(|e| Error::Message(e.to_string()))?;
789
790                Ok(MessageProcessingResult::Unprocessable)
791            }
792            _ => {
793                tracing::error!(target: "nostr_mls::messages::process_message", "Unexpected error processing message: {:?}", error);
794                let processed_message = message_types::ProcessedMessage {
795                    wrapper_event_id: event.id,
796                    message_event_id: None,
797                    processed_at: Timestamp::now(),
798                    state: message_types::ProcessedMessageState::Failed,
799                    failure_reason: Some(error.to_string()),
800                };
801                self.storage()
802                    .save_processed_message(processed_message)
803                    .map_err(|e| Error::Message(e.to_string()))?;
804
805                Ok(MessageProcessingResult::Unprocessable)
806            }
807        }
808    }
809
810    /// Tries to decrypt a message using exporter secrets from multiple recent epochs excluding the current one
811    ///
812    /// This helper method attempts to decrypt a message by trying exporter secrets from
813    /// the most recent epoch backwards for a configurable number of epochs. This handles
814    /// the case where a message was encrypted with an older epoch's secret due to timing
815    /// issues or delayed message processing.
816    ///
817    /// # Arguments
818    ///
819    /// * `mls_group` - The MLS group
820    /// * `encrypted_content` - The NIP-44 encrypted message content
821    /// * `max_epoch_lookback` - Maximum number of epochs to search backwards (default: 5)
822    ///
823    /// # Returns
824    ///
825    /// * `Ok(Vec<u8>)` - The decrypted message bytes
826    /// * `Err(Error)` - If decryption fails with all available exporter secrets
827    fn try_decrypt_with_past_epochs(
828        &self,
829        mls_group: &MlsGroup,
830        encrypted_content: &str,
831        max_epoch_lookback: u64,
832    ) -> Result<Vec<u8>, Error> {
833        let group_id = mls_group.group_id();
834        let current_epoch: u64 = mls_group.epoch().as_u64();
835
836        // Start from current epoch and go backwards
837        let start_epoch: u64 = current_epoch.saturating_sub(1);
838        let end_epoch: u64 = start_epoch.saturating_sub(max_epoch_lookback);
839
840        for epoch in (end_epoch..=start_epoch).rev() {
841            tracing::debug!(
842                target: "nostr_mls::messages::try_decrypt_with_recent_epochs",
843                "Trying to decrypt with epoch {} for group {:?}",
844                epoch,
845                group_id
846            );
847
848            // Try to get the exporter secret for this epoch
849            if let Ok(Some(secret)) = self
850                .storage()
851                .get_group_exporter_secret(group_id, epoch)
852                .map_err(|e| Error::Group(e.to_string()))
853            {
854                // Try to decrypt with this epoch's secret
855                match util::decrypt_with_exporter_secret(&secret, encrypted_content) {
856                    Ok(decrypted_bytes) => {
857                        tracing::debug!(
858                            target: "nostr_mls::messages::try_decrypt_with_recent_epochs",
859                            "Successfully decrypted message with epoch {} for group {:?}",
860                            epoch,
861                            group_id
862                        );
863                        return Ok(decrypted_bytes);
864                    }
865                    Err(e) => {
866                        tracing::trace!(
867                            target: "nostr_mls::messages::try_decrypt_with_recent_epochs",
868                            "Failed to decrypt with epoch {}: {:?}",
869                            epoch,
870                            e
871                        );
872                        // Continue to next epoch
873                    }
874                }
875            } else {
876                tracing::trace!(
877                    target: "nostr_mls::messages::try_decrypt_with_recent_epochs",
878                    "No exporter secret found for epoch {} in group {:?}",
879                    epoch,
880                    group_id
881                );
882            }
883        }
884
885        Err(Error::Message(format!(
886            "Failed to decrypt message with any exporter secret from epochs {} to {} for group {:?}",
887            end_epoch, start_epoch, group_id
888        )))
889    }
890
891    /// Try to decrypt using the current exporter secret and if fails try with the past ones until a max loopback of [`DEFAULT_EPOCH_LOOKBACK`].
892    fn try_decrypt_with_recent_epochs(
893        &self,
894        mls_group: &MlsGroup,
895        encrypted_content: &str,
896    ) -> Result<Vec<u8>, Error> {
897        // Get exporter secret for current epoch
898        let secret = self.exporter_secret(mls_group.group_id())?;
899
900        // Try to decrypt it for the current epoch
901        match util::decrypt_with_exporter_secret(&secret, encrypted_content) {
902            Ok(decrypted_bytes) => {
903                tracing::debug!(
904                    "Successfully decrypted message with current exporter secret for group {:?}",
905                    mls_group.group_id()
906                );
907                Ok(decrypted_bytes)
908            }
909            // Decryption failed using the current epoch exporter secret
910            Err(_) => {
911                tracing::debug!("Failed to decrypt message with current exporter secret. Trying with past ones.");
912
913                // Try with past exporter secrets
914                self.try_decrypt_with_past_epochs(
915                    mls_group,
916                    encrypted_content,
917                    DEFAULT_EPOCH_LOOKBACK,
918                )
919            }
920        }
921    }
922
923    /// Processes an incoming encrypted Nostr event containing an MLS message
924    ///
925    /// This is the main entry point for processing received messages. The function orchestrates
926    /// the message processing workflow by delegating to specialized private methods:
927    /// 1. Validates the event and extracts group ID
928    /// 2. Loads the group and decrypts the message content
929    /// 3. Processes the decrypted message based on its type
930    /// 4. Handles errors with specialized recovery logic
931    ///
932    /// # Arguments
933    ///
934    /// * `event` - The received Nostr event containing the encrypted MLS message
935    ///
936    /// # Returns
937    ///
938    /// * `Ok(MessageProcessingResult)` - Result indicating the type of message processed
939    /// * `Err(Error)` - If message processing fails
940    pub fn process_message(&self, event: &Event) -> Result<MessageProcessingResult, Error> {
941        // Step 1: Validate event and extract group ID
942        let nostr_group_id = self.validate_event_and_extract_group_id(event)?;
943
944        // Step 2: Load group and decrypt message
945        let (group, mut mls_group, message_bytes) =
946            self.load_group_and_decrypt_message(nostr_group_id, event)?;
947
948        // Step 3: Process the decrypted message
949        match self.process_decrypted_message(group.clone(), &mut mls_group, &message_bytes, event) {
950            Ok(result) => Ok(result),
951            Err(error) => {
952                // Step 4: Handle errors with specialized recovery logic
953                self.handle_message_processing_error(error, event, &group)
954            }
955        }
956    }
957}
958
959#[cfg(test)]
960mod tests {
961    use nostr::{EventBuilder, Keys, Kind, PublicKey, Tag, TagKind};
962
963    use super::*;
964    use crate::test_util::*;
965    use crate::tests::create_test_nostr_mls;
966
967    #[test]
968    fn test_get_message_not_found() {
969        let nostr_mls = create_test_nostr_mls();
970        let non_existent_event_id = EventId::all_zeros();
971
972        let result = nostr_mls.get_message(&non_existent_event_id);
973        assert!(result.is_ok());
974        assert!(result.unwrap().is_none());
975    }
976
977    #[test]
978    fn test_get_messages_empty_group() {
979        let nostr_mls = create_test_nostr_mls();
980        let (creator, members, admins) = create_test_group_members();
981        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
982
983        let messages = nostr_mls
984            .get_messages(&group_id)
985            .expect("Failed to get messages");
986        assert!(messages.is_empty());
987    }
988
989    #[test]
990    fn test_create_message_success() {
991        let nostr_mls = create_test_nostr_mls();
992        let (creator, members, admins) = create_test_group_members();
993        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
994
995        // Create a test message
996        let mut rumor = create_test_rumor(&creator, "Hello, world!");
997        let rumor_id = rumor.id();
998
999        let result = nostr_mls.create_message(&group_id, rumor);
1000        assert!(result.is_ok());
1001
1002        let event = result.unwrap();
1003        assert_eq!(event.kind, Kind::MlsGroupMessage);
1004
1005        // Verify the message was stored
1006        let stored_message = nostr_mls
1007            .get_message(&rumor_id)
1008            .expect("Failed to get message")
1009            .expect("Message should exist");
1010
1011        assert_eq!(stored_message.id, rumor_id);
1012        assert_eq!(stored_message.content, "Hello, world!");
1013        assert_eq!(stored_message.state, message_types::MessageState::Created);
1014        assert_eq!(stored_message.wrapper_event_id, event.id);
1015    }
1016
1017    #[test]
1018    fn test_create_message_group_not_found() {
1019        let nostr_mls = create_test_nostr_mls();
1020        let creator = Keys::generate();
1021        let rumor = create_test_rumor(&creator, "Hello, world!");
1022        let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4]);
1023
1024        let result = nostr_mls.create_message(&non_existent_group_id, rumor);
1025        assert!(result.is_err());
1026        assert!(matches!(result.unwrap_err(), Error::GroupNotFound));
1027    }
1028
1029    #[test]
1030    fn test_create_message_updates_group_metadata() {
1031        let nostr_mls = create_test_nostr_mls();
1032        let (creator, members, admins) = create_test_group_members();
1033        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
1034
1035        // Get initial group state
1036        let initial_group = nostr_mls
1037            .get_group(&group_id)
1038            .expect("Failed to get group")
1039            .expect("Group should exist");
1040        assert!(initial_group.last_message_at.is_none());
1041        assert!(initial_group.last_message_id.is_none());
1042
1043        // Create a message
1044        let mut rumor = create_test_rumor(&creator, "Hello, world!");
1045        let rumor_id = rumor.id();
1046        let rumor_timestamp = rumor.created_at;
1047
1048        let _event = nostr_mls
1049            .create_message(&group_id, rumor)
1050            .expect("Failed to create message");
1051
1052        // Verify group metadata was updated
1053        let updated_group = nostr_mls
1054            .get_group(&group_id)
1055            .expect("Failed to get group")
1056            .expect("Group should exist");
1057
1058        assert_eq!(updated_group.last_message_at, Some(rumor_timestamp));
1059        assert_eq!(updated_group.last_message_id, Some(rumor_id));
1060    }
1061
1062    #[test]
1063    fn test_process_message_invalid_kind() {
1064        let nostr_mls = create_test_nostr_mls();
1065        let creator = Keys::generate();
1066
1067        // Create an event with wrong kind
1068        let event = EventBuilder::new(Kind::TextNote, "test content")
1069            .sign_with_keys(&creator)
1070            .expect("Failed to sign event");
1071
1072        let result = nostr_mls.process_message(&event);
1073        assert!(result.is_err());
1074        assert!(matches!(result.unwrap_err(), Error::UnexpectedEvent { .. }));
1075    }
1076
1077    #[test]
1078    fn test_process_message_missing_group_id_tag() {
1079        let nostr_mls = create_test_nostr_mls();
1080        let creator = Keys::generate();
1081
1082        // Create an event without group ID tag
1083        let event = EventBuilder::new(Kind::MlsGroupMessage, "test content")
1084            .sign_with_keys(&creator)
1085            .expect("Failed to sign event");
1086
1087        let result = nostr_mls.process_message(&event);
1088        assert!(result.is_err());
1089        assert!(matches!(result.unwrap_err(), Error::Message(_)));
1090    }
1091
1092    #[test]
1093    fn test_process_message_group_not_found() {
1094        let nostr_mls = create_test_nostr_mls();
1095        let creator = Keys::generate();
1096
1097        // Create a valid MLS group message event with non-existent group ID
1098        let fake_group_id = hex::encode([1u8; 32]);
1099        let tag = Tag::custom(TagKind::h(), [fake_group_id]);
1100
1101        let event = EventBuilder::new(Kind::MlsGroupMessage, "encrypted_content")
1102            .tag(tag)
1103            .sign_with_keys(&creator)
1104            .expect("Failed to sign event");
1105
1106        let result = nostr_mls.process_message(&event);
1107        assert!(result.is_err());
1108        assert!(matches!(result.unwrap_err(), Error::GroupNotFound));
1109    }
1110
1111    #[test]
1112    fn test_message_state_tracking() {
1113        let nostr_mls = create_test_nostr_mls();
1114        let (creator, members, admins) = create_test_group_members();
1115        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
1116
1117        // Create a message
1118        let mut rumor = create_test_rumor(&creator, "Test message state");
1119        let rumor_id = rumor.id();
1120
1121        let event = nostr_mls
1122            .create_message(&group_id, rumor)
1123            .expect("Failed to create message");
1124
1125        // Verify initial state
1126        let message = nostr_mls
1127            .get_message(&rumor_id)
1128            .expect("Failed to get message")
1129            .expect("Message should exist");
1130
1131        assert_eq!(message.state, message_types::MessageState::Created);
1132
1133        // Verify processed message state
1134        let processed_message = nostr_mls
1135            .storage()
1136            .find_processed_message_by_event_id(&event.id)
1137            .expect("Failed to get processed message")
1138            .expect("Processed message should exist");
1139
1140        assert_eq!(
1141            processed_message.state,
1142            message_types::ProcessedMessageState::Created
1143        );
1144        assert_eq!(processed_message.message_event_id, Some(rumor_id));
1145        assert_eq!(processed_message.wrapper_event_id, event.id);
1146    }
1147
1148    #[test]
1149    fn test_get_messages_for_group() {
1150        let nostr_mls = create_test_nostr_mls();
1151        let (creator, members, admins) = create_test_group_members();
1152        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
1153
1154        // Create multiple messages
1155        let rumor1 = create_test_rumor(&creator, "First message");
1156        let rumor2 = create_test_rumor(&creator, "Second message");
1157
1158        let _event1 = nostr_mls
1159            .create_message(&group_id, rumor1)
1160            .expect("Failed to create first message");
1161        let _event2 = nostr_mls
1162            .create_message(&group_id, rumor2)
1163            .expect("Failed to create second message");
1164
1165        // Get all messages for the group
1166        let messages = nostr_mls
1167            .get_messages(&group_id)
1168            .expect("Failed to get messages");
1169
1170        assert_eq!(messages.len(), 2);
1171
1172        // Verify message contents
1173        let contents: Vec<&str> = messages.iter().map(|m| m.content.as_str()).collect();
1174        assert!(contents.contains(&"First message"));
1175        assert!(contents.contains(&"Second message"));
1176
1177        // Verify all messages belong to the correct group
1178        for message in &messages {
1179            assert_eq!(message.mls_group_id, group_id);
1180        }
1181    }
1182
1183    #[test]
1184    fn test_message_processing_result_variants() {
1185        // Test that MessageProcessingResult variants can be created and matched
1186        let dummy_message = message_types::Message {
1187            id: EventId::all_zeros(),
1188            pubkey: PublicKey::from_hex(
1189                "8a9de562cbbed225b6ea0118dd3997a02df92c0bffd2224f71081a7450c3e549",
1190            )
1191            .unwrap(),
1192            kind: Kind::TextNote,
1193            mls_group_id: GroupId::from_slice(&[1, 2, 3, 4]),
1194            created_at: Timestamp::now(),
1195            content: "Test".to_string(),
1196            tags: Tags::new(),
1197            event: EventBuilder::new(Kind::TextNote, "Test").build(
1198                PublicKey::from_hex(
1199                    "8a9de562cbbed225b6ea0118dd3997a02df92c0bffd2224f71081a7450c3e549",
1200                )
1201                .unwrap(),
1202            ),
1203            wrapper_event_id: EventId::all_zeros(),
1204            state: message_types::MessageState::Processed,
1205        };
1206
1207        let app_result = MessageProcessingResult::ApplicationMessage(dummy_message);
1208        let commit_result = MessageProcessingResult::Commit;
1209        let external_join_result = MessageProcessingResult::ExternalJoinProposal;
1210        let unprocessable_result = MessageProcessingResult::Unprocessable;
1211
1212        // Test that we can match on variants
1213        match app_result {
1214            MessageProcessingResult::ApplicationMessage(_) => {}
1215            _ => panic!("Expected ApplicationMessage variant"),
1216        }
1217
1218        match commit_result {
1219            MessageProcessingResult::Commit => {}
1220            _ => panic!("Expected Commit variant"),
1221        }
1222
1223        match external_join_result {
1224            MessageProcessingResult::ExternalJoinProposal => {}
1225            _ => panic!("Expected ExternalJoinProposal variant"),
1226        }
1227
1228        match unprocessable_result {
1229            MessageProcessingResult::Unprocessable => {}
1230            _ => panic!("Expected Unprocessable variant"),
1231        }
1232    }
1233
1234    #[test]
1235    fn test_message_content_preservation() {
1236        let nostr_mls = create_test_nostr_mls();
1237        let (creator, members, admins) = create_test_group_members();
1238        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
1239
1240        // Test with various content types
1241        let test_cases = vec![
1242            "Simple text message",
1243            "Message with emojis 🚀 🎉 ✨",
1244            "Message with\nmultiple\nlines",
1245            "Message with special chars: !@#$%^&*()",
1246            "Minimal content",
1247        ];
1248
1249        for content in test_cases {
1250            let mut rumor = create_test_rumor(&creator, content);
1251            let rumor_id = rumor.id();
1252
1253            let _event = nostr_mls
1254                .create_message(&group_id, rumor)
1255                .expect("Failed to create message");
1256
1257            let stored_message = nostr_mls
1258                .get_message(&rumor_id)
1259                .expect("Failed to get message")
1260                .expect("Message should exist");
1261
1262            assert_eq!(stored_message.content, content);
1263            assert_eq!(stored_message.pubkey, creator.public_key());
1264        }
1265    }
1266
1267    #[test]
1268    fn test_create_message_ensures_rumor_id() {
1269        let nostr_mls = create_test_nostr_mls();
1270        let (creator, members, admins) = create_test_group_members();
1271        let group_id = create_test_group(&nostr_mls, &creator, &members, &admins);
1272
1273        // Create a rumor - EventBuilder.build() ensures the ID is set
1274        let rumor = create_test_rumor(&creator, "Test message");
1275
1276        let result = nostr_mls.create_message(&group_id, rumor);
1277        assert!(result.is_ok());
1278
1279        // The message should have been stored with a valid ID
1280        let event = result.unwrap();
1281        let messages = nostr_mls
1282            .get_messages(&group_id)
1283            .expect("Failed to get messages");
1284
1285        assert_eq!(messages.len(), 1);
1286        assert_eq!(messages[0].wrapper_event_id, event.id);
1287    }
1288
1289    #[test]
1290    fn test_merge_pending_commit_syncs_group_metadata() {
1291        let nostr_mls = create_test_nostr_mls();
1292
1293        // Create test group members
1294        let creator_keys = Keys::generate();
1295        let member1_keys = Keys::generate();
1296        let member2_keys = Keys::generate();
1297
1298        let creator_pk = creator_keys.public_key();
1299        let member1_pk = member1_keys.public_key();
1300
1301        let members = vec![member1_keys.clone(), member2_keys.clone()];
1302        let admins = vec![creator_pk, member1_pk]; // Creator and member1 are admins
1303
1304        // Create group
1305        let group_id = create_test_group(&nostr_mls, &creator_keys, &members, &admins);
1306
1307        // Get initial stored group state
1308        let initial_group = nostr_mls
1309            .get_group(&group_id)
1310            .expect("Failed to get initial group")
1311            .expect("Initial group should exist");
1312
1313        let initial_epoch = initial_group.epoch;
1314        let initial_name = initial_group.name.clone();
1315
1316        // Create a commit by updating the group name
1317        let new_name = "Updated Group Name via MLS Commit".to_string();
1318        let update = crate::groups::NostrGroupDataUpdate::new().name(new_name.clone());
1319        let _update_result = nostr_mls
1320            .update_group_data(&group_id, update)
1321            .expect("Failed to update group name");
1322
1323        // Before merging commit - verify stored group still has old data
1324        let pre_merge_group = nostr_mls
1325            .get_group(&group_id)
1326            .expect("Failed to get pre-merge group")
1327            .expect("Pre-merge group should exist");
1328
1329        assert_eq!(
1330            pre_merge_group.name, initial_name,
1331            "Stored group name should still be old before merge"
1332        );
1333        assert_eq!(
1334            pre_merge_group.epoch, initial_epoch,
1335            "Stored group epoch should still be old before merge"
1336        );
1337
1338        // Get MLS group state before merge (epoch shouldn't advance until merge)
1339        let pre_merge_mls_group = nostr_mls
1340            .load_mls_group(&group_id)
1341            .expect("Failed to load pre-merge MLS group")
1342            .expect("Pre-merge MLS group should exist");
1343
1344        let pre_merge_mls_epoch = pre_merge_mls_group.epoch().as_u64();
1345        assert_eq!(
1346            pre_merge_mls_epoch, initial_epoch,
1347            "MLS group epoch should not advance until commit is merged"
1348        );
1349
1350        // This is the key test: merge_pending_commit should sync the stored group metadata
1351        nostr_mls
1352            .merge_pending_commit(&group_id)
1353            .expect("Failed to merge pending commit");
1354
1355        // Verify stored group is now synchronized after merge
1356        let post_merge_group = nostr_mls
1357            .get_group(&group_id)
1358            .expect("Failed to get post-merge group")
1359            .expect("Post-merge group should exist");
1360
1361        // Verify epoch is synchronized
1362        assert!(
1363            post_merge_group.epoch > initial_epoch,
1364            "Stored group epoch should advance after merge"
1365        );
1366
1367        // Verify extension data is synchronized
1368        let post_merge_mls_group = nostr_mls
1369            .load_mls_group(&group_id)
1370            .expect("Failed to load post-merge MLS group")
1371            .expect("Post-merge MLS group should exist");
1372
1373        let group_data =
1374            super::extension::NostrGroupDataExtension::from_group(&post_merge_mls_group)
1375                .expect("Failed to get group data extension");
1376
1377        assert_eq!(
1378            post_merge_group.name, group_data.name,
1379            "Stored group name should match extension after merge"
1380        );
1381        assert_eq!(
1382            post_merge_group.name, new_name,
1383            "Stored group name should be updated after merge"
1384        );
1385        assert_eq!(
1386            post_merge_group.description, group_data.description,
1387            "Stored group description should match extension"
1388        );
1389        assert_eq!(
1390            post_merge_group.admin_pubkeys, group_data.admins,
1391            "Stored group admins should match extension"
1392        );
1393
1394        // Test that the sync function itself works correctly by manually de-syncing and re-syncing
1395        let mut manually_desync_group = post_merge_group.clone();
1396        manually_desync_group.name = "Manually Corrupted Name".to_string();
1397        manually_desync_group.epoch = initial_epoch;
1398        nostr_mls
1399            .storage()
1400            .save_group(manually_desync_group)
1401            .expect("Failed to save corrupted group");
1402
1403        // Verify it's out of sync
1404        let corrupted_group = nostr_mls
1405            .get_group(&group_id)
1406            .expect("Failed to get corrupted group")
1407            .expect("Corrupted group should exist");
1408
1409        assert_eq!(
1410            corrupted_group.name, "Manually Corrupted Name",
1411            "Group should be manually corrupted"
1412        );
1413        assert_eq!(
1414            corrupted_group.epoch, initial_epoch,
1415            "Group epoch should be manually corrupted"
1416        );
1417
1418        // Call sync function directly
1419        nostr_mls
1420            .sync_group_metadata_from_mls(&group_id)
1421            .expect("Failed to sync group metadata");
1422
1423        // Verify it's back in sync
1424        let re_synced_group = nostr_mls
1425            .get_group(&group_id)
1426            .expect("Failed to get re-synced group")
1427            .expect("Re-synced group should exist");
1428
1429        assert_eq!(
1430            re_synced_group.name, new_name,
1431            "Group name should be re-synced"
1432        );
1433        assert!(
1434            re_synced_group.epoch > initial_epoch,
1435            "Group epoch should be re-synced"
1436        );
1437        assert_eq!(
1438            re_synced_group.admin_pubkeys, group_data.admins,
1439            "Group admins should be re-synced"
1440        );
1441    }
1442
1443    #[test]
1444    fn test_processing_own_commit_syncs_group_metadata() {
1445        let nostr_mls = create_test_nostr_mls();
1446
1447        // Create test group
1448        let creator_keys = Keys::generate();
1449        let member1_keys = Keys::generate();
1450        let member2_keys = Keys::generate();
1451
1452        let creator_pk = creator_keys.public_key();
1453        let member1_pk = member1_keys.public_key();
1454
1455        let members = vec![member1_keys.clone(), member2_keys.clone()];
1456        let admins = vec![creator_pk, member1_pk];
1457
1458        let group_id = create_test_group(&nostr_mls, &creator_keys, &members, &admins);
1459
1460        // Get initial state
1461        let initial_group = nostr_mls
1462            .get_group(&group_id)
1463            .expect("Failed to get initial group")
1464            .expect("Initial group should exist");
1465
1466        let initial_epoch = initial_group.epoch;
1467
1468        // Create and merge a commit to update group name
1469        let new_name = "Updated Name for Own Commit Test".to_string();
1470        let update = crate::groups::NostrGroupDataUpdate::new().name(new_name.clone());
1471        let update_result = nostr_mls
1472            .update_group_data(&group_id, update)
1473            .expect("Failed to update group name");
1474
1475        nostr_mls
1476            .merge_pending_commit(&group_id)
1477            .expect("Failed to merge pending commit");
1478
1479        // Verify the commit event is marked as ProcessedCommit
1480        let commit_event_id = update_result.evolution_event.id;
1481        let processed_message = nostr_mls
1482            .storage()
1483            .find_processed_message_by_event_id(&commit_event_id)
1484            .expect("Failed to find processed message")
1485            .expect("Processed message should exist");
1486
1487        assert_eq!(
1488            processed_message.state,
1489            message_types::ProcessedMessageState::ProcessedCommit
1490        );
1491
1492        // Manually corrupt the stored group to simulate desync
1493        let mut corrupted_group = initial_group.clone();
1494        corrupted_group.name = "Corrupted Name".to_string();
1495        corrupted_group.epoch = initial_epoch;
1496        nostr_mls
1497            .storage()
1498            .save_group(corrupted_group)
1499            .expect("Failed to save corrupted group");
1500
1501        // Verify it's out of sync
1502        let out_of_sync_group = nostr_mls
1503            .get_group(&group_id)
1504            .expect("Failed to get out of sync group")
1505            .expect("Out of sync group should exist");
1506
1507        assert_eq!(out_of_sync_group.name, "Corrupted Name");
1508        assert_eq!(out_of_sync_group.epoch, initial_epoch);
1509
1510        // Process our own commit message - this should trigger sync even though it's marked as ProcessedCommit
1511        let message_result = nostr_mls
1512            .process_message(&update_result.evolution_event)
1513            .expect("Failed to process own commit message");
1514
1515        // Verify it returns Commit result (our fix should handle epoch mismatch errors)
1516        assert!(matches!(message_result, MessageProcessingResult::Commit));
1517
1518        // Most importantly: verify that processing our own commit synchronized the stored group metadata
1519        let synced_group = nostr_mls
1520            .get_group(&group_id)
1521            .expect("Failed to get synced group")
1522            .expect("Synced group should exist");
1523
1524        assert_eq!(
1525            synced_group.name, new_name,
1526            "Processing own commit should sync group name"
1527        );
1528        assert!(
1529            synced_group.epoch > initial_epoch,
1530            "Processing own commit should sync group epoch"
1531        );
1532
1533        // Verify the stored group matches the MLS group state
1534        let mls_group = nostr_mls
1535            .load_mls_group(&group_id)
1536            .expect("Failed to load MLS group")
1537            .expect("MLS group should exist");
1538
1539        assert_eq!(
1540            synced_group.epoch,
1541            mls_group.epoch().as_u64(),
1542            "Stored and MLS group epochs should match"
1543        );
1544
1545        let group_data = super::extension::NostrGroupDataExtension::from_group(&mls_group)
1546            .expect("Failed to get group data extension");
1547
1548        assert_eq!(
1549            synced_group.name, group_data.name,
1550            "Stored group name should match extension"
1551        );
1552        assert_eq!(
1553            synced_group.admin_pubkeys, group_data.admins,
1554            "Stored group admins should match extension"
1555        );
1556    }
1557}