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                            if *secret_version != current_secret_version {
119                                return Err(format!(
120                                    "Private message secret version {} does not match current version {}",
121                                    secret_version, current_secret_version
122                                ));
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 ALL public messages including actions
137                        // Privacy is a layer - everything in a private room must be encrypted
138                        if *privacy_mode == PrivacyMode::Private {
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/// - Future types can be added without contract changes
376///
377/// # Extensibility
378/// - New content types: Just use a new content_type number
379/// - New action types: Just use a new action_type number within ActionContentV1
380/// - New fields: Add to content structs (old clients ignore unknown fields)
381/// - Breaking changes: Bump content_version
382#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
383pub enum RoomMessageBody {
384    /// Public (unencrypted) message
385    Public {
386        /// Content type identifier (see content module for constants)
387        content_type: u32,
388        /// Version of the content format
389        content_version: u32,
390        /// CBOR-encoded content bytes
391        data: Vec<u8>,
392    },
393    /// Private (encrypted) message
394    Private {
395        /// Content type identifier (see content module for constants)
396        content_type: u32,
397        /// Version of the content format
398        content_version: u32,
399        /// Encrypted CBOR-encoded content
400        ciphertext: Vec<u8>,
401        /// Nonce used for encryption
402        nonce: [u8; 12],
403        /// Version of the room secret used for encryption
404        secret_version: SecretVersion,
405    },
406}
407
408impl RoomMessageBody {
409    /// Create a new public text message
410    pub fn public(text: String) -> Self {
411        use crate::room_state::content::{TextContentV1, CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
412        let content = TextContentV1::new(text);
413        Self::Public {
414            content_type: CONTENT_TYPE_TEXT,
415            content_version: TEXT_CONTENT_VERSION,
416            data: content.encode(),
417        }
418    }
419
420    /// Create a new public message with raw content
421    pub fn public_raw(content_type: u32, content_version: u32, data: Vec<u8>) -> Self {
422        Self::Public {
423            content_type,
424            content_version,
425            data,
426        }
427    }
428
429    /// Create a new private message
430    pub fn private(
431        content_type: u32,
432        content_version: u32,
433        ciphertext: Vec<u8>,
434        nonce: [u8; 12],
435        secret_version: SecretVersion,
436    ) -> Self {
437        Self::Private {
438            content_type,
439            content_version,
440            ciphertext,
441            nonce,
442            secret_version,
443        }
444    }
445
446    /// Create a private text message (convenience method)
447    pub fn private_text(
448        ciphertext: Vec<u8>,
449        nonce: [u8; 12],
450        secret_version: SecretVersion,
451    ) -> Self {
452        use crate::room_state::content::{CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
453        Self::Private {
454            content_type: CONTENT_TYPE_TEXT,
455            content_version: TEXT_CONTENT_VERSION,
456            ciphertext,
457            nonce,
458            secret_version,
459        }
460    }
461
462    /// Create an edit action (public)
463    pub fn edit(target: MessageId, new_text: String) -> Self {
464        use crate::room_state::content::{
465            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
466        };
467        let action = ActionContentV1::edit(target, new_text);
468        Self::Public {
469            content_type: CONTENT_TYPE_ACTION,
470            content_version: ACTION_CONTENT_VERSION,
471            data: action.encode(),
472        }
473    }
474
475    /// Create a delete action (public)
476    pub fn delete(target: MessageId) -> Self {
477        use crate::room_state::content::{
478            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
479        };
480        let action = ActionContentV1::delete(target);
481        Self::Public {
482            content_type: CONTENT_TYPE_ACTION,
483            content_version: ACTION_CONTENT_VERSION,
484            data: action.encode(),
485        }
486    }
487
488    /// Create a reaction action (public)
489    pub fn reaction(target: MessageId, emoji: String) -> Self {
490        use crate::room_state::content::{
491            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
492        };
493        let action = ActionContentV1::reaction(target, emoji);
494        Self::Public {
495            content_type: CONTENT_TYPE_ACTION,
496            content_version: ACTION_CONTENT_VERSION,
497            data: action.encode(),
498        }
499    }
500
501    /// Create a remove reaction action (public)
502    pub fn remove_reaction(target: MessageId, emoji: String) -> Self {
503        use crate::room_state::content::{
504            ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
505        };
506        let action = ActionContentV1::remove_reaction(target, emoji);
507        Self::Public {
508            content_type: CONTENT_TYPE_ACTION,
509            content_version: ACTION_CONTENT_VERSION,
510            data: action.encode(),
511        }
512    }
513
514    /// Create a public reply message
515    pub fn reply(
516        text: String,
517        target_message_id: MessageId,
518        target_author_name: String,
519        target_content_preview: String,
520    ) -> Self {
521        use crate::room_state::content::{
522            ReplyContentV1, CONTENT_TYPE_REPLY, REPLY_CONTENT_VERSION,
523        };
524        let reply = ReplyContentV1::new(
525            text,
526            target_message_id,
527            target_author_name,
528            target_content_preview,
529        );
530        Self::Public {
531            content_type: CONTENT_TYPE_REPLY,
532            content_version: REPLY_CONTENT_VERSION,
533            data: reply.encode(),
534        }
535    }
536
537    /// Create a private action message (encrypted)
538    ///
539    /// Use this for any action (edit, delete, reaction, remove_reaction) in a private room.
540    /// The caller should:
541    /// 1. Create the ActionContentV1 (e.g., `ActionContentV1::edit(target, new_text)`)
542    /// 2. Encode it: `action.encode()`
543    /// 3. Encrypt the bytes with the room secret
544    /// 4. Pass the ciphertext here
545    pub fn private_action(
546        ciphertext: Vec<u8>,
547        nonce: [u8; 12],
548        secret_version: SecretVersion,
549    ) -> Self {
550        use crate::room_state::content::{ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION};
551        Self::Private {
552            content_type: CONTENT_TYPE_ACTION,
553            content_version: ACTION_CONTENT_VERSION,
554            ciphertext,
555            nonce,
556            secret_version,
557        }
558    }
559
560    /// Check if this is a public message
561    pub fn is_public(&self) -> bool {
562        matches!(self, Self::Public { .. })
563    }
564
565    /// Check if this is a private message
566    pub fn is_private(&self) -> bool {
567        matches!(self, Self::Private { .. })
568    }
569
570    /// Get the content type
571    pub fn content_type(&self) -> u32 {
572        match self {
573            Self::Public { content_type, .. } | Self::Private { content_type, .. } => *content_type,
574        }
575    }
576
577    /// Get the content version
578    pub fn content_version(&self) -> u32 {
579        match self {
580            Self::Public {
581                content_version, ..
582            }
583            | Self::Private {
584                content_version, ..
585            } => *content_version,
586        }
587    }
588
589    /// Check if this is an action message (content_type = ACTION)
590    pub fn is_action(&self) -> bool {
591        use crate::room_state::content::CONTENT_TYPE_ACTION;
592        self.content_type() == CONTENT_TYPE_ACTION
593    }
594
595    /// Decode the content (for public messages only)
596    /// Returns None for private messages - decrypt first
597    pub fn decode_content(&self) -> Option<crate::room_state::content::DecodedContent> {
598        use crate::room_state::content::{
599            ActionContentV1, DecodedContent, ReplyContentV1, TextContentV1, CONTENT_TYPE_ACTION,
600            CONTENT_TYPE_REPLY, CONTENT_TYPE_TEXT,
601        };
602        match self {
603            Self::Public {
604                content_type,
605                content_version,
606                data,
607            } => match *content_type {
608                CONTENT_TYPE_TEXT => TextContentV1::decode(data).ok().map(DecodedContent::Text),
609                CONTENT_TYPE_ACTION => ActionContentV1::decode(data)
610                    .ok()
611                    .map(DecodedContent::Action),
612                CONTENT_TYPE_REPLY => ReplyContentV1::decode(data).ok().map(DecodedContent::Reply),
613                _ => Some(DecodedContent::Unknown {
614                    content_type: *content_type,
615                    content_version: *content_version,
616                }),
617            },
618            Self::Private { .. } => None,
619        }
620    }
621
622    /// Get the target message ID if this is an action
623    pub fn target_id(&self) -> Option<MessageId> {
624        use crate::room_state::content::{ActionContentV1, CONTENT_TYPE_ACTION};
625        match self {
626            Self::Public {
627                content_type, data, ..
628            } if *content_type == CONTENT_TYPE_ACTION => {
629                ActionContentV1::decode(data).ok().map(|a| a.target)
630            }
631            _ => None,
632        }
633    }
634
635    /// Get the content length for validation (contract uses this for size limits)
636    pub fn content_len(&self) -> usize {
637        match self {
638            Self::Public { data, .. } => data.len(),
639            Self::Private { ciphertext, .. } => ciphertext.len(),
640        }
641    }
642
643    /// Get the secret version (if private)
644    pub fn secret_version(&self) -> Option<SecretVersion> {
645        match self {
646            Self::Public { .. } => None,
647            Self::Private { secret_version, .. } => Some(*secret_version),
648        }
649    }
650
651    /// Get a string representation for display purposes
652    pub fn to_string_lossy(&self) -> String {
653        match self {
654            Self::Public { .. } => {
655                if let Some(decoded) = self.decode_content() {
656                    decoded.to_display_string()
657                } else {
658                    "[Failed to decode message]".to_string()
659                }
660            }
661            Self::Private {
662                ciphertext,
663                secret_version,
664                ..
665            } => {
666                format!(
667                    "[Encrypted message: {} bytes, v{}]",
668                    ciphertext.len(),
669                    secret_version
670                )
671            }
672        }
673    }
674
675    /// Try to get the public plaintext, returns None if private or not a text message
676    pub fn as_public_string(&self) -> Option<String> {
677        self.decode_content()
678            .and_then(|c| c.as_text().map(|s| s.to_string()))
679    }
680}
681
682impl fmt::Display for RoomMessageBody {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        write!(f, "{}", self.to_string_lossy())
685    }
686}
687
688#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
689pub struct MessageV1 {
690    pub room_owner: MemberId,
691    pub author: MemberId,
692    pub time: SystemTime,
693    pub content: RoomMessageBody,
694}
695
696impl Default for MessageV1 {
697    fn default() -> Self {
698        Self {
699            room_owner: MemberId(FastHash(0)),
700            author: MemberId(FastHash(0)),
701            time: SystemTime::UNIX_EPOCH,
702            content: RoomMessageBody::public(String::new()),
703        }
704    }
705}
706
707#[derive(Clone, PartialEq, Serialize, Deserialize)]
708pub struct AuthorizedMessageV1 {
709    pub message: MessageV1,
710    pub signature: Signature,
711}
712
713impl fmt::Debug for AuthorizedMessageV1 {
714    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715        f.debug_struct("AuthorizedMessage")
716            .field("message", &self.message)
717            .field(
718                "signature",
719                &format_args!("{}", truncated_base64(self.signature.to_bytes())),
720            )
721            .finish()
722    }
723}
724
725#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Clone, Debug, Ord, PartialOrd)]
726pub struct MessageId(pub FastHash);
727
728impl fmt::Display for MessageId {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        write!(f, "{:?}", self.0)
731    }
732}
733
734impl AuthorizedMessageV1 {
735    pub fn new(message: MessageV1, signing_key: &SigningKey) -> Self {
736        Self {
737            message: message.clone(),
738            signature: sign_struct(&message, signing_key),
739        }
740    }
741
742    /// Create an AuthorizedMessageV1 with a pre-computed signature.
743    /// Use this when signing is done externally (e.g., via delegate).
744    pub fn with_signature(message: MessageV1, signature: Signature) -> Self {
745        Self { message, signature }
746    }
747
748    pub fn validate(
749        &self,
750        verifying_key: &VerifyingKey,
751    ) -> Result<(), ed25519_dalek::SignatureError> {
752        verify_struct(&self.message, &self.signature, verifying_key)
753    }
754
755    pub fn id(&self) -> MessageId {
756        MessageId(fast_hash(&self.signature.to_bytes()))
757    }
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763    use ed25519_dalek::{Signer, SigningKey};
764    use rand::rngs::OsRng;
765    use std::time::Duration;
766
767    fn create_test_message(owner_id: MemberId, author_id: MemberId) -> MessageV1 {
768        MessageV1 {
769            room_owner: owner_id,
770            author: author_id,
771            time: SystemTime::now(),
772            content: RoomMessageBody::public("Test message".to_string()),
773        }
774    }
775
776    #[test]
777    fn test_messages_v1_default() {
778        let default_messages = MessagesV1::default();
779        assert!(default_messages.messages.is_empty());
780    }
781
782    #[test]
783    fn test_authorized_message_v1_debug() {
784        let signing_key = SigningKey::generate(&mut OsRng);
785        let owner_id = MemberId(FastHash(0));
786        let author_id = MemberId(FastHash(1));
787
788        let message = create_test_message(owner_id, author_id);
789        let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
790
791        let debug_output = format!("{:?}", authorized_message);
792        assert!(debug_output.contains("AuthorizedMessage"));
793        assert!(debug_output.contains("message"));
794        assert!(debug_output.contains("signature"));
795    }
796
797    #[test]
798    fn test_authorized_message_new_and_validate() {
799        let signing_key = SigningKey::generate(&mut OsRng);
800        let verifying_key = signing_key.verifying_key();
801        let owner_id = MemberId(FastHash(0));
802        let author_id = MemberId(FastHash(1));
803
804        let message = create_test_message(owner_id, author_id);
805        let authorized_message = AuthorizedMessageV1::new(message.clone(), &signing_key);
806
807        assert_eq!(authorized_message.message, message);
808        assert!(authorized_message.validate(&verifying_key).is_ok());
809
810        // Test with wrong key
811        let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
812        assert!(authorized_message.validate(&wrong_key).is_err());
813
814        // Test with tampered message
815        let mut tampered_message = authorized_message.clone();
816        tampered_message.message.content = RoomMessageBody::public("Tampered content".to_string());
817        assert!(tampered_message.validate(&verifying_key).is_err());
818    }
819
820    #[test]
821    fn test_message_id() {
822        let signing_key = SigningKey::generate(&mut OsRng);
823        let owner_id = MemberId(FastHash(0));
824        let author_id = MemberId(FastHash(1));
825
826        let message = create_test_message(owner_id, author_id);
827        let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
828
829        let id1 = authorized_message.id();
830        let id2 = authorized_message.id();
831
832        assert_eq!(id1, id2);
833
834        // Test that different messages have different IDs
835        let message2 = create_test_message(owner_id, author_id);
836        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
837        assert_ne!(authorized_message.id(), authorized_message2.id());
838    }
839
840    #[test]
841    fn test_messages_verify() {
842        // Generate a new signing key and its corresponding verifying key for the owner
843        let owner_signing_key = SigningKey::generate(&mut OsRng);
844        let owner_verifying_key = owner_signing_key.verifying_key();
845        let owner_id = MemberId::from(&owner_verifying_key);
846
847        // Generate a signing key for the author
848        let author_signing_key = SigningKey::generate(&mut OsRng);
849        let author_verifying_key = author_signing_key.verifying_key();
850        let author_id = MemberId::from(&author_verifying_key);
851
852        // Create a test message and authorize it with the author's signing key
853        let message = create_test_message(owner_id, author_id);
854        let authorized_message = AuthorizedMessageV1::new(message, &author_signing_key);
855
856        // Create a Messages struct with the authorized message
857        let messages = MessagesV1 {
858            messages: vec![authorized_message],
859            ..Default::default()
860        };
861
862        // Set up a parent room_state (ChatRoomState) with the author as a member
863        let mut parent_state = ChatRoomStateV1::default();
864        let author_member = crate::room_state::member::Member {
865            owner_member_id: owner_id,
866            invited_by: owner_id,
867            member_vk: author_verifying_key,
868        };
869        let authorized_author =
870            crate::room_state::member::AuthorizedMember::new(author_member, &owner_signing_key);
871        parent_state.members.members = vec![authorized_author];
872
873        // Set up parameters for verification
874        let parameters = ChatRoomParametersV1 {
875            owner: owner_verifying_key,
876        };
877
878        // Verify that a valid message passes verification
879        assert!(
880            messages.verify(&parent_state, &parameters).is_ok(),
881            "Valid messages should pass verification: {:?}",
882            messages.verify(&parent_state, &parameters)
883        );
884
885        // Test with invalid signature
886        let mut invalid_messages = messages.clone();
887        invalid_messages.messages[0].signature = Signature::from_bytes(&[0; 64]); // Replace with an invalid signature
888        assert!(
889            invalid_messages.verify(&parent_state, &parameters).is_err(),
890            "Messages with invalid signature should fail verification"
891        );
892
893        // Test with non-existent author
894        let non_existent_author_id =
895            MemberId::from(&SigningKey::generate(&mut OsRng).verifying_key());
896        let invalid_message = create_test_message(owner_id, non_existent_author_id);
897        let invalid_authorized_message =
898            AuthorizedMessageV1::new(invalid_message, &author_signing_key);
899        let invalid_messages = MessagesV1 {
900            messages: vec![invalid_authorized_message],
901            ..Default::default()
902        };
903        assert!(
904            invalid_messages.verify(&parent_state, &parameters).is_err(),
905            "Messages with non-existent author should fail verification"
906        );
907    }
908
909    #[test]
910    fn test_messages_summarize() {
911        let signing_key = SigningKey::generate(&mut OsRng);
912        let owner_id = MemberId(FastHash(0));
913        let author_id = MemberId(FastHash(1));
914
915        let message1 = create_test_message(owner_id, author_id);
916        let message2 = create_test_message(owner_id, author_id);
917
918        let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
919        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
920
921        let messages = MessagesV1 {
922            messages: vec![authorized_message1.clone(), authorized_message2.clone()],
923            ..Default::default()
924        };
925
926        let parent_state = ChatRoomStateV1::default();
927        let parameters = ChatRoomParametersV1 {
928            owner: signing_key.verifying_key(),
929        };
930
931        let summary = messages.summarize(&parent_state, &parameters);
932        assert_eq!(summary.len(), 2);
933        assert_eq!(summary[0], authorized_message1.id());
934        assert_eq!(summary[1], authorized_message2.id());
935
936        // Test empty messages
937        let empty_messages = MessagesV1::default();
938        let empty_summary = empty_messages.summarize(&parent_state, &parameters);
939        assert!(empty_summary.is_empty());
940    }
941
942    #[test]
943    fn test_messages_delta() {
944        let signing_key = SigningKey::generate(&mut OsRng);
945        let owner_id = MemberId(FastHash(0));
946        let author_id = MemberId(FastHash(1));
947
948        let message1 = create_test_message(owner_id, author_id);
949        let message2 = create_test_message(owner_id, author_id);
950        let message3 = create_test_message(owner_id, author_id);
951
952        let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
953        let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
954        let authorized_message3 = AuthorizedMessageV1::new(message3, &signing_key);
955
956        let messages = MessagesV1 {
957            messages: vec![
958                authorized_message1.clone(),
959                authorized_message2.clone(),
960                authorized_message3.clone(),
961            ],
962            ..Default::default()
963        };
964
965        let parent_state = ChatRoomStateV1::default();
966        let parameters = ChatRoomParametersV1 {
967            owner: signing_key.verifying_key(),
968        };
969
970        // Test with partial old summary
971        let old_summary = vec![authorized_message1.id(), authorized_message2.id()];
972        let delta = messages
973            .delta(&parent_state, &parameters, &old_summary)
974            .unwrap();
975        assert_eq!(delta.len(), 1);
976        assert_eq!(delta[0], authorized_message3);
977
978        // Test with empty old summary
979        let empty_summary: Vec<MessageId> = vec![];
980        let full_delta = messages
981            .delta(&parent_state, &parameters, &empty_summary)
982            .unwrap();
983        assert_eq!(full_delta.len(), 3);
984        assert_eq!(full_delta, messages.messages);
985
986        // Test with full old summary (no changes)
987        let full_summary = vec![
988            authorized_message1.id(),
989            authorized_message2.id(),
990            authorized_message3.id(),
991        ];
992        let no_delta = messages.delta(&parent_state, &parameters, &full_summary);
993        assert!(no_delta.is_none());
994    }
995
996    #[test]
997    fn test_messages_apply_delta() {
998        // Setup
999        let owner_signing_key = SigningKey::generate(&mut OsRng);
1000        let owner_verifying_key = owner_signing_key.verifying_key();
1001        let owner_id = MemberId::from(&owner_verifying_key);
1002
1003        let author_signing_key = SigningKey::generate(&mut OsRng);
1004        let author_verifying_key = author_signing_key.verifying_key();
1005        let author_id = MemberId::from(&author_verifying_key);
1006
1007        let mut parent_state = ChatRoomStateV1::default();
1008        parent_state.configuration.configuration.max_recent_messages = 3;
1009        parent_state.configuration.configuration.max_message_size = 100;
1010        parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
1011            member: crate::room_state::member::Member {
1012                owner_member_id: owner_id,
1013                invited_by: owner_id,
1014                member_vk: author_verifying_key,
1015            },
1016            signature: owner_signing_key.try_sign(&[0; 32]).unwrap(),
1017        }];
1018
1019        let parameters = ChatRoomParametersV1 {
1020            owner: owner_verifying_key,
1021        };
1022
1023        // Create messages
1024        let create_message = |time: SystemTime| {
1025            let message = MessageV1 {
1026                room_owner: owner_id,
1027                author: author_id,
1028                time,
1029                content: RoomMessageBody::public("Test message".to_string()),
1030            };
1031            AuthorizedMessageV1::new(message, &author_signing_key)
1032        };
1033
1034        let now = SystemTime::now();
1035        let message1 = create_message(now - Duration::from_secs(3));
1036        let message2 = create_message(now - Duration::from_secs(2));
1037        let message3 = create_message(now - Duration::from_secs(1));
1038        let message4 = create_message(now);
1039
1040        // Initial room_state with 2 messages
1041        let mut messages = MessagesV1 {
1042            messages: vec![message1.clone(), message2.clone()],
1043            ..Default::default()
1044        };
1045
1046        // Apply delta with 2 new messages
1047        let delta = vec![message3.clone(), message4.clone()];
1048        assert!(messages
1049            .apply_delta(&parent_state, &parameters, &Some(delta))
1050            .is_ok());
1051
1052        // Check results
1053        assert_eq!(
1054            messages.messages.len(),
1055            3,
1056            "Should have 3 messages after applying delta"
1057        );
1058        assert!(
1059            !messages.messages.contains(&message1),
1060            "Oldest message should be removed"
1061        );
1062        assert!(
1063            messages.messages.contains(&message2),
1064            "Second oldest message should be retained"
1065        );
1066        assert!(
1067            messages.messages.contains(&message3),
1068            "New message should be added"
1069        );
1070        assert!(
1071            messages.messages.contains(&message4),
1072            "Newest message should be added"
1073        );
1074
1075        // Apply delta with an older message
1076        let old_message = create_message(now - Duration::from_secs(4));
1077        let delta = vec![old_message.clone()];
1078        assert!(messages
1079            .apply_delta(&parent_state, &parameters, &Some(delta))
1080            .is_ok());
1081
1082        // Check results
1083        assert_eq!(messages.messages.len(), 3, "Should still have 3 messages");
1084        assert!(
1085            !messages.messages.contains(&old_message),
1086            "Older message should not be added"
1087        );
1088        assert!(
1089            messages.messages.contains(&message2),
1090            "Message2 should be retained"
1091        );
1092        assert!(
1093            messages.messages.contains(&message3),
1094            "Message3 should be retained"
1095        );
1096        assert!(
1097            messages.messages.contains(&message4),
1098            "Newest message should be retained"
1099        );
1100    }
1101
1102    #[test]
1103    fn test_message_author_preservation_across_users() {
1104        // Create two users
1105        let user1_sk = SigningKey::generate(&mut OsRng);
1106        let user1_vk = user1_sk.verifying_key();
1107        let user1_id = MemberId::from(&user1_vk);
1108
1109        let user2_sk = SigningKey::generate(&mut OsRng);
1110        let user2_vk = user2_sk.verifying_key();
1111        let user2_id = MemberId::from(&user2_vk);
1112
1113        let owner_sk = SigningKey::generate(&mut OsRng);
1114        let owner_vk = owner_sk.verifying_key();
1115        let owner_id = MemberId::from(&owner_vk);
1116
1117        println!("User1 ID: {}", user1_id);
1118        println!("User2 ID: {}", user2_id);
1119        println!("Owner ID: {}", owner_id);
1120
1121        // Create messages from different users
1122        let msg1 = MessageV1 {
1123            room_owner: owner_id,
1124            author: user1_id,
1125            content: RoomMessageBody::public("Message from user1".to_string()),
1126            time: SystemTime::now(),
1127        };
1128
1129        let msg2 = MessageV1 {
1130            room_owner: owner_id,
1131            author: user2_id,
1132            content: RoomMessageBody::public("Message from user2".to_string()),
1133            time: SystemTime::now() + Duration::from_secs(1),
1134        };
1135
1136        let auth_msg1 = AuthorizedMessageV1::new(msg1.clone(), &user1_sk);
1137        let auth_msg2 = AuthorizedMessageV1::new(msg2.clone(), &user2_sk);
1138
1139        // Create a messages state with both messages
1140        let messages = MessagesV1 {
1141            messages: vec![auth_msg1.clone(), auth_msg2.clone()],
1142            ..Default::default()
1143        };
1144
1145        // Verify authors are preserved
1146        assert_eq!(messages.messages.len(), 2);
1147
1148        let stored_msg1 = &messages.messages[0];
1149        let stored_msg2 = &messages.messages[1];
1150
1151        assert_eq!(
1152            stored_msg1.message.author, user1_id,
1153            "Message 1 author should be user1, but got {}",
1154            stored_msg1.message.author
1155        );
1156        assert_eq!(
1157            stored_msg2.message.author, user2_id,
1158            "Message 2 author should be user2, but got {}",
1159            stored_msg2.message.author
1160        );
1161
1162        // Test that author IDs are different
1163        assert_ne!(user1_id, user2_id, "User IDs should be different");
1164
1165        // Test Display implementation
1166        let user1_id_str = user1_id.to_string();
1167        let user2_id_str = user2_id.to_string();
1168
1169        println!("User1 ID string: {}", user1_id_str);
1170        println!("User2 ID string: {}", user2_id_str);
1171
1172        assert_ne!(
1173            user1_id_str, user2_id_str,
1174            "User ID strings should be different"
1175        );
1176    }
1177
1178    #[test]
1179    fn test_edit_action() {
1180        let signing_key = SigningKey::generate(&mut OsRng);
1181        let verifying_key = signing_key.verifying_key();
1182        let owner_id = MemberId::from(&verifying_key);
1183        let author_id = owner_id;
1184
1185        // Create original message
1186        let original_msg = MessageV1 {
1187            room_owner: owner_id,
1188            author: author_id,
1189            time: SystemTime::now(),
1190            content: RoomMessageBody::public("Original content".to_string()),
1191        };
1192        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1193        let original_id = auth_original.id();
1194
1195        // Create edit action
1196        let edit_msg = MessageV1 {
1197            room_owner: owner_id,
1198            author: author_id,
1199            time: SystemTime::now() + Duration::from_secs(1),
1200            content: RoomMessageBody::edit(original_id.clone(), "Edited content".to_string()),
1201        };
1202        let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
1203
1204        // Create messages state and rebuild
1205        let mut messages = MessagesV1 {
1206            messages: vec![auth_original.clone(), auth_edit],
1207            ..Default::default()
1208        };
1209        messages.rebuild_actions_state();
1210
1211        // Verify edit was applied
1212        assert!(messages.is_edited(&original_id));
1213        let effective = messages.effective_text(&auth_original);
1214        assert_eq!(effective, Some("Edited content".to_string()));
1215
1216        // Verify display_messages still shows the original message
1217        let display: Vec<_> = messages.display_messages().collect();
1218        assert_eq!(display.len(), 1);
1219    }
1220
1221    #[test]
1222    fn test_edit_by_non_author_ignored() {
1223        let owner_sk = SigningKey::generate(&mut OsRng);
1224        let owner_vk = owner_sk.verifying_key();
1225        let owner_id = MemberId::from(&owner_vk);
1226
1227        let other_sk = SigningKey::generate(&mut OsRng);
1228        let other_id = MemberId::from(&other_sk.verifying_key());
1229
1230        // Create message by owner
1231        let original_msg = MessageV1 {
1232            room_owner: owner_id,
1233            author: owner_id,
1234            time: SystemTime::now(),
1235            content: RoomMessageBody::public("Original content".to_string()),
1236        };
1237        let auth_original = AuthorizedMessageV1::new(original_msg, &owner_sk);
1238        let original_id = auth_original.id();
1239
1240        // Create edit action by OTHER user (should be ignored)
1241        let edit_msg = MessageV1 {
1242            room_owner: owner_id,
1243            author: other_id,
1244            time: SystemTime::now() + Duration::from_secs(1),
1245            content: RoomMessageBody::edit(original_id.clone(), "Hacked content".to_string()),
1246        };
1247        let auth_edit = AuthorizedMessageV1::new(edit_msg, &other_sk);
1248
1249        let mut messages = MessagesV1 {
1250            messages: vec![auth_original.clone(), auth_edit],
1251            ..Default::default()
1252        };
1253        messages.rebuild_actions_state();
1254
1255        // Edit should be ignored - original content preserved
1256        assert!(!messages.is_edited(&original_id));
1257        let effective = messages.effective_text(&auth_original);
1258        assert_eq!(effective, Some("Original content".to_string()));
1259    }
1260
1261    #[test]
1262    fn test_delete_action() {
1263        let signing_key = SigningKey::generate(&mut OsRng);
1264        let verifying_key = signing_key.verifying_key();
1265        let owner_id = MemberId::from(&verifying_key);
1266
1267        // Create original message
1268        let original_msg = MessageV1 {
1269            room_owner: owner_id,
1270            author: owner_id,
1271            time: SystemTime::now(),
1272            content: RoomMessageBody::public("Will be deleted".to_string()),
1273        };
1274        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1275        let original_id = auth_original.id();
1276
1277        // Create delete action
1278        let delete_msg = MessageV1 {
1279            room_owner: owner_id,
1280            author: owner_id,
1281            time: SystemTime::now() + Duration::from_secs(1),
1282            content: RoomMessageBody::delete(original_id.clone()),
1283        };
1284        let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
1285
1286        let mut messages = MessagesV1 {
1287            messages: vec![auth_original, auth_delete],
1288            ..Default::default()
1289        };
1290        messages.rebuild_actions_state();
1291
1292        // Verify message is deleted
1293        assert!(messages.is_deleted(&original_id));
1294
1295        // Verify display_messages excludes deleted message
1296        let display: Vec<_> = messages.display_messages().collect();
1297        assert_eq!(display.len(), 0);
1298    }
1299
1300    #[test]
1301    fn test_reaction_action() {
1302        let user1_sk = SigningKey::generate(&mut OsRng);
1303        let user1_id = MemberId::from(&user1_sk.verifying_key());
1304
1305        let user2_sk = SigningKey::generate(&mut OsRng);
1306        let user2_id = MemberId::from(&user2_sk.verifying_key());
1307
1308        let owner_id = user1_id;
1309
1310        // Create original message
1311        let original_msg = MessageV1 {
1312            room_owner: owner_id,
1313            author: user1_id,
1314            time: SystemTime::now(),
1315            content: RoomMessageBody::public("React to me!".to_string()),
1316        };
1317        let auth_original = AuthorizedMessageV1::new(original_msg, &user1_sk);
1318        let original_id = auth_original.id();
1319
1320        // Create reaction from user2
1321        let reaction_msg = MessageV1 {
1322            room_owner: owner_id,
1323            author: user2_id,
1324            time: SystemTime::now() + Duration::from_secs(1),
1325            content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1326        };
1327        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user2_sk);
1328
1329        // Create another reaction from user1
1330        let reaction_msg2 = MessageV1 {
1331            room_owner: owner_id,
1332            author: user1_id,
1333            time: SystemTime::now() + Duration::from_secs(2),
1334            content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1335        };
1336        let auth_reaction2 = AuthorizedMessageV1::new(reaction_msg2, &user1_sk);
1337
1338        let mut messages = MessagesV1 {
1339            messages: vec![auth_original, auth_reaction, auth_reaction2],
1340            ..Default::default()
1341        };
1342        messages.rebuild_actions_state();
1343
1344        // Verify reactions
1345        let reactions = messages.reactions(&original_id).unwrap();
1346        let thumbs_up = reactions.get("👍").unwrap();
1347        assert_eq!(thumbs_up.len(), 2);
1348        assert!(thumbs_up.contains(&user1_id));
1349        assert!(thumbs_up.contains(&user2_id));
1350    }
1351
1352    #[test]
1353    fn test_remove_reaction_action() {
1354        let user_sk = SigningKey::generate(&mut OsRng);
1355        let user_id = MemberId::from(&user_sk.verifying_key());
1356        let owner_id = user_id;
1357
1358        // Create original message
1359        let original_msg = MessageV1 {
1360            room_owner: owner_id,
1361            author: user_id,
1362            time: SystemTime::now(),
1363            content: RoomMessageBody::public("Test message".to_string()),
1364        };
1365        let auth_original = AuthorizedMessageV1::new(original_msg, &user_sk);
1366        let original_id = auth_original.id();
1367
1368        // Add reaction
1369        let reaction_msg = MessageV1 {
1370            room_owner: owner_id,
1371            author: user_id,
1372            time: SystemTime::now() + Duration::from_secs(1),
1373            content: RoomMessageBody::reaction(original_id.clone(), "❤️".to_string()),
1374        };
1375        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user_sk);
1376
1377        // Remove reaction
1378        let remove_msg = MessageV1 {
1379            room_owner: owner_id,
1380            author: user_id,
1381            time: SystemTime::now() + Duration::from_secs(2),
1382            content: RoomMessageBody::remove_reaction(original_id.clone(), "❤️".to_string()),
1383        };
1384        let auth_remove = AuthorizedMessageV1::new(remove_msg, &user_sk);
1385
1386        let mut messages = MessagesV1 {
1387            messages: vec![auth_original, auth_reaction, auth_remove],
1388            ..Default::default()
1389        };
1390        messages.rebuild_actions_state();
1391
1392        // Verify reaction was removed
1393        assert!(messages.reactions(&original_id).is_none());
1394    }
1395
1396    #[test]
1397    fn test_action_on_deleted_message_ignored() {
1398        let signing_key = SigningKey::generate(&mut OsRng);
1399        let verifying_key = signing_key.verifying_key();
1400        let owner_id = MemberId::from(&verifying_key);
1401
1402        // Create original message
1403        let original_msg = MessageV1 {
1404            room_owner: owner_id,
1405            author: owner_id,
1406            time: SystemTime::now(),
1407            content: RoomMessageBody::public("Will be deleted".to_string()),
1408        };
1409        let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1410        let original_id = auth_original.id();
1411
1412        // Delete it
1413        let delete_msg = MessageV1 {
1414            room_owner: owner_id,
1415            author: owner_id,
1416            time: SystemTime::now() + Duration::from_secs(1),
1417            content: RoomMessageBody::delete(original_id.clone()),
1418        };
1419        let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
1420
1421        // Try to edit deleted message (should be ignored)
1422        let edit_msg = MessageV1 {
1423            room_owner: owner_id,
1424            author: owner_id,
1425            time: SystemTime::now() + Duration::from_secs(2),
1426            content: RoomMessageBody::edit(original_id.clone(), "Too late!".to_string()),
1427        };
1428        let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
1429
1430        let mut messages = MessagesV1 {
1431            messages: vec![auth_original, auth_delete, auth_edit],
1432            ..Default::default()
1433        };
1434        messages.rebuild_actions_state();
1435
1436        // Message should be deleted, edit should be ignored
1437        assert!(messages.is_deleted(&original_id));
1438        assert!(!messages.is_edited(&original_id));
1439    }
1440
1441    #[test]
1442    fn test_display_messages_filters_actions() {
1443        let signing_key = SigningKey::generate(&mut OsRng);
1444        let verifying_key = signing_key.verifying_key();
1445        let owner_id = MemberId::from(&verifying_key);
1446
1447        // Create regular message
1448        let msg1 = MessageV1 {
1449            room_owner: owner_id,
1450            author: owner_id,
1451            time: SystemTime::now(),
1452            content: RoomMessageBody::public("Hello".to_string()),
1453        };
1454        let auth_msg1 = AuthorizedMessageV1::new(msg1, &signing_key);
1455        let msg1_id = auth_msg1.id();
1456
1457        // Create reaction (action message)
1458        let reaction_msg = MessageV1 {
1459            room_owner: owner_id,
1460            author: owner_id,
1461            time: SystemTime::now() + Duration::from_secs(1),
1462            content: RoomMessageBody::reaction(msg1_id, "👍".to_string()),
1463        };
1464        let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &signing_key);
1465
1466        // Create another regular message
1467        let msg2 = MessageV1 {
1468            room_owner: owner_id,
1469            author: owner_id,
1470            time: SystemTime::now() + Duration::from_secs(2),
1471            content: RoomMessageBody::public("World".to_string()),
1472        };
1473        let auth_msg2 = AuthorizedMessageV1::new(msg2, &signing_key);
1474
1475        let mut messages = MessagesV1 {
1476            messages: vec![auth_msg1, auth_reaction, auth_msg2],
1477            ..Default::default()
1478        };
1479        messages.rebuild_actions_state();
1480
1481        // display_messages should only return regular messages, not actions
1482        let display: Vec<_> = messages.display_messages().collect();
1483        assert_eq!(display.len(), 2);
1484        assert_eq!(
1485            display[0].message.content.as_public_string(),
1486            Some("Hello".to_string())
1487        );
1488        assert_eq!(
1489            display[1].message.content.as_public_string(),
1490            Some("World".to_string())
1491        );
1492    }
1493}