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 async_trait::async_trait;
25use serde::{Deserialize, Serialize};
26
27pub(crate) fn html_escape(s: &str) -> String {
29 s.replace('&', "&")
30 .replace('<', "<")
31 .replace('>', ">")
32}
33
34pub(crate) fn split_utf8(content: &str, max_bytes: usize) -> Vec<String> {
36 if content.len() <= max_bytes {
37 return vec![content.to_string()];
38 }
39
40 let mut chunks = Vec::new();
41 let mut start = 0;
42 let total = content.len();
43
44 while start < total {
45 let mut end = (start + max_bytes).min(total);
46 while end > start && !content.is_char_boundary(end) {
47 end -= 1;
48 }
49 if end == start {
50 if let Some(ch) = content[start..].chars().next() {
52 end = start + ch.len_utf8();
53 } else {
54 break;
55 }
56 }
57 chunks.push(content[start..end].to_string());
58 start = end;
59 }
60
61 chunks
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct IncomingMessage {
67 pub channel: String,
69 pub id: String,
71 pub thread_id: String,
73 pub author: String,
75 pub body: String,
77 pub timestamp: chrono::DateTime<chrono::Utc>,
79 #[serde(default)]
81 pub metadata: serde_json::Value,
82 #[serde(default)]
84 pub topic_id: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct OutgoingMessage {
90 pub thread_id: String,
92 pub body: String,
94 pub reply_to: Option<String>,
96 #[serde(default)]
98 pub metadata: serde_json::Value,
99 #[serde(default)]
101 pub topic_id: Option<String>,
102}
103
104#[derive(Debug, Clone)]
106pub struct OutputChunk {
107 pub content: String,
108 pub is_final: bool,
109}
110
111#[async_trait]
113pub trait Channel: Send + Sync {
114 fn name(&self) -> &str;
116
117 async fn start(&self) -> anyhow::Result<tokio::sync::mpsc::Receiver<IncomingMessage>>;
120
121 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()>;
123
124 async fn ack_interaction(&self, _callback_query_id: &str) -> anyhow::Result<()> {
130 Ok(())
131 }
132
133 async fn health_check(&self) -> anyhow::Result<()>;
135}
136
137pub struct ChannelRegistry {
139 channels: Vec<Box<dyn Channel>>,
140}
141
142impl std::fmt::Debug for ChannelRegistry {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 f.debug_list()
145 .entries(self.channels.iter().map(|c| c.name()))
146 .finish()
147 }
148}
149
150impl ChannelRegistry {
151 pub fn new() -> Self {
152 Self {
153 channels: Vec::new(),
154 }
155 }
156
157 pub fn register(&mut self, channel: Box<dyn Channel>) {
158 tracing::info!(channel = channel.name(), "registered channel");
159 self.channels.push(channel);
160 }
161
162 pub fn iter(&self) -> impl Iterator<Item = &dyn Channel> {
163 self.channels.iter().map(std::convert::AsRef::as_ref)
164 }
165}