1pub mod capture;
13pub mod discord;
14pub mod discord_ws;
15pub mod github;
16pub mod notification;
17pub mod routing;
18pub mod slack;
19pub mod stream;
20pub mod telegram;
21pub mod tmux;
22pub mod transport;
23
24use std::fmt;
25use std::fmt::Debug;
26use std::fmt::Formatter;
27
28use async_trait::async_trait;
29use serde::{Deserialize, Serialize};
30use tokio::sync::mpsc::Receiver;
31
32pub(crate) fn html_escape(s: &str) -> String {
34 s.replace('&', "&")
35 .replace('<', "<")
36 .replace('>', ">")
37}
38
39pub(crate) fn split_utf8(content: &str, max_bytes: usize) -> Vec<String> {
41 if content.len() <= max_bytes {
42 return vec![content.to_string()];
43 }
44
45 let mut chunks = Vec::new();
46 let mut start = 0;
47 let total = content.len();
48
49 while start < total {
50 let mut end = (start + max_bytes).min(total);
51 while end > start && !content.is_char_boundary(end) {
52 end -= 1;
53 }
54 if end == start {
55 if let Some(ch) = content[start..].chars().next() {
57 end = start + ch.len_utf8();
58 } else {
59 break;
60 }
61 }
62 chunks.push(content[start..end].to_string());
63 start = end;
64 }
65
66 chunks
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct IncomingMessage {
72 pub channel: String,
74 pub id: String,
76 pub thread_id: String,
78 pub author: String,
80 pub body: String,
82 pub timestamp: chrono::DateTime<chrono::Utc>,
84 #[serde(default)]
86 pub metadata: serde_json::Value,
87 #[serde(default)]
89 pub topic_id: Option<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct OutgoingMessage {
95 pub thread_id: String,
97 pub body: String,
99 pub reply_to: Option<String>,
101 #[serde(default)]
103 pub metadata: serde_json::Value,
104 #[serde(default)]
106 pub topic_id: Option<String>,
107}
108
109#[derive(Debug, Clone)]
111pub struct OutputChunk {
112 pub content: String,
113 pub is_final: bool,
114}
115
116#[async_trait]
118pub trait Channel: Send + Sync {
119 fn name(&self) -> &str;
121
122 async fn start(&self) -> anyhow::Result<Receiver<IncomingMessage>>;
125
126 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()>;
128
129 async fn ack_interaction(&self, _callback_query_id: &str) -> anyhow::Result<()> {
135 Ok(())
136 }
137
138 async fn health_check(&self) -> anyhow::Result<()>;
140}
141
142pub struct ChannelRegistry {
144 channels: Vec<Box<dyn Channel>>,
145}
146
147impl Debug for ChannelRegistry {
148 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
149 f.debug_list()
150 .entries(self.channels.iter().map(|c| c.name()))
151 .finish()
152 }
153}
154
155impl ChannelRegistry {
156 pub fn new() -> Self {
157 Self {
158 channels: Vec::new(),
159 }
160 }
161
162 pub fn register(&mut self, channel: Box<dyn Channel>) {
163 tracing::info!(channel = channel.name(), "registered channel");
164 self.channels.push(channel);
165 }
166
167 pub fn iter(&self) -> impl Iterator<Item = &dyn Channel> {
168 self.channels.iter().map(AsRef::as_ref)
169 }
170}