1use crate::connection::Direction;
2use crate::ids::{ConnectionId, ConversationId, MessageId, ParticipantId};
3use bytes::Bytes;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Clone)]
9pub enum MessageOrigin {
10 Connection(ConnectionId),
11 System,
12 Ai(ParticipantId),
13}
14
15impl fmt::Debug for MessageOrigin {
16 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
17 formatter.write_str(match self {
18 Self::Connection(_) => "MessageOrigin::Connection",
19 Self::System => "MessageOrigin::System",
20 Self::Ai(_) => "MessageOrigin::Ai",
21 })
22 }
23}
24
25#[derive(Clone)]
26pub enum MessageRecipients {
27 All,
28 Participants(Vec<ParticipantId>),
29}
30
31impl fmt::Debug for MessageRecipients {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::All => formatter.write_str("MessageRecipients::All"),
35 Self::Participants(participants) => formatter
36 .debug_struct("MessageRecipients::Participants")
37 .field("count", &participants.len())
38 .finish(),
39 }
40 }
41}
42
43#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
44pub enum ContentType {
45 Text,
46 Json,
47 Binary,
48 Image,
49 Audio,
50 Attachment(String),
51}
52
53impl fmt::Debug for ContentType {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 formatter.write_str(match self {
56 Self::Text => "ContentType::Text",
57 Self::Json => "ContentType::Json",
58 Self::Binary => "ContentType::Binary",
59 Self::Image => "ContentType::Image",
60 Self::Audio => "ContentType::Audio",
61 Self::Attachment(_) => "ContentType::Attachment",
62 })
63 }
64}
65
66#[derive(Clone)]
67pub struct Attachment {
68 pub url: String,
69 pub content_type: ContentType,
70 pub size_bytes: u64,
71}
72
73impl fmt::Debug for Attachment {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 formatter
76 .debug_struct("Attachment")
77 .field("url_present", &!self.url.is_empty())
78 .field("content_type", &self.content_type)
79 .field("size_bytes", &self.size_bytes)
80 .finish()
81 }
82}
83
84#[derive(Clone)]
85pub struct Message {
86 pub id: MessageId,
87 pub conversation_id: ConversationId,
88 pub origin: MessageOrigin,
89 pub from_participant: ParticipantId,
90 pub to: MessageRecipients,
91 pub direction: Direction,
92 pub content_type: ContentType,
93 pub body: Bytes,
94 pub attachments: Vec<Attachment>,
95 pub in_reply_to: Option<MessageId>,
96 pub timestamp: DateTime<Utc>,
97}
98
99impl fmt::Debug for Message {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter
102 .debug_struct("Message")
103 .field("origin", &self.origin)
104 .field("recipients", &self.to)
105 .field("direction", &self.direction)
106 .field("content_type", &self.content_type)
107 .field("body_bytes", &self.body.len())
108 .field("attachment_count", &self.attachments.len())
109 .field("in_reply_to_present", &self.in_reply_to.is_some())
110 .field("timestamp", &self.timestamp)
111 .finish()
112 }
113}
114
115#[cfg(test)]
116mod diagnostic_tests {
117 use super::*;
118
119 #[test]
120 fn message_debug_exposes_shape_without_routing_or_payload_values() {
121 let secret = "credential-canary-value";
122 let message = Message {
123 id: MessageId::new(),
124 conversation_id: ConversationId::new(),
125 origin: MessageOrigin::Connection(ConnectionId::new()),
126 from_participant: ParticipantId::new(),
127 to: MessageRecipients::Participants(vec![ParticipantId::new()]),
128 direction: Direction::Inbound,
129 content_type: ContentType::Attachment(secret.into()),
130 body: Bytes::from(secret),
131 attachments: vec![Attachment {
132 url: format!("https://{secret}.invalid/object"),
133 content_type: ContentType::Attachment(secret.into()),
134 size_bytes: 23,
135 }],
136 in_reply_to: Some(MessageId::new()),
137 timestamp: Utc::now(),
138 };
139
140 let debug = format!("{message:?}");
141 assert!(!debug.contains(secret));
142 assert!(debug.contains("body_bytes: 23"));
143 assert!(debug.contains("attachment_count: 1"));
144 assert!(debug.contains("count: 1"));
145 }
146}