Skip to main content

rvoip_core/
message.rs

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}