Skip to main content

bob_chat/
message.rs

1//! Core message types for the chat channel layer.
2
3use serde::{Deserialize, Serialize};
4
5use crate::file::Attachment;
6
7// ---------------------------------------------------------------------------
8// Author
9// ---------------------------------------------------------------------------
10
11/// Identifies the author of an incoming message.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Author {
14    /// Platform-specific user identifier.
15    pub user_id: String,
16    /// Short username / handle.
17    pub user_name: String,
18    /// Display name.
19    pub full_name: String,
20    /// Whether this author is a bot.
21    pub is_bot: bool,
22}
23
24// ---------------------------------------------------------------------------
25// Postable messages
26// ---------------------------------------------------------------------------
27
28/// A message that the agent wants to send to a channel.
29///
30/// Additional variants (`Card`, `Stream`) will be added when the
31/// corresponding modules are implemented.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(tag = "type", content = "value")]
34pub enum PostableMessage {
35    /// Plain text content.
36    Text(String),
37    /// Markdown-formatted content.
38    Markdown(String),
39}
40
41/// Adapter-level postable message, extending [`PostableMessage`] with
42/// attachment support.
43///
44/// Additional variants (`Card`, `WithAttachments`) will be added when
45/// the corresponding modules are implemented.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(tag = "type", content = "value")]
48pub enum AdapterPostableMessage {
49    /// Plain text content.
50    Text(String),
51    /// Markdown-formatted content.
52    Markdown(String),
53}
54
55impl From<String> for PostableMessage {
56    fn from(text: String) -> Self {
57        Self::Text(text)
58    }
59}
60
61impl From<&str> for PostableMessage {
62    fn from(text: &str) -> Self {
63        Self::Text(text.to_owned())
64    }
65}
66
67// ---------------------------------------------------------------------------
68// Sent / Ephemeral / Incoming
69// ---------------------------------------------------------------------------
70
71/// Confirmation returned after a message has been successfully sent.
72///
73/// The `raw` field carries the adapter-native response for advanced
74/// use-cases and is intentionally excluded from serialization.
75pub struct SentMessage {
76    /// Platform-assigned message identifier.
77    pub id: String,
78    /// Thread the message belongs to.
79    pub thread_id: String,
80    /// Name of the adapter that sent this message.
81    pub adapter_name: String,
82    /// Adapter-native raw response object.
83    pub raw: Option<Box<dyn std::any::Any + Send + Sync>>,
84}
85
86impl std::fmt::Debug for SentMessage {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("SentMessage")
89            .field("id", &self.id)
90            .field("thread_id", &self.thread_id)
91            .field("adapter_name", &self.adapter_name)
92            .field("raw", &self.raw.as_ref().map(|_| "..."))
93            .finish()
94    }
95}
96
97/// An ephemeral (visible-only-to-user) message confirmation.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct EphemeralMessage {
100    /// Platform-assigned message identifier.
101    pub id: String,
102    /// Thread the message belongs to.
103    pub thread_id: String,
104    /// Whether a plaintext fallback was used because the adapter does
105    /// not support true ephemeral messages.
106    pub used_fallback: bool,
107}
108
109/// A message received from an external chat platform.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct IncomingMessage {
112    /// Platform-assigned message identifier.
113    pub id: String,
114    /// The text body of the message.
115    pub text: String,
116    /// Who sent this message.
117    pub author: Author,
118    /// Attachments included with the message.
119    pub attachments: Vec<Attachment>,
120    /// Whether the bot was explicitly mentioned.
121    pub is_mention: bool,
122    /// Thread this message belongs to.
123    pub thread_id: String,
124    /// ISO 8601 timestamp of the message, if available.
125    pub timestamp: Option<String>,
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn author_serde_roundtrip() {
134        let author = Author {
135            user_id: "U123".into(),
136            user_name: "alice".into(),
137            full_name: "Alice Smith".into(),
138            is_bot: false,
139        };
140        let json = serde_json::to_string(&author).expect("serialize");
141        let back: Author = serde_json::from_str(&json).expect("deserialize");
142        assert_eq!(back.user_id, "U123");
143        assert!(!back.is_bot);
144    }
145
146    #[test]
147    fn postable_message_serde_roundtrip() {
148        let msg = PostableMessage::Text("hello".into());
149        let json = serde_json::to_string(&msg).expect("serialize");
150        let back: PostableMessage = serde_json::from_str(&json).expect("deserialize");
151        assert!(matches!(back, PostableMessage::Text(t) if t == "hello"));
152    }
153
154    #[test]
155    fn sent_message_debug() {
156        let sm = SentMessage {
157            id: "msg-1".into(),
158            thread_id: "t-1".into(),
159            adapter_name: "slack".into(),
160            raw: None,
161        };
162        let dbg = format!("{sm:?}");
163        assert!(dbg.contains("msg-1"));
164    }
165
166    #[test]
167    fn ephemeral_message_serde() {
168        let em =
169            EphemeralMessage { id: "e-1".into(), thread_id: "t-1".into(), used_fallback: true };
170        let json = serde_json::to_string(&em).expect("serialize");
171        assert!(json.contains("used_fallback"));
172    }
173
174    #[test]
175    fn incoming_message_debug() {
176        let msg = IncomingMessage {
177            id: "m-1".into(),
178            text: "hi bot".into(),
179            author: Author {
180                user_id: "U1".into(),
181                user_name: "bob".into(),
182                full_name: "Bob".into(),
183                is_bot: false,
184            },
185            attachments: vec![],
186            is_mention: true,
187            thread_id: "t-1".into(),
188            timestamp: Some("2026-03-03T12:00:00Z".into()),
189        };
190        let dbg = format!("{msg:?}");
191        assert!(dbg.contains("hi bot"));
192    }
193}