1use crate::channels::{ChannelMessage, ChannelType, ChannelUser};
8use crate::multi::messaging::{AgentEnvelope, AgentPayload};
9use crate::multi::routing::{AgentRouter, RouteRequest};
10use crate::pairing::PairingManager;
11use std::collections::HashMap;
12use uuid::Uuid;
13
14pub struct ChannelAgentBridge {
20 router: AgentRouter,
21 pairing: Option<PairingManager>,
22}
23
24impl ChannelAgentBridge {
25 pub fn new(router: AgentRouter) -> Self {
26 Self {
27 router,
28 pairing: None,
29 }
30 }
31
32 pub fn with_pairing(mut self, pairing: PairingManager) -> Self {
34 self.pairing = Some(pairing);
35 self
36 }
37
38 pub fn is_sender_paired(&self, sender_id: &str) -> bool {
40 match &self.pairing {
41 None => true,
42 Some(pm) => pm
43 .paired_devices()
44 .iter()
45 .any(|d| d.device_name == sender_id || d.device_id.to_string() == sender_id),
46 }
47 }
48
49 pub fn pairing(&self) -> Option<&PairingManager> {
51 self.pairing.as_ref()
52 }
53
54 pub fn pairing_mut(&mut self) -> Option<&mut PairingManager> {
56 self.pairing.as_mut()
57 }
58
59 pub fn route_channel_message(&self, msg: &ChannelMessage, default_agent: Uuid) -> Uuid {
65 if !self.is_sender_paired(&msg.sender.id) {
67 return default_agent;
68 }
69
70 let text = msg.content.as_text().unwrap_or("").to_string();
71 let request = RouteRequest::new()
72 .with_channel(msg.channel_type)
73 .with_user(&msg.sender.id)
74 .with_message(text);
75 self.router.route(&request).unwrap_or(default_agent)
76 }
77
78 pub fn channel_message_to_envelope(
80 msg: &ChannelMessage,
81 from: Uuid,
82 to: Uuid,
83 ) -> AgentEnvelope {
84 let text = msg.content.as_text().unwrap_or("").to_string();
85 let mut args = HashMap::new();
86 args.insert("channel_type".into(), format!("{:?}", msg.channel_type));
87 args.insert("channel_id".into(), msg.channel_id.clone());
88 args.insert("sender".into(), msg.sender.id.clone());
89
90 AgentEnvelope::new(
91 from,
92 to,
93 AgentPayload::TaskRequest {
94 description: text,
95 args,
96 },
97 )
98 }
99
100 pub fn envelope_to_channel_message(
102 envelope: &AgentEnvelope,
103 channel_type: ChannelType,
104 ) -> Option<ChannelMessage> {
105 match &envelope.payload {
106 AgentPayload::TaskResult { output, .. } => {
107 let sender = ChannelUser::new("agent", channel_type);
108 Some(ChannelMessage::text(
109 channel_type,
110 "agent-response",
111 sender,
112 output,
113 ))
114 }
115 AgentPayload::Response { answer, .. } => {
116 let sender = ChannelUser::new("agent", channel_type);
117 Some(ChannelMessage::text(
118 channel_type,
119 "agent-response",
120 sender,
121 answer,
122 ))
123 }
124 _ => None,
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::multi::routing::{AgentRoute, RouteCondition};
133
134 #[test]
135 fn test_agent_bridge_route_by_channel_type() {
136 let mut router = AgentRouter::new();
137 let agent_id = Uuid::new_v4();
138 let default = Uuid::new_v4();
139 router.add_route(AgentRoute {
140 priority: 1,
141 target_agent_id: agent_id,
142 conditions: vec![RouteCondition::ChannelType(ChannelType::Telegram)],
143 });
144
145 let bridge = ChannelAgentBridge::new(router);
146 let sender = ChannelUser::new("user1", ChannelType::Telegram);
147 let msg = ChannelMessage::text(ChannelType::Telegram, "telegram-chat", sender, "hi");
148
149 assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
150 }
151
152 #[test]
153 fn test_agent_bridge_route_by_user() {
154 let mut router = AgentRouter::new();
155 let agent_id = Uuid::new_v4();
156 let default = Uuid::new_v4();
157 router.add_route(AgentRoute {
158 priority: 1,
159 target_agent_id: agent_id,
160 conditions: vec![RouteCondition::UserId("user-42".into())],
161 });
162
163 let bridge = ChannelAgentBridge::new(router);
164 let sender = ChannelUser::new("user-42", ChannelType::Slack);
165 let msg = ChannelMessage::text(ChannelType::Slack, "unknown-channel", sender, "hi");
166
167 assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
168 }
169
170 #[test]
171 fn test_agent_bridge_fallback_to_default() {
172 let router = AgentRouter::new();
173 let default = Uuid::new_v4();
174
175 let bridge = ChannelAgentBridge::new(router);
176 let sender = ChannelUser::new("nobody", ChannelType::Discord);
177 let msg = ChannelMessage::text(ChannelType::Discord, "random", sender, "hi");
178
179 assert_eq!(bridge.route_channel_message(&msg, default), default);
180 }
181
182 #[test]
183 fn test_agent_bridge_channel_to_envelope() {
184 let sender = ChannelUser::new("user1", ChannelType::Telegram);
185 let msg = ChannelMessage::text(ChannelType::Telegram, "chat1", sender, "build project");
186 let from = Uuid::new_v4();
187 let to = Uuid::new_v4();
188
189 let envelope = ChannelAgentBridge::channel_message_to_envelope(&msg, from, to);
190 assert_eq!(envelope.from, from);
191 assert_eq!(envelope.to, to);
192 match &envelope.payload {
193 AgentPayload::TaskRequest { description, args } => {
194 assert_eq!(description, "build project");
195 assert_eq!(args.get("sender").unwrap(), "user1");
196 }
197 _ => panic!("Expected TaskRequest"),
198 }
199 }
200
201 #[test]
202 fn test_agent_bridge_envelope_to_channel() {
203 let from = Uuid::new_v4();
204 let to = Uuid::new_v4();
205 let envelope = AgentEnvelope::new(
206 from,
207 to,
208 AgentPayload::TaskResult {
209 success: true,
210 output: "Done!".into(),
211 },
212 );
213
214 let msg = ChannelAgentBridge::envelope_to_channel_message(&envelope, ChannelType::Telegram);
215 assert!(msg.is_some());
216 let msg = msg.unwrap();
217 assert_eq!(msg.content.as_text(), Some("Done!"));
218 assert_eq!(msg.channel_type, ChannelType::Telegram);
219 }
220
221 #[test]
222 fn test_agent_bridge_non_response_returns_none() {
223 let from = Uuid::new_v4();
224 let to = Uuid::new_v4();
225 let envelope = AgentEnvelope::new(from, to, AgentPayload::StatusQuery);
226
227 let msg = ChannelAgentBridge::envelope_to_channel_message(&envelope, ChannelType::Slack);
228 assert!(msg.is_none());
229 }
230
231 #[test]
234 fn test_bridge_without_pairing_allows_all() {
235 let router = AgentRouter::new();
236 let bridge = ChannelAgentBridge::new(router);
237 assert!(bridge.is_sender_paired("anyone"));
238 assert!(bridge.pairing().is_none());
239 }
240
241 #[test]
242 fn test_bridge_with_pairing_rejects_unpaired_sender() {
243 let mut router = AgentRouter::new();
244 let agent_id = Uuid::new_v4();
245 let default = Uuid::new_v4();
246 router.add_route(AgentRoute {
247 priority: 1,
248 target_agent_id: agent_id,
249 conditions: vec![RouteCondition::ChannelType(ChannelType::IMessage)],
250 });
251
252 let pm = PairingManager::new(b"secret");
253 let bridge = ChannelAgentBridge::new(router).with_pairing(pm);
254
255 let sender = ChannelUser::new("stranger", ChannelType::IMessage);
257 let msg = ChannelMessage::text(ChannelType::IMessage, "dm", sender, "hello");
258 assert_eq!(bridge.route_channel_message(&msg, default), default);
259 }
260
261 #[test]
262 fn test_bridge_with_pairing_routes_paired_device() {
263 use crate::pairing::PairingResponse;
264
265 let secret = b"shared-secret-key-for-tests-32b!";
266 let mut router = AgentRouter::new();
267 let agent_id = Uuid::new_v4();
268 let default = Uuid::new_v4();
269 router.add_route(AgentRoute {
270 priority: 1,
271 target_agent_id: agent_id,
272 conditions: vec![RouteCondition::ChannelType(ChannelType::IMessage)],
273 });
274
275 let mut pm = PairingManager::new(secret);
276 let challenge = pm.create_challenge();
277
278 use hmac::{Hmac, Mac};
280 use sha2::Sha256;
281 type HmacSha256 = Hmac<Sha256>;
282 let mut mac = HmacSha256::new_from_slice(secret).unwrap();
283 mac.update(challenge.nonce.as_bytes());
284 let hmac_result = mac.finalize().into_bytes();
285 let response_hmac: String = hmac_result.iter().map(|b| format!("{:02x}", b)).collect();
286
287 let device_id = Uuid::new_v4();
288 let pair_resp = PairingResponse {
289 challenge_id: challenge.challenge_id,
290 device_id,
291 device_name: "my-phone".into(),
292 public_key: "pk-abc".into(),
293 response_hmac,
294 };
295 pm.verify_response(&pair_resp);
296
297 let bridge = ChannelAgentBridge::new(router).with_pairing(pm);
298
299 assert!(bridge.is_sender_paired("my-phone"));
301 let sender = ChannelUser::new("my-phone", ChannelType::IMessage);
302 let msg = ChannelMessage::text(ChannelType::IMessage, "dm", sender, "hello");
303 assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
304
305 assert!(!bridge.is_sender_paired("stranger"));
307 let sender2 = ChannelUser::new("stranger", ChannelType::IMessage);
308 let msg2 = ChannelMessage::text(ChannelType::IMessage, "dm", sender2, "hello");
309 assert_eq!(bridge.route_channel_message(&msg2, default), default);
310 }
311
312 #[test]
313 fn test_bridge_pairing_revoke_device() {
314 use crate::pairing::PairingResponse;
315
316 let secret = b"shared-secret-key-for-tests-32b!";
317 let mut pm = PairingManager::new(secret);
318 let challenge = pm.create_challenge();
319
320 use hmac::{Hmac, Mac};
321 use sha2::Sha256;
322 type HmacSha256 = Hmac<Sha256>;
323 let mut mac = HmacSha256::new_from_slice(secret).unwrap();
324 mac.update(challenge.nonce.as_bytes());
325 let hmac_result = mac.finalize().into_bytes();
326 let response_hmac: String = hmac_result.iter().map(|b| format!("{:02x}", b)).collect();
327
328 let device_id = Uuid::new_v4();
329 let pair_resp = PairingResponse {
330 challenge_id: challenge.challenge_id,
331 device_id,
332 device_name: "laptop".into(),
333 public_key: "pk".into(),
334 response_hmac,
335 };
336 pm.verify_response(&pair_resp);
337
338 let router = AgentRouter::new();
339 let mut bridge = ChannelAgentBridge::new(router).with_pairing(pm);
340
341 assert!(bridge.is_sender_paired("laptop"));
342
343 bridge.pairing_mut().unwrap().revoke_device(&device_id);
345 assert!(!bridge.is_sender_paired("laptop"));
346 }
347}