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::{BTreeSet, HashMap};
12use std::fmt;
13use std::time::SystemTime;
14
15pub const ENCRYPTION_TAG_OVERHEAD: usize = 16;
21
22#[derive(Clone, PartialEq, Debug, Default)]
25pub struct MessageActionsState {
26 pub edited_content: HashMap<MessageId, String>,
28 pub deleted: std::collections::HashSet<MessageId>,
30 pub reactions: HashMap<MessageId, HashMap<String, Vec<MemberId>>>,
32}
33
34#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Default)]
35pub struct MessagesV1 {
36 pub messages: Vec<AuthorizedMessageV1>,
37 #[serde(skip)]
39 pub actions_state: MessageActionsState,
40}
41
42#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
49pub struct MessageOrderKey {
50 pub time: SystemTime,
51 pub id: MessageId,
52}
53
54#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
86pub enum RetentionHorizon {
87 Open,
89 OldestRetained(MessageOrderKey),
92 Closed,
100}
101
102#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
112pub struct MessagesSummary {
113 pub message_ids: BTreeSet<MessageId>,
114 pub horizon: RetentionHorizon,
115}
116
117impl ComposableState for MessagesV1 {
118 type ParentState = ChatRoomStateV1;
119 type Summary = MessagesSummary;
120 type Delta = Vec<AuthorizedMessageV1>;
121 type Parameters = ChatRoomParametersV1;
122
123 fn verify(
124 &self,
125 parent_state: &Self::ParentState,
126 parameters: &Self::Parameters,
127 ) -> Result<(), String> {
128 let members_by_id = parent_state.members.members_by_member_id();
129 let owner_id = parameters.owner_id();
130
131 for message in &self.messages {
132 let verifying_key = if message.message.author == owner_id {
133 ¶meters.owner
135 } else if let Some(member) = members_by_id.get(&message.message.author) {
136 &member.member.member_vk
138 } else {
139 return Err(format!(
140 "Message author not found: {:?}",
141 message.message.author
142 ));
143 };
144
145 if message.validate(verifying_key).is_err() {
146 return Err(format!("Invalid message signature: id:{:?}", message.id()));
147 }
148 }
149
150 Ok(())
151 }
152
153 fn summarize(
162 &self,
163 parent_state: &Self::ParentState,
164 _parameters: &Self::Parameters,
165 ) -> Self::Summary {
166 MessagesSummary {
167 message_ids: self.messages.iter().map(|m| m.id()).collect(),
168 horizon: self
169 .retention_horizon(parent_state.configuration.configuration.max_recent_messages),
170 }
171 }
172
173 fn delta(
179 &self,
180 _parent_state: &Self::ParentState,
181 _parameters: &Self::Parameters,
182 old_state_summary: &Self::Summary,
183 ) -> Option<Self::Delta> {
184 let retained_by_receiver = |m: &AuthorizedMessageV1| match &old_state_summary.horizon {
187 RetentionHorizon::Open => true,
188 RetentionHorizon::OldestRetained(oldest) => m.order_key() > *oldest,
189 RetentionHorizon::Closed => false,
190 };
191
192 let delta: Vec<AuthorizedMessageV1> = self
193 .messages
194 .iter()
195 .filter(|m| !old_state_summary.message_ids.contains(&m.id()))
196 .filter(|m| retained_by_receiver(m))
197 .cloned()
198 .collect();
199 if delta.is_empty() {
200 None
201 } else {
202 Some(delta)
203 }
204 }
205
206 fn apply_delta(
207 &mut self,
208 parent_state: &Self::ParentState,
209 parameters: &Self::Parameters,
210 delta: &Option<Self::Delta>,
211 ) -> Result<(), String> {
212 let max_recent_messages = parent_state.configuration.configuration.max_recent_messages;
213 let max_message_size = parent_state.configuration.configuration.max_message_size;
214 let privacy_mode = &parent_state.configuration.configuration.privacy_mode;
215
216 if let Some(delta) = delta {
218 for msg in delta {
219 let content = &msg.message.content;
220
221 match content {
222 RoomMessageBody::Private { secret_version, .. } => {
223 if *privacy_mode == PrivacyMode::Private
276 && !parent_state
277 .secrets
278 .versions
279 .iter()
280 .any(|v| v.record.version == *secret_version)
281 {
282 return Err(format!(
283 "Private message references unknown secret version {}",
284 secret_version
285 ));
286 }
287 }
288 RoomMessageBody::Public { .. } => {
289 if *privacy_mode == PrivacyMode::Private && !content.is_event() {
292 return Err("Cannot send public messages in private room".to_string());
293 }
294 }
295 }
296 }
297
298 let existing_ids: std::collections::HashSet<_> =
300 self.messages.iter().map(|m| m.id()).collect();
301 self.messages.extend(
302 delta
303 .iter()
304 .filter(|msg| !existing_ids.contains(&msg.id()))
305 .cloned(),
306 );
307 }
308
309 self.messages
312 .retain(|m| m.message.content.content_len() <= max_message_size);
313
314 let members_by_id = parent_state.members.members_by_member_id();
316 let owner_id = MemberId::from(¶meters.owner);
317 self.messages.retain(|m| {
318 members_by_id.contains_key(&m.message.author) || m.message.author == owner_id
319 });
320
321 self.messages.sort_by(|a, b| {
324 a.message
325 .time
326 .cmp(&b.message.time)
327 .then_with(|| a.id().cmp(&b.id()))
328 });
329
330 if self.messages.len() > max_recent_messages {
338 self.messages
339 .drain(0..self.messages.len() - max_recent_messages);
340 }
341
342 self.rebuild_actions_state();
344
345 Ok(())
346 }
347}
348
349impl MessagesV1 {
350 pub fn retention_horizon(&self, max_recent_messages: usize) -> RetentionHorizon {
365 if max_recent_messages == 0 {
366 return RetentionHorizon::Closed;
367 }
368 if self.messages.len() < max_recent_messages {
369 return RetentionHorizon::Open;
370 }
371 match self.messages.iter().map(|m| m.order_key()).min() {
372 Some(oldest) => RetentionHorizon::OldestRetained(oldest),
373 None => RetentionHorizon::Open,
376 }
377 }
378
379 pub fn rebuild_actions_state(&mut self) {
385 self.rebuild_actions_state_with_decrypted(&HashMap::new());
386 }
387
388 pub fn rebuild_actions_state_with_decrypted(
397 &mut self,
398 decrypted_content: &HashMap<MessageId, Vec<u8>>,
399 ) {
400 use crate::room_state::content::{
401 ActionContentV1, DecodedContent, ACTION_TYPE_DELETE, ACTION_TYPE_EDIT,
402 ACTION_TYPE_REACTION, ACTION_TYPE_REMOVE_REACTION,
403 };
404
405 self.actions_state = MessageActionsState::default();
407
408 let message_authors: HashMap<MessageId, MemberId> = self
410 .messages
411 .iter()
412 .filter(|m| !m.message.content.is_action())
413 .map(|m| (m.id(), m.message.author))
414 .collect();
415
416 for msg in &self.messages {
418 let actor = msg.message.author;
419
420 if !msg.message.content.is_action() {
422 continue;
423 }
424
425 let action = match &msg.message.content {
427 RoomMessageBody::Public { .. } => {
428 match msg.message.content.decode_content() {
430 Some(DecodedContent::Action(action)) => action,
431 _ => continue,
432 }
433 }
434 RoomMessageBody::Private { .. } => {
435 let msg_id = msg.id();
437 if let Some(plaintext) = decrypted_content.get(&msg_id) {
438 match ActionContentV1::decode(plaintext) {
439 Ok(action) => action,
440 Err(_) => continue,
441 }
442 } else {
443 continue;
445 }
446 }
447 };
448
449 let target = &action.target;
450
451 match action.action_type {
452 ACTION_TYPE_EDIT => {
453 if let Some(&original_author) = message_authors.get(target) {
455 if actor == original_author {
456 if !self.actions_state.deleted.contains(target) {
458 if let Some(payload) = action.edit_payload() {
459 self.actions_state
460 .edited_content
461 .insert(target.clone(), payload.new_text);
462 }
463 }
464 }
465 }
466 }
467 ACTION_TYPE_DELETE => {
468 if let Some(&original_author) = message_authors.get(target) {
470 if actor == original_author {
471 self.actions_state.deleted.insert(target.clone());
472 self.actions_state.edited_content.remove(target);
474 }
475 }
476 }
477 ACTION_TYPE_REACTION => {
478 if message_authors.contains_key(target)
480 && !self.actions_state.deleted.contains(target)
481 {
482 if let Some(payload) = action.reaction_payload() {
483 let reactions = self
484 .actions_state
485 .reactions
486 .entry(target.clone())
487 .or_default();
488 let reactors = reactions.entry(payload.emoji).or_default();
489 if !reactors.contains(&actor) {
491 reactors.push(actor);
492 }
493 }
494 }
495 }
496 ACTION_TYPE_REMOVE_REACTION => {
497 if let Some(payload) = action.reaction_payload() {
499 if let Some(reactions) = self.actions_state.reactions.get_mut(target) {
500 if let Some(reactors) = reactions.get_mut(&payload.emoji) {
501 reactors.retain(|r| r != &actor);
502 if reactors.is_empty() {
504 reactions.remove(&payload.emoji);
505 }
506 }
507 if reactions.is_empty() {
508 self.actions_state.reactions.remove(target);
509 }
510 }
511 }
512 }
513 _ => {
514 }
516 }
517 }
518 }
519
520 pub fn is_edited(&self, message_id: &MessageId) -> bool {
522 self.actions_state.edited_content.contains_key(message_id)
523 }
524
525 pub fn is_deleted(&self, message_id: &MessageId) -> bool {
527 self.actions_state.deleted.contains(message_id)
528 }
529
530 pub fn effective_text(&self, message: &AuthorizedMessageV1) -> Option<String> {
533 let id = message.id();
534 if let Some(edited_text) = self.actions_state.edited_content.get(&id) {
536 return Some(edited_text.clone());
537 }
538 message.message.content.as_public_string()
540 }
541
542 pub fn reactions(&self, message_id: &MessageId) -> Option<&HashMap<String, Vec<MemberId>>> {
544 self.actions_state.reactions.get(message_id)
545 }
546
547 pub fn display_messages(&self) -> impl Iterator<Item = &AuthorizedMessageV1> {
549 self.messages.iter().filter(|m| {
550 !m.message.content.is_action() && !self.actions_state.deleted.contains(&m.id())
551 })
552 }
553}
554
555#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
589pub enum RoomMessageBody {
590 Public {
592 content_type: u32,
594 content_version: u32,
596 data: Vec<u8>,
600 },
601 Private {
603 content_type: u32,
605 content_version: u32,
607 ciphertext: Vec<u8>,
611 nonce: [u8; 12],
613 secret_version: SecretVersion,
615 },
616}
617
618impl RoomMessageBody {
619 pub fn public(text: String) -> Self {
621 use crate::room_state::content::{TextContentV1, CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
622 let content = TextContentV1::new(text);
623 Self::Public {
624 content_type: CONTENT_TYPE_TEXT,
625 content_version: TEXT_CONTENT_VERSION,
626 data: content.encode(),
627 }
628 }
629
630 pub fn join_event() -> Self {
632 use crate::room_state::content::{
633 EventContentV1, CONTENT_TYPE_EVENT, EVENT_CONTENT_VERSION,
634 };
635 let content = EventContentV1::join();
636 Self::Public {
637 content_type: CONTENT_TYPE_EVENT,
638 content_version: EVENT_CONTENT_VERSION,
639 data: content.encode(),
640 }
641 }
642
643 pub fn public_raw(content_type: u32, content_version: u32, data: Vec<u8>) -> Self {
645 Self::Public {
646 content_type,
647 content_version,
648 data,
649 }
650 }
651
652 pub fn private(
654 content_type: u32,
655 content_version: u32,
656 ciphertext: Vec<u8>,
657 nonce: [u8; 12],
658 secret_version: SecretVersion,
659 ) -> Self {
660 Self::Private {
661 content_type,
662 content_version,
663 ciphertext,
664 nonce,
665 secret_version,
666 }
667 }
668
669 pub fn private_text(
671 ciphertext: Vec<u8>,
672 nonce: [u8; 12],
673 secret_version: SecretVersion,
674 ) -> Self {
675 use crate::room_state::content::{CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
676 Self::Private {
677 content_type: CONTENT_TYPE_TEXT,
678 content_version: TEXT_CONTENT_VERSION,
679 ciphertext,
680 nonce,
681 secret_version,
682 }
683 }
684
685 pub fn edit(target: MessageId, new_text: String) -> Self {
687 use crate::room_state::content::{
688 ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
689 };
690 let action = ActionContentV1::edit(target, new_text);
691 Self::Public {
692 content_type: CONTENT_TYPE_ACTION,
693 content_version: ACTION_CONTENT_VERSION,
694 data: action.encode(),
695 }
696 }
697
698 pub fn delete(target: MessageId) -> Self {
700 use crate::room_state::content::{
701 ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
702 };
703 let action = ActionContentV1::delete(target);
704 Self::Public {
705 content_type: CONTENT_TYPE_ACTION,
706 content_version: ACTION_CONTENT_VERSION,
707 data: action.encode(),
708 }
709 }
710
711 pub fn reaction(target: MessageId, emoji: String) -> Self {
713 use crate::room_state::content::{
714 ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
715 };
716 let action = ActionContentV1::reaction(target, emoji);
717 Self::Public {
718 content_type: CONTENT_TYPE_ACTION,
719 content_version: ACTION_CONTENT_VERSION,
720 data: action.encode(),
721 }
722 }
723
724 pub fn remove_reaction(target: MessageId, emoji: String) -> Self {
726 use crate::room_state::content::{
727 ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
728 };
729 let action = ActionContentV1::remove_reaction(target, emoji);
730 Self::Public {
731 content_type: CONTENT_TYPE_ACTION,
732 content_version: ACTION_CONTENT_VERSION,
733 data: action.encode(),
734 }
735 }
736
737 pub fn reply(
739 text: String,
740 target_message_id: MessageId,
741 target_author_name: String,
742 target_content_preview: String,
743 ) -> Self {
744 use crate::room_state::content::{
745 ReplyContentV1, CONTENT_TYPE_REPLY, REPLY_CONTENT_VERSION,
746 };
747 let reply = ReplyContentV1::new(
748 text,
749 target_message_id,
750 target_author_name,
751 target_content_preview,
752 );
753 Self::Public {
754 content_type: CONTENT_TYPE_REPLY,
755 content_version: REPLY_CONTENT_VERSION,
756 data: reply.encode(),
757 }
758 }
759
760 pub fn private_action(
769 ciphertext: Vec<u8>,
770 nonce: [u8; 12],
771 secret_version: SecretVersion,
772 ) -> Self {
773 use crate::room_state::content::{ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION};
774 Self::Private {
775 content_type: CONTENT_TYPE_ACTION,
776 content_version: ACTION_CONTENT_VERSION,
777 ciphertext,
778 nonce,
779 secret_version,
780 }
781 }
782
783 pub fn is_public(&self) -> bool {
785 matches!(self, Self::Public { .. })
786 }
787
788 pub fn is_private(&self) -> bool {
790 matches!(self, Self::Private { .. })
791 }
792
793 pub fn content_type(&self) -> u32 {
795 match self {
796 Self::Public { content_type, .. } | Self::Private { content_type, .. } => *content_type,
797 }
798 }
799
800 pub fn content_version(&self) -> u32 {
802 match self {
803 Self::Public {
804 content_version, ..
805 }
806 | Self::Private {
807 content_version, ..
808 } => *content_version,
809 }
810 }
811
812 pub fn is_action(&self) -> bool {
814 use crate::room_state::content::CONTENT_TYPE_ACTION;
815 self.content_type() == CONTENT_TYPE_ACTION
816 }
817
818 pub fn is_event(&self) -> bool {
820 use crate::room_state::content::CONTENT_TYPE_EVENT;
821 self.content_type() == CONTENT_TYPE_EVENT
822 }
823
824 pub fn decode_content(&self) -> Option<crate::room_state::content::DecodedContent> {
827 use crate::room_state::content::{
828 ActionContentV1, DecodedContent, EventContentV1, ReplyContentV1, TextContentV1,
829 CONTENT_TYPE_ACTION, CONTENT_TYPE_EVENT, CONTENT_TYPE_REPLY, CONTENT_TYPE_TEXT,
830 };
831 match self {
832 Self::Public {
833 content_type,
834 content_version,
835 data,
836 } => match *content_type {
837 CONTENT_TYPE_TEXT => TextContentV1::decode(data).ok().map(DecodedContent::Text),
838 CONTENT_TYPE_ACTION => ActionContentV1::decode(data)
839 .ok()
840 .map(DecodedContent::Action),
841 CONTENT_TYPE_REPLY => ReplyContentV1::decode(data).ok().map(DecodedContent::Reply),
842 CONTENT_TYPE_EVENT => EventContentV1::decode(data).ok().map(DecodedContent::Event),
843 _ => Some(DecodedContent::Unknown {
844 content_type: *content_type,
845 content_version: *content_version,
846 }),
847 },
848 Self::Private { .. } => None,
849 }
850 }
851
852 pub fn target_id(&self) -> Option<MessageId> {
854 use crate::room_state::content::{ActionContentV1, CONTENT_TYPE_ACTION};
855 match self {
856 Self::Public {
857 content_type, data, ..
858 } if *content_type == CONTENT_TYPE_ACTION => {
859 ActionContentV1::decode(data).ok().map(|a| a.target)
860 }
861 _ => None,
862 }
863 }
864
865 pub fn content_len(&self) -> usize {
867 match self {
868 Self::Public { data, .. } => data.len(),
869 Self::Private { ciphertext, .. } => ciphertext.len(),
870 }
871 }
872
873 pub fn measure_text(text: &str, encrypted: bool) -> usize {
883 use crate::room_state::content::TextContentV1;
884 let plain = TextContentV1::new(text.to_owned()).encode().len();
885 Self::with_encryption_overhead(plain, encrypted)
886 }
887
888 pub fn measure_reply(
893 text: &str,
894 target_message_id: MessageId,
895 target_author_name: &str,
896 target_content_preview: &str,
897 encrypted: bool,
898 ) -> usize {
899 use crate::room_state::content::ReplyContentV1;
900 let plain = ReplyContentV1::new(
901 text.to_owned(),
902 target_message_id,
903 target_author_name.to_owned(),
904 target_content_preview.to_owned(),
905 )
906 .encode()
907 .len();
908 Self::with_encryption_overhead(plain, encrypted)
909 }
910
911 pub fn measure_edit(target: MessageId, new_text: &str, encrypted: bool) -> usize {
915 use crate::room_state::content::ActionContentV1;
916 let plain = ActionContentV1::edit(target, new_text.to_owned())
917 .encode()
918 .len();
919 Self::with_encryption_overhead(plain, encrypted)
920 }
921
922 fn with_encryption_overhead(plain_len: usize, encrypted: bool) -> usize {
923 if encrypted {
924 plain_len + ENCRYPTION_TAG_OVERHEAD
925 } else {
926 plain_len
927 }
928 }
929
930 pub fn secret_version(&self) -> Option<SecretVersion> {
932 match self {
933 Self::Public { .. } => None,
934 Self::Private { secret_version, .. } => Some(*secret_version),
935 }
936 }
937
938 pub fn to_string_lossy(&self) -> String {
940 match self {
941 Self::Public { .. } => {
942 if let Some(decoded) = self.decode_content() {
943 decoded.to_display_string()
944 } else {
945 "[Failed to decode message]".to_string()
946 }
947 }
948 Self::Private {
949 ciphertext,
950 secret_version,
951 ..
952 } => {
953 format!(
954 "[Encrypted message: {} bytes, v{}]",
955 ciphertext.len(),
956 secret_version
957 )
958 }
959 }
960 }
961
962 pub fn as_public_string(&self) -> Option<String> {
964 self.decode_content()
965 .and_then(|c| c.as_text().map(|s| s.to_string()))
966 }
967}
968
969impl fmt::Display for RoomMessageBody {
970 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
971 write!(f, "{}", self.to_string_lossy())
972 }
973}
974
975#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
976pub struct MessageV1 {
977 pub room_owner: MemberId,
978 pub author: MemberId,
979 pub time: SystemTime,
980 pub content: RoomMessageBody,
981}
982
983impl Default for MessageV1 {
984 fn default() -> Self {
985 Self {
986 room_owner: MemberId(FastHash(0)),
987 author: MemberId(FastHash(0)),
988 time: SystemTime::UNIX_EPOCH,
989 content: RoomMessageBody::public(String::new()),
990 }
991 }
992}
993
994#[derive(Clone, PartialEq, Serialize, Deserialize)]
995pub struct AuthorizedMessageV1 {
996 pub message: MessageV1,
997 pub signature: Signature,
998}
999
1000impl fmt::Debug for AuthorizedMessageV1 {
1001 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1002 f.debug_struct("AuthorizedMessage")
1003 .field("message", &self.message)
1004 .field(
1005 "signature",
1006 &format_args!("{}", truncated_base64(self.signature.to_bytes())),
1007 )
1008 .finish()
1009 }
1010}
1011
1012#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Clone, Debug, Ord, PartialOrd)]
1013pub struct MessageId(pub FastHash);
1014
1015impl fmt::Display for MessageId {
1016 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1017 write!(f, "{:?}", self.0)
1018 }
1019}
1020
1021impl AuthorizedMessageV1 {
1022 pub fn new(message: MessageV1, signing_key: &SigningKey) -> Self {
1023 Self {
1024 message: message.clone(),
1025 signature: sign_struct(&message, signing_key),
1026 }
1027 }
1028
1029 pub fn with_signature(message: MessageV1, signature: Signature) -> Self {
1032 Self { message, signature }
1033 }
1034
1035 pub fn validate(
1036 &self,
1037 verifying_key: &VerifyingKey,
1038 ) -> Result<(), ed25519_dalek::SignatureError> {
1039 verify_struct(&self.message, &self.signature, verifying_key)
1040 }
1041
1042 pub fn id(&self) -> MessageId {
1043 MessageId(fast_hash(&self.signature.to_bytes()))
1044 }
1045
1046 pub fn order_key(&self) -> MessageOrderKey {
1050 MessageOrderKey {
1051 time: self.message.time,
1052 id: self.id(),
1053 }
1054 }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060 use ed25519_dalek::{Signer, SigningKey};
1061 use rand::rngs::OsRng;
1062 use std::time::Duration;
1063
1064 fn create_test_message(owner_id: MemberId, author_id: MemberId) -> MessageV1 {
1065 MessageV1 {
1066 room_owner: owner_id,
1067 author: author_id,
1068 time: SystemTime::now(),
1069 content: RoomMessageBody::public("Test message".to_string()),
1070 }
1071 }
1072
1073 #[test]
1074 fn test_messages_v1_default() {
1075 let default_messages = MessagesV1::default();
1076 assert!(default_messages.messages.is_empty());
1077 }
1078
1079 #[test]
1089 fn legacy_encoded_actions_still_render_through_rebuild_actions_state() {
1090 use crate::room_state::content::{
1091 ActionContentV1, ACTION_TYPE_DELETE, ACTION_TYPE_EDIT, ACTION_TYPE_REACTION,
1092 CONTENT_TYPE_ACTION,
1093 };
1094
1095 #[derive(Serialize)]
1097 struct LegacyAction {
1098 action_type: u32,
1099 target: MessageId,
1100 payload: Vec<u8>,
1101 }
1102
1103 fn legacy_action_body(action: &ActionContentV1) -> RoomMessageBody {
1104 let mut data = Vec::new();
1105 ciborium::into_writer(
1106 &LegacyAction {
1107 action_type: action.action_type,
1108 target: action.target.clone(),
1109 payload: action.payload.clone(),
1110 },
1111 &mut data,
1112 )
1113 .expect("serialize legacy action");
1114 RoomMessageBody::public_raw(CONTENT_TYPE_ACTION, 1, data)
1115 }
1116
1117 let signing_key = SigningKey::generate(&mut OsRng);
1118 let owner_id = MemberId(FastHash(0));
1119 let author_id = MemberId::from(&signing_key.verifying_key());
1120
1121 let original = AuthorizedMessageV1::new(
1122 MessageV1 {
1123 room_owner: owner_id,
1124 author: author_id,
1125 time: SystemTime::now(),
1126 content: RoomMessageBody::public("original text".to_string()),
1127 },
1128 &signing_key,
1129 );
1130 let target = original.id();
1131
1132 let push_action = |messages: &mut MessagesV1, action: ActionContentV1| {
1133 messages.messages.push(AuthorizedMessageV1::new(
1134 MessageV1 {
1135 room_owner: owner_id,
1136 author: author_id,
1137 time: SystemTime::now() + Duration::from_secs(1),
1138 content: legacy_action_body(&action),
1139 },
1140 &signing_key,
1141 ));
1142 };
1143
1144 let mut messages = MessagesV1 {
1146 messages: vec![original.clone()],
1147 ..Default::default()
1148 };
1149 push_action(
1150 &mut messages,
1151 ActionContentV1 {
1152 action_type: ACTION_TYPE_EDIT,
1153 target: target.clone(),
1154 payload: ActionContentV1::edit(target.clone(), "edited text".to_string()).payload,
1155 },
1156 );
1157 messages.rebuild_actions_state();
1158 assert!(
1159 messages.is_edited(&target),
1160 "a pre-#443 stored edit must still be seen as an edit"
1161 );
1162 assert_eq!(
1163 messages.effective_text(&original).as_deref(),
1164 Some("edited text"),
1165 "a pre-#443 stored edit must still render its new text"
1166 );
1167
1168 let mut messages = MessagesV1 {
1170 messages: vec![original.clone()],
1171 ..Default::default()
1172 };
1173 push_action(
1174 &mut messages,
1175 ActionContentV1 {
1176 action_type: ACTION_TYPE_REACTION,
1177 target: target.clone(),
1178 payload: ActionContentV1::reaction(target.clone(), "👍".to_string()).payload,
1179 },
1180 );
1181 messages.rebuild_actions_state();
1182 assert!(
1183 messages
1184 .actions_state
1185 .reactions
1186 .get(&target)
1187 .is_some_and(|r| r.contains_key("👍")),
1188 "a pre-#443 stored emoji reaction must still render"
1189 );
1190
1191 let mut messages = MessagesV1 {
1193 messages: vec![original.clone()],
1194 ..Default::default()
1195 };
1196 push_action(
1197 &mut messages,
1198 ActionContentV1 {
1199 action_type: ACTION_TYPE_DELETE,
1200 target: target.clone(),
1201 payload: Vec::new(),
1202 },
1203 );
1204 messages.rebuild_actions_state();
1205 assert!(
1206 messages.is_deleted(&target),
1207 "a pre-#443 stored delete must still apply"
1208 );
1209 }
1210
1211 #[test]
1212 fn test_authorized_message_v1_debug() {
1213 let signing_key = SigningKey::generate(&mut OsRng);
1214 let owner_id = MemberId(FastHash(0));
1215 let author_id = MemberId(FastHash(1));
1216
1217 let message = create_test_message(owner_id, author_id);
1218 let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
1219
1220 let debug_output = format!("{:?}", authorized_message);
1221 assert!(debug_output.contains("AuthorizedMessage"));
1222 assert!(debug_output.contains("message"));
1223 assert!(debug_output.contains("signature"));
1224 }
1225
1226 #[test]
1227 fn test_authorized_message_new_and_validate() {
1228 let signing_key = SigningKey::generate(&mut OsRng);
1229 let verifying_key = signing_key.verifying_key();
1230 let owner_id = MemberId(FastHash(0));
1231 let author_id = MemberId(FastHash(1));
1232
1233 let message = create_test_message(owner_id, author_id);
1234 let authorized_message = AuthorizedMessageV1::new(message.clone(), &signing_key);
1235
1236 assert_eq!(authorized_message.message, message);
1237 assert!(authorized_message.validate(&verifying_key).is_ok());
1238
1239 let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
1241 assert!(authorized_message.validate(&wrong_key).is_err());
1242
1243 let mut tampered_message = authorized_message.clone();
1245 tampered_message.message.content = RoomMessageBody::public("Tampered content".to_string());
1246 assert!(tampered_message.validate(&verifying_key).is_err());
1247 }
1248
1249 #[test]
1250 fn test_message_id() {
1251 let signing_key = SigningKey::generate(&mut OsRng);
1252 let owner_id = MemberId(FastHash(0));
1253 let author_id = MemberId(FastHash(1));
1254
1255 let message = create_test_message(owner_id, author_id);
1256 let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
1257
1258 let id1 = authorized_message.id();
1259 let id2 = authorized_message.id();
1260
1261 assert_eq!(id1, id2);
1262
1263 let message2 = create_test_message(owner_id, author_id);
1265 let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
1266 assert_ne!(authorized_message.id(), authorized_message2.id());
1267 }
1268
1269 #[test]
1270 fn test_messages_verify() {
1271 let owner_signing_key = SigningKey::generate(&mut OsRng);
1273 let owner_verifying_key = owner_signing_key.verifying_key();
1274 let owner_id = MemberId::from(&owner_verifying_key);
1275
1276 let author_signing_key = SigningKey::generate(&mut OsRng);
1278 let author_verifying_key = author_signing_key.verifying_key();
1279 let author_id = MemberId::from(&author_verifying_key);
1280
1281 let message = create_test_message(owner_id, author_id);
1283 let authorized_message = AuthorizedMessageV1::new(message, &author_signing_key);
1284
1285 let messages = MessagesV1 {
1287 messages: vec![authorized_message],
1288 ..Default::default()
1289 };
1290
1291 let mut parent_state = ChatRoomStateV1::default();
1293 let author_member = crate::room_state::member::Member {
1294 owner_member_id: owner_id,
1295 invited_by: owner_id,
1296 member_vk: author_verifying_key,
1297 };
1298 let authorized_author =
1299 crate::room_state::member::AuthorizedMember::new(author_member, &owner_signing_key);
1300 parent_state.members.members = vec![authorized_author];
1301
1302 let parameters = ChatRoomParametersV1 {
1304 owner: owner_verifying_key,
1305 };
1306
1307 assert!(
1309 messages.verify(&parent_state, ¶meters).is_ok(),
1310 "Valid messages should pass verification: {:?}",
1311 messages.verify(&parent_state, ¶meters)
1312 );
1313
1314 let mut invalid_messages = messages.clone();
1316 invalid_messages.messages[0].signature = Signature::from_bytes(&[0; 64]); assert!(
1318 invalid_messages.verify(&parent_state, ¶meters).is_err(),
1319 "Messages with invalid signature should fail verification"
1320 );
1321
1322 let non_existent_author_id =
1324 MemberId::from(&SigningKey::generate(&mut OsRng).verifying_key());
1325 let invalid_message = create_test_message(owner_id, non_existent_author_id);
1326 let invalid_authorized_message =
1327 AuthorizedMessageV1::new(invalid_message, &author_signing_key);
1328 let invalid_messages = MessagesV1 {
1329 messages: vec![invalid_authorized_message],
1330 ..Default::default()
1331 };
1332 assert!(
1333 invalid_messages.verify(&parent_state, ¶meters).is_err(),
1334 "Messages with non-existent author should fail verification"
1335 );
1336 }
1337
1338 #[test]
1339 fn test_messages_summarize() {
1340 let signing_key = SigningKey::generate(&mut OsRng);
1341 let owner_id = MemberId(FastHash(0));
1342 let author_id = MemberId(FastHash(1));
1343
1344 let message1 = create_test_message(owner_id, author_id);
1345 let message2 = create_test_message(owner_id, author_id);
1346
1347 let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
1348 let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
1349
1350 let messages = MessagesV1 {
1351 messages: vec![authorized_message1.clone(), authorized_message2.clone()],
1352 ..Default::default()
1353 };
1354
1355 let parent_state = ChatRoomStateV1::default();
1356 let parameters = ChatRoomParametersV1 {
1357 owner: signing_key.verifying_key(),
1358 };
1359
1360 let summary = messages.summarize(&parent_state, ¶meters);
1361 assert_eq!(summary.message_ids.len(), 2);
1362 assert!(summary.message_ids.contains(&authorized_message1.id()));
1363 assert!(summary.message_ids.contains(&authorized_message2.id()));
1364 assert_eq!(summary.horizon, RetentionHorizon::Open);
1366
1367 let empty_messages = MessagesV1::default();
1369 let empty_summary = empty_messages.summarize(&parent_state, ¶meters);
1370 assert!(empty_summary.message_ids.is_empty());
1371 assert_eq!(empty_summary.horizon, RetentionHorizon::Open);
1372 }
1373
1374 #[test]
1375 fn test_messages_delta() {
1376 let signing_key = SigningKey::generate(&mut OsRng);
1377 let owner_id = MemberId(FastHash(0));
1378 let author_id = MemberId(FastHash(1));
1379
1380 let base = SystemTime::now();
1382 let message1 = MessageV1 {
1383 room_owner: owner_id,
1384 author: author_id,
1385 time: base,
1386 content: RoomMessageBody::public("Message 1".to_string()),
1387 };
1388 let message2 = MessageV1 {
1389 room_owner: owner_id,
1390 author: author_id,
1391 time: base + Duration::from_millis(1),
1392 content: RoomMessageBody::public("Message 2".to_string()),
1393 };
1394 let message3 = MessageV1 {
1395 room_owner: owner_id,
1396 author: author_id,
1397 time: base + Duration::from_millis(2),
1398 content: RoomMessageBody::public("Message 3".to_string()),
1399 };
1400
1401 let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
1402 let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
1403 let authorized_message3 = AuthorizedMessageV1::new(message3, &signing_key);
1404
1405 let messages = MessagesV1 {
1406 messages: vec![
1407 authorized_message1.clone(),
1408 authorized_message2.clone(),
1409 authorized_message3.clone(),
1410 ],
1411 ..Default::default()
1412 };
1413
1414 let parent_state = ChatRoomStateV1::default();
1415 let parameters = ChatRoomParametersV1 {
1416 owner: signing_key.verifying_key(),
1417 };
1418
1419 let open_summary = |ids: &[MessageId]| MessagesSummary {
1422 message_ids: ids.iter().cloned().collect(),
1423 horizon: RetentionHorizon::Open,
1424 };
1425
1426 let old_summary = open_summary(&[authorized_message1.id(), authorized_message2.id()]);
1428 let delta = messages
1429 .delta(&parent_state, ¶meters, &old_summary)
1430 .unwrap();
1431 assert_eq!(delta.len(), 1);
1432 assert_eq!(delta[0], authorized_message3);
1433
1434 let empty_summary = open_summary(&[]);
1436 let full_delta = messages
1437 .delta(&parent_state, ¶meters, &empty_summary)
1438 .unwrap();
1439 assert_eq!(full_delta.len(), 3);
1440 assert_eq!(full_delta, messages.messages);
1441
1442 let full_summary = open_summary(&[
1444 authorized_message1.id(),
1445 authorized_message2.id(),
1446 authorized_message3.id(),
1447 ]);
1448 let no_delta = messages.delta(&parent_state, ¶meters, &full_summary);
1449 assert!(no_delta.is_none());
1450 }
1451
1452 #[test]
1453 fn test_messages_apply_delta() {
1454 let owner_signing_key = SigningKey::generate(&mut OsRng);
1456 let owner_verifying_key = owner_signing_key.verifying_key();
1457 let owner_id = MemberId::from(&owner_verifying_key);
1458
1459 let author_signing_key = SigningKey::generate(&mut OsRng);
1460 let author_verifying_key = author_signing_key.verifying_key();
1461 let author_id = MemberId::from(&author_verifying_key);
1462
1463 let mut parent_state = ChatRoomStateV1::default();
1464 parent_state.configuration.configuration.max_recent_messages = 3;
1465 parent_state.configuration.configuration.max_message_size = 100;
1466 parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
1467 member: crate::room_state::member::Member {
1468 owner_member_id: owner_id,
1469 invited_by: owner_id,
1470 member_vk: author_verifying_key,
1471 },
1472 signature: owner_signing_key.try_sign(&[0; 32]).unwrap(),
1473 }];
1474
1475 let parameters = ChatRoomParametersV1 {
1476 owner: owner_verifying_key,
1477 };
1478
1479 let create_message = |time: SystemTime| {
1481 let message = MessageV1 {
1482 room_owner: owner_id,
1483 author: author_id,
1484 time,
1485 content: RoomMessageBody::public("Test message".to_string()),
1486 };
1487 AuthorizedMessageV1::new(message, &author_signing_key)
1488 };
1489
1490 let now = SystemTime::now();
1491 let message1 = create_message(now - Duration::from_secs(3));
1492 let message2 = create_message(now - Duration::from_secs(2));
1493 let message3 = create_message(now - Duration::from_secs(1));
1494 let message4 = create_message(now);
1495
1496 let mut messages = MessagesV1 {
1498 messages: vec![message1.clone(), message2.clone()],
1499 ..Default::default()
1500 };
1501
1502 let delta = vec![message3.clone(), message4.clone()];
1504 assert!(messages
1505 .apply_delta(&parent_state, ¶meters, &Some(delta))
1506 .is_ok());
1507
1508 assert_eq!(
1510 messages.messages.len(),
1511 3,
1512 "Should have 3 messages after applying delta"
1513 );
1514 assert!(
1515 !messages.messages.contains(&message1),
1516 "Oldest message should be removed"
1517 );
1518 assert!(
1519 messages.messages.contains(&message2),
1520 "Second oldest message should be retained"
1521 );
1522 assert!(
1523 messages.messages.contains(&message3),
1524 "New message should be added"
1525 );
1526 assert!(
1527 messages.messages.contains(&message4),
1528 "Newest message should be added"
1529 );
1530
1531 let old_message = create_message(now - Duration::from_secs(4));
1533 let delta = vec![old_message.clone()];
1534 assert!(messages
1535 .apply_delta(&parent_state, ¶meters, &Some(delta))
1536 .is_ok());
1537
1538 assert_eq!(messages.messages.len(), 3, "Should still have 3 messages");
1540 assert!(
1541 !messages.messages.contains(&old_message),
1542 "Older message should not be added"
1543 );
1544 assert!(
1545 messages.messages.contains(&message2),
1546 "Message2 should be retained"
1547 );
1548 assert!(
1549 messages.messages.contains(&message3),
1550 "Message3 should be retained"
1551 );
1552 assert!(
1553 messages.messages.contains(&message4),
1554 "Newest message should be retained"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_oversized_message_filtered_by_apply_delta() {
1560 let owner_sk = SigningKey::generate(&mut OsRng);
1561 let owner_vk = owner_sk.verifying_key();
1562 let owner_id = MemberId::from(&owner_vk);
1563
1564 let author_sk = SigningKey::generate(&mut OsRng);
1565 let author_vk = author_sk.verifying_key();
1566 let author_id = MemberId::from(&author_vk);
1567
1568 let mut parent_state = ChatRoomStateV1::default();
1569 parent_state.configuration.configuration.max_message_size = 50;
1570 parent_state.configuration.configuration.max_recent_messages = 10;
1571 parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
1572 member: crate::room_state::member::Member {
1573 owner_member_id: owner_id,
1574 invited_by: owner_id,
1575 member_vk: author_vk,
1576 },
1577 signature: owner_sk.try_sign(&[0; 32]).unwrap(),
1578 }];
1579
1580 let parameters = ChatRoomParametersV1 { owner: owner_vk };
1581
1582 let small_msg = AuthorizedMessageV1::new(
1584 MessageV1 {
1585 room_owner: owner_id,
1586 author: author_id,
1587 time: SystemTime::now(),
1588 content: RoomMessageBody::public("short".to_string()),
1589 },
1590 &author_sk,
1591 );
1592 let big_msg = AuthorizedMessageV1::new(
1593 MessageV1 {
1594 room_owner: owner_id,
1595 author: author_id,
1596 time: SystemTime::now(),
1597 content: RoomMessageBody::public("x".repeat(100)),
1598 },
1599 &author_sk,
1600 );
1601
1602 assert!(small_msg.message.content.content_len() <= 50);
1603 assert!(big_msg.message.content.content_len() > 50);
1604
1605 let mut messages = MessagesV1::default();
1606 let delta = vec![small_msg.clone(), big_msg.clone()];
1607 assert!(messages
1608 .apply_delta(&parent_state, ¶meters, &Some(delta))
1609 .is_ok());
1610
1611 assert_eq!(
1612 messages.messages.len(),
1613 1,
1614 "Only small message should survive"
1615 );
1616 assert!(messages.messages.contains(&small_msg));
1617 assert!(
1618 !messages.messages.contains(&big_msg),
1619 "Oversized message should be filtered"
1620 );
1621 }
1622
1623 #[test]
1624 fn test_message_author_preservation_across_users() {
1625 let user1_sk = SigningKey::generate(&mut OsRng);
1627 let user1_vk = user1_sk.verifying_key();
1628 let user1_id = MemberId::from(&user1_vk);
1629
1630 let user2_sk = SigningKey::generate(&mut OsRng);
1631 let user2_vk = user2_sk.verifying_key();
1632 let user2_id = MemberId::from(&user2_vk);
1633
1634 let owner_sk = SigningKey::generate(&mut OsRng);
1635 let owner_vk = owner_sk.verifying_key();
1636 let owner_id = MemberId::from(&owner_vk);
1637
1638 println!("User1 ID: {}", user1_id);
1639 println!("User2 ID: {}", user2_id);
1640 println!("Owner ID: {}", owner_id);
1641
1642 let msg1 = MessageV1 {
1644 room_owner: owner_id,
1645 author: user1_id,
1646 content: RoomMessageBody::public("Message from user1".to_string()),
1647 time: SystemTime::now(),
1648 };
1649
1650 let msg2 = MessageV1 {
1651 room_owner: owner_id,
1652 author: user2_id,
1653 content: RoomMessageBody::public("Message from user2".to_string()),
1654 time: SystemTime::now() + Duration::from_secs(1),
1655 };
1656
1657 let auth_msg1 = AuthorizedMessageV1::new(msg1.clone(), &user1_sk);
1658 let auth_msg2 = AuthorizedMessageV1::new(msg2.clone(), &user2_sk);
1659
1660 let messages = MessagesV1 {
1662 messages: vec![auth_msg1.clone(), auth_msg2.clone()],
1663 ..Default::default()
1664 };
1665
1666 assert_eq!(messages.messages.len(), 2);
1668
1669 let stored_msg1 = &messages.messages[0];
1670 let stored_msg2 = &messages.messages[1];
1671
1672 assert_eq!(
1673 stored_msg1.message.author, user1_id,
1674 "Message 1 author should be user1, but got {}",
1675 stored_msg1.message.author
1676 );
1677 assert_eq!(
1678 stored_msg2.message.author, user2_id,
1679 "Message 2 author should be user2, but got {}",
1680 stored_msg2.message.author
1681 );
1682
1683 assert_ne!(user1_id, user2_id, "User IDs should be different");
1685
1686 let user1_id_str = user1_id.to_string();
1688 let user2_id_str = user2_id.to_string();
1689
1690 println!("User1 ID string: {}", user1_id_str);
1691 println!("User2 ID string: {}", user2_id_str);
1692
1693 assert_ne!(
1694 user1_id_str, user2_id_str,
1695 "User ID strings should be different"
1696 );
1697 }
1698
1699 #[test]
1700 fn test_edit_action() {
1701 let signing_key = SigningKey::generate(&mut OsRng);
1702 let verifying_key = signing_key.verifying_key();
1703 let owner_id = MemberId::from(&verifying_key);
1704 let author_id = owner_id;
1705
1706 let original_msg = MessageV1 {
1708 room_owner: owner_id,
1709 author: author_id,
1710 time: SystemTime::now(),
1711 content: RoomMessageBody::public("Original content".to_string()),
1712 };
1713 let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1714 let original_id = auth_original.id();
1715
1716 let edit_msg = MessageV1 {
1718 room_owner: owner_id,
1719 author: author_id,
1720 time: SystemTime::now() + Duration::from_secs(1),
1721 content: RoomMessageBody::edit(original_id.clone(), "Edited content".to_string()),
1722 };
1723 let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
1724
1725 let mut messages = MessagesV1 {
1727 messages: vec![auth_original.clone(), auth_edit],
1728 ..Default::default()
1729 };
1730 messages.rebuild_actions_state();
1731
1732 assert!(messages.is_edited(&original_id));
1734 let effective = messages.effective_text(&auth_original);
1735 assert_eq!(effective, Some("Edited content".to_string()));
1736
1737 let display: Vec<_> = messages.display_messages().collect();
1739 assert_eq!(display.len(), 1);
1740 }
1741
1742 #[test]
1743 fn test_edit_by_non_author_ignored() {
1744 let owner_sk = SigningKey::generate(&mut OsRng);
1745 let owner_vk = owner_sk.verifying_key();
1746 let owner_id = MemberId::from(&owner_vk);
1747
1748 let other_sk = SigningKey::generate(&mut OsRng);
1749 let other_id = MemberId::from(&other_sk.verifying_key());
1750
1751 let original_msg = MessageV1 {
1753 room_owner: owner_id,
1754 author: owner_id,
1755 time: SystemTime::now(),
1756 content: RoomMessageBody::public("Original content".to_string()),
1757 };
1758 let auth_original = AuthorizedMessageV1::new(original_msg, &owner_sk);
1759 let original_id = auth_original.id();
1760
1761 let edit_msg = MessageV1 {
1763 room_owner: owner_id,
1764 author: other_id,
1765 time: SystemTime::now() + Duration::from_secs(1),
1766 content: RoomMessageBody::edit(original_id.clone(), "Hacked content".to_string()),
1767 };
1768 let auth_edit = AuthorizedMessageV1::new(edit_msg, &other_sk);
1769
1770 let mut messages = MessagesV1 {
1771 messages: vec![auth_original.clone(), auth_edit],
1772 ..Default::default()
1773 };
1774 messages.rebuild_actions_state();
1775
1776 assert!(!messages.is_edited(&original_id));
1778 let effective = messages.effective_text(&auth_original);
1779 assert_eq!(effective, Some("Original content".to_string()));
1780 }
1781
1782 #[test]
1783 fn test_delete_action() {
1784 let signing_key = SigningKey::generate(&mut OsRng);
1785 let verifying_key = signing_key.verifying_key();
1786 let owner_id = MemberId::from(&verifying_key);
1787
1788 let original_msg = MessageV1 {
1790 room_owner: owner_id,
1791 author: owner_id,
1792 time: SystemTime::now(),
1793 content: RoomMessageBody::public("Will be deleted".to_string()),
1794 };
1795 let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
1796 let original_id = auth_original.id();
1797
1798 let delete_msg = MessageV1 {
1800 room_owner: owner_id,
1801 author: owner_id,
1802 time: SystemTime::now() + Duration::from_secs(1),
1803 content: RoomMessageBody::delete(original_id.clone()),
1804 };
1805 let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
1806
1807 let mut messages = MessagesV1 {
1808 messages: vec![auth_original, auth_delete],
1809 ..Default::default()
1810 };
1811 messages.rebuild_actions_state();
1812
1813 assert!(messages.is_deleted(&original_id));
1815
1816 let display: Vec<_> = messages.display_messages().collect();
1818 assert_eq!(display.len(), 0);
1819 }
1820
1821 #[test]
1822 fn test_reaction_action() {
1823 let user1_sk = SigningKey::generate(&mut OsRng);
1824 let user1_id = MemberId::from(&user1_sk.verifying_key());
1825
1826 let user2_sk = SigningKey::generate(&mut OsRng);
1827 let user2_id = MemberId::from(&user2_sk.verifying_key());
1828
1829 let owner_id = user1_id;
1830
1831 let original_msg = MessageV1 {
1833 room_owner: owner_id,
1834 author: user1_id,
1835 time: SystemTime::now(),
1836 content: RoomMessageBody::public("React to me!".to_string()),
1837 };
1838 let auth_original = AuthorizedMessageV1::new(original_msg, &user1_sk);
1839 let original_id = auth_original.id();
1840
1841 let reaction_msg = MessageV1 {
1843 room_owner: owner_id,
1844 author: user2_id,
1845 time: SystemTime::now() + Duration::from_secs(1),
1846 content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1847 };
1848 let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user2_sk);
1849
1850 let reaction_msg2 = MessageV1 {
1852 room_owner: owner_id,
1853 author: user1_id,
1854 time: SystemTime::now() + Duration::from_secs(2),
1855 content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
1856 };
1857 let auth_reaction2 = AuthorizedMessageV1::new(reaction_msg2, &user1_sk);
1858
1859 let mut messages = MessagesV1 {
1860 messages: vec![auth_original, auth_reaction, auth_reaction2],
1861 ..Default::default()
1862 };
1863 messages.rebuild_actions_state();
1864
1865 let reactions = messages.reactions(&original_id).unwrap();
1867 let thumbs_up = reactions.get("👍").unwrap();
1868 assert_eq!(thumbs_up.len(), 2);
1869 assert!(thumbs_up.contains(&user1_id));
1870 assert!(thumbs_up.contains(&user2_id));
1871 }
1872
1873 #[test]
1874 fn test_remove_reaction_action() {
1875 let user_sk = SigningKey::generate(&mut OsRng);
1876 let user_id = MemberId::from(&user_sk.verifying_key());
1877 let owner_id = user_id;
1878
1879 let original_msg = MessageV1 {
1881 room_owner: owner_id,
1882 author: user_id,
1883 time: SystemTime::now(),
1884 content: RoomMessageBody::public("Test message".to_string()),
1885 };
1886 let auth_original = AuthorizedMessageV1::new(original_msg, &user_sk);
1887 let original_id = auth_original.id();
1888
1889 let reaction_msg = MessageV1 {
1891 room_owner: owner_id,
1892 author: user_id,
1893 time: SystemTime::now() + Duration::from_secs(1),
1894 content: RoomMessageBody::reaction(original_id.clone(), "❤️".to_string()),
1895 };
1896 let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user_sk);
1897
1898 let remove_msg = MessageV1 {
1900 room_owner: owner_id,
1901 author: user_id,
1902 time: SystemTime::now() + Duration::from_secs(2),
1903 content: RoomMessageBody::remove_reaction(original_id.clone(), "❤️".to_string()),
1904 };
1905 let auth_remove = AuthorizedMessageV1::new(remove_msg, &user_sk);
1906
1907 let mut messages = MessagesV1 {
1908 messages: vec![auth_original, auth_reaction, auth_remove],
1909 ..Default::default()
1910 };
1911 messages.rebuild_actions_state();
1912
1913 assert!(messages.reactions(&original_id).is_none());
1915 }
1916
1917 #[test]
1932 fn a_same_millisecond_reaction_swap_converges_either_way() {
1933 let user_sk = SigningKey::generate(&mut OsRng);
1934 let user_id = MemberId::from(&user_sk.verifying_key());
1935 let owner_id = user_id;
1936
1937 let original = AuthorizedMessageV1::new(
1938 MessageV1 {
1939 room_owner: owner_id,
1940 author: user_id,
1941 time: SystemTime::UNIX_EPOCH,
1942 content: RoomMessageBody::public("React to me!".to_string()),
1943 },
1944 &user_sk,
1945 );
1946 let target = original.id();
1947
1948 let react = |emoji: &str, remove: bool, time: SystemTime| {
1949 let content = if remove {
1950 RoomMessageBody::remove_reaction(target.clone(), emoji.to_string())
1951 } else {
1952 RoomMessageBody::reaction(target.clone(), emoji.to_string())
1953 };
1954 AuthorizedMessageV1::new(
1955 MessageV1 {
1956 room_owner: owner_id,
1957 author: user_id,
1958 time,
1959 content,
1960 },
1961 &user_sk,
1962 )
1963 };
1964
1965 let first = react("👍", false, SystemTime::UNIX_EPOCH + Duration::from_secs(1));
1966 let swap_at = SystemTime::UNIX_EPOCH + Duration::from_secs(2);
1968 let remove_old = react("👍", true, swap_at);
1969 let add_new = react("❤️", false, swap_at);
1970
1971 for (label, pair) in [
1972 ("remove first", vec![remove_old.clone(), add_new.clone()]),
1973 ("add first", vec![add_new, remove_old]),
1974 ] {
1975 let mut messages = MessagesV1 {
1976 messages: [vec![original.clone(), first.clone()], pair].concat(),
1977 ..Default::default()
1978 };
1979 let replay_order: Vec<_> = messages.messages.iter().map(|m| m.id()).collect();
1980 messages.rebuild_actions_state();
1981 assert_eq!(
1987 messages.messages.iter().map(|m| m.id()).collect::<Vec<_>>(),
1988 replay_order,
1989 "{label}: rebuild_actions_state must replay in vector order"
1990 );
1991
1992 let reactions = messages
1993 .reactions(&target)
1994 .unwrap_or_else(|| panic!("{label}: the swapped-in reaction must survive"));
1995 assert_eq!(
1996 reactions.get("❤️").map(|r| r.as_slice()),
1997 Some([user_id].as_slice()),
1998 "{label}: the new reaction must be present"
1999 );
2000 assert!(
2001 !reactions.contains_key("👍"),
2002 "{label}: the old reaction must be gone"
2003 );
2004 }
2005 }
2006
2007 #[test]
2008 fn test_action_on_deleted_message_ignored() {
2009 let signing_key = SigningKey::generate(&mut OsRng);
2010 let verifying_key = signing_key.verifying_key();
2011 let owner_id = MemberId::from(&verifying_key);
2012
2013 let original_msg = MessageV1 {
2015 room_owner: owner_id,
2016 author: owner_id,
2017 time: SystemTime::now(),
2018 content: RoomMessageBody::public("Will be deleted".to_string()),
2019 };
2020 let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
2021 let original_id = auth_original.id();
2022
2023 let delete_msg = MessageV1 {
2025 room_owner: owner_id,
2026 author: owner_id,
2027 time: SystemTime::now() + Duration::from_secs(1),
2028 content: RoomMessageBody::delete(original_id.clone()),
2029 };
2030 let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
2031
2032 let edit_msg = MessageV1 {
2034 room_owner: owner_id,
2035 author: owner_id,
2036 time: SystemTime::now() + Duration::from_secs(2),
2037 content: RoomMessageBody::edit(original_id.clone(), "Too late!".to_string()),
2038 };
2039 let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
2040
2041 let mut messages = MessagesV1 {
2042 messages: vec![auth_original, auth_delete, auth_edit],
2043 ..Default::default()
2044 };
2045 messages.rebuild_actions_state();
2046
2047 assert!(messages.is_deleted(&original_id));
2049 assert!(!messages.is_edited(&original_id));
2050 }
2051
2052 #[test]
2053 fn test_display_messages_filters_actions() {
2054 let signing_key = SigningKey::generate(&mut OsRng);
2055 let verifying_key = signing_key.verifying_key();
2056 let owner_id = MemberId::from(&verifying_key);
2057
2058 let msg1 = MessageV1 {
2060 room_owner: owner_id,
2061 author: owner_id,
2062 time: SystemTime::now(),
2063 content: RoomMessageBody::public("Hello".to_string()),
2064 };
2065 let auth_msg1 = AuthorizedMessageV1::new(msg1, &signing_key);
2066 let msg1_id = auth_msg1.id();
2067
2068 let reaction_msg = MessageV1 {
2070 room_owner: owner_id,
2071 author: owner_id,
2072 time: SystemTime::now() + Duration::from_secs(1),
2073 content: RoomMessageBody::reaction(msg1_id, "👍".to_string()),
2074 };
2075 let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &signing_key);
2076
2077 let msg2 = MessageV1 {
2079 room_owner: owner_id,
2080 author: owner_id,
2081 time: SystemTime::now() + Duration::from_secs(2),
2082 content: RoomMessageBody::public("World".to_string()),
2083 };
2084 let auth_msg2 = AuthorizedMessageV1::new(msg2, &signing_key);
2085
2086 let mut messages = MessagesV1 {
2087 messages: vec![auth_msg1, auth_reaction, auth_msg2],
2088 ..Default::default()
2089 };
2090 messages.rebuild_actions_state();
2091
2092 let display: Vec<_> = messages.display_messages().collect();
2094 assert_eq!(display.len(), 2);
2095 assert_eq!(
2096 display[0].message.content.as_public_string(),
2097 Some("Hello".to_string())
2098 );
2099 assert_eq!(
2100 display[1].message.content.as_public_string(),
2101 Some("World".to_string())
2102 );
2103 }
2104}
2105
2106#[cfg(test)]
2107mod measure_tests {
2108 use super::*;
2109
2110 fn samples() -> Vec<String> {
2114 vec![
2115 String::new(),
2116 "a".repeat(1),
2117 "a".repeat(23),
2118 "a".repeat(24),
2119 "a".repeat(255),
2120 "a".repeat(256),
2121 "a".repeat(997),
2122 "a".repeat(998),
2123 "a".repeat(1000),
2124 "é".repeat(400), "🎉".repeat(200), format!("{}é🎉", "a".repeat(990)),
2127 ]
2128 }
2129
2130 fn target_id() -> MessageId {
2131 MessageId(FastHash(0x1234_5678_9abc_def0_u64 as i64))
2132 }
2133
2134 #[test]
2135 fn measure_text_matches_public_body() {
2136 for text in samples() {
2137 let body = RoomMessageBody::public(text.clone());
2138 assert_eq!(
2139 RoomMessageBody::measure_text(&text, false),
2140 body.content_len(),
2141 "text bytes={} chars={}",
2142 text.len(),
2143 text.chars().count()
2144 );
2145 }
2146 }
2147
2148 #[test]
2149 fn measure_reply_matches_reply_body() {
2150 let previews = ["", "short", &"préview🎉 ".repeat(10)];
2151 let authors = ["", "Alice", "HöstFat"];
2152 for text in samples() {
2153 for preview in previews {
2154 for author in authors {
2155 let body = RoomMessageBody::reply(
2156 text.clone(),
2157 target_id(),
2158 author.to_string(),
2159 preview.to_string(),
2160 );
2161 assert_eq!(
2162 RoomMessageBody::measure_reply(&text, target_id(), author, preview, false),
2163 body.content_len(),
2164 "text bytes={} author={:?} preview bytes={}",
2165 text.len(),
2166 author,
2167 preview.len()
2168 );
2169 }
2170 }
2171 }
2172 }
2173
2174 #[test]
2175 fn measure_edit_matches_edit_body() {
2176 for text in samples() {
2177 let body = RoomMessageBody::edit(target_id(), text.clone());
2178 let measured = RoomMessageBody::measure_edit(target_id(), &text, false);
2179 assert_eq!(measured, body.content_len(), "text bytes={}", text.len());
2180
2181 assert!(
2188 measured <= text.len() + 80,
2189 "edit overhead must be a small constant: {measured} bytes for {} text bytes",
2190 text.len()
2191 );
2192 }
2193 }
2194
2195 #[test]
2200 fn raw_text_gate_undercounts_encoded_size() {
2201 let max = 1000;
2202 let text = "a".repeat(998); assert!(text.len() <= max);
2204 assert!(RoomMessageBody::measure_text(&text, false) > max);
2205
2206 let reply_text = "a".repeat(900);
2208 assert!(reply_text.len() <= max);
2209 assert!(
2210 RoomMessageBody::measure_reply(
2211 &reply_text,
2212 target_id(),
2213 "Alice",
2214 &"p".repeat(100),
2215 false
2216 ) > max
2217 );
2218 }
2219
2220 #[test]
2242 fn edit_cost_is_not_proportional_to_length() {
2243 for n in [100usize, 400, 900] {
2244 let text = "a".repeat(n);
2245 let overhead = RoomMessageBody::measure_edit(target_id(), &text, false) - n;
2246 assert!(
2247 overhead < 80,
2248 "edit overhead must be a small constant, got {overhead} bytes for {n} chars"
2249 );
2250 }
2251
2252 let text = "a".repeat(900);
2255 assert!(RoomMessageBody::measure_edit(target_id(), &text, false) <= 1000);
2256
2257 }
2264
2265 #[test]
2270 fn edit_overhead_over_send_is_a_small_constant() {
2271 let mut overheads = vec![];
2272 for n in [10usize, 100, 500, 900] {
2273 let text = "a".repeat(n);
2274 let send = RoomMessageBody::measure_text(&text, false);
2275 let edit = RoomMessageBody::measure_edit(target_id(), &text, false);
2276 assert!(
2277 edit > send,
2278 "an edit carries strictly more framing than a send"
2279 );
2280 overheads.push(edit - send);
2281 }
2282
2283 let max_overhead = *overheads.iter().max().expect("non-empty");
2284 assert!(
2285 max_overhead <= 60,
2286 "edit-over-send overhead must stay a small constant, got {overheads:?}"
2287 );
2288 let min_overhead = *overheads.iter().min().expect("non-empty");
2290 assert!(
2291 max_overhead - min_overhead <= 4,
2292 "edit-over-send overhead must not scale with length, got {overheads:?}"
2293 );
2294 }
2295
2296 #[cfg(feature = "ecies-randomized")]
2297 mod private_bodies {
2298 use super::*;
2299 use crate::ecies::encrypt_with_symmetric_key;
2303 use crate::room_state::content::{
2304 CONTENT_TYPE_REPLY, CONTENT_TYPE_TEXT, REPLY_CONTENT_VERSION, TEXT_CONTENT_VERSION,
2305 };
2306
2307 const SECRET: [u8; 32] = [7u8; 32];
2308
2309 #[test]
2310 fn measure_text_matches_private_body() {
2311 for text in samples() {
2312 let content_bytes =
2313 crate::room_state::content::TextContentV1::new(text.clone()).encode();
2314 let (ciphertext, nonce) = encrypt_with_symmetric_key(&SECRET, &content_bytes);
2315 let body = RoomMessageBody::private(
2316 CONTENT_TYPE_TEXT,
2317 TEXT_CONTENT_VERSION,
2318 ciphertext,
2319 nonce,
2320 1,
2321 );
2322 assert_eq!(
2323 RoomMessageBody::measure_text(&text, true),
2324 body.content_len(),
2325 "text bytes={}",
2326 text.len()
2327 );
2328 }
2329 }
2330
2331 #[test]
2332 fn measure_reply_matches_private_body() {
2333 for text in samples() {
2334 let reply = crate::room_state::content::ReplyContentV1::new(
2335 text.clone(),
2336 target_id(),
2337 "Alice".to_string(),
2338 "some preview".to_string(),
2339 );
2340 let (ciphertext, nonce) = encrypt_with_symmetric_key(&SECRET, &reply.encode());
2341 let body = RoomMessageBody::private(
2342 CONTENT_TYPE_REPLY,
2343 REPLY_CONTENT_VERSION,
2344 ciphertext,
2345 nonce,
2346 1,
2347 );
2348 assert_eq!(
2349 RoomMessageBody::measure_reply(
2350 &text,
2351 target_id(),
2352 "Alice",
2353 "some preview",
2354 true
2355 ),
2356 body.content_len(),
2357 "text bytes={}",
2358 text.len()
2359 );
2360 }
2361 }
2362
2363 #[test]
2364 fn measure_edit_matches_private_body() {
2365 for text in samples() {
2366 let action =
2367 crate::room_state::content::ActionContentV1::edit(target_id(), text.clone());
2368 let (ciphertext, nonce) = encrypt_with_symmetric_key(&SECRET, &action.encode());
2369 let body = RoomMessageBody::private_action(ciphertext, nonce, 1);
2370 assert_eq!(
2371 RoomMessageBody::measure_edit(target_id(), &text, true),
2372 body.content_len(),
2373 "text bytes={}",
2374 text.len()
2375 );
2376 }
2377 }
2378 }
2379}