feat: add free_response_channels support for Discord

Allow certain Discord channel IDs to respond without requiring @mention,
similar to Hermes gateway's free_response_channels.

- Add free_response_channels field to DiscordConfig
- Add free_response_channels method to ChannelBridgeHandle trait
- Implement free_response_channels in KernelBridgeAdapter
- Modify dispatch_message to bypass mention_only policy for free channels
- Add test for free_response_channels deserialization
This commit is contained in:
Matteo De Agazio
2026-04-09 17:10:21 +02:00
parent af51f6c971
commit d94508e72e
3 changed files with 62 additions and 10 deletions
+13
View File
@@ -816,6 +816,19 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
}
}
async fn free_response_channels(&self, channel_type: &str) -> Vec<String> {
let channels = &self.kernel.config.channels;
match channel_type {
"discord" => channels
.discord
.as_ref()
.map(|c| c.free_response_channels.clone())
.unwrap_or_default(),
// Add other channel types here as needed (e.g., "telegram" => ...)
_ => Vec::new(),
}
}
async fn authorize_channel_user(
&self,
channel_type: &str,
+24 -10
View File
@@ -220,6 +220,13 @@ pub trait ChannelBridgeHandle: Send + Sync {
None
}
/// Get channel IDs that respond without requiring @mention (free response mode).
///
/// Returns an empty vector if the channel type is not configured or has no free response channels.
async fn free_response_channels(&self, _channel_type: &str) -> Vec<String> {
Vec::new()
}
/// Record a delivery result for tracking (optional — default no-op).
///
/// `thread_id` preserves Telegram forum-topic context so cron/workflow
@@ -659,16 +666,23 @@ async fn dispatch_message(
}
}
GroupPolicy::MentionOnly => {
// Only allow messages where the bot was @mentioned or commands.
let was_mentioned = message
.metadata
.get("was_mentioned")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let is_command = matches!(&message.content, ChannelContent::Command { .. });
if !was_mentioned && !is_command {
debug!("Ignoring group message on {ct_str} (group_policy=mention_only, not mentioned)");
return;
// Check if this channel is in the free_response list - if so, allow all messages
let free_channels = handle.free_response_channels(ct_str).await;
let channel_id = &message.sender.platform_id;
let is_free_channel = free_channels.iter().any(|id| id == channel_id);
if !is_free_channel {
// Only allow messages where the bot was @mentioned or commands.
let was_mentioned = message
.metadata
.get("was_mentioned")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let is_command = matches!(&message.content, ChannelContent::Command { .. });
if !was_mentioned && !is_command {
debug!("Ignoring group message on {ct_str} (group_policy=mention_only, not mentioned)");
return;
}
}
}
GroupPolicy::All => {}
+25
View File
@@ -1792,6 +1792,10 @@ pub struct DiscordConfig {
/// Default channel ID for outgoing messages when no recipient is specified.
#[serde(default)]
pub default_channel_id: Option<String>,
/// Channel IDs that respond without requiring @mention (free response mode).
/// In these channels, the bot responds to all group messages without needing to be mentioned.
#[serde(default, deserialize_with = "deserialize_string_or_int_vec")]
pub free_response_channels: Vec<String>,
/// Per-channel behavior overrides.
#[serde(default)]
pub overrides: ChannelOverrides,
@@ -1807,6 +1811,7 @@ impl Default for DiscordConfig {
intents: 37376,
ignore_bots: true,
default_channel_id: None,
free_response_channels: vec![],
overrides: ChannelOverrides::default(),
}
}
@@ -3706,6 +3711,26 @@ mod tests {
assert!(dc2.ignore_bots);
}
#[test]
fn test_discord_config_free_response_channels_deserialization() {
// Test with free_response_channels as list of strings
let toml_str = r#"
bot_token_env = "DISCORD_BOT_TOKEN"
free_response_channels = ["123456789", "987654321"]
"#;
let dc: DiscordConfig = toml::from_str(toml_str).unwrap();
assert_eq!(dc.free_response_channels.len(), 2);
assert_eq!(dc.free_response_channels[0], "123456789");
assert_eq!(dc.free_response_channels[1], "987654321");
// Test default (empty list)
let toml_str2 = r#"
bot_token_env = "DISCORD_BOT_TOKEN"
"#;
let dc2: DiscordConfig = toml::from_str(toml_str2).unwrap();
assert!(dc2.free_response_channels.is_empty());
}
#[test]
fn test_slack_config_defaults() {
let sl = SlackConfig::default();