Skip to main content

river_core/room_state/
message.rs

1use crate::room_state::member::MemberId;
2use crate::room_state::privacy::{PrivacyMode, SecretVersion};
3use crate::room_state::ChatRoomParametersV1;
4use crate::util::sign_struct;
5use crate::util::{truncated_base64, verify_struct};
6use crate::ChatRoomStateV1;
7use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
8use freenet_scaffold::util::{fast_hash, FastHash};
9use freenet_scaffold::ComposableState;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fmt;
13use std::time::SystemTime;
14
15/// Computed state for message actions (edits, deletes, reactions)
16/// This is rebuilt from action messages and not serialized
17#[derive(Clone, PartialEq, Debug, Default)]
18pub struct MessageActionsState {
19    /// Messages that have been edited: message_id -> new text content
20    pub edited_content: HashMap<MessageId, String>,
21    /// Messages that have been deleted
22    pub deleted: std::collections::HashSet<MessageId>,
23    /// Reactions on messages: message_id -> (emoji -> list of reactors)
24    pub reactions: HashMap<MessageId, HashMap<String, Vec<MemberId>>>,
25}
26
27#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Default)]
28pub struct MessagesV1 {
29    pub messages: Vec<AuthorizedMessageV1>,
30    /// Computed state from action messages (not serialized - rebuilt on each delta)
31    #[serde(skip)]
32    pub actions_state: MessageActionsState,
33}
34
35impl ComposableState for MessagesV1 {
36    type ParentState = ChatRoomStateV1;
37    type Summary = Vec<MessageId>;
38    type Delta = Vec<AuthorizedMessageV1>;
39    type Parameters = ChatRoomParametersV1;
40
41    fn verify(
42        &self,
43        parent_state: &Self::ParentState,
44        parameters: &Self::Parameters,
45    ) -> Result<(), String> {
46        let members_by_id = parent_state.members.members_by_member_id();
47        let owner_id = parameters.owner_id();
48
49        for message in &self.messages {
50            let verifying_key = if message.message.author == owner_id {
51                // Owner's messages are validated against the owner's key
52                &parameters.owner
53            } else if let Some(member) = members_by_id.get(&message.message.author) {
54                // Regular member messages are validated against their member key
55                &member.member.member_vk
56            } else {
57                return Err(format!(
58                    "Message author not found: {:?}",
59                    message.message.author
60                ));
61            };
62
63            if message.validate(verifying_key).is_err() {
64                return Err(format!("Invalid message signature: id:{:?}", message.id()));
65            }
66        }
67
68        Ok(())
69    }
70
71    fn summarize(
72        &self,
73        _parent_state: &Self::ParentState,
74        _parameters: &Self::Parameters,
75    ) -> Self::Summary {
76        self.messages.iter().map(|m| m.id()).collect()
77    }
78
79    fn delta(
80        &self,
81        _parent_state: &Self::ParentState,
82        _parameters: &Self::Parameters,
83        old_state_summary: &Self::Summary,
84    ) -> Option<Self::Delta> {
85        let delta: Vec<AuthorizedMessageV1> = self
86            .messages
87            .iter()
88            .filter(|m| !old_state_summary.contains(&m.id()))
89            .cloned()
90            .collect();
91        if delta.is_empty() {
92            None
93        } else {
94            Some(delta)
95        }
96    }
97
98    fn apply_delta(
99        &mut self,
100        parent_state: &Self::ParentState,
101        parameters: &Self::Parameters,
102        delta: &Option<Self::Delta>,
103    ) -> Result<(), String> {
104        let max_recent_messages = parent_state.configuration.configuration.max_recent_messages;
105        let max_message_size = parent_state.configuration.configuration.max_message_size;
106        let privacy_mode = &parent_state.configuration.configuration.privacy_mode;
107        let current_secret_version = parent_state.secrets.current_version;
108
109        // Validate message constraints before adding
110        if let Some(delta) = delta {
111            for msg in delta {
112                let content = &msg.message.content;
113
114                match content {
115                    RoomMessageBody::Private { secret_version, .. } => {
116                        // In private mode, verify secret version matches current
117                        if *privacy_mode == PrivacyMode::Private
118                            && *secret_version != current_secret_version
119                        {
120                            return Err(format!(
121                                "Private message secret version {} does not match current version {}",
122                                secret_version, current_secret_version
123                            ));
124                        }
125
126                        // Verify all current members have encrypted blobs for this version
127                        let members = parent_state.members.members_by_member_id();
128                        if !parent_state.secrets.has_complete_distribution(&members) {
129                            return Err(
130                                "Cannot accept private messages: incomplete secret distribution"
131                                    .to_string(),
132                            );
133                        }
134                    }
135                    RoomMessageBody::Public { .. } => {
136                        // In private mode, reject public messages (everything must be encrypted)
137                        // Exception: event messages (joins, etc.) contain no sensitive content
138                        if *privacy_mode == PrivacyMode::Private && !content.is_event() {
139                            return Err("Cannot send public messages in private room".to_string());
140                        }
141                    }
142                }
143            }
144
145            // Deduplicate by message ID to prevent duplicate messages from race conditions
146            let existing_ids: std::collections::HashSet<_> =
147                self.messages.iter().map(|m| m.id()).collect();
148            self.messages.extend(
149                delta
150                    .iter()
151                    .filter(|msg| !existing_ids.contains(&msg.id()))
152                    .cloned(),
153            );
154        }
155
156        // Always enforce message constraints
157        // Ensure there are no messages over the size limit
158        self.messages
159            .retain(|m| m.message.content.content_len() <= max_message_size);
160
161        // Ensure all messages are signed by a valid member or the room owner, remove if not
162        let members_by_id = parent_state.members.members_by_member_id();
163        let owner_id = MemberId::from(&parameters.owner);
164        self.messages.retain(|m| {
165            members_by_id.contains_key(&m.message.author) || m.message.author == owner_id
166        });
167
168        // Sort messages by time, with MessageId as secondary sort for deterministic ordering
169        // (CRDT convergence requirement - without this, ties produce non-deterministic order)
170        self.messages.sort_by(|a, b| {
171            a.message
172                .time
173                .cmp(&b.message.time)
174                .then_with(|| a.id().cmp(&b.id()))
175        });
176
177        // Remove oldest messages if there are too many
178        if self.messages.len() > max_recent_messages {
179            self.messages
180                .drain(0..self.messages.len() - max_recent_messages);
181        }
182
183        // Rebuild computed state from action messages
184        self.rebuild_actions_state();
185
186        Ok(())
187    }
188}
189
190impl MessagesV1 {
191    /// Rebuild the computed actions state by scanning all action messages.
192    ///
193    /// This method only processes PUBLIC action messages. For private rooms,
194    /// use `rebuild_actions_state_with_decrypted` and provide the decrypted
195    /// content for each private action message.
196    pub fn rebuild_actions_state(&mut self) {
197        self.rebuild_actions_state_with_decrypted(&HashMap::new());
198    }
199
200    /// Rebuild actions state with decrypted content for private action messages.
201    ///
202    /// For private rooms, the caller should decrypt each private action message
203    /// and provide the plaintext bytes in `decrypted_content`, keyed by message ID.
204    ///
205    /// # Arguments
206    /// * `decrypted_content` - Map of message_id -> decrypted plaintext bytes for
207    ///   private action messages. Public actions are decoded directly.
208    pub fn rebuild_actions_state_with_decrypted(
209        &mut self,
210        decrypted_content: &HashMap<MessageId, Vec<u8>>,
211    ) {
212        use crate::room_state::content::{
213            ActionContentV1, DecodedContent, ACTION_TYPE_DELETE, ACTION_TYPE_EDIT,
214            ACTION_TYPE_REACTION, ACTION_TYPE_REMOVE_REACTION,
215        };
216
217        // Clear existing computed state
218        self.actions_state = MessageActionsState::default();
219
220        // Build a map of message_id -> author for authorization checks
221        let message_authors: HashMap<MessageId, MemberId> = self
222            .messages
223            .iter()
224            .filter(|m| !m.message.content.is_action())
225            .map(|m| (m.id(), m.message.author))
226            .collect();
227
228        // Process action messages in timestamp order (messages are already sorted)
229        for msg in &self.messages {
230            let actor = msg.message.author;
231
232            // Skip non-action messages
233            if !msg.message.content.is_action() {
234                continue;
235            }
236
237            // Decode the action content - either from public data or decrypted bytes
238            let action = match &msg.message.content {
239                RoomMessageBody::Public { .. } => {
240                    // Public action - decode directly
241                    match msg.message.content.decode_content() {
242                        Some(DecodedContent::Action(action)) => action,
243                        _ => continue,
244                    }
245                }
246                RoomMessageBody::Private { .. } => {
247                    // Private action - use provided decrypted content
248                    let msg_id = msg.id();
249                    if let Some(plaintext) = decrypted_content.get(&msg_id) {
250                        match ActionContentV1::decode(plaintext) {
251                            Ok(action) => action,
252                            Err(_) => continue,
253                        }
254                    } else {
255                        // No decrypted content provided - skip this action
256                        continue;
257                    }
258                }
259            };
260
261            let target = &action.target;
262
263            match action.action_type {
264                ACTION_TYPE_EDIT => {
265                    // Only the original author can edit their message
266                    if let Some(&original_author) = message_authors.get(target) {
267                        if actor == original_author {
268                            // Don't allow editing deleted messages
269                            if !self.actions_state.deleted.contains(target) {
270                                if let Some(payload) = action.edit_payload() {
271                                    self.actions_state
272                                        .edited_content
273                                        .insert(target.clone(), payload.new_text);
274                                }
275                            }
276                        }
277                    }
278                }
279                ACTION_TYPE_DELETE => {
280                    // Only the original author can delete their message
281                    if let Some(&original_author) = message_authors.get(target) {
282                        if actor == original_author {
283                            self.actions_state.deleted.insert(target.clone());
284                            // Also remove any edited content for deleted messages
285                            self.actions_state.edited_content.remove(target);
286                        }
287                    }
288                }
289                ACTION_TYPE_REACTION => {
290                    // Anyone can add reactions to non-deleted messages
291                    if message_authors.contains_key(target)
292                        && !self.actions_state.deleted.contains(target)
293                    {
294                        if let Some(payload) = action.reaction_payload() {
295                            let reactions = self
296                                .actions_state
297                                .reactions
298                                .entry(target.clone())
299                                .or_default();
300                            let reactors = reactions.entry(payload.emoji).or_default();
301                            // Idempotent: only add if not already present
302                            if !reactors.contains(&actor) {
303                                reactors.push(actor);
304                            }
305                        }
306                    }
307                }
308                ACTION_TYPE_REMOVE_REACTION => {
309                    // Users can only remove their own reactions
310                    if let Some(payload) = action.reaction_payload() {
311                        if let Some(reactions) = self.actions_state.reactions.get_mut(target) {
312                            if let Some(reactors) = reactions.get_mut(&payload.emoji) {
313                                reactors.retain(|r| r != &actor);
314                                // Clean up empty entries
315                                if reactors.is_empty() {
316                                    reactions.remove(&payload.emoji);
317                                }
318                            }
319                            if reactions.is_empty() {
320                                self.actions_state.reactions.remove(target);
321                            }
322                        }
323                    }
324                }
325                _ => {
326                    // Unknown action type - ignore for forward compatibility
327                }
328            }
329        }
330    }
331
332    /// Check if a message has been edited
333    pub fn is_edited(&self, message_id: &MessageId) -> bool {
334        self.actions_state.edited_content.contains_key(message_id)
335    }
336
337    /// Check if a message has been deleted
338    pub fn is_deleted(&self, message_id: &MessageId) -> bool {
339        self.actions_state.deleted.contains(message_id)
340    }
341
342    /// Get the effective text content for a message (edited content if edited, original otherwise)
343    /// Returns the text content as a string, or None if the message is encrypted/undecodable
344    pub fn effective_text(&self, message: &AuthorizedMessageV1) -> Option<String> {
345        let id = message.id();
346        // Check if there's edited content first
347        if let Some(edited_text) = self.actions_state.edited_content.get(&id) {
348            return Some(edited_text.clone());
349        }
350        // Otherwise return the original content's text
351        message.message.content.as_public_string()
352    }
353
354    /// Get reactions for a message
355    pub fn reactions(&self, message_id: &MessageId) -> Option<&HashMap<String, Vec<MemberId>>> {
356        self.actions_state.reactions.get(message_id)
357    }
358
359    /// Get all non-deleted, non-action messages for display
360    pub fn display_messages(&self) -> impl Iterator<Item = &AuthorizedMessageV1> {
361        self.messages.iter().filter(|m| {
362            !m.message.content.is_action() && !self.actions_state.deleted.contains(&m.id())
363        })
364    }
365}
366
367/// Message body that can be either public or private (encrypted).
368///
369/// Content is opaque to the contract - interpretation happens client-side.
370/// This design enables adding new content types without contract redeployment.
371///
372/// # Content Types
373/// - `content_type = 1`: Text message (TextContentV1)
374/// - `content_type = 2`: Action on another message (ActionContentV1)
375/// - `content_type = 3`: Reply to another message (ReplyContentV1)
376/// - `content_type = 4`: Room event like join/leave (EventContentV1)
377///   - Allowed as Public even in private rooms (contains no sensitive content)
378///   - Old clients display as "[Unsupported message type 4.1 - please upgrade]"
379/// - Future types can be added without contract changes
380///
381/// # Extensibility
382/// - New content types: Just use a new content_type number
383/// - New action types: Just use a new action_type number within ActionContentV1
384/// - New fields: Add to content structs (old clients ignore unknown fields)
385/// - Breaking changes: Bump content_version
386#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
387pub enum RoomMessageBody {
388    /// Public (unencrypted) message
389    Public {
390        /// Content type identifier (see content module for constants)
391        content_type: u32,
392        /// Version of the content format
393        content_version: u32,
394        /// CBOR-encoded content bytes
395        data: Vec<u8>,
396    },
397    /// Private (encrypted) message
398    Private {
399        /// Content type identifier (see content module for constants)
400        content_type: u32,
401        /// Version of the content format
402        content_version: u32,
403        /// Encrypted CBOR-encoded content
404        ciphertext: Vec<u8>,
405        /// Nonce used for encryption
406        nonce: [u8; 12],
407        /// Version of the room secret used for encryption
408        secret_version: SecretVersion,
409    },
410}
411
412impl RoomMessageBody {
413    /// Create a new public text message
414    pub fn public(text: String) -> Self {
415        use crate::room_state::content::{TextContentV1, CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
416        let content = TextContentV1::new(text);
417        Self::Public {
418            content_type: CONTENT_TYPE_TEXT,
419            content_version: TEXT_CONTENT_VERSION,
420            data: content.encode(),
421        }
422    }
423
424    /// Create a join event message
425    pub fn join_event() -> Self {
426        use crate::room_state::content::{
427            EventContentV1, CONTENT_TYPE_EVENT, EVENT_CONTENT_VERSION,
428        };
429        let content = EventContentV1::join();
430        Self::Public {
431            content_type: CONTENT_TYPE_EVENT,
432            content_version: EVENT_CONTENT_VERSION,
433            data: content.encode(),
434        }
435    }
436
437    /// Create a new public message with raw content
438    pub fn public_raw(content_type: u32, content_version: u32, data: Vec<u8>) -> Self {
439        Self::Public {
440            content_type,
441            content_version,
442            data,
443        }
444    }
445
446    /// Create a new private message
447    pub fn private(
448        content_type: u32,
449        content_version: u32,
450        ciphertext: Vec<u8>,
451        nonce: [u8; 12],
452        secret_version: SecretVersion,
453    ) -> Self {
454        Self::Private {
455            content_type,
456            content_version,
457            ciphertext,
458            nonce,
459            secret_version,
460        }
461    }
462
463    /// Create a private text message (convenience method)
464    pub fn private_text(
465        ciphertext: Vec<u8>,
466        nonce: [u8; 12],
467        secret_version: SecretVersion,
468    ) -> Self {
469        use crate::room_state::content::{CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
470        Self::Private {
471            content_type: CONTENT_TYPE_TEXT,
472            content_version: TEXT_CONTENT_VERSION,
473            ciphertext,
474            nonce,
475            secret_version,
476        }
477    }
478
479    /// Create an edit action (public)
480    pub fn edit(target: MessageId, new_text: String) -> Self {
481        use crate::room_state::content::{
482            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
483        };
484        let action = ActionContentV1::edit(target, new_text);
485        Self::Public {
486            content_type: CONTENT_TYPE_ACTION,
487            content_version: ACTION_CONTENT_VERSION,
488            data: action.encode(),
489        }
490    }
491
492    /// Create a delete action (public)
493    pub fn delete(target: MessageId) -> Self {
494        use crate::room_state::content::{
495            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
496        };
497        let action = ActionContentV1::delete(target);
498        Self::Public {
499            content_type: CONTENT_TYPE_ACTION,
500            content_version: ACTION_CONTENT_VERSION,
501            data: action.encode(),
502        }
503    }
504
505    /// Create a reaction action (public)
506    pub fn reaction(target: MessageId, emoji: String) -> Self {
507        use crate::room_state::content::{
508            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
509        };
510        let action = ActionContentV1::reaction(target, emoji);
511        Self::Public {
512            content_type: CONTENT_TYPE_ACTION,
513            content_version: ACTION_CONTENT_VERSION,
514            data: action.encode(),
515        }
516    }
517
518    /// Create a remove reaction action (public)
519    pub fn remove_reaction(target: MessageId, emoji: String) -> Self {
520        use crate::room_state::content::{
521            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
522        };
523        let action = ActionContentV1::remove_reaction(target, emoji);
524        Self::Public {
525            content_type: CONTENT_TYPE_ACTION,
526            content_version: ACTION_CONTENT_VERSION,
527            data: action.encode(),
528        }
529    }
530
531    /// Create a public reply message
532    pub fn reply(
533        text: String,
534        target_message_id: MessageId,
535        target_author_name: String,
536        target_content_preview: String,
537    ) -> Self {
538        use crate::room_state::content::{
539            ReplyContentV1, CONTENT_TYPE_REPLY, REPLY_CONTENT_VERSION,
540        };
541        let reply = ReplyContentV1::new(
542            text,
543            target_message_id,
544            target_author_name,
545            target_content_preview,
546        );
547        Self::Public {
548            content_type: CONTENT_TYPE_REPLY,
549            content_version: REPLY_CONTENT_VERSION,
550            data: reply.encode(),
551        }
552    }
553
554    /// Create a private action message (encrypted)
555    ///
556    /// Use this for any action (edit, delete, reaction, remove_reaction) in a private room.
557    /// The caller should:
558    /// 1. Create the ActionContentV1 (e.g., `ActionContentV1::edit(target, new_text)`)
559    /// 2. Encode it: `action.encode()`
560    /// 3. Encrypt the bytes with the room secret
561    /// 4. Pass the ciphertext here
562    pub fn private_action(
563        ciphertext: Vec<u8>,
564        nonce: [u8; 12],
565        secret_version: SecretVersion,
566    ) -> Self {
567        use crate::room_state::content::{ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION};
568        Self::Private {
569            content_type: CONTENT_TYPE_ACTION,
570            content_version: ACTION_CONTENT_VERSION,
571            ciphertext,
572            nonce,
573            secret_version,
574        }
575    }
576
577    /// Check if this is a public message
578    pub fn is_public(&self) -> bool {
579        matches!(self, Self::Public { .. })
580    }
581
582    /// Check if this is a private message
583    pub fn is_private(&self) -> bool {
584        matches!(self, Self::Private { .. })
585    }
586
587    /// Get the content type
588    pub fn content_type(&self) -> u32 {
589        match self {
590            Self::Public { content_type, .. } | Self::Private { content_type, .. } => *content_type,
591        }
592    }
593
594    /// Get the content version
595    pub fn content_version(&self) -> u32 {
596        match self {
597            Self::Public {
598                content_version, ..
599            }
600            | Self::Private {
601                content_version, ..
602            } => *content_version,
603        }
604    }
605
606    /// Check if this is an action message (content_type = ACTION)
607    pub fn is_action(&self) -> bool {
608        use crate::room_state::content::CONTENT_TYPE_ACTION;
609        self.content_type() == CONTENT_TYPE_ACTION
610    }
611
612    /// Check if this is an event message (content_type = EVENT)
613    pub fn is_event(&self) -> bool {
614        use crate::room_state::content::CONTENT_TYPE_EVENT;
615        self.content_type() == CONTENT_TYPE_EVENT
616    }
617
618    /// Decode the content (for public messages only)
619    /// Returns None for private messages - decrypt first
620    pub fn decode_content(&self) -> Option<crate::room_state::content::DecodedContent> {
621        use crate::room_state::content::{
622            ActionContentV1, DecodedContent, EventContentV1, ReplyContentV1, TextContentV1,
623            CONTENT_TYPE_ACTION, CONTENT_TYPE_EVENT, CONTENT_TYPE_REPLY, CONTENT_TYPE_TEXT,
624        };
625        match self {
626            Self::Public {
627                content_type,
628                content_version,
629                data,
630            } => match *content_type {
631                CONTENT_TYPE_TEXT => TextContentV1::decode(data).ok().map(DecodedContent::Text),
632                CONTENT_TYPE_ACTION => ActionContentV1::decode(data)
633                    .ok()
634                    .map(DecodedContent::Action),
635                CONTENT_TYPE_REPLY => ReplyContentV1::decode(data).ok().map(DecodedContent::Reply),
636                CONTENT_TYPE_EVENT => EventContentV1::decode(data).ok().map(DecodedContent::Event),
637                _ => Some(DecodedContent::Unknown {
638                    content_type: *content_type,
639                    content_version: *content_version,
640                }),
641            },
642            Self::Private { .. } => None,
643        }
644    }
645
646    /// Get the target message ID if this is an action
647    pub fn target_id(&self) -> Option<MessageId> {
648        use crate::room_state::content::{ActionContentV1, CONTENT_TYPE_ACTION};
649        match self {
650            Self::Public {
651                content_type, data, ..
652            } if *content_type == CONTENT_TYPE_ACTION => {
653                ActionContentV1::decode(data).ok().map(|a| a.target)
654            }
655            _ => None,
656        }
657    }
658
659    /// Get the content length for validation (contract uses this for size limits)
660    pub fn content_len(&self) -> usize {
661        match self {
662            Self::Public { data, .. } => data.len(),
663            Self::Private { ciphertext, .. } => ciphertext.len(),
664        }
665    }
666
667    /// Get the secret version (if private)
668    pub fn secret_version(&self) -> Option<SecretVersion> {
669        match self {
670            Self::Public { .. } => None,
671            Self::Private { secret_version, .. } => Some(*secret_version),
672        }
673    }
674
675    /// Get a string representation for display purposes
676    pub fn to_string_lossy(&self) -> String {
677        match self {
678            Self::Public { .. } => {
679                if let Some(decoded) = self.decode_content() {
680                    decoded.to_display_string()
681                } else {
682                    "[Failed to decode message]".to_string()
683                }
684            }
685            Self::Private {
686                ciphertext,
687                secret_version,
688                ..
689            } => {
690                format!(
691                    "[Encrypted message: {} bytes, v{}]",
692                    ciphertext.len(),
693                    secret_version
694                )
695            }
696        }
697    }
698
699    /// Try to get the public plaintext, returns None if private or not a text message
700    pub fn as_public_string(&self) -> Option<String> {
701        self.decode_content()
702            .and_then(|c| c.as_text().map(|s| s.to_string()))
703    }
704}
705
706impl fmt::Display for RoomMessageBody {
707    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708        write!(f, "{}", self.to_string_lossy())
709    }
710}
711
712#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
713pub struct MessageV1 {
714    pub room_owner: MemberId,
715    pub author: MemberId,
716    pub time: SystemTime,
717    pub content: RoomMessageBody,
718}
719
720impl Default for MessageV1 {
721    fn default() -> Self {
722        Self {
723            room_owner: MemberId(FastHash(0)),
724            author: MemberId(FastHash(0)),
725            time: SystemTime::UNIX_EPOCH,
726            content: RoomMessageBody::public(String::new()),
727        }
728    }
729}
730
731#[derive(Clone, PartialEq, Serialize, Deserialize)]
732pub struct AuthorizedMessageV1 {
733    pub message: MessageV1,
734    pub signature: Signature,
735}
736
737impl fmt::Debug for AuthorizedMessageV1 {
738    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739        f.debug_struct("AuthorizedMessage")
740            .field("message", &self.message)
741            .field(
742                "signature",
743                &format_args!("{}", truncated_base64(self.signature.to_bytes())),
744            )
745            .finish()
746    }
747}
748
749#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Clone, Debug, Ord, PartialOrd)]
750pub struct MessageId(pub FastHash);
751
752impl fmt::Display for MessageId {
753    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754        write!(f, "{:?}", self.0)
755    }
756}
757
758impl AuthorizedMessageV1 {
759    pub fn new(message: MessageV1, signing_key: &SigningKey) -> Self {
760        Self {
761            message: message.clone(),
762            signature: sign_struct(&message, signing_key),
763        }
764    }
765
766    /// Create an AuthorizedMessageV1 with a pre-computed signature.
767    /// Use this when signing is done externally (e.g., via delegate).
768    pub fn with_signature(message: MessageV1, signature: Signature) -> Self {
769        Self { message, signature }
770    }
771
772    pub fn validate(
773        &self,
774        verifying_key: &VerifyingKey,
775    ) -> Result<(), ed25519_dalek::SignatureError> {
776        verify_struct(&self.message, &self.signature, verifying_key)
777    }
778
779    pub fn id(&self) -> MessageId {
780        MessageId(fast_hash(&self.signature.to_bytes()))
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use ed25519_dalek::{Signer, SigningKey};
788    use rand::rngs::OsRng;
789    use std::time::Duration;
790
791    fn create_test_message(owner_id: MemberId, author_id: MemberId) -> MessageV1 {
792        MessageV1 {
793            room_owner: owner_id,
794            author: author_id,
795            time: SystemTime::now(),
796            content: RoomMessageBody::public("Test message".to_string()),
797        }
798    }
799
800    #[test]
801    fn test_messages_v1_default() {
802        let default_messages = MessagesV1::default();
803        assert!(default_messages.messages.is_empty());
804    }
805
806    #[test]
807    fn test_authorized_message_v1_debug() {
808        let signing_key = SigningKey::generate(&mut OsRng);
809        let owner_id = MemberId(FastHash(0));
810        let author_id = MemberId(FastHash(1));
811
812        let message = create_test_message(owner_id, author_id);
813        let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
814
815        let debug_output = format!("{:?}", authorized_message);
816        assert!(debug_output.contains("AuthorizedMessage"));
817        assert!(debug_output.contains("message"));
818        assert!(debug_output.contains("signature"));
819    }
820
821    #[test]
822    fn test_authorized_message_new_and_validate() {
823        let signing_key = SigningKey::generate(&mut OsRng);
824        let verifying_key = signing_key.verifying_key();
825        let owner_id = MemberId(FastHash(0));
826        let author_id = MemberId(FastHash(1));
827
828        let message = create_test_message(owner_id, author_id);
829        let authorized_message = AuthorizedMessageV1::new(message.clone(), &signing_key);
830
831        assert_eq!(authorized_message.message, message);
832        assert!(authorized_message.validate(&verifying_key).is_ok());
833
834        // Test with wrong key
835        let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
836        assert!(authorized_message.validate(&wrong_key).is_err());
837
838        // Test with tampered message
839        let mut tampered_message = authorized_message.clone();
840        tampered_message.message.content = RoomMessageBody::public("Tampered content".to_string());
841        assert!(tampered_message.validate(&verifying_key).is_err());
842    }
843
844    #[test]
845    fn test_message_id() {
846        let signing_key = SigningKey::generate(&mut OsRng);
847        let owner_id = MemberId(FastHash(0));
848        let author_id = MemberId(FastHash(1));
849
850        let message = create_test_message(owner_id, author_id);
851        let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
852
853        let id1 = authorized_message.id();
854        let id2 = authorized_message.id();
855
856        assert_eq!(id1, id2);
857
858        // Test that different messages have different IDs
859        let message2 = create_test_message(owner_id, author_id);
860        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
861        assert_ne!(authorized_message.id(), authorized_message2.id());
862    }
863
864    #[test]
865    fn test_messages_verify() {
866        // Generate a new signing key and its corresponding verifying key for the owner
867        let owner_signing_key = SigningKey::generate(&mut OsRng);
868        let owner_verifying_key = owner_signing_key.verifying_key();
869        let owner_id = MemberId::from(&owner_verifying_key);
870
871        // Generate a signing key for the author
872        let author_signing_key = SigningKey::generate(&mut OsRng);
873        let author_verifying_key = author_signing_key.verifying_key();
874        let author_id = MemberId::from(&author_verifying_key);
875
876        // Create a test message and authorize it with the author's signing key
877        let message = create_test_message(owner_id, author_id);
878        let authorized_message = AuthorizedMessageV1::new(message, &author_signing_key);
879
880        // Create a Messages struct with the authorized message
881        let messages = MessagesV1 {
882            messages: vec![authorized_message],
883            ..Default::default()
884        };
885
886        // Set up a parent room_state (ChatRoomState) with the author as a member
887        let mut parent_state = ChatRoomStateV1::default();
888        let author_member = crate::room_state::member::Member {
889            owner_member_id: owner_id,
890            invited_by: owner_id,
891            member_vk: author_verifying_key,
892        };
893        let authorized_author =
894            crate::room_state::member::AuthorizedMember::new(author_member, &owner_signing_key);
895        parent_state.members.members = vec![authorized_author];
896
897        // Set up parameters for verification
898        let parameters = ChatRoomParametersV1 {
899            owner: owner_verifying_key,
900        };
901
902        // Verify that a valid message passes verification
903        assert!(
904            messages.verify(&parent_state, &parameters).is_ok(),
905            "Valid messages should pass verification: {:?}",
906            messages.verify(&parent_state, &parameters)
907        );
908
909        // Test with invalid signature
910        let mut invalid_messages = messages.clone();
911        invalid_messages.messages[0].signature = Signature::from_bytes(&[0; 64]); // Replace with an invalid signature
912        assert!(
913            invalid_messages.verify(&parent_state, &parameters).is_err(),
914            "Messages with invalid signature should fail verification"
915        );
916
917        // Test with non-existent author
918        let non_existent_author_id =
919            MemberId::from(&SigningKey::generate(&mut OsRng).verifying_key());
920        let invalid_message = create_test_message(owner_id, non_existent_author_id);
921        let invalid_authorized_message =
922            AuthorizedMessageV1::new(invalid_message, &author_signing_key);
923        let invalid_messages = MessagesV1 {
924            messages: vec![invalid_authorized_message],
925            ..Default::default()
926        };
927        assert!(
928            invalid_messages.verify(&parent_state, &parameters).is_err(),
929            "Messages with non-existent author should fail verification"
930        );
931    }
932
933    #[test]
934    fn test_messages_summarize() {
935        let signing_key = SigningKey::generate(&mut OsRng);
936        let owner_id = MemberId(FastHash(0));
937        let author_id = MemberId(FastHash(1));
938
939        let message1 = create_test_message(owner_id, author_id);
940        let message2 = create_test_message(owner_id, author_id);
941
942        let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
943        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
944
945        let messages = MessagesV1 {
946            messages: vec![authorized_message1.clone(), authorized_message2.clone()],
947            ..Default::default()
948        };
949
950        let parent_state = ChatRoomStateV1::default();
951        let parameters = ChatRoomParametersV1 {
952            owner: signing_key.verifying_key(),
953        };
954
955        let summary = messages.summarize(&parent_state, &parameters);
956        assert_eq!(summary.len(), 2);
957        assert_eq!(summary[0], authorized_message1.id());
958        assert_eq!(summary[1], authorized_message2.id());
959
960        // Test empty messages
961        let empty_messages = MessagesV1::default();
962        let empty_summary = empty_messages.summarize(&parent_state, &parameters);
963        assert!(empty_summary.is_empty());
964    }
965
966    #[test]
967    fn test_messages_delta() {
968        let signing_key = SigningKey::generate(&mut OsRng);
969        let owner_id = MemberId(FastHash(0));
970        let author_id = MemberId(FastHash(1));
971
972        // Use distinct timestamps to ensure unique message IDs
973        let base = SystemTime::now();
974        let message1 = MessageV1 {
975            room_owner: owner_id,
976            author: author_id,
977            time: base,
978            content: RoomMessageBody::public("Message 1".to_string()),
979        };
980        let message2 = MessageV1 {
981            room_owner: owner_id,
982            author: author_id,
983            time: base + Duration::from_millis(1),
984            content: RoomMessageBody::public("Message 2".to_string()),
985        };
986        let message3 = MessageV1 {
987            room_owner: owner_id,
988            author: author_id,
989            time: base + Duration::from_millis(2),
990            content: RoomMessageBody::public("Message 3".to_string()),
991        };
992
993        let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
994        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
995        let authorized_message3 = AuthorizedMessageV1::new(message3, &signing_key);
996
997        let messages = MessagesV1 {
998            messages: vec![
999                authorized_message1.clone(),
1000                authorized_message2.clone(),
1001                authorized_message3.clone(),
1002            ],
1003            ..Default::default()
1004        };
1005
1006        let parent_state = ChatRoomStateV1::default();
1007        let parameters = ChatRoomParametersV1 {
1008            owner: signing_key.verifying_key(),
1009        };
1010
1011        // Test with partial old summary
1012        let old_summary = vec![authorized_message1.id(), authorized_message2.id()];
1013        let delta = messages
1014            .delta(&parent_state, &parameters, &old_summary)
1015            .unwrap();
1016        assert_eq!(delta.len(), 1);
1017        assert_eq!(delta[0], authorized_message3);
1018
1019        // Test with empty old summary
1020        let empty_summary: Vec<MessageId> = vec![];
1021        let full_delta = messages
1022            .delta(&parent_state, &parameters, &empty_summary)
1023            .unwrap();
1024        assert_eq!(full_delta.len(), 3);
1025        assert_eq!(full_delta, messages.messages);
1026
1027        // Test with full old summary (no changes)
1028        let full_summary = vec![
1029            authorized_message1.id(),
1030            authorized_message2.id(),
1031            authorized_message3.id(),
1032        ];
1033        let no_delta = messages.delta(&parent_state, &parameters, &full_summary);
1034        assert!(no_delta.is_none());
1035    }
1036
1037    #[test]
1038    fn test_messages_apply_delta() {
1039        // Setup
1040        let owner_signing_key = SigningKey::generate(&mut OsRng);
1041        let owner_verifying_key = owner_signing_key.verifying_key();
1042        let owner_id = MemberId::from(&owner_verifying_key);
1043
1044        let author_signing_key = SigningKey::generate(&mut OsRng);
1045        let author_verifying_key = author_signing_key.verifying_key();
1046        let author_id = MemberId::from(&author_verifying_key);
1047
1048        let mut parent_state = ChatRoomStateV1::default();
1049        parent_state.configuration.configuration.max_recent_messages = 3;
1050        parent_state.configuration.configuration.max_message_size = 100;
1051        parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
1052            member: crate::room_state::member::Member {
1053                owner_member_id: owner_id,
1054                invited_by: owner_id,
1055                member_vk: author_verifying_key,
1056            },
1057            signature: owner_signing_key.try_sign(&[0; 32]).unwrap(),
1058        }];
1059
1060        let parameters = ChatRoomParametersV1 {
1061            owner: owner_verifying_key,
1062        };
1063
1064        // Create messages
1065        let create_message = |time: SystemTime| {
1066            let message = MessageV1 {
1067                room_owner: owner_id,
1068                author: author_id,
1069                time,
1070                content: RoomMessageBody::public("Test message".to_string()),
1071            };
1072            AuthorizedMessageV1::new(message, &author_signing_key)
1073        };
1074
1075        let now = SystemTime::now();
1076        let message1 = create_message(now - Duration::from_secs(3));
1077        let message2 = create_message(now - Duration::from_secs(2));
1078        let message3 = create_message(now - Duration::from_secs(1));
1079        let message4 = create_message(now);
1080
1081        // Initial room_state with 2 messages
1082        let mut messages = MessagesV1 {
1083            messages: vec![message1.clone(), message2.clone()],
1084            ..Default::default()
1085        };
1086
1087        // Apply delta with 2 new messages
1088        let delta = vec![message3.clone(), message4.clone()];
1089        assert!(messages
1090            .apply_delta(&parent_state, &parameters, &Some(delta))
1091            .is_ok());
1092
1093        // Check results
1094        assert_eq!(
1095            messages.messages.len(),
1096            3,
1097            "Should have 3 messages after applying delta"
1098        );
1099        assert!(
1100            !messages.messages.contains(&message1),
1101            "Oldest message should be removed"
1102        );
1103        assert!(
1104            messages.messages.contains(&message2),
1105            "Second oldest message should be retained"
1106        );
1107        assert!(
1108            messages.messages.contains(&message3),
1109            "New message should be added"
1110        );
1111        assert!(
1112            messages.messages.contains(&message4),
1113            "Newest message should be added"
1114        );
1115
1116        // Apply delta with an older message
1117        let old_message = create_message(now - Duration::from_secs(4));
1118        let delta = vec![old_message.clone()];
1119        assert!(messages
1120            .apply_delta(&parent_state, &parameters, &Some(delta))
1121            .is_ok());
1122
1123        // Check results
1124        assert_eq!(messages.messages.len(), 3, "Should still have 3 messages");
1125        assert!(
1126            !messages.messages.contains(&old_message),
1127            "Older message should not be added"
1128        );
1129        assert!(
1130            messages.messages.contains(&message2),
1131            "Message2 should be retained"
1132        );
1133        assert!(
1134            messages.messages.contains(&message3),
1135            "Message3 should be retained"
1136        );
1137        assert!(
1138            messages.messages.contains(&message4),
1139            "Newest message should be retained"
1140        );
1141    }
1142
1143    #[test]
1144    fn test_message_author_preservation_across_users() {
1145        // Create two users
1146        let user1_sk = SigningKey::generate(&mut OsRng);
1147        let user1_vk = user1_sk.verifying_key();
1148        let user1_id = MemberId::from(&user1_vk);
1149
1150        let user2_sk = SigningKey::generate(&mut OsRng);
1151        let user2_vk = user2_sk.verifying_key();
1152        let user2_id = MemberId::from(&user2_vk);
1153
1154        let owner_sk = SigningKey::generate(&mut OsRng);
1155        let owner_vk = owner_sk.verifying_key();
1156        let owner_id = MemberId::from(&owner_vk);
1157
1158        println!("User1 ID: {}", user1_id);
1159        println!("User2 ID: {}", user2_id);
1160        println!("Owner ID: {}", owner_id);
1161
1162        // Create messages from different users
1163        let msg1 = MessageV1 {
1164            room_owner: owner_id,
1165            author: user1_id,
1166            content: RoomMessageBody::public("Message from user1".to_string()),
1167            time: SystemTime::now(),
1168        };
1169
1170        let msg2 = MessageV1 {
1171            room_owner: owner_id,
1172            author: user2_id,
1173            content: RoomMessageBody::public("Message from user2".to_string()),
1174            time: SystemTime::now() + Duration::from_secs(1),
1175        };
1176
1177        let auth_msg1 = AuthorizedMessageV1::new(msg1.clone(), &user1_sk);
1178        let auth_msg2 = AuthorizedMessageV1::new(msg2.clone(), &user2_sk);
1179
1180        // Create a messages state with both messages
1181        let messages = MessagesV1 {
1182            messages: vec![auth_msg1.clone(), auth_msg2.clone()],
1183            ..Default::default()
1184        };
1185
1186        // Verify authors are preserved
1187        assert_eq!(messages.messages.len(), 2);
1188
1189        let stored_msg1 = &messages.messages[0];
1190        let stored_msg2 = &messages.messages[1];
1191
1192        assert_eq!(
1193            stored_msg1.message.author, user1_id,
1194            "Message 1 author should be user1, but got {}",
1195            stored_msg1.message.author
1196        );
1197        assert_eq!(
1198            stored_msg2.message.author, user2_id,
1199            "Message 2 author should be user2, but got {}",
1200            stored_msg2.message.author
1201        );
1202
1203        // Test that author IDs are different
1204        assert_ne!(user1_id, user2_id, "User IDs should be different");
1205
1206        // Test Display implementation
1207        let user1_id_str = user1_id.to_string();
1208        let user2_id_str = user2_id.to_string();
1209
1210        println!("User1 ID string: {}", user1_id_str);
1211        println!("User2 ID string: {}", user2_id_str);
1212
1213        assert_ne!(
1214            user1_id_str, user2_id_str,
1215            "User ID strings should be different"
1216        );
1217    }
1218
1219    #[test]
1220    fn test_edit_action() {
1221        let signing_key = SigningKey::generate(&mut OsRng);
1222        let verifying_key = signing_key.verifying_key();
1223        let owner_id = MemberId::from(&verifying_key);
1224        let author_id = owner_id;
1225
1226        // Create original message
1227        let original_msg = MessageV1 {
1228            room_owner: owner_id,
1229            author: author_id,
1230            time: SystemTime::now(),
1231            content: RoomMessageBody::public("Original content".to_string()),
1232        };
1233        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1234        let original_id = auth_original.id();
1235
1236        // Create edit action
1237        let edit_msg = MessageV1 {
1238            room_owner: owner_id,
1239            author: author_id,
1240            time: SystemTime::now() + Duration::from_secs(1),
1241            content: RoomMessageBody::edit(original_id.clone(), "Edited content".to_string()),
1242        };
1243        let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
1244
1245        // Create messages state and rebuild
1246        let mut messages = MessagesV1 {
1247            messages: vec![auth_original.clone(), auth_edit],
1248            ..Default::default()
1249        };
1250        messages.rebuild_actions_state();
1251
1252        // Verify edit was applied
1253        assert!(messages.is_edited(&original_id));
1254        let effective = messages.effective_text(&auth_original);
1255        assert_eq!(effective, Some("Edited content".to_string()));
1256
1257        // Verify display_messages still shows the original message
1258        let display: Vec<_> = messages.display_messages().collect();
1259        assert_eq!(display.len(), 1);
1260    }
1261
1262    #[test]
1263    fn test_edit_by_non_author_ignored() {
1264        let owner_sk = SigningKey::generate(&mut OsRng);
1265        let owner_vk = owner_sk.verifying_key();
1266        let owner_id = MemberId::from(&owner_vk);
1267
1268        let other_sk = SigningKey::generate(&mut OsRng);
1269        let other_id = MemberId::from(&other_sk.verifying_key());
1270
1271        // Create message by owner
1272        let original_msg = MessageV1 {
1273            room_owner: owner_id,
1274            author: owner_id,
1275            time: SystemTime::now(),
1276            content: RoomMessageBody::public("Original content".to_string()),
1277        };
1278        let auth_original = AuthorizedMessageV1::new(original_msg, &owner_sk);
1279        let original_id = auth_original.id();
1280
1281        // Create edit action by OTHER user (should be ignored)
1282        let edit_msg = MessageV1 {
1283            room_owner: owner_id,
1284            author: other_id,
1285            time: SystemTime::now() + Duration::from_secs(1),
1286            content: RoomMessageBody::edit(original_id.clone(), "Hacked content".to_string()),
1287        };
1288        let auth_edit = AuthorizedMessageV1::new(edit_msg, &other_sk);
1289
1290        let mut messages = MessagesV1 {
1291            messages: vec![auth_original.clone(), auth_edit],
1292            ..Default::default()
1293        };
1294        messages.rebuild_actions_state();
1295
1296        // Edit should be ignored - original content preserved
1297        assert!(!messages.is_edited(&original_id));
1298        let effective = messages.effective_text(&auth_original);
1299        assert_eq!(effective, Some("Original content".to_string()));
1300    }
1301
1302    #[test]
1303    fn test_delete_action() {
1304        let signing_key = SigningKey::generate(&mut OsRng);
1305        let verifying_key = signing_key.verifying_key();
1306        let owner_id = MemberId::from(&verifying_key);
1307
1308        // Create original message
1309        let original_msg = MessageV1 {
1310            room_owner: owner_id,
1311            author: owner_id,
1312            time: SystemTime::now(),
1313            content: RoomMessageBody::public("Will be deleted".to_string()),
1314        };
1315        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1316        let original_id = auth_original.id();
1317
1318        // Create delete action
1319        let delete_msg = MessageV1 {
1320            room_owner: owner_id,
1321            author: owner_id,
1322            time: SystemTime::now() + Duration::from_secs(1),
1323            content: RoomMessageBody::delete(original_id.clone()),
1324        };
1325        let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
1326
1327        let mut messages = MessagesV1 {
1328            messages: vec![auth_original, auth_delete],
1329            ..Default::default()
1330        };
1331        messages.rebuild_actions_state();
1332
1333        // Verify message is deleted
1334        assert!(messages.is_deleted(&original_id));
1335
1336        // Verify display_messages excludes deleted message
1337        let display: Vec<_> = messages.display_messages().collect();
1338        assert_eq!(display.len(), 0);
1339    }
1340
1341    #[test]
1342    fn test_reaction_action() {
1343        let user1_sk = SigningKey::generate(&mut OsRng);
1344        let user1_id = MemberId::from(&user1_sk.verifying_key());
1345
1346        let user2_sk = SigningKey::generate(&mut OsRng);
1347        let user2_id = MemberId::from(&user2_sk.verifying_key());
1348
1349        let owner_id = user1_id;
1350
1351        // Create original message
1352        let original_msg = MessageV1 {
1353            room_owner: owner_id,
1354            author: user1_id,
1355            time: SystemTime::now(),
1356            content: RoomMessageBody::public("React to me!".to_string()),
1357        };
1358        let auth_original = AuthorizedMessageV1::new(original_msg, &user1_sk);
1359        let original_id = auth_original.id();
1360
1361        // Create reaction from user2
1362        let reaction_msg = MessageV1 {
1363            room_owner: owner_id,
1364            author: user2_id,
1365            time: SystemTime::now() + Duration::from_secs(1),
1366            content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1367        };
1368        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user2_sk);
1369
1370        // Create another reaction from user1
1371        let reaction_msg2 = MessageV1 {
1372            room_owner: owner_id,
1373            author: user1_id,
1374            time: SystemTime::now() + Duration::from_secs(2),
1375            content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1376        };
1377        let auth_reaction2 = AuthorizedMessageV1::new(reaction_msg2, &user1_sk);
1378
1379        let mut messages = MessagesV1 {
1380            messages: vec![auth_original, auth_reaction, auth_reaction2],
1381            ..Default::default()
1382        };
1383        messages.rebuild_actions_state();
1384
1385        // Verify reactions
1386        let reactions = messages.reactions(&original_id).unwrap();
1387        let thumbs_up = reactions.get("👍").unwrap();
1388        assert_eq!(thumbs_up.len(), 2);
1389        assert!(thumbs_up.contains(&user1_id));
1390        assert!(thumbs_up.contains(&user2_id));
1391    }
1392
1393    #[test]
1394    fn test_remove_reaction_action() {
1395        let user_sk = SigningKey::generate(&mut OsRng);
1396        let user_id = MemberId::from(&user_sk.verifying_key());
1397        let owner_id = user_id;
1398
1399        // Create original message
1400        let original_msg = MessageV1 {
1401            room_owner: owner_id,
1402            author: user_id,
1403            time: SystemTime::now(),
1404            content: RoomMessageBody::public("Test message".to_string()),
1405        };
1406        let auth_original = AuthorizedMessageV1::new(original_msg, &user_sk);
1407        let original_id = auth_original.id();
1408
1409        // Add reaction
1410        let reaction_msg = MessageV1 {
1411            room_owner: owner_id,
1412            author: user_id,
1413            time: SystemTime::now() + Duration::from_secs(1),
1414            content: RoomMessageBody::reaction(original_id.clone(), "❤️".to_string()),
1415        };
1416        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user_sk);
1417
1418        // Remove reaction
1419        let remove_msg = MessageV1 {
1420            room_owner: owner_id,
1421            author: user_id,
1422            time: SystemTime::now() + Duration::from_secs(2),
1423            content: RoomMessageBody::remove_reaction(original_id.clone(), "❤️".to_string()),
1424        };
1425        let auth_remove = AuthorizedMessageV1::new(remove_msg, &user_sk);
1426
1427        let mut messages = MessagesV1 {
1428            messages: vec![auth_original, auth_reaction, auth_remove],
1429            ..Default::default()
1430        };
1431        messages.rebuild_actions_state();
1432
1433        // Verify reaction was removed
1434        assert!(messages.reactions(&original_id).is_none());
1435    }
1436
1437    #[test]
1438    fn test_action_on_deleted_message_ignored() {
1439        let signing_key = SigningKey::generate(&mut OsRng);
1440        let verifying_key = signing_key.verifying_key();
1441        let owner_id = MemberId::from(&verifying_key);
1442
1443        // Create original message
1444        let original_msg = MessageV1 {
1445            room_owner: owner_id,
1446            author: owner_id,
1447            time: SystemTime::now(),
1448            content: RoomMessageBody::public("Will be deleted".to_string()),
1449        };
1450        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1451        let original_id = auth_original.id();
1452
1453        // Delete it
1454        let delete_msg = MessageV1 {
1455            room_owner: owner_id,
1456            author: owner_id,
1457            time: SystemTime::now() + Duration::from_secs(1),
1458            content: RoomMessageBody::delete(original_id.clone()),
1459        };
1460        let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
1461
1462        // Try to edit deleted message (should be ignored)
1463        let edit_msg = MessageV1 {
1464            room_owner: owner_id,
1465            author: owner_id,
1466            time: SystemTime::now() + Duration::from_secs(2),
1467            content: RoomMessageBody::edit(original_id.clone(), "Too late!".to_string()),
1468        };
1469        let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
1470
1471        let mut messages = MessagesV1 {
1472            messages: vec![auth_original, auth_delete, auth_edit],
1473            ..Default::default()
1474        };
1475        messages.rebuild_actions_state();
1476
1477        // Message should be deleted, edit should be ignored
1478        assert!(messages.is_deleted(&original_id));
1479        assert!(!messages.is_edited(&original_id));
1480    }
1481
1482    #[test]
1483    fn test_display_messages_filters_actions() {
1484        let signing_key = SigningKey::generate(&mut OsRng);
1485        let verifying_key = signing_key.verifying_key();
1486        let owner_id = MemberId::from(&verifying_key);
1487
1488        // Create regular message
1489        let msg1 = MessageV1 {
1490            room_owner: owner_id,
1491            author: owner_id,
1492            time: SystemTime::now(),
1493            content: RoomMessageBody::public("Hello".to_string()),
1494        };
1495        let auth_msg1 = AuthorizedMessageV1::new(msg1, &signing_key);
1496        let msg1_id = auth_msg1.id();
1497
1498        // Create reaction (action message)
1499        let reaction_msg = MessageV1 {
1500            room_owner: owner_id,
1501            author: owner_id,
1502            time: SystemTime::now() + Duration::from_secs(1),
1503            content: RoomMessageBody::reaction(msg1_id, "👍".to_string()),
1504        };
1505        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &signing_key);
1506
1507        // Create another regular message
1508        let msg2 = MessageV1 {
1509            room_owner: owner_id,
1510            author: owner_id,
1511            time: SystemTime::now() + Duration::from_secs(2),
1512            content: RoomMessageBody::public("World".to_string()),
1513        };
1514        let auth_msg2 = AuthorizedMessageV1::new(msg2, &signing_key);
1515
1516        let mut messages = MessagesV1 {
1517            messages: vec![auth_msg1, auth_reaction, auth_msg2],
1518            ..Default::default()
1519        };
1520        messages.rebuild_actions_state();
1521
1522        // display_messages should only return regular messages, not actions
1523        let display: Vec<_> = messages.display_messages().collect();
1524        assert_eq!(display.len(), 2);
1525        assert_eq!(
1526            display[0].message.content.as_public_string(),
1527            Some("Hello".to_string())
1528        );
1529        assert_eq!(
1530            display[1].message.content.as_public_string(),
1531            Some("World".to_string())
1532        );
1533    }
1534}