use crate::agent_config::ChannelBinding;
use crate::channel::InboundMessage;
use crate::config::{RoutingBinding, RoutingConfig};
#[derive(Debug, Clone)]
pub struct AgentBinding {
pub channel_type: String,
pub account_id: Option<String>,
pub peer_filter: Option<String>,
pub agent_id: String,
}
#[derive(Clone)]
pub struct MessageRouter {
bindings: Vec<RoutingBinding>,
agent_bindings: Vec<AgentBinding>,
default_agent_id: String,
}
impl MessageRouter {
pub fn new(routing: &RoutingConfig, default_agent: String) -> Self {
Self {
bindings: routing.bindings.clone(),
agent_bindings: Vec::new(),
default_agent_id: default_agent,
}
}
#[allow(dead_code)]
pub fn default_agent_id(&self) -> &str {
&self.default_agent_id
}
pub fn add_agent_bindings(&mut self, agent_id: &str, channel_bindings: &[ChannelBinding]) {
for cb in channel_bindings {
self.agent_bindings.push(AgentBinding {
channel_type: cb.channel_type.clone(),
account_id: cb.account_id.clone(),
peer_filter: cb.peer_filter.clone(),
agent_id: agent_id.to_string(),
});
}
}
pub fn remove_agent_bindings(&mut self, agent_id: &str) {
self.agent_bindings.retain(|b| b.agent_id != agent_id);
}
pub fn update_agent_bindings(&mut self, agent_id: &str, channel_bindings: &[ChannelBinding]) {
self.remove_agent_bindings(agent_id);
self.add_agent_bindings(agent_id, channel_bindings);
}
pub fn resolve_agent(&self, msg: &InboundMessage) -> &str {
let channel_name = msg.channel_type.to_string();
let msg_account = if msg.account_id.is_empty() {
None
} else {
Some(msg.account_id.as_str())
};
let msg_peer = msg.sender_id.as_str();
for ab in &self.agent_bindings {
if ab.channel_type != channel_name {
continue;
}
let account_matches = match (&ab.account_id, msg_account) {
(Some(bind_acct), Some(msg_acct)) => bind_acct == msg_acct,
(Some(_), None) => false,
(None, _) => false, };
if !account_matches {
continue;
}
let peer_matches = match &ab.peer_filter {
Some(peer) => peer == msg_peer,
None => false, };
if peer_matches {
return &ab.agent_id;
}
}
for ab in &self.agent_bindings {
if ab.channel_type != channel_name {
continue;
}
if ab.peer_filter.is_some() {
continue; }
let account_matches = match (&ab.account_id, msg_account) {
(Some(bind_acct), Some(msg_acct)) => bind_acct == msg_acct,
(Some(_), None) => false,
(None, _) => false, };
if account_matches {
return &ab.agent_id;
}
}
for ab in &self.agent_bindings {
if ab.channel_type != channel_name {
continue;
}
if ab.account_id.is_some() || ab.peer_filter.is_some() {
continue; }
return &ab.agent_id;
}
for binding in &self.bindings {
let channel_matches = binding
.match_rule
.channel
.as_ref()
.map(|c| c == &channel_name)
.unwrap_or(true);
if !channel_matches {
continue;
}
if let Some(ref bind_account) = binding.match_rule.account_id {
if msg_account != Some(bind_account.as_str()) {
continue;
}
}
if let Some(ref peer) = binding.match_rule.peer {
if let Some(kind) = peer.get("kind").and_then(|v| v.as_str()) {
if let Some(id) = peer.get("id").and_then(|v| v.as_str()) {
let peer_matches = match kind {
"user" => {
id.trim_start_matches('@') == msg.sender_id
|| msg.sender_name.as_deref()
== Some(id.trim_start_matches('@'))
}
"group" => msg.group_id.as_deref() == Some(id),
_ => false,
};
if !peer_matches {
continue;
}
}
}
}
return &binding.agent_id;
}
&self.default_agent_id
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::channel::ChannelType;
use crate::config::{RoutingBinding, RoutingConfig, RoutingMatch};
fn make_msg(channel: ChannelType, account_id: &str, sender_id: &str) -> InboundMessage {
InboundMessage {
channel_type: channel,
account_id: account_id.to_string(),
sender_id: sender_id.to_string(),
sender_name: None,
text: "hello".into(),
is_group: false,
group_id: None,
is_mention: false,
platform_message_id: "1".into(),
attachments: vec![],
metadata: std::collections::HashMap::new(),
source: crate::channel::MessageSource::Channel,
timestamp: chrono::Utc::now(),
}
}
#[test]
fn test_default_routing() {
let router = MessageRouter::new(&RoutingConfig { bindings: vec![] }, "main".into());
let msg = make_msg(ChannelType::Telegram, "", "123");
assert_eq!(router.resolve_agent(&msg), "main");
}
#[test]
fn test_channel_routing() {
let routing = RoutingConfig {
bindings: vec![RoutingBinding {
agent_id: "work".into(),
match_rule: RoutingMatch {
channel: Some("slack".into()),
account_id: None,
peer: None,
},
}],
};
let router = MessageRouter::new(&routing, "main".into());
assert_eq!(
router.resolve_agent(&make_msg(ChannelType::Slack, "", "U123")),
"work"
);
assert_eq!(
router.resolve_agent(&make_msg(ChannelType::Telegram, "", "456")),
"main"
);
}
#[test]
fn routing_falls_back_to_system_when_no_binding_matches() {
let mut router = MessageRouter::new(&RoutingConfig { bindings: vec![] }, "system".into());
router.add_agent_bindings(
"research",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: Some("default".into()),
peer_filter: None,
}],
);
router.add_agent_bindings(
"writer",
&[ChannelBinding {
channel_type: "slack".into(),
account_id: Some("team-alpha".into()),
peer_filter: None,
}],
);
let msg = make_msg(ChannelType::Webhook, "some-hook", "sender1");
assert_eq!(router.resolve_agent(&msg), "system");
let msg2 = make_msg(ChannelType::Discord, "guild1", "user1");
assert_eq!(router.resolve_agent(&msg2), "system");
}
#[test]
fn agent_binding_exact_match_takes_priority() {
let mut router = MessageRouter::new(&RoutingConfig { bindings: vec![] }, "system".into());
router.add_agent_bindings(
"general-tg",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: None,
peer_filter: None,
}],
);
router.add_agent_bindings(
"team-tg",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: Some("default".into()),
peer_filter: None,
}],
);
router.add_agent_bindings(
"vip-tg",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: Some("default".into()),
peer_filter: Some("vip-user".into()),
}],
);
let msg = make_msg(ChannelType::Telegram, "default", "vip-user");
assert_eq!(router.resolve_agent(&msg), "vip-tg");
let msg2 = make_msg(ChannelType::Telegram, "default", "regular-user");
assert_eq!(router.resolve_agent(&msg2), "team-tg");
let msg3 = make_msg(ChannelType::Telegram, "other-account", "someone");
assert_eq!(router.resolve_agent(&msg3), "general-tg");
}
#[test]
fn remove_agent_bindings_works() {
let mut router = MessageRouter::new(&RoutingConfig { bindings: vec![] }, "system".into());
router.add_agent_bindings(
"research",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: None,
peer_filter: None,
}],
);
let msg = make_msg(ChannelType::Telegram, "", "123");
assert_eq!(router.resolve_agent(&msg), "research");
router.remove_agent_bindings("research");
assert_eq!(router.resolve_agent(&msg), "system");
}
#[test]
fn update_agent_bindings_replaces() {
let mut router = MessageRouter::new(&RoutingConfig { bindings: vec![] }, "system".into());
router.add_agent_bindings(
"research",
&[ChannelBinding {
channel_type: "telegram".into(),
account_id: None,
peer_filter: None,
}],
);
assert_eq!(
router.resolve_agent(&make_msg(ChannelType::Telegram, "", "x")),
"research"
);
router.update_agent_bindings(
"research",
&[ChannelBinding {
channel_type: "slack".into(),
account_id: None,
peer_filter: None,
}],
);
assert_eq!(
router.resolve_agent(&make_msg(ChannelType::Telegram, "", "x")),
"system"
);
assert_eq!(
router.resolve_agent(&make_msg(ChannelType::Slack, "", "x")),
"research"
);
}
}