Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ Thumbs.db
*.swo
*~
.serena/

# Personal deploy scripts
scripts/deploy-remote.sh
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ clap_complete = "4"

# HTTP client (for LLM drivers)
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart", "rustls-tls", "gzip", "deflate", "brotli"] }
rustls = { version = "0.23", default-features = false, features = ["ring"] }

# Async trait
async-trait = "0.1"
Expand Down
13 changes: 13 additions & 0 deletions crates/openfang-api/src/channel_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 24 additions & 10 deletions crates/openfang-channels/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 => {}
Expand Down
1 change: 1 addition & 0 deletions crates/openfang-kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ subtle = { workspace = true }
rand = { workspace = true }
hex = { workspace = true }
reqwest = { workspace = true }
rustls = { workspace = true }
cron = "0.15"
zeroize = { workspace = true }

Expand Down
7 changes: 7 additions & 0 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,13 @@ impl OpenFangKernel {

/// Boot the kernel with an explicit configuration.
pub fn boot_with_config(mut config: KernelConfig) -> KernelResult<Self> {
if rustls::crypto::ring::default_provider()
.install_default()
.is_err()
{
debug!("rustls crypto provider already installed, skipping");
}

use openfang_types::config::KernelMode;

// Env var overrides — useful for Docker where config.toml is baked in.
Expand Down
2 changes: 1 addition & 1 deletion crates/openfang-runtime/src/web_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ mod tests {
let allow = vec!["*.example.com".to_string()];
assert!(check_ssrf("http://api.example.com", &allow).is_ok());
// Non-matching domain should still go through normal checks
assert!(is_host_allowed("other.net", &allow) == false);
assert!(!is_host_allowed("other.net", &allow));
}

#[test]
Expand Down
25 changes: 25 additions & 0 deletions crates/openfang-types/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1807,6 +1811,7 @@ impl Default for DiscordConfig {
intents: 37376,
ignore_bots: true,
default_channel_id: None,
free_response_channels: vec![],
overrides: ChannelOverrides::default(),
}
}
Expand Down Expand Up @@ -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();
Expand Down