1use crate::types::{AppId, HopCount, LamportClock, Timestamp, UserId, TTL};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct MessageId(Uuid);
12
13impl MessageId {
14 pub fn new() -> Self {
16 Self(Uuid::new_v4())
17 }
18
19 #[allow(clippy::should_implement_trait)]
21 pub fn from_str(id: &str) -> crate::Result<Self> {
22 let uuid = Uuid::parse_str(id)
23 .map_err(|e| crate::Error::InvalidMessage(format!("Invalid message id: {}", e)))?;
24 Ok(Self::from_uuid(uuid))
25 }
26
27 pub fn from_uuid(uuid: Uuid) -> Self {
29 Self(uuid)
30 }
31
32 pub fn as_str(&self) -> String {
34 self.0.to_string()
35 }
36
37 pub fn as_bytes(&self) -> [u8; 16] {
42 *self.0.as_bytes()
43 }
44
45 pub fn from_bytes(bytes: [u8; 16]) -> Self {
47 Self(Uuid::from_bytes(bytes))
48 }
49}
50
51impl Default for MessageId {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl fmt::Display for MessageId {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}", self.0)
60 }
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
90pub enum ContentType {
91 #[default]
93 Text,
94 Image,
96 Video,
98 Audio,
100 VoiceNote,
102 VideoNote,
104 File,
106 FileChunk,
108 Poll,
110}
111
112impl ContentType {
113 pub const ALL: [Self; 9] = [
119 Self::Text,
120 Self::Image,
121 Self::Video,
122 Self::Audio,
123 Self::VoiceNote,
124 Self::VideoNote,
125 Self::File,
126 Self::FileChunk,
127 Self::Poll,
128 ];
129
130 pub fn is_media(&self) -> bool {
132 !matches!(self, Self::Text | Self::Poll)
133 }
134
135 pub const fn as_wire_str(self) -> &'static str {
142 match self {
143 Self::Text => "text",
144 Self::Image => "image",
145 Self::Video => "video",
146 Self::Audio => "audio",
147 Self::VoiceNote => "voice_note",
148 Self::VideoNote => "video_note",
149 Self::File => "file",
150 Self::FileChunk => "file_chunk",
151 Self::Poll => "poll",
152 }
153 }
154
155 pub fn parse(s: &str) -> Self {
164 match s {
165 "text" => Self::Text,
166 "image" => Self::Image,
167 "video" => Self::Video,
168 "audio" => Self::Audio,
169 "voice_note" => Self::VoiceNote,
170 "video_note" => Self::VideoNote,
171 "file" => Self::File,
172 "file_chunk" => Self::FileChunk,
173 "poll" => Self::Poll,
174 _ => Self::File,
175 }
176 }
177}
178
179const _: () = {
192 const fn listed_at_own_index(ct: ContentType) -> bool {
193 match ct {
194 ContentType::Text => matches!(ContentType::ALL[0], ContentType::Text),
195 ContentType::Image => matches!(ContentType::ALL[1], ContentType::Image),
196 ContentType::Video => matches!(ContentType::ALL[2], ContentType::Video),
197 ContentType::Audio => matches!(ContentType::ALL[3], ContentType::Audio),
198 ContentType::VoiceNote => matches!(ContentType::ALL[4], ContentType::VoiceNote),
199 ContentType::VideoNote => matches!(ContentType::ALL[5], ContentType::VideoNote),
200 ContentType::File => matches!(ContentType::ALL[6], ContentType::File),
201 ContentType::FileChunk => matches!(ContentType::ALL[7], ContentType::FileChunk),
202 ContentType::Poll => matches!(ContentType::ALL[8], ContentType::Poll),
203 }
204 }
205
206 let mut i = 0;
207 while i < ContentType::ALL.len() {
208 assert!(listed_at_own_index(ContentType::ALL[i]));
209 i += 1;
210 }
211};
212
213impl Serialize for ContentType {
218 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
219 where
220 S: serde::Serializer,
221 {
222 serializer.serialize_str(self.as_wire_str())
223 }
224}
225
226impl<'de> Deserialize<'de> for ContentType {
232 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233 where
234 D: serde::Deserializer<'de>,
235 {
236 struct ContentTypeVisitor;
237
238 impl serde::de::Visitor<'_> for ContentTypeVisitor {
239 type Value = ContentType;
240
241 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str("a content type string")
243 }
244
245 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<ContentType, E> {
246 Ok(ContentType::parse(s))
247 }
248 }
249
250 deserializer.deserialize_str(ContentTypeVisitor)
251 }
252}
253
254impl fmt::Display for ContentType {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.write_str(self.as_wire_str())
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct MediaMetadata {
263 pub mime_type: String,
265
266 pub file_name: String,
268
269 pub file_size: u64,
271
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub duration_ms: Option<u64>,
275
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub width: Option<u32>,
279
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub height: Option<u32>,
283
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub thumbnail_base64: Option<String>,
287
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub media_id: Option<String>,
291
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub download_url: Option<String>,
295
296 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub thumbnail_url: Option<String>,
299
300 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub encryption_key: Option<String>,
310
311 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub iv: Option<String>,
315
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub ciphertext_hash: Option<String>,
319
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub sticker_provider: Option<String>,
323
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub sticker_remote_id: Option<String>,
327
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub sticker_kind: Option<String>,
331}
332
333impl MediaMetadata {
334 pub fn has_secrets(&self) -> bool {
337 self.encryption_key.is_some() || self.iv.is_some()
338 }
339
340 pub fn without_secrets(&self) -> Self {
350 Self {
351 encryption_key: None,
352 iv: None,
353 ..self.clone()
354 }
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365pub struct ForwardInfo {
366 pub original_sender: UserId,
368 pub original_message_id: MessageId,
370 pub original_timestamp: Timestamp,
372 pub forward_count: u32,
374}
375
376impl ForwardInfo {
377 pub fn from_message(message: &Message) -> Self {
383 match &message.forwarded_from {
384 Some(existing) => ForwardInfo {
385 original_sender: existing.original_sender.clone(),
386 original_message_id: existing.original_message_id.clone(),
387 original_timestamp: existing.original_timestamp,
388 forward_count: existing.forward_count.saturating_add(1),
389 },
390 None => ForwardInfo {
391 original_sender: message.sender.clone(),
392 original_message_id: message.id.clone(),
393 original_timestamp: message.timestamp,
394 forward_count: 1,
395 },
396 }
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414pub struct ReplyContext {
415 pub sender: UserId,
417 pub text: String,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub timestamp: Option<Timestamp>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub reply_media_label: Option<String>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub reply_content_type: Option<String>,
428}
429
430#[derive(
437 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
438)]
439#[serde(rename_all = "lowercase")]
440pub enum MessagePriority {
441 #[serde(alias = "Low")]
443 Low,
444 #[default]
446 #[serde(alias = "Medium")]
447 Medium,
448 #[serde(alias = "High")]
450 High,
451 #[serde(alias = "Critical")]
453 Critical,
454}
455
456impl MessagePriority {
457 pub fn score(&self) -> u8 {
459 match self {
460 Self::Low => 1,
461 Self::Medium => 2,
462 Self::High => 3,
463 Self::Critical => 4,
464 }
465 }
466
467 pub fn as_str(&self) -> &'static str {
469 match self {
470 Self::Low => "Low",
471 Self::Medium => "Medium",
472 Self::High => "High",
473 Self::Critical => "Critical",
474 }
475 }
476}
477
478#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
486pub enum WireCodec {
487 #[default]
489 Json,
490 BinaryV1,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct Message {
497 pub id: MessageId,
499
500 pub sender: UserId,
502
503 pub recipient: UserId,
505
506 pub app_id: AppId,
508
509 pub priority: MessagePriority,
511
512 pub ttl: TTL,
514
515 pub hop_count: HopCount,
517
518 pub timestamp: Timestamp,
520
521 #[serde(default)]
523 pub lamport_clock: LamportClock,
524
525 #[serde(default)]
527 pub content_type: ContentType,
528
529 pub content: String,
531
532 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub binary_content: Option<Vec<u8>>,
537
538 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub media_metadata: Option<MediaMetadata>,
541
542 #[serde(default)]
544 pub metadata: HashMap<String, String>,
545
546 #[serde(default = "default_requires_ack")]
548 pub requires_ack: bool,
549
550 #[serde(default)]
552 pub reply_to_msg: Option<MessageId>,
553
554 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub forwarded_from: Option<ForwardInfo>,
557
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub reply_context: Option<ReplyContext>,
561
562 #[serde(skip)]
578 transport_peer_id: Option<String>,
579
580 #[serde(skip)]
585 wire_codec: WireCodec,
586}
587
588fn default_requires_ack() -> bool {
589 true
590}
591
592impl Message {
593 pub fn new(
602 sender: UserId,
603 recipient: UserId,
604 app_id: AppId,
605 content: impl Into<String>,
606 ) -> Self {
607 Self {
608 id: MessageId::new(),
609 sender,
610 recipient,
611 app_id,
612 priority: MessagePriority::default(),
613 ttl: TTL::default(),
614 hop_count: HopCount::new(),
615 timestamp: Timestamp::now(),
616 lamport_clock: LamportClock::default(),
617 content_type: ContentType::default(),
618 content: content.into(),
619 binary_content: None,
620 media_metadata: None,
621 metadata: HashMap::new(),
622 requires_ack: true,
623 reply_to_msg: None,
624 forwarded_from: None,
625 reply_context: None,
626 transport_peer_id: None,
627 wire_codec: WireCodec::Json,
628 }
629 }
630
631 pub fn builder(sender: UserId, recipient: UserId, app_id: AppId) -> MessageBuilder {
633 MessageBuilder::new(sender, recipient, app_id)
634 }
635
636 pub fn increment_hop(&mut self) -> crate::Result<()> {
642 self.hop_count = self
643 .hop_count
644 .increment()
645 .ok_or(crate::Error::InvalidHopCount(u8::MAX))?;
646 Ok(())
647 }
648
649 pub fn decrement_ttl(&mut self) -> crate::Result<()> {
655 self.ttl = self.ttl.decrement().ok_or(crate::Error::InvalidTTL(0))?;
656 Ok(())
657 }
658
659 pub fn is_ttl_exhausted(&self) -> bool {
661 self.ttl.is_exhausted()
662 }
663
664 pub fn to_json(&self) -> crate::Result<String> {
666 serde_json::to_string(self).map_err(|e| crate::Error::SerializationError(e.to_string()))
667 }
668
669 pub fn from_json(json: &str) -> crate::Result<Self> {
671 serde_json::from_str(json).map_err(|e| crate::Error::DeserializationError(e.to_string()))
672 }
673
674 #[doc(hidden)]
697 pub fn set_transport_peer_id(&mut self, peer_id: String) -> crate::Result<()> {
698 if peer_id.is_empty() {
699 return Err(crate::Error::InvalidMessage(
700 "transport_peer_id must not be empty".to_string(),
701 ));
702 }
703 self.transport_peer_id = Some(peer_id);
704 Ok(())
705 }
706
707 pub fn transport_peer_id(&self) -> Option<&str> {
709 self.transport_peer_id.as_deref()
710 }
711
712 #[doc(hidden)]
718 pub fn set_wire_codec(&mut self, codec: WireCodec) {
719 self.wire_codec = codec;
720 }
721
722 pub fn wire_codec(&self) -> WireCodec {
724 self.wire_codec
725 }
726
727 pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
729 serde_json::to_vec(self).map_err(|e| crate::Error::SerializationError(e.to_string()))
730 }
731
732 pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
734 serde_json::from_slice(bytes).map_err(|e| crate::Error::DeserializationError(e.to_string()))
735 }
736
737 pub fn to_wire_v1_bytes(&self) -> crate::Result<Vec<u8>> {
743 let media_metadata_json =
744 match &self.media_metadata {
745 Some(m) => Some(serde_json::to_vec(m).map_err(|e| {
746 crate::Error::SerializationError(format!("media_metadata: {e}"))
747 })?),
748 None => None,
749 };
750 let forwarded_from_json =
751 match &self.forwarded_from {
752 Some(f) => Some(serde_json::to_vec(f).map_err(|e| {
753 crate::Error::SerializationError(format!("forwarded_from: {e}"))
754 })?),
755 None => None,
756 };
757 let mut metadata: Vec<(String, String)> = self
758 .metadata
759 .iter()
760 .map(|(k, v)| (k.clone(), v.clone()))
761 .collect();
762 metadata.sort_by(|a, b| a.0.cmp(&b.0));
764
765 let (content, mut ext) = match crate::wire::split_b64_tail(&self.content) {
770 Some((head, raw)) => (head.to_string(), vec![(crate::wire::EXT_TAG_B64_TAIL, raw)]),
771 None => (self.content.clone(), Vec::new()),
772 };
773
774 if let Some(rc) = &self.reply_context {
779 ext.push((
780 crate::wire::EXT_TAG_REPLY_CONTEXT,
781 serde_json::to_vec(rc)
782 .map_err(|e| crate::Error::SerializationError(format!("reply_context: {e}")))?,
783 ));
784 }
785
786 let wire = crate::wire::WireMessageV1 {
787 id: self.id.as_bytes(),
788 sender: self.sender.as_str().to_string(),
789 recipient: self.recipient.as_str().to_string(),
790 app_id: self.app_id.as_str().to_string(),
791 priority: crate::wire::priority_to_u8(self.priority),
792 ttl: self.ttl.value(),
793 hop_count: self.hop_count.value(),
794 timestamp: self.timestamp.as_millis(),
795 lamport_clock: self.lamport_clock.value(),
796 content_type: crate::wire::content_type_to_u8(self.content_type),
797 content,
798 binary_content: self.binary_content.clone(),
799 media_metadata_json,
800 metadata,
801 requires_ack: self.requires_ack,
802 reply_to_msg: self.reply_to_msg.as_ref().map(|m| m.as_bytes()),
803 forwarded_from_json,
804 ext,
805 };
806 crate::wire::encode(&wire)
807 }
808
809 pub fn from_wire_v1_bytes(data: &[u8]) -> crate::Result<Self> {
816 let wire = crate::wire::decode(data)?;
817 let mut content = wire.content;
822 if let Some((_, raw)) = wire
823 .ext
824 .iter()
825 .find(|(tag, _)| *tag == crate::wire::EXT_TAG_B64_TAIL)
826 {
827 content.push_str(&crate::wire::encode_b64_tail(raw));
828 }
829 let media_metadata =
830 match wire.media_metadata_json {
831 Some(bytes) => Some(serde_json::from_slice(&bytes).map_err(|e| {
832 crate::Error::DeserializationError(format!("media_metadata: {e}"))
833 })?),
834 None => None,
835 };
836 let forwarded_from =
837 match wire.forwarded_from_json {
838 Some(bytes) => Some(serde_json::from_slice(&bytes).map_err(|e| {
839 crate::Error::DeserializationError(format!("forwarded_from: {e}"))
840 })?),
841 None => None,
842 };
843 let reply_context =
847 match wire
848 .ext
849 .iter()
850 .find(|(tag, _)| *tag == crate::wire::EXT_TAG_REPLY_CONTEXT)
851 {
852 Some((_, bytes)) => Some(serde_json::from_slice(bytes).map_err(|e| {
853 crate::Error::DeserializationError(format!("reply_context: {e}"))
854 })?),
855 None => None,
856 };
857 Ok(Self {
858 id: MessageId::from_bytes(wire.id),
859 sender: UserId::new(wire.sender)?,
860 recipient: UserId::new(wire.recipient)?,
861 app_id: AppId::new(wire.app_id)?,
862 priority: crate::wire::priority_from_u8(wire.priority),
863 ttl: TTL::from_value(wire.ttl),
864 hop_count: HopCount::from_value(wire.hop_count),
865 timestamp: Timestamp::from_millis(wire.timestamp),
866 lamport_clock: LamportClock::from_value(wire.lamport_clock),
867 content_type: crate::wire::content_type_from_u8(wire.content_type),
868 content,
869 binary_content: wire.binary_content,
870 media_metadata,
871 metadata: wire.metadata.into_iter().collect(),
872 requires_ack: wire.requires_ack,
873 reply_to_msg: wire.reply_to_msg.map(MessageId::from_bytes),
874 forwarded_from,
875 reply_context,
876 transport_peer_id: None,
877 wire_codec: WireCodec::Json,
878 })
879 }
880}
881
882pub struct MessageBuilder {
884 sender: UserId,
885 recipient: UserId,
886 app_id: AppId,
887 content_type: ContentType,
888 content: String,
889 media_metadata: Option<MediaMetadata>,
890 priority: MessagePriority,
891 ttl: TTL,
892 lamport_clock: LamportClock,
893 metadata: HashMap<String, String>,
894 requires_ack: bool,
895 reply_to_msg: Option<MessageId>,
896 forwarded_from: Option<ForwardInfo>,
897 reply_context: Option<ReplyContext>,
898}
899
900impl MessageBuilder {
901 pub fn new(sender: UserId, recipient: UserId, app_id: AppId) -> Self {
903 Self {
904 sender,
905 recipient,
906 app_id,
907 content_type: ContentType::default(),
908 content: String::new(),
909 media_metadata: None,
910 priority: MessagePriority::default(),
911 ttl: TTL::default(),
912 lamport_clock: LamportClock::default(),
913 metadata: HashMap::new(),
914 requires_ack: true,
915 reply_to_msg: None,
916 forwarded_from: None,
917 reply_context: None,
918 }
919 }
920
921 pub fn content_type(mut self, content_type: ContentType) -> Self {
923 self.content_type = content_type;
924 self
925 }
926
927 pub fn content(mut self, content: impl Into<String>) -> Self {
929 self.content = content.into();
930 self
931 }
932
933 pub fn media_metadata(mut self, meta: MediaMetadata) -> Self {
935 self.media_metadata = Some(meta);
936 self
937 }
938
939 pub fn priority(mut self, priority: MessagePriority) -> Self {
941 self.priority = priority;
942 self
943 }
944
945 pub fn ttl(mut self, ttl: TTL) -> Self {
947 self.ttl = ttl;
948 self
949 }
950
951 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
953 self.metadata.insert(key.into(), value.into());
954 self
955 }
956
957 pub fn requires_ack(mut self, requires_ack: bool) -> Self {
959 self.requires_ack = requires_ack;
960 self
961 }
962
963 pub fn reply_to_msg(mut self, reply_to: MessageId) -> Self {
965 self.reply_to_msg = Some(reply_to);
966 self
967 }
968
969 pub fn forwarded_from(mut self, info: ForwardInfo) -> Self {
971 self.forwarded_from = Some(info);
972 self
973 }
974
975 pub fn reply_context(mut self, ctx: ReplyContext) -> Self {
977 self.reply_context = Some(ctx);
978 self
979 }
980
981 pub fn lamport_clock(mut self, clock: LamportClock) -> Self {
983 self.lamport_clock = clock;
984 self
985 }
986
987 pub fn build(self) -> Message {
989 Message {
990 id: MessageId::new(),
991 sender: self.sender,
992 recipient: self.recipient,
993 app_id: self.app_id,
994 priority: self.priority,
995 ttl: self.ttl,
996 hop_count: HopCount::new(),
997 timestamp: Timestamp::now(),
998 lamport_clock: self.lamport_clock,
999 content_type: self.content_type,
1000 content: self.content,
1001 binary_content: None,
1002 media_metadata: self.media_metadata,
1003 metadata: self.metadata,
1004 requires_ack: self.requires_ack,
1005 reply_to_msg: self.reply_to_msg,
1006 forwarded_from: self.forwarded_from,
1007 reply_context: self.reply_context,
1008 transport_peer_id: None,
1009 wire_codec: WireCodec::Json,
1010 }
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 fn create_test_message() -> Message {
1019 Message::new(
1020 UserId::new("alice").unwrap(),
1021 UserId::new("bob").unwrap(),
1022 AppId::new("test-app").unwrap(),
1023 "Hello, Bob!",
1024 )
1025 }
1026
1027 #[test]
1028 fn test_message_creation() {
1029 let msg = create_test_message();
1030 assert_eq!(msg.sender.as_str(), "alice");
1031 assert_eq!(msg.recipient.as_str(), "bob");
1032 assert_eq!(msg.content, "Hello, Bob!");
1033 assert_eq!(msg.priority, MessagePriority::Medium);
1034 assert_eq!(msg.hop_count.value(), 0);
1035 }
1036
1037 #[test]
1038 fn test_message_builder() {
1039 let msg = Message::builder(
1040 UserId::new("alice").unwrap(),
1041 UserId::new("bob").unwrap(),
1042 AppId::new("test-app").unwrap(),
1043 )
1044 .content("Test message")
1045 .priority(MessagePriority::High)
1046 .metadata("key1", "value1")
1047 .requires_ack(false)
1048 .build();
1049
1050 assert_eq!(msg.content, "Test message");
1051 assert_eq!(msg.priority, MessagePriority::High);
1052 assert_eq!(msg.metadata.get("key1").unwrap(), "value1");
1053 assert!(!msg.requires_ack);
1054 }
1055
1056 #[test]
1057 fn reply_context_json_round_trip_and_default() {
1058 let mut msg = create_test_message();
1059 msg.reply_context = Some(ReplyContext {
1060 sender: UserId::new("carol").unwrap(),
1061 text: "quoted".into(),
1062 timestamp: Some(Timestamp::from_millis(1_700_000_000_000)),
1063 reply_media_label: None,
1064 reply_content_type: Some("text".into()),
1065 });
1066 let decoded = Message::from_bytes(&msg.to_bytes().unwrap()).unwrap();
1067 assert_eq!(decoded.reply_context, msg.reply_context);
1068
1069 let plain = create_test_message();
1071 let json = serde_json::to_string(&plain).unwrap();
1072 assert!(!json.contains("reply_context"));
1073 let decoded = Message::from_bytes(json.as_bytes()).unwrap();
1074 assert!(decoded.reply_context.is_none());
1075 }
1076
1077 #[test]
1078 fn media_metadata_without_secrets_clears_only_key_material() {
1079 let meta = MediaMetadata {
1080 mime_type: "image/jpeg".into(),
1081 file_name: "x.jpg".into(),
1082 file_size: 10,
1083 duration_ms: None,
1084 width: Some(1),
1085 height: Some(1),
1086 thumbnail_base64: None,
1087 media_id: Some("m1".into()),
1088 download_url: Some("https://cdn.example/m1".into()),
1089 thumbnail_url: Some("https://cdn.example/m1-t".into()),
1090 encryption_key: Some("a2V5".into()),
1091 iv: Some("aXY=".into()),
1092 ciphertext_hash: Some("aGFzaA==".into()),
1093 sticker_provider: Some("prov".into()),
1094 sticker_remote_id: Some("rid".into()),
1095 sticker_kind: Some("static".into()),
1096 };
1097 assert!(meta.has_secrets());
1098
1099 let clean = meta.without_secrets();
1100 assert!(!clean.has_secrets());
1101 assert!(clean.encryption_key.is_none());
1102 assert!(clean.iv.is_none());
1103 assert_eq!(clean.media_id, meta.media_id);
1105 assert_eq!(clean.download_url, meta.download_url);
1106 assert_eq!(clean.thumbnail_url, meta.thumbnail_url);
1107 assert_eq!(clean.ciphertext_hash, meta.ciphertext_hash);
1108 assert_eq!(clean.sticker_provider, meta.sticker_provider);
1109 assert_eq!(clean.sticker_remote_id, meta.sticker_remote_id);
1110 assert_eq!(clean.sticker_kind, meta.sticker_kind);
1111 assert_eq!(clean.file_name, meta.file_name);
1112 }
1113
1114 #[test]
1115 fn message_json_ignores_unknown_fields() {
1116 let mut json = serde_json::to_value(create_test_message()).unwrap();
1120 json["some_future_field"] = serde_json::Value::String("x".into());
1121 let bytes = serde_json::to_vec(&json).unwrap();
1122 assert!(Message::from_bytes(&bytes).is_ok());
1123 }
1124
1125 #[test]
1126 fn test_message_hop_operations() {
1127 let mut msg = create_test_message();
1128 assert_eq!(msg.hop_count.value(), 0);
1129
1130 msg.increment_hop().unwrap();
1131 assert_eq!(msg.hop_count.value(), 1);
1132 }
1133
1134 #[test]
1135 fn test_message_ttl_operations() {
1136 let mut msg = create_test_message();
1137 let initial_ttl = msg.ttl.value();
1138
1139 msg.decrement_ttl().unwrap();
1140 assert_eq!(msg.ttl.value(), initial_ttl - 1);
1141 }
1142
1143 #[test]
1144 fn test_message_serialization() {
1145 let msg = create_test_message();
1146
1147 let json = msg.to_json().unwrap();
1149 let deserialized = Message::from_json(&json).unwrap();
1150 assert_eq!(msg.id, deserialized.id);
1151 assert_eq!(msg.sender, deserialized.sender);
1152 assert_eq!(msg.content, deserialized.content);
1153
1154 let bytes = msg.to_bytes().unwrap();
1156 let deserialized = Message::from_bytes(&bytes).unwrap();
1157 assert_eq!(msg.id, deserialized.id);
1158 }
1159
1160 #[test]
1161 fn test_message_priority_score() {
1162 assert_eq!(MessagePriority::Low.score(), 1);
1163 assert_eq!(MessagePriority::Medium.score(), 2);
1164 assert_eq!(MessagePriority::High.score(), 3);
1165 assert_eq!(MessagePriority::Critical.score(), 4);
1166 }
1167
1168 #[test]
1169 fn test_forward_info_serialization() {
1170 use crate::types::Timestamp;
1171
1172 let info = ForwardInfo {
1173 original_sender: UserId::new("alice").unwrap(),
1174 original_message_id: MessageId::new(),
1175 original_timestamp: Timestamp::now(),
1176 forward_count: 1,
1177 };
1178
1179 let msg = Message::builder(
1180 UserId::new("bob").unwrap(),
1181 UserId::new("charlie").unwrap(),
1182 AppId::new("test-app").unwrap(),
1183 )
1184 .content("Forwarded message")
1185 .forwarded_from(info.clone())
1186 .build();
1187
1188 assert!(msg.forwarded_from.is_some());
1189 let fwd = msg.forwarded_from.as_ref().unwrap();
1190 assert_eq!(fwd.original_sender.as_str(), "alice");
1191 assert_eq!(fwd.forward_count, 1);
1192
1193 let json = msg.to_json().unwrap();
1195 let deserialized = Message::from_json(&json).unwrap();
1196 let fwd2 = deserialized.forwarded_from.as_ref().unwrap();
1197 assert_eq!(fwd2.original_sender.as_str(), "alice");
1198 assert_eq!(fwd2.forward_count, 1);
1199 }
1200
1201 #[test]
1202 fn test_backward_compat_no_forwarded_from() {
1203 let msg = create_test_message();
1205 let json = msg.to_json().unwrap();
1206
1207 assert!(!json.contains("forwarded_from"));
1209
1210 let deserialized = Message::from_json(&json).unwrap();
1211 assert!(deserialized.forwarded_from.is_none());
1212 }
1213
1214 #[test]
1215 fn test_forward_chain_increment() {
1216 let original_msg = Message::builder(
1218 UserId::new("alice").unwrap(),
1219 UserId::new("bob").unwrap(),
1220 AppId::new("test-app").unwrap(),
1221 )
1222 .content("Hello from Alice")
1223 .build();
1224
1225 let first_forward = ForwardInfo::from_message(&original_msg);
1227 assert_eq!(first_forward.original_sender.as_str(), "alice");
1228 assert_eq!(first_forward.original_message_id, original_msg.id);
1229 assert_eq!(first_forward.forward_count, 1);
1230
1231 let forwarded_msg = Message::builder(
1233 UserId::new("bob").unwrap(),
1234 UserId::new("charlie").unwrap(),
1235 AppId::new("test-app").unwrap(),
1236 )
1237 .content("Hello from Alice")
1238 .forwarded_from(first_forward)
1239 .build();
1240
1241 let second_forward = ForwardInfo::from_message(&forwarded_msg);
1242 assert_eq!(second_forward.original_sender.as_str(), "alice");
1243 assert_eq!(second_forward.forward_count, 2);
1244 assert_eq!(second_forward.original_message_id, original_msg.id);
1245 }
1246
1247 #[test]
1248 fn test_forward_count_saturates_at_max() {
1249 use crate::types::Timestamp;
1250
1251 let saturated = ForwardInfo {
1252 original_sender: UserId::new("alice").unwrap(),
1253 original_message_id: MessageId::new(),
1254 original_timestamp: Timestamp::now(),
1255 forward_count: u32::MAX,
1256 };
1257
1258 let msg = Message::builder(
1259 UserId::new("bob").unwrap(),
1260 UserId::new("charlie").unwrap(),
1261 AppId::new("test-app").unwrap(),
1262 )
1263 .content("Hello")
1264 .forwarded_from(saturated)
1265 .build();
1266
1267 let next = ForwardInfo::from_message(&msg);
1268 assert_eq!(next.forward_count, u32::MAX);
1269 }
1270
1271 #[test]
1272 fn content_type_json_round_trips_every_variant() {
1273 for ct in ContentType::ALL {
1278 let json = serde_json::to_string(&ct).unwrap();
1279 assert_eq!(json, format!("\"{ct}\""));
1280 assert_eq!(serde_json::from_str::<ContentType>(&json).unwrap(), ct);
1281 assert_eq!(ContentType::parse(ct.as_wire_str()), ct);
1282 }
1283 }
1284
1285 #[test]
1286 fn content_type_round_trips_in_non_self_describing_formats() {
1287 for ct in ContentType::ALL {
1293 let bytes = postcard::to_allocvec(&ct).unwrap();
1294 assert_eq!(postcard::from_bytes::<ContentType>(&bytes).unwrap(), ct);
1295 }
1296 }
1297
1298 #[test]
1299 fn unknown_content_type_degrades_to_file_on_json_decode() {
1300 assert_eq!(
1304 serde_json::from_str::<ContentType>("\"some_future_type\"").unwrap(),
1305 ContentType::File
1306 );
1307 assert!(serde_json::from_str::<ContentType>("7").is_err());
1310 }
1311
1312 #[test]
1313 fn message_with_unknown_content_type_still_decodes() {
1314 let msg = create_test_message();
1315 let mut value = serde_json::to_value(&msg).unwrap();
1316 value["content_type"] = serde_json::Value::String("some_future_type".to_string());
1317 let decoded: Message = serde_json::from_value(value).unwrap();
1318 assert_eq!(decoded.content_type, ContentType::File);
1319 assert_eq!(decoded.content, msg.content);
1320 assert_eq!(decoded.id, msg.id);
1321 }
1322}