1use crate::types::{AppId, HopCount, LamportClock, MetadataMap, Timestamp, UserId, TTL};
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec;
7use alloc::vec::Vec;
8use core::fmt;
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct MessageId(Uuid);
15
16impl MessageId {
17 #[cfg(feature = "std")]
24 pub fn new() -> Self {
25 Self(Uuid::new_v4())
26 }
27
28 #[allow(clippy::should_implement_trait)]
30 pub fn from_str(id: &str) -> crate::Result<Self> {
31 let uuid = Uuid::parse_str(id)
32 .map_err(|e| crate::Error::InvalidMessage(format!("Invalid message id: {}", e)))?;
33 Ok(Self::from_uuid(uuid))
34 }
35
36 pub fn from_uuid(uuid: Uuid) -> Self {
38 Self(uuid)
39 }
40
41 pub fn as_str(&self) -> String {
43 self.0.to_string()
44 }
45
46 pub fn as_bytes(&self) -> [u8; 16] {
51 *self.0.as_bytes()
52 }
53
54 pub fn from_bytes(bytes: [u8; 16]) -> Self {
56 Self(Uuid::from_bytes(bytes))
57 }
58}
59
60#[cfg(feature = "std")]
61impl Default for MessageId {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl fmt::Display for MessageId {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
100pub enum ContentType {
101 #[default]
103 Text,
104 Image,
106 Video,
108 Audio,
110 VoiceNote,
112 VideoNote,
114 File,
116 FileChunk,
118 Poll,
120}
121
122impl ContentType {
123 pub const ALL: [Self; 9] = [
129 Self::Text,
130 Self::Image,
131 Self::Video,
132 Self::Audio,
133 Self::VoiceNote,
134 Self::VideoNote,
135 Self::File,
136 Self::FileChunk,
137 Self::Poll,
138 ];
139
140 pub fn is_media(&self) -> bool {
142 !matches!(self, Self::Text | Self::Poll)
143 }
144
145 pub const fn as_wire_str(self) -> &'static str {
152 match self {
153 Self::Text => "text",
154 Self::Image => "image",
155 Self::Video => "video",
156 Self::Audio => "audio",
157 Self::VoiceNote => "voice_note",
158 Self::VideoNote => "video_note",
159 Self::File => "file",
160 Self::FileChunk => "file_chunk",
161 Self::Poll => "poll",
162 }
163 }
164
165 pub fn parse(s: &str) -> Self {
174 match s {
175 "text" => Self::Text,
176 "image" => Self::Image,
177 "video" => Self::Video,
178 "audio" => Self::Audio,
179 "voice_note" => Self::VoiceNote,
180 "video_note" => Self::VideoNote,
181 "file" => Self::File,
182 "file_chunk" => Self::FileChunk,
183 "poll" => Self::Poll,
184 _ => Self::File,
185 }
186 }
187}
188
189const _: () = {
202 const fn listed_at_own_index(ct: ContentType) -> bool {
203 match ct {
204 ContentType::Text => matches!(ContentType::ALL[0], ContentType::Text),
205 ContentType::Image => matches!(ContentType::ALL[1], ContentType::Image),
206 ContentType::Video => matches!(ContentType::ALL[2], ContentType::Video),
207 ContentType::Audio => matches!(ContentType::ALL[3], ContentType::Audio),
208 ContentType::VoiceNote => matches!(ContentType::ALL[4], ContentType::VoiceNote),
209 ContentType::VideoNote => matches!(ContentType::ALL[5], ContentType::VideoNote),
210 ContentType::File => matches!(ContentType::ALL[6], ContentType::File),
211 ContentType::FileChunk => matches!(ContentType::ALL[7], ContentType::FileChunk),
212 ContentType::Poll => matches!(ContentType::ALL[8], ContentType::Poll),
213 }
214 }
215
216 let mut i = 0;
217 while i < ContentType::ALL.len() {
218 assert!(listed_at_own_index(ContentType::ALL[i]));
219 i += 1;
220 }
221};
222
223impl Serialize for ContentType {
228 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
229 where
230 S: serde::Serializer,
231 {
232 serializer.serialize_str(self.as_wire_str())
233 }
234}
235
236impl<'de> Deserialize<'de> for ContentType {
242 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243 where
244 D: serde::Deserializer<'de>,
245 {
246 struct ContentTypeVisitor;
247
248 impl serde::de::Visitor<'_> for ContentTypeVisitor {
249 type Value = ContentType;
250
251 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str("a content type string")
253 }
254
255 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<ContentType, E> {
256 Ok(ContentType::parse(s))
257 }
258 }
259
260 deserializer.deserialize_str(ContentTypeVisitor)
261 }
262}
263
264impl fmt::Display for ContentType {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.write_str(self.as_wire_str())
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct MediaMetadata {
273 pub mime_type: String,
275
276 pub file_name: String,
278
279 pub file_size: u64,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub duration_ms: Option<u64>,
285
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub width: Option<u32>,
289
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub height: Option<u32>,
293
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub thumbnail_base64: Option<String>,
297
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub media_id: Option<String>,
301
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub download_url: Option<String>,
305
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub thumbnail_url: Option<String>,
309
310 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub encryption_key: Option<String>,
320
321 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub iv: Option<String>,
325
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub ciphertext_hash: Option<String>,
329
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub sticker_provider: Option<String>,
333
334 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub sticker_remote_id: Option<String>,
337
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub sticker_kind: Option<String>,
341}
342
343impl MediaMetadata {
344 pub fn has_secrets(&self) -> bool {
347 self.encryption_key.is_some() || self.iv.is_some()
348 }
349
350 pub fn without_secrets(&self) -> Self {
360 Self {
361 encryption_key: None,
362 iv: None,
363 ..self.clone()
364 }
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375pub struct ForwardInfo {
376 pub original_sender: UserId,
378 pub original_message_id: MessageId,
380 pub original_timestamp: Timestamp,
382 pub forward_count: u32,
384}
385
386impl ForwardInfo {
387 pub fn from_message(message: &Message) -> Self {
393 match &message.forwarded_from {
394 Some(existing) => ForwardInfo {
395 original_sender: existing.original_sender.clone(),
396 original_message_id: existing.original_message_id.clone(),
397 original_timestamp: existing.original_timestamp,
398 forward_count: existing.forward_count.saturating_add(1),
399 },
400 None => ForwardInfo {
401 original_sender: message.sender.clone(),
402 original_message_id: message.id.clone(),
403 original_timestamp: message.timestamp,
404 forward_count: 1,
405 },
406 }
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
424pub struct ReplyContext {
425 pub sender: UserId,
427 pub text: String,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub timestamp: Option<Timestamp>,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub reply_media_label: Option<String>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub reply_content_type: Option<String>,
438}
439
440#[derive(
447 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
448)]
449#[serde(rename_all = "lowercase")]
450pub enum MessagePriority {
451 #[serde(alias = "Low")]
453 Low,
454 #[default]
456 #[serde(alias = "Medium")]
457 Medium,
458 #[serde(alias = "High")]
460 High,
461 #[serde(alias = "Critical")]
463 Critical,
464}
465
466impl MessagePriority {
467 pub fn score(&self) -> u8 {
469 match self {
470 Self::Low => 1,
471 Self::Medium => 2,
472 Self::High => 3,
473 Self::Critical => 4,
474 }
475 }
476
477 pub fn as_str(&self) -> &'static str {
479 match self {
480 Self::Low => "Low",
481 Self::Medium => "Medium",
482 Self::High => "High",
483 Self::Critical => "Critical",
484 }
485 }
486}
487
488#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
496pub enum WireCodec {
497 #[default]
499 Json,
500 BinaryV1,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct Message {
507 pub id: MessageId,
509
510 pub sender: UserId,
512
513 pub recipient: UserId,
515
516 pub app_id: AppId,
518
519 pub priority: MessagePriority,
521
522 pub ttl: TTL,
524
525 pub hop_count: HopCount,
527
528 pub timestamp: Timestamp,
530
531 #[serde(default)]
533 pub lamport_clock: LamportClock,
534
535 #[serde(default)]
537 pub content_type: ContentType,
538
539 pub content: String,
541
542 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub binary_content: Option<Vec<u8>>,
547
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub media_metadata: Option<MediaMetadata>,
551
552 #[serde(default)]
554 pub metadata: MetadataMap,
555
556 #[serde(default = "default_requires_ack")]
558 pub requires_ack: bool,
559
560 #[serde(default)]
562 pub reply_to_msg: Option<MessageId>,
563
564 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub forwarded_from: Option<ForwardInfo>,
567
568 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub reply_context: Option<ReplyContext>,
571
572 #[serde(skip)]
588 transport_peer_id: Option<String>,
589
590 #[serde(skip)]
595 wire_codec: WireCodec,
596}
597
598fn default_requires_ack() -> bool {
599 true
600}
601
602impl Message {
603 #[cfg(feature = "std")]
616 pub fn new(
617 sender: UserId,
618 recipient: UserId,
619 app_id: AppId,
620 content: impl Into<String>,
621 ) -> Self {
622 Self::from_parts(
623 MessageId::new(),
624 sender,
625 recipient,
626 app_id,
627 content,
628 Timestamp::now(),
629 )
630 }
631
632 pub fn from_parts(
662 id: MessageId,
663 sender: UserId,
664 recipient: UserId,
665 app_id: AppId,
666 content: impl Into<String>,
667 timestamp: Timestamp,
668 ) -> Self {
669 Self {
670 id,
671 sender,
672 recipient,
673 app_id,
674 priority: MessagePriority::default(),
675 ttl: TTL::default(),
676 hop_count: HopCount::new(),
677 timestamp,
678 lamport_clock: LamportClock::default(),
679 content_type: ContentType::default(),
680 content: content.into(),
681 binary_content: None,
682 media_metadata: None,
683 metadata: MetadataMap::new(),
684 requires_ack: true,
685 reply_to_msg: None,
686 forwarded_from: None,
687 reply_context: None,
688 transport_peer_id: None,
689 wire_codec: WireCodec::Json,
690 }
691 }
692
693 #[cfg(feature = "std")]
697 pub fn builder(sender: UserId, recipient: UserId, app_id: AppId) -> MessageBuilder {
698 MessageBuilder::new(sender, recipient, app_id)
699 }
700
701 pub fn increment_hop(&mut self) -> crate::Result<()> {
707 self.hop_count = self
708 .hop_count
709 .increment()
710 .ok_or(crate::Error::InvalidHopCount(u8::MAX))?;
711 Ok(())
712 }
713
714 pub fn decrement_ttl(&mut self) -> crate::Result<()> {
720 self.ttl = self.ttl.decrement().ok_or(crate::Error::InvalidTTL(0))?;
721 Ok(())
722 }
723
724 pub fn is_ttl_exhausted(&self) -> bool {
726 self.ttl.is_exhausted()
727 }
728
729 pub fn to_json(&self) -> crate::Result<String> {
731 serde_json::to_string(self).map_err(|e| crate::Error::SerializationError(e.to_string()))
732 }
733
734 pub fn from_json(json: &str) -> crate::Result<Self> {
736 serde_json::from_str(json).map_err(|e| crate::Error::DeserializationError(e.to_string()))
737 }
738
739 #[doc(hidden)]
762 pub fn set_transport_peer_id(&mut self, peer_id: String) -> crate::Result<()> {
763 if peer_id.is_empty() {
764 return Err(crate::Error::InvalidMessage(
765 "transport_peer_id must not be empty".to_string(),
766 ));
767 }
768 self.transport_peer_id = Some(peer_id);
769 Ok(())
770 }
771
772 pub fn transport_peer_id(&self) -> Option<&str> {
774 self.transport_peer_id.as_deref()
775 }
776
777 #[doc(hidden)]
783 pub fn set_wire_codec(&mut self, codec: WireCodec) {
784 self.wire_codec = codec;
785 }
786
787 pub fn wire_codec(&self) -> WireCodec {
789 self.wire_codec
790 }
791
792 pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
794 serde_json::to_vec(self).map_err(|e| crate::Error::SerializationError(e.to_string()))
795 }
796
797 pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
799 serde_json::from_slice(bytes).map_err(|e| crate::Error::DeserializationError(e.to_string()))
800 }
801
802 pub fn to_wire_v1_bytes(&self) -> crate::Result<Vec<u8>> {
808 let media_metadata_json =
809 match &self.media_metadata {
810 Some(m) => Some(serde_json::to_vec(m).map_err(|e| {
811 crate::Error::SerializationError(format!("media_metadata: {e}"))
812 })?),
813 None => None,
814 };
815 let forwarded_from_json =
816 match &self.forwarded_from {
817 Some(f) => Some(serde_json::to_vec(f).map_err(|e| {
818 crate::Error::SerializationError(format!("forwarded_from: {e}"))
819 })?),
820 None => None,
821 };
822 let mut metadata: Vec<(String, String)> = self
823 .metadata
824 .iter()
825 .map(|(k, v)| (k.clone(), v.clone()))
826 .collect();
827 metadata.sort_by(|a, b| a.0.cmp(&b.0));
830
831 let (content, mut ext) = match crate::wire::split_b64_tail(&self.content) {
836 Some((head, raw)) => (head.to_string(), vec![(crate::wire::EXT_TAG_B64_TAIL, raw)]),
837 None => (self.content.clone(), Vec::new()),
838 };
839
840 if let Some(rc) = &self.reply_context {
845 ext.push((
846 crate::wire::EXT_TAG_REPLY_CONTEXT,
847 serde_json::to_vec(rc)
848 .map_err(|e| crate::Error::SerializationError(format!("reply_context: {e}")))?,
849 ));
850 }
851
852 let wire = crate::wire::WireMessageV1 {
853 id: self.id.as_bytes(),
854 sender: self.sender.as_str().to_string(),
855 recipient: self.recipient.as_str().to_string(),
856 app_id: self.app_id.as_str().to_string(),
857 priority: crate::wire::priority_to_u8(self.priority),
858 ttl: self.ttl.value(),
859 hop_count: self.hop_count.value(),
860 timestamp: self.timestamp.as_millis(),
861 lamport_clock: self.lamport_clock.value(),
862 content_type: crate::wire::content_type_to_u8(self.content_type),
863 content,
864 binary_content: self.binary_content.clone(),
865 media_metadata_json,
866 metadata,
867 requires_ack: self.requires_ack,
868 reply_to_msg: self.reply_to_msg.as_ref().map(|m| m.as_bytes()),
869 forwarded_from_json,
870 ext,
871 };
872 crate::wire::encode(&wire)
873 }
874
875 pub fn from_wire_v1_bytes(data: &[u8]) -> crate::Result<Self> {
882 let wire = crate::wire::decode(data)?;
883 let mut content = wire.content;
888 if let Some((_, raw)) = wire
889 .ext
890 .iter()
891 .find(|(tag, _)| *tag == crate::wire::EXT_TAG_B64_TAIL)
892 {
893 content.push_str(&crate::wire::encode_b64_tail(raw));
894 }
895 let media_metadata =
896 match wire.media_metadata_json {
897 Some(bytes) => Some(serde_json::from_slice(&bytes).map_err(|e| {
898 crate::Error::DeserializationError(format!("media_metadata: {e}"))
899 })?),
900 None => None,
901 };
902 let forwarded_from =
903 match wire.forwarded_from_json {
904 Some(bytes) => Some(serde_json::from_slice(&bytes).map_err(|e| {
905 crate::Error::DeserializationError(format!("forwarded_from: {e}"))
906 })?),
907 None => None,
908 };
909 let reply_context =
913 match wire
914 .ext
915 .iter()
916 .find(|(tag, _)| *tag == crate::wire::EXT_TAG_REPLY_CONTEXT)
917 {
918 Some((_, bytes)) => Some(serde_json::from_slice(bytes).map_err(|e| {
919 crate::Error::DeserializationError(format!("reply_context: {e}"))
920 })?),
921 None => None,
922 };
923 Ok(Self {
924 id: MessageId::from_bytes(wire.id),
925 sender: UserId::new(wire.sender)?,
926 recipient: UserId::new(wire.recipient)?,
927 app_id: AppId::new(wire.app_id)?,
928 priority: crate::wire::priority_from_u8(wire.priority),
929 ttl: TTL::from_value(wire.ttl),
930 hop_count: HopCount::from_value(wire.hop_count),
931 timestamp: Timestamp::from_millis(wire.timestamp),
932 lamport_clock: LamportClock::from_value(wire.lamport_clock),
933 content_type: crate::wire::content_type_from_u8(wire.content_type),
934 content,
935 binary_content: wire.binary_content,
936 media_metadata,
937 metadata: wire.metadata.into_iter().collect(),
938 requires_ack: wire.requires_ack,
939 reply_to_msg: wire.reply_to_msg.map(MessageId::from_bytes),
940 forwarded_from,
941 reply_context,
942 transport_peer_id: None,
943 wire_codec: WireCodec::Json,
944 })
945 }
946}
947
948#[cfg(feature = "std")]
952pub struct MessageBuilder {
953 sender: UserId,
954 recipient: UserId,
955 app_id: AppId,
956 content_type: ContentType,
957 content: String,
958 media_metadata: Option<MediaMetadata>,
959 priority: MessagePriority,
960 ttl: TTL,
961 lamport_clock: LamportClock,
962 metadata: MetadataMap,
963 requires_ack: bool,
964 reply_to_msg: Option<MessageId>,
965 forwarded_from: Option<ForwardInfo>,
966 reply_context: Option<ReplyContext>,
967}
968
969#[cfg(feature = "std")]
970impl MessageBuilder {
971 pub fn new(sender: UserId, recipient: UserId, app_id: AppId) -> Self {
973 Self {
974 sender,
975 recipient,
976 app_id,
977 content_type: ContentType::default(),
978 content: String::new(),
979 media_metadata: None,
980 priority: MessagePriority::default(),
981 ttl: TTL::default(),
982 lamport_clock: LamportClock::default(),
983 metadata: MetadataMap::new(),
984 requires_ack: true,
985 reply_to_msg: None,
986 forwarded_from: None,
987 reply_context: None,
988 }
989 }
990
991 pub fn content_type(mut self, content_type: ContentType) -> Self {
993 self.content_type = content_type;
994 self
995 }
996
997 pub fn content(mut self, content: impl Into<String>) -> Self {
999 self.content = content.into();
1000 self
1001 }
1002
1003 pub fn media_metadata(mut self, meta: MediaMetadata) -> Self {
1005 self.media_metadata = Some(meta);
1006 self
1007 }
1008
1009 pub fn priority(mut self, priority: MessagePriority) -> Self {
1011 self.priority = priority;
1012 self
1013 }
1014
1015 pub fn ttl(mut self, ttl: TTL) -> Self {
1017 self.ttl = ttl;
1018 self
1019 }
1020
1021 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1023 self.metadata.insert(key.into(), value.into());
1024 self
1025 }
1026
1027 pub fn requires_ack(mut self, requires_ack: bool) -> Self {
1029 self.requires_ack = requires_ack;
1030 self
1031 }
1032
1033 pub fn reply_to_msg(mut self, reply_to: MessageId) -> Self {
1035 self.reply_to_msg = Some(reply_to);
1036 self
1037 }
1038
1039 pub fn forwarded_from(mut self, info: ForwardInfo) -> Self {
1041 self.forwarded_from = Some(info);
1042 self
1043 }
1044
1045 pub fn reply_context(mut self, ctx: ReplyContext) -> Self {
1047 self.reply_context = Some(ctx);
1048 self
1049 }
1050
1051 pub fn lamport_clock(mut self, clock: LamportClock) -> Self {
1053 self.lamport_clock = clock;
1054 self
1055 }
1056
1057 pub fn build(self) -> Message {
1059 Message {
1060 id: MessageId::new(),
1061 sender: self.sender,
1062 recipient: self.recipient,
1063 app_id: self.app_id,
1064 priority: self.priority,
1065 ttl: self.ttl,
1066 hop_count: HopCount::new(),
1067 timestamp: Timestamp::now(),
1068 lamport_clock: self.lamport_clock,
1069 content_type: self.content_type,
1070 content: self.content,
1071 binary_content: None,
1072 media_metadata: self.media_metadata,
1073 metadata: self.metadata,
1074 requires_ack: self.requires_ack,
1075 reply_to_msg: self.reply_to_msg,
1076 forwarded_from: self.forwarded_from,
1077 reply_context: self.reply_context,
1078 transport_peer_id: None,
1079 wire_codec: WireCodec::Json,
1080 }
1081 }
1082}
1083
1084#[cfg(all(test, feature = "std"))]
1085mod tests {
1086 use super::*;
1087
1088 fn create_test_message() -> Message {
1089 Message::new(
1090 UserId::new("alice").unwrap(),
1091 UserId::new("bob").unwrap(),
1092 AppId::new("test-app").unwrap(),
1093 "Hello, Bob!",
1094 )
1095 }
1096
1097 #[test]
1098 fn test_message_creation() {
1099 let msg = create_test_message();
1100 assert_eq!(msg.sender.as_str(), "alice");
1101 assert_eq!(msg.recipient.as_str(), "bob");
1102 assert_eq!(msg.content, "Hello, Bob!");
1103 assert_eq!(msg.priority, MessagePriority::Medium);
1104 assert_eq!(msg.hop_count.value(), 0);
1105 }
1106
1107 #[test]
1108 fn test_message_builder() {
1109 let msg = Message::builder(
1110 UserId::new("alice").unwrap(),
1111 UserId::new("bob").unwrap(),
1112 AppId::new("test-app").unwrap(),
1113 )
1114 .content("Test message")
1115 .priority(MessagePriority::High)
1116 .metadata("key1", "value1")
1117 .requires_ack(false)
1118 .build();
1119
1120 assert_eq!(msg.content, "Test message");
1121 assert_eq!(msg.priority, MessagePriority::High);
1122 assert_eq!(msg.metadata.get("key1").unwrap(), "value1");
1123 assert!(!msg.requires_ack);
1124 }
1125
1126 #[test]
1127 fn reply_context_json_round_trip_and_default() {
1128 let mut msg = create_test_message();
1129 msg.reply_context = Some(ReplyContext {
1130 sender: UserId::new("carol").unwrap(),
1131 text: "quoted".into(),
1132 timestamp: Some(Timestamp::from_millis(1_700_000_000_000)),
1133 reply_media_label: None,
1134 reply_content_type: Some("text".into()),
1135 });
1136 let decoded = Message::from_bytes(&msg.to_bytes().unwrap()).unwrap();
1137 assert_eq!(decoded.reply_context, msg.reply_context);
1138
1139 let plain = create_test_message();
1141 let json = serde_json::to_string(&plain).unwrap();
1142 assert!(!json.contains("reply_context"));
1143 let decoded = Message::from_bytes(json.as_bytes()).unwrap();
1144 assert!(decoded.reply_context.is_none());
1145 }
1146
1147 #[test]
1148 fn media_metadata_without_secrets_clears_only_key_material() {
1149 let meta = MediaMetadata {
1150 mime_type: "image/jpeg".into(),
1151 file_name: "x.jpg".into(),
1152 file_size: 10,
1153 duration_ms: None,
1154 width: Some(1),
1155 height: Some(1),
1156 thumbnail_base64: None,
1157 media_id: Some("m1".into()),
1158 download_url: Some("https://cdn.example/m1".into()),
1159 thumbnail_url: Some("https://cdn.example/m1-t".into()),
1160 encryption_key: Some("a2V5".into()),
1161 iv: Some("aXY=".into()),
1162 ciphertext_hash: Some("aGFzaA==".into()),
1163 sticker_provider: Some("prov".into()),
1164 sticker_remote_id: Some("rid".into()),
1165 sticker_kind: Some("static".into()),
1166 };
1167 assert!(meta.has_secrets());
1168
1169 let clean = meta.without_secrets();
1170 assert!(!clean.has_secrets());
1171 assert!(clean.encryption_key.is_none());
1172 assert!(clean.iv.is_none());
1173 assert_eq!(clean.media_id, meta.media_id);
1175 assert_eq!(clean.download_url, meta.download_url);
1176 assert_eq!(clean.thumbnail_url, meta.thumbnail_url);
1177 assert_eq!(clean.ciphertext_hash, meta.ciphertext_hash);
1178 assert_eq!(clean.sticker_provider, meta.sticker_provider);
1179 assert_eq!(clean.sticker_remote_id, meta.sticker_remote_id);
1180 assert_eq!(clean.sticker_kind, meta.sticker_kind);
1181 assert_eq!(clean.file_name, meta.file_name);
1182 }
1183
1184 #[test]
1185 fn message_json_ignores_unknown_fields() {
1186 let mut json = serde_json::to_value(create_test_message()).unwrap();
1190 json["some_future_field"] = serde_json::Value::String("x".into());
1191 let bytes = serde_json::to_vec(&json).unwrap();
1192 assert!(Message::from_bytes(&bytes).is_ok());
1193 }
1194
1195 #[test]
1196 fn test_message_hop_operations() {
1197 let mut msg = create_test_message();
1198 assert_eq!(msg.hop_count.value(), 0);
1199
1200 msg.increment_hop().unwrap();
1201 assert_eq!(msg.hop_count.value(), 1);
1202 }
1203
1204 #[test]
1205 fn test_message_ttl_operations() {
1206 let mut msg = create_test_message();
1207 let initial_ttl = msg.ttl.value();
1208
1209 msg.decrement_ttl().unwrap();
1210 assert_eq!(msg.ttl.value(), initial_ttl - 1);
1211 }
1212
1213 #[test]
1214 fn test_message_serialization() {
1215 let msg = create_test_message();
1216
1217 let json = msg.to_json().unwrap();
1219 let deserialized = Message::from_json(&json).unwrap();
1220 assert_eq!(msg.id, deserialized.id);
1221 assert_eq!(msg.sender, deserialized.sender);
1222 assert_eq!(msg.content, deserialized.content);
1223
1224 let bytes = msg.to_bytes().unwrap();
1226 let deserialized = Message::from_bytes(&bytes).unwrap();
1227 assert_eq!(msg.id, deserialized.id);
1228 }
1229
1230 #[test]
1231 fn test_message_priority_score() {
1232 assert_eq!(MessagePriority::Low.score(), 1);
1233 assert_eq!(MessagePriority::Medium.score(), 2);
1234 assert_eq!(MessagePriority::High.score(), 3);
1235 assert_eq!(MessagePriority::Critical.score(), 4);
1236 }
1237
1238 #[test]
1239 fn test_forward_info_serialization() {
1240 use crate::types::Timestamp;
1241
1242 let info = ForwardInfo {
1243 original_sender: UserId::new("alice").unwrap(),
1244 original_message_id: MessageId::new(),
1245 original_timestamp: Timestamp::now(),
1246 forward_count: 1,
1247 };
1248
1249 let msg = Message::builder(
1250 UserId::new("bob").unwrap(),
1251 UserId::new("charlie").unwrap(),
1252 AppId::new("test-app").unwrap(),
1253 )
1254 .content("Forwarded message")
1255 .forwarded_from(info.clone())
1256 .build();
1257
1258 assert!(msg.forwarded_from.is_some());
1259 let fwd = msg.forwarded_from.as_ref().unwrap();
1260 assert_eq!(fwd.original_sender.as_str(), "alice");
1261 assert_eq!(fwd.forward_count, 1);
1262
1263 let json = msg.to_json().unwrap();
1265 let deserialized = Message::from_json(&json).unwrap();
1266 let fwd2 = deserialized.forwarded_from.as_ref().unwrap();
1267 assert_eq!(fwd2.original_sender.as_str(), "alice");
1268 assert_eq!(fwd2.forward_count, 1);
1269 }
1270
1271 #[test]
1272 fn test_backward_compat_no_forwarded_from() {
1273 let msg = create_test_message();
1275 let json = msg.to_json().unwrap();
1276
1277 assert!(!json.contains("forwarded_from"));
1279
1280 let deserialized = Message::from_json(&json).unwrap();
1281 assert!(deserialized.forwarded_from.is_none());
1282 }
1283
1284 #[test]
1285 fn test_forward_chain_increment() {
1286 let original_msg = Message::builder(
1288 UserId::new("alice").unwrap(),
1289 UserId::new("bob").unwrap(),
1290 AppId::new("test-app").unwrap(),
1291 )
1292 .content("Hello from Alice")
1293 .build();
1294
1295 let first_forward = ForwardInfo::from_message(&original_msg);
1297 assert_eq!(first_forward.original_sender.as_str(), "alice");
1298 assert_eq!(first_forward.original_message_id, original_msg.id);
1299 assert_eq!(first_forward.forward_count, 1);
1300
1301 let forwarded_msg = Message::builder(
1303 UserId::new("bob").unwrap(),
1304 UserId::new("charlie").unwrap(),
1305 AppId::new("test-app").unwrap(),
1306 )
1307 .content("Hello from Alice")
1308 .forwarded_from(first_forward)
1309 .build();
1310
1311 let second_forward = ForwardInfo::from_message(&forwarded_msg);
1312 assert_eq!(second_forward.original_sender.as_str(), "alice");
1313 assert_eq!(second_forward.forward_count, 2);
1314 assert_eq!(second_forward.original_message_id, original_msg.id);
1315 }
1316
1317 #[test]
1318 fn test_forward_count_saturates_at_max() {
1319 use crate::types::Timestamp;
1320
1321 let saturated = ForwardInfo {
1322 original_sender: UserId::new("alice").unwrap(),
1323 original_message_id: MessageId::new(),
1324 original_timestamp: Timestamp::now(),
1325 forward_count: u32::MAX,
1326 };
1327
1328 let msg = Message::builder(
1329 UserId::new("bob").unwrap(),
1330 UserId::new("charlie").unwrap(),
1331 AppId::new("test-app").unwrap(),
1332 )
1333 .content("Hello")
1334 .forwarded_from(saturated)
1335 .build();
1336
1337 let next = ForwardInfo::from_message(&msg);
1338 assert_eq!(next.forward_count, u32::MAX);
1339 }
1340
1341 #[test]
1342 fn content_type_json_round_trips_every_variant() {
1343 for ct in ContentType::ALL {
1348 let json = serde_json::to_string(&ct).unwrap();
1349 assert_eq!(json, format!("\"{ct}\""));
1350 assert_eq!(serde_json::from_str::<ContentType>(&json).unwrap(), ct);
1351 assert_eq!(ContentType::parse(ct.as_wire_str()), ct);
1352 }
1353 }
1354
1355 #[test]
1356 fn content_type_round_trips_in_non_self_describing_formats() {
1357 for ct in ContentType::ALL {
1363 let bytes = postcard::to_allocvec(&ct).unwrap();
1364 assert_eq!(postcard::from_bytes::<ContentType>(&bytes).unwrap(), ct);
1365 }
1366 }
1367
1368 #[test]
1369 fn unknown_content_type_degrades_to_file_on_json_decode() {
1370 assert_eq!(
1374 serde_json::from_str::<ContentType>("\"some_future_type\"").unwrap(),
1375 ContentType::File
1376 );
1377 assert!(serde_json::from_str::<ContentType>("7").is_err());
1380 }
1381
1382 #[test]
1383 fn message_with_unknown_content_type_still_decodes() {
1384 let msg = create_test_message();
1385 let mut value = serde_json::to_value(&msg).unwrap();
1386 value["content_type"] = serde_json::Value::String("some_future_type".to_string());
1387 let decoded: Message = serde_json::from_value(value).unwrap();
1388 assert_eq!(decoded.content_type, ContentType::File);
1389 assert_eq!(decoded.content, msg.content);
1390 assert_eq!(decoded.id, msg.id);
1391 }
1392}