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