1use async_trait::async_trait;
2use tokio_util::sync::CancellationToken;
3
4#[derive(Debug, Clone)]
6pub struct ChannelMessage {
7 pub id: String,
8 pub sender: String,
9 pub reply_target: String,
10 pub content: String,
11 pub channel: String,
12 pub timestamp: u64,
13 pub thread_ts: Option<String>,
16 pub interruption_scope_id: Option<String>,
21 pub attachments: Vec<super::media_pipeline::MediaAttachment>,
25}
26
27#[derive(Debug, Clone)]
29pub struct SendMessage {
30 pub content: String,
31 pub recipient: String,
32 pub subject: Option<String>,
33 pub thread_ts: Option<String>,
35 pub cancellation_token: Option<CancellationToken>,
37}
38
39impl SendMessage {
40 pub fn new(content: impl Into<String>, recipient: impl Into<String>) -> Self {
42 Self {
43 content: content.into(),
44 recipient: recipient.into(),
45 subject: None,
46 thread_ts: None,
47 cancellation_token: None,
48 }
49 }
50
51 pub fn with_subject(
53 content: impl Into<String>,
54 recipient: impl Into<String>,
55 subject: impl Into<String>,
56 ) -> Self {
57 Self {
58 content: content.into(),
59 recipient: recipient.into(),
60 subject: Some(subject.into()),
61 thread_ts: None,
62 cancellation_token: None,
63 }
64 }
65
66 pub fn in_thread(mut self, thread_ts: Option<String>) -> Self {
68 self.thread_ts = thread_ts;
69 self
70 }
71
72 pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
74 self.cancellation_token = Some(token);
75 self
76 }
77}
78
79#[async_trait]
81pub trait Channel: Send + Sync {
82 fn name(&self) -> &str;
84
85 async fn send(&self, message: &SendMessage) -> anyhow::Result<()>;
87
88 async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()>;
90
91 async fn health_check(&self) -> bool {
93 true
94 }
95
96 async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {
99 Ok(())
100 }
101
102 async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {
104 Ok(())
105 }
106
107 fn supports_draft_updates(&self) -> bool {
109 false
110 }
111
112 fn supports_multi_message_streaming(&self) -> bool {
116 false
117 }
118
119 fn multi_message_delay_ms(&self) -> u64 {
122 800
123 }
124
125 async fn send_draft(&self, _message: &SendMessage) -> anyhow::Result<Option<String>> {
127 Ok(None)
128 }
129
130 async fn update_draft(
132 &self,
133 _recipient: &str,
134 _message_id: &str,
135 _text: &str,
136 ) -> anyhow::Result<()> {
137 Ok(())
138 }
139
140 async fn update_draft_progress(
144 &self,
145 _recipient: &str,
146 _message_id: &str,
147 _text: &str,
148 ) -> anyhow::Result<()> {
149 Ok(())
150 }
151
152 async fn finalize_draft(
154 &self,
155 _recipient: &str,
156 _message_id: &str,
157 _text: &str,
158 ) -> anyhow::Result<()> {
159 Ok(())
160 }
161
162 async fn cancel_draft(&self, _recipient: &str, _message_id: &str) -> anyhow::Result<()> {
164 Ok(())
165 }
166
167 async fn add_reaction(
173 &self,
174 _channel_id: &str,
175 _message_id: &str,
176 _emoji: &str,
177 ) -> anyhow::Result<()> {
178 Ok(())
179 }
180
181 async fn remove_reaction(
183 &self,
184 _channel_id: &str,
185 _message_id: &str,
186 _emoji: &str,
187 ) -> anyhow::Result<()> {
188 Ok(())
189 }
190
191 async fn pin_message(&self, _channel_id: &str, _message_id: &str) -> anyhow::Result<()> {
193 Ok(())
194 }
195
196 async fn unpin_message(&self, _channel_id: &str, _message_id: &str) -> anyhow::Result<()> {
198 Ok(())
199 }
200
201 async fn redact_message(
207 &self,
208 _channel_id: &str,
209 _message_id: &str,
210 _reason: Option<String>,
211 ) -> anyhow::Result<()> {
212 Ok(())
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 struct DummyChannel;
221
222 #[async_trait]
223 impl Channel for DummyChannel {
224 fn name(&self) -> &str {
225 "dummy"
226 }
227
228 async fn send(&self, _message: &SendMessage) -> anyhow::Result<()> {
229 Ok(())
230 }
231
232 async fn listen(
233 &self,
234 tx: tokio::sync::mpsc::Sender<ChannelMessage>,
235 ) -> anyhow::Result<()> {
236 tx.send(ChannelMessage {
237 id: "1".into(),
238 sender: "tester".into(),
239 reply_target: "tester".into(),
240 content: "hello".into(),
241 channel: "dummy".into(),
242 timestamp: 123,
243 thread_ts: None,
244 interruption_scope_id: None,
245 attachments: vec![],
246 })
247 .await
248 .map_err(|e| anyhow::anyhow!(e.to_string()))
249 }
250 }
251
252 #[test]
253 fn channel_message_clone_preserves_fields() {
254 let message = ChannelMessage {
255 id: "42".into(),
256 sender: "alice".into(),
257 reply_target: "alice".into(),
258 content: "ping".into(),
259 channel: "dummy".into(),
260 timestamp: 999,
261 thread_ts: None,
262 interruption_scope_id: None,
263 attachments: vec![],
264 };
265
266 let cloned = message.clone();
267 assert_eq!(cloned.id, "42");
268 assert_eq!(cloned.sender, "alice");
269 assert_eq!(cloned.reply_target, "alice");
270 assert_eq!(cloned.content, "ping");
271 assert_eq!(cloned.channel, "dummy");
272 assert_eq!(cloned.timestamp, 999);
273 }
274
275 #[tokio::test]
276 async fn default_trait_methods_return_success() {
277 let channel = DummyChannel;
278
279 assert!(channel.health_check().await);
280 assert!(channel.start_typing("bob").await.is_ok());
281 assert!(channel.stop_typing("bob").await.is_ok());
282 assert!(
283 channel
284 .send(&SendMessage::new("hello", "bob"))
285 .await
286 .is_ok()
287 );
288 }
289
290 #[tokio::test]
291 async fn default_reaction_methods_return_success() {
292 let channel = DummyChannel;
293
294 assert!(
295 channel
296 .add_reaction("chan_1", "msg_1", "\u{1F440}")
297 .await
298 .is_ok()
299 );
300 assert!(
301 channel
302 .remove_reaction("chan_1", "msg_1", "\u{1F440}")
303 .await
304 .is_ok()
305 );
306 }
307
308 #[tokio::test]
309 async fn default_draft_methods_return_success() {
310 let channel = DummyChannel;
311
312 assert!(!channel.supports_draft_updates());
313 assert!(
314 channel
315 .send_draft(&SendMessage::new("draft", "bob"))
316 .await
317 .unwrap()
318 .is_none()
319 );
320 assert!(channel.update_draft("bob", "msg_1", "text").await.is_ok());
321 assert!(
322 channel
323 .finalize_draft("bob", "msg_1", "final text")
324 .await
325 .is_ok()
326 );
327 assert!(channel.cancel_draft("bob", "msg_1").await.is_ok());
328 }
329
330 #[tokio::test]
331 async fn listen_sends_message_to_channel() {
332 let channel = DummyChannel;
333 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
334
335 channel.listen(tx).await.unwrap();
336
337 let received = rx.recv().await.expect("message should be sent");
338 assert_eq!(received.sender, "tester");
339 assert_eq!(received.content, "hello");
340 assert_eq!(received.channel, "dummy");
341 }
342
343 #[tokio::test]
344 async fn default_redact_message_returns_success() {
345 let channel = DummyChannel;
346
347 assert!(
348 channel
349 .redact_message("chan_1", "msg_1", Some("spam".to_string()))
350 .await
351 .is_ok()
352 );
353 assert!(
354 channel
355 .redact_message("chan_1", "msg_2", None)
356 .await
357 .is_ok()
358 );
359 }
360}