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#[allow(clippy::double_must_use)] #[async_trait]
114pub trait Channel: Send + Sync {
115 fn name(&self) -> &str;
117
118 async fn start(&self) -> anyhow::Result<tokio::sync::mpsc::Receiver<IncomingMessage>>;
121
122 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()>;
124
125 async fn ack_interaction(&self, _callback_query_id: &str) -> anyhow::Result<()> {
131 Ok(())
132 }
133
134 async fn health_check(&self) -> anyhow::Result<()>;
136}
137
138pub struct ChannelRegistry {
140 channels: Vec<Box<dyn Channel>>,
141}
142
143impl ChannelRegistry {
144 pub fn new() -> Self {
145 Self {
146 channels: Vec::new(),
147 }
148 }
149
150 pub fn register(&mut self, channel: Box<dyn Channel>) {
151 tracing::info!(channel = channel.name(), "registered channel");
152 self.channels.push(channel);
153 }
154
155 pub fn iter(&self) -> impl Iterator<Item = &dyn Channel> {
156 self.channels.iter().map(|c| c.as_ref())
157 }
158}