Merge pull request #1024 from Hypn0sis/fix/crypto-discord-free-response

fix: crypto provider, silent failure debug, Discord free_response_channels
This commit is contained in:
Jaber Jaber
2026-04-10 19:13:47 +03:00
committed by GitHub
8 changed files with 75 additions and 10 deletions

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ Thumbs.db
*.swo
*~
.serena/
# Personal deploy scripts
scripts/deploy-remote.sh

1
Cargo.lock generated
View File

@@ -4180,6 +4180,7 @@ dependencies = [
"openfang-wire",
"rand 0.8.5",
"reqwest 0.12.28",
"rustls 0.23.37",
"serde",
"serde_json",
"subtle",

View File

@@ -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"

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,

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
@@ -660,16 +667,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 => {}

View File

@@ -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 }

View File

@@ -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.

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();