Compare commits

...

2 Commits

Author SHA1 Message Date
argenis de la rosa
303c182a4f fix(channel): add missing proxy_url fields in test initializers 2026-03-21 07:31:28 -04:00
simianastronaut
6cdea12508 feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies
Allow each channel to optionally specify a `proxy_url` in its config,
enabling users behind restrictive networks to route channel traffic
through HTTP or SOCKS5 proxies. When set, the per-channel proxy takes
precedence over the global `[proxy]` config; when absent, the channel
falls back to the existing runtime proxy behavior.

Adds `proxy_url: Option<String>` to all 12 channel config structs
(Telegram, Discord, Slack, Mattermost, Signal, WhatsApp, Wati,
NextcloudTalk, DingTalk, QQ, Lark, Feishu) and introduces
`build_channel_proxy_client`, `build_channel_proxy_client_with_timeouts`,
and `apply_channel_proxy_to_builder` helpers that normalize proxy URLs
and integrate with the existing client cache.

Closes #3262

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:13:20 -04:00
18 changed files with 404 additions and 52 deletions

View File

@ -18,6 +18,8 @@ pub struct DingTalkChannel {
/// Per-chat session webhooks for sending replies (chatID -> webhook URL). /// Per-chat session webhooks for sending replies (chatID -> webhook URL).
/// DingTalk provides a unique webhook URL with each incoming message. /// DingTalk provides a unique webhook URL with each incoming message.
session_webhooks: Arc<RwLock<HashMap<String, String>>>, session_webhooks: Arc<RwLock<HashMap<String, String>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
/// Response from DingTalk gateway connection registration. /// Response from DingTalk gateway connection registration.
@ -34,11 +36,18 @@ impl DingTalkChannel {
client_secret, client_secret,
allowed_users, allowed_users,
session_webhooks: Arc::new(RwLock::new(HashMap::new())), session_webhooks: Arc::new(RwLock::new(HashMap::new())),
proxy_url: None,
} }
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.dingtalk") crate::config::build_channel_proxy_client("channel.dingtalk", self.proxy_url.as_deref())
} }
fn is_user_allowed(&self, user_id: &str) -> bool { fn is_user_allowed(&self, user_id: &str) -> bool {

View File

@ -18,6 +18,8 @@ pub struct DiscordChannel {
listen_to_bots: bool, listen_to_bots: bool,
mention_only: bool, mention_only: bool,
typing_handles: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>, typing_handles: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
impl DiscordChannel { impl DiscordChannel {
@ -35,11 +37,18 @@ impl DiscordChannel {
listen_to_bots, listen_to_bots,
mention_only, mention_only,
typing_handles: Mutex::new(HashMap::new()), typing_handles: Mutex::new(HashMap::new()),
proxy_url: None,
} }
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.discord") crate::config::build_channel_proxy_client("channel.discord", self.proxy_url.as_deref())
} }
/// Check if a Discord user ID is in the allowlist. /// Check if a Discord user ID is in the allowlist.

View File

@ -380,6 +380,8 @@ pub struct LarkChannel {
tenant_token: Arc<RwLock<Option<CachedTenantToken>>>, tenant_token: Arc<RwLock<Option<CachedTenantToken>>>,
/// Dedup set: WS message_ids seen in last ~30 min to prevent double-dispatch /// Dedup set: WS message_ids seen in last ~30 min to prevent double-dispatch
ws_seen_ids: Arc<RwLock<HashMap<String, Instant>>>, ws_seen_ids: Arc<RwLock<HashMap<String, Instant>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
impl LarkChannel { impl LarkChannel {
@ -423,6 +425,7 @@ impl LarkChannel {
receive_mode: crate::config::schema::LarkReceiveMode::default(), receive_mode: crate::config::schema::LarkReceiveMode::default(),
tenant_token: Arc::new(RwLock::new(None)), tenant_token: Arc::new(RwLock::new(None)),
ws_seen_ids: Arc::new(RwLock::new(HashMap::new())), ws_seen_ids: Arc::new(RwLock::new(HashMap::new())),
proxy_url: None,
} }
} }
@ -444,6 +447,7 @@ impl LarkChannel {
platform, platform,
); );
ch.receive_mode = config.receive_mode.clone(); ch.receive_mode = config.receive_mode.clone();
ch.proxy_url = config.proxy_url.clone();
ch ch
} }
@ -461,6 +465,7 @@ impl LarkChannel {
LarkPlatform::Lark, LarkPlatform::Lark,
); );
ch.receive_mode = config.receive_mode.clone(); ch.receive_mode = config.receive_mode.clone();
ch.proxy_url = config.proxy_url.clone();
ch ch
} }
@ -476,11 +481,15 @@ impl LarkChannel {
LarkPlatform::Feishu, LarkPlatform::Feishu,
); );
ch.receive_mode = config.receive_mode.clone(); ch.receive_mode = config.receive_mode.clone();
ch.proxy_url = config.proxy_url.clone();
ch ch
} }
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client(self.platform.proxy_service_key()) crate::config::build_channel_proxy_client(
self.platform.proxy_service_key(),
self.proxy_url.as_deref(),
)
} }
fn channel_name(&self) -> &'static str { fn channel_name(&self) -> &'static str {
@ -2113,6 +2122,7 @@ mod tests {
use_feishu: false, use_feishu: false,
receive_mode: LarkReceiveMode::default(), receive_mode: LarkReceiveMode::default(),
port: None, port: None,
proxy_url: None,
}; };
let json = serde_json::to_string(&lc).unwrap(); let json = serde_json::to_string(&lc).unwrap();
let parsed: LarkConfig = serde_json::from_str(&json).unwrap(); let parsed: LarkConfig = serde_json::from_str(&json).unwrap();
@ -2135,6 +2145,7 @@ mod tests {
use_feishu: false, use_feishu: false,
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let toml_str = toml::to_string(&lc).unwrap(); let toml_str = toml::to_string(&lc).unwrap();
let parsed: LarkConfig = toml::from_str(&toml_str).unwrap(); let parsed: LarkConfig = toml::from_str(&toml_str).unwrap();
@ -2169,6 +2180,7 @@ mod tests {
use_feishu: false, use_feishu: false,
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let ch = LarkChannel::from_config(&cfg); let ch = LarkChannel::from_config(&cfg);
@ -2193,6 +2205,7 @@ mod tests {
use_feishu: true, use_feishu: true,
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let ch = LarkChannel::from_lark_config(&cfg); let ch = LarkChannel::from_lark_config(&cfg);
@ -2214,6 +2227,7 @@ mod tests {
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let ch = LarkChannel::from_feishu_config(&cfg); let ch = LarkChannel::from_feishu_config(&cfg);
@ -2386,6 +2400,7 @@ mod tests {
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: crate::config::schema::LarkReceiveMode::Webhook, receive_mode: crate::config::schema::LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let ch_feishu = LarkChannel::from_feishu_config(&feishu_cfg); let ch_feishu = LarkChannel::from_feishu_config(&feishu_cfg);
assert_eq!( assert_eq!(

View File

@ -17,6 +17,8 @@ pub struct MattermostChannel {
mention_only: bool, mention_only: bool,
/// Handle for the background typing-indicator loop (aborted on stop_typing). /// Handle for the background typing-indicator loop (aborted on stop_typing).
typing_handle: Mutex<Option<tokio::task::JoinHandle<()>>>, typing_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
impl MattermostChannel { impl MattermostChannel {
@ -38,11 +40,18 @@ impl MattermostChannel {
thread_replies, thread_replies,
mention_only, mention_only,
typing_handle: Mutex::new(None), typing_handle: Mutex::new(None),
proxy_url: None,
} }
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.mattermost") crate::config::build_channel_proxy_client("channel.mattermost", self.proxy_url.as_deref())
} }
/// Check if a user ID is in the allowlist. /// Check if a user ID is in the allowlist.

View File

@ -3691,7 +3691,8 @@ fn collect_configured_channels(
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms) .with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
.with_transcription(config.transcription.clone()) .with_transcription(config.transcription.clone())
.with_tts(config.tts.clone()) .with_tts(config.tts.clone())
.with_workspace_dir(config.workspace_dir.clone()), .with_workspace_dir(config.workspace_dir.clone())
.with_proxy_url(tg.proxy_url.clone()),
), ),
}); });
} }
@ -3699,13 +3700,16 @@ fn collect_configured_channels(
if let Some(ref dc) = config.channels_config.discord { if let Some(ref dc) = config.channels_config.discord {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "Discord", display_name: "Discord",
channel: Arc::new(DiscordChannel::new( channel: Arc::new(
dc.bot_token.clone(), DiscordChannel::new(
dc.guild_id.clone(), dc.bot_token.clone(),
dc.allowed_users.clone(), dc.guild_id.clone(),
dc.listen_to_bots, dc.allowed_users.clone(),
dc.mention_only, dc.listen_to_bots,
)), dc.mention_only,
)
.with_proxy_url(dc.proxy_url.clone()),
),
}); });
} }
@ -3722,7 +3726,8 @@ fn collect_configured_channels(
) )
.with_thread_replies(sl.thread_replies.unwrap_or(true)) .with_thread_replies(sl.thread_replies.unwrap_or(true))
.with_group_reply_policy(sl.mention_only, Vec::new()) .with_group_reply_policy(sl.mention_only, Vec::new())
.with_workspace_dir(config.workspace_dir.clone()), .with_workspace_dir(config.workspace_dir.clone())
.with_proxy_url(sl.proxy_url.clone()),
), ),
}); });
} }
@ -3730,14 +3735,17 @@ fn collect_configured_channels(
if let Some(ref mm) = config.channels_config.mattermost { if let Some(ref mm) = config.channels_config.mattermost {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "Mattermost", display_name: "Mattermost",
channel: Arc::new(MattermostChannel::new( channel: Arc::new(
mm.url.clone(), MattermostChannel::new(
mm.bot_token.clone(), mm.url.clone(),
mm.channel_id.clone(), mm.bot_token.clone(),
mm.allowed_users.clone(), mm.channel_id.clone(),
mm.thread_replies.unwrap_or(true), mm.allowed_users.clone(),
mm.mention_only.unwrap_or(false), mm.thread_replies.unwrap_or(true),
)), mm.mention_only.unwrap_or(false),
)
.with_proxy_url(mm.proxy_url.clone()),
),
}); });
} }
@ -3775,14 +3783,17 @@ fn collect_configured_channels(
if let Some(ref sig) = config.channels_config.signal { if let Some(ref sig) = config.channels_config.signal {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "Signal", display_name: "Signal",
channel: Arc::new(SignalChannel::new( channel: Arc::new(
sig.http_url.clone(), SignalChannel::new(
sig.account.clone(), sig.http_url.clone(),
sig.group_id.clone(), sig.account.clone(),
sig.allowed_from.clone(), sig.group_id.clone(),
sig.ignore_attachments, sig.allowed_from.clone(),
sig.ignore_stories, sig.ignore_attachments,
)), sig.ignore_stories,
)
.with_proxy_url(sig.proxy_url.clone()),
),
}); });
} }
@ -3799,12 +3810,15 @@ fn collect_configured_channels(
if wa.is_cloud_config() { if wa.is_cloud_config() {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "WhatsApp", display_name: "WhatsApp",
channel: Arc::new(WhatsAppChannel::new( channel: Arc::new(
wa.access_token.clone().unwrap_or_default(), WhatsAppChannel::new(
wa.phone_number_id.clone().unwrap_or_default(), wa.access_token.clone().unwrap_or_default(),
wa.verify_token.clone().unwrap_or_default(), wa.phone_number_id.clone().unwrap_or_default(),
wa.allowed_numbers.clone(), wa.verify_token.clone().unwrap_or_default(),
)), wa.allowed_numbers.clone(),
)
.with_proxy_url(wa.proxy_url.clone()),
),
}); });
} else { } else {
tracing::warn!("WhatsApp Cloud API configured but missing required fields (phone_number_id, access_token, verify_token)"); tracing::warn!("WhatsApp Cloud API configured but missing required fields (phone_number_id, access_token, verify_token)");
@ -3861,11 +3875,12 @@ fn collect_configured_channels(
if let Some(ref wati_cfg) = config.channels_config.wati { if let Some(ref wati_cfg) = config.channels_config.wati {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "WATI", display_name: "WATI",
channel: Arc::new(WatiChannel::new( channel: Arc::new(WatiChannel::new_with_proxy(
wati_cfg.api_token.clone(), wati_cfg.api_token.clone(),
wati_cfg.api_url.clone(), wati_cfg.api_url.clone(),
wati_cfg.tenant_id.clone(), wati_cfg.tenant_id.clone(),
wati_cfg.allowed_numbers.clone(), wati_cfg.allowed_numbers.clone(),
wati_cfg.proxy_url.clone(),
)), )),
}); });
} }
@ -3873,10 +3888,11 @@ fn collect_configured_channels(
if let Some(ref nc) = config.channels_config.nextcloud_talk { if let Some(ref nc) = config.channels_config.nextcloud_talk {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "Nextcloud Talk", display_name: "Nextcloud Talk",
channel: Arc::new(NextcloudTalkChannel::new( channel: Arc::new(NextcloudTalkChannel::new_with_proxy(
nc.base_url.clone(), nc.base_url.clone(),
nc.app_token.clone(), nc.app_token.clone(),
nc.allowed_users.clone(), nc.allowed_users.clone(),
nc.proxy_url.clone(),
)), )),
}); });
} }
@ -3948,11 +3964,14 @@ fn collect_configured_channels(
if let Some(ref dt) = config.channels_config.dingtalk { if let Some(ref dt) = config.channels_config.dingtalk {
channels.push(ConfiguredChannel { channels.push(ConfiguredChannel {
display_name: "DingTalk", display_name: "DingTalk",
channel: Arc::new(DingTalkChannel::new( channel: Arc::new(
dt.client_id.clone(), DingTalkChannel::new(
dt.client_secret.clone(), dt.client_id.clone(),
dt.allowed_users.clone(), dt.client_secret.clone(),
)), dt.allowed_users.clone(),
)
.with_proxy_url(dt.proxy_url.clone()),
),
}); });
} }
@ -3965,7 +3984,8 @@ fn collect_configured_channels(
qq.app_secret.clone(), qq.app_secret.clone(),
qq.allowed_users.clone(), qq.allowed_users.clone(),
) )
.with_workspace_dir(config.workspace_dir.clone()), .with_workspace_dir(config.workspace_dir.clone())
.with_proxy_url(qq.proxy_url.clone()),
), ),
}); });
} }
@ -8721,6 +8741,7 @@ This is an example JSON object for profile settings."#;
thread_replies: Some(true), thread_replies: Some(true),
mention_only: Some(false), mention_only: Some(false),
interrupt_on_new_message: false, interrupt_on_new_message: false,
proxy_url: None,
}); });
let channels = collect_configured_channels(&config, "test"); let channels = collect_configured_channels(&config, "test");
@ -9613,6 +9634,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
match build_channel_by_id(&config, "telegram") { match build_channel_by_id(&config, "telegram") {
Ok(channel) => assert_eq!(channel.name(), "telegram"), Ok(channel) => assert_eq!(channel.name(), "telegram"),

View File

@ -17,11 +17,23 @@ pub struct NextcloudTalkChannel {
impl NextcloudTalkChannel { impl NextcloudTalkChannel {
pub fn new(base_url: String, app_token: String, allowed_users: Vec<String>) -> Self { pub fn new(base_url: String, app_token: String, allowed_users: Vec<String>) -> Self {
Self::new_with_proxy(base_url, app_token, allowed_users, None)
}
pub fn new_with_proxy(
base_url: String,
app_token: String,
allowed_users: Vec<String>,
proxy_url: Option<String>,
) -> Self {
Self { Self {
base_url: base_url.trim_end_matches('/').to_string(), base_url: base_url.trim_end_matches('/').to_string(),
app_token, app_token,
allowed_users, allowed_users,
client: reqwest::Client::new(), client: crate::config::build_channel_proxy_client(
"channel.nextcloud_talk",
proxy_url.as_deref(),
),
} }
} }

View File

@ -285,6 +285,8 @@ pub struct QQChannel {
upload_cache: Arc<RwLock<HashMap<String, UploadCacheEntry>>>, upload_cache: Arc<RwLock<HashMap<String, UploadCacheEntry>>>,
/// Passive reply tracker for QQ API rate limiting. /// Passive reply tracker for QQ API rate limiting.
reply_tracker: Arc<RwLock<HashMap<String, ReplyRecord>>>, reply_tracker: Arc<RwLock<HashMap<String, ReplyRecord>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
impl QQChannel { impl QQChannel {
@ -298,6 +300,7 @@ impl QQChannel {
workspace_dir: None, workspace_dir: None,
upload_cache: Arc::new(RwLock::new(HashMap::new())), upload_cache: Arc::new(RwLock::new(HashMap::new())),
reply_tracker: Arc::new(RwLock::new(HashMap::new())), reply_tracker: Arc::new(RwLock::new(HashMap::new())),
proxy_url: None,
} }
} }
@ -307,8 +310,14 @@ impl QQChannel {
self self
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.qq") crate::config::build_channel_proxy_client("channel.qq", self.proxy_url.as_deref())
} }
fn is_user_allowed(&self, user_id: &str) -> bool { fn is_user_allowed(&self, user_id: &str) -> bool {

View File

@ -28,6 +28,8 @@ pub struct SignalChannel {
allowed_from: Vec<String>, allowed_from: Vec<String>,
ignore_attachments: bool, ignore_attachments: bool,
ignore_stories: bool, ignore_stories: bool,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
// ── signal-cli SSE event JSON shapes ──────────────────────────── // ── signal-cli SSE event JSON shapes ────────────────────────────
@ -87,12 +89,23 @@ impl SignalChannel {
allowed_from, allowed_from,
ignore_attachments, ignore_attachments,
ignore_stories, ignore_stories,
proxy_url: None,
} }
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> Client { fn http_client(&self) -> Client {
let builder = Client::builder().connect_timeout(Duration::from_secs(10)); let builder = Client::builder().connect_timeout(Duration::from_secs(10));
let builder = crate::config::apply_runtime_proxy_to_builder(builder, "channel.signal"); let builder = crate::config::apply_channel_proxy_to_builder(
builder,
"channel.signal",
self.proxy_url.as_deref(),
);
builder.build().expect("Signal HTTP client should build") builder.build().expect("Signal HTTP client should build")
} }

View File

@ -32,6 +32,8 @@ pub struct SlackChannel {
workspace_dir: Option<PathBuf>, workspace_dir: Option<PathBuf>,
/// Maps channel_id -> thread_ts for active assistant threads (used for status indicators). /// Maps channel_id -> thread_ts for active assistant threads (used for status indicators).
active_assistant_thread: Mutex<HashMap<String, String>>, active_assistant_thread: Mutex<HashMap<String, String>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
const SLACK_HISTORY_MAX_RETRIES: u32 = 3; const SLACK_HISTORY_MAX_RETRIES: u32 = 3;
@ -121,6 +123,7 @@ impl SlackChannel {
user_display_name_cache: Mutex::new(HashMap::new()), user_display_name_cache: Mutex::new(HashMap::new()),
workspace_dir: None, workspace_dir: None,
active_assistant_thread: Mutex::new(HashMap::new()), active_assistant_thread: Mutex::new(HashMap::new()),
proxy_url: None,
} }
} }
@ -148,8 +151,19 @@ impl SlackChannel {
self self
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client_with_timeouts("channel.slack", 30, 10) crate::config::build_channel_proxy_client_with_timeouts(
"channel.slack",
self.proxy_url.as_deref(),
30,
10,
)
} }
/// Check if a Slack user ID is in the allowlist. /// Check if a Slack user ID is in the allowlist.
@ -804,12 +818,13 @@ impl SlackChannel {
} }
fn slack_media_http_client_no_redirect(&self) -> anyhow::Result<reqwest::Client> { fn slack_media_http_client_no_redirect(&self) -> anyhow::Result<reqwest::Client> {
let builder = crate::config::apply_runtime_proxy_to_builder( let builder = crate::config::apply_channel_proxy_to_builder(
reqwest::Client::builder() reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none()) .redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10)), .connect_timeout(Duration::from_secs(10)),
"channel.slack", "channel.slack",
self.proxy_url.as_deref(),
); );
builder builder
.build() .build()

View File

@ -337,6 +337,8 @@ pub struct TelegramChannel {
voice_chats: Arc<std::sync::Mutex<std::collections::HashSet<String>>>, voice_chats: Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
pending_voice: pending_voice:
Arc<std::sync::Mutex<std::collections::HashMap<String, (String, std::time::Instant)>>>, Arc<std::sync::Mutex<std::collections::HashMap<String, (String, std::time::Instant)>>>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -379,6 +381,7 @@ impl TelegramChannel {
tts_config: None, tts_config: None,
voice_chats: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())), voice_chats: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
pending_voice: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), pending_voice: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
proxy_url: None,
} }
} }
@ -388,6 +391,12 @@ impl TelegramChannel {
self self
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
/// Configure workspace directory for saving downloaded attachments. /// Configure workspace directory for saving downloaded attachments.
pub fn with_workspace_dir(mut self, dir: std::path::PathBuf) -> Self { pub fn with_workspace_dir(mut self, dir: std::path::PathBuf) -> Self {
self.workspace_dir = Some(dir); self.workspace_dir = Some(dir);
@ -478,7 +487,7 @@ impl TelegramChannel {
} }
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.telegram") crate::config::build_channel_proxy_client("channel.telegram", self.proxy_url.as_deref())
} }
fn normalize_identity(value: &str) -> String { fn normalize_identity(value: &str) -> String {

View File

@ -22,13 +22,23 @@ impl WatiChannel {
api_url: String, api_url: String,
tenant_id: Option<String>, tenant_id: Option<String>,
allowed_numbers: Vec<String>, allowed_numbers: Vec<String>,
) -> Self {
Self::new_with_proxy(api_token, api_url, tenant_id, allowed_numbers, None)
}
pub fn new_with_proxy(
api_token: String,
api_url: String,
tenant_id: Option<String>,
allowed_numbers: Vec<String>,
proxy_url: Option<String>,
) -> Self { ) -> Self {
Self { Self {
api_token, api_token,
api_url, api_url,
tenant_id, tenant_id,
allowed_numbers, allowed_numbers,
client: crate::config::build_runtime_proxy_client("channel.wati"), client: crate::config::build_channel_proxy_client("channel.wati", proxy_url.as_deref()),
} }
} }

View File

@ -27,6 +27,8 @@ pub struct WhatsAppChannel {
endpoint_id: String, endpoint_id: String,
verify_token: String, verify_token: String,
allowed_numbers: Vec<String>, allowed_numbers: Vec<String>,
/// Per-channel proxy URL override.
proxy_url: Option<String>,
} }
impl WhatsAppChannel { impl WhatsAppChannel {
@ -41,11 +43,18 @@ impl WhatsAppChannel {
endpoint_id, endpoint_id,
verify_token, verify_token,
allowed_numbers, allowed_numbers,
proxy_url: None,
} }
} }
/// Set a per-channel proxy URL that overrides the global proxy config.
pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
self.proxy_url = proxy_url;
self
}
fn http_client(&self) -> reqwest::Client { fn http_client(&self) -> reqwest::Client {
crate::config::build_runtime_proxy_client("channel.whatsapp") crate::config::build_channel_proxy_client("channel.whatsapp", self.proxy_url.as_deref())
} }
/// Check if a phone number is allowed (E.164 format: +1234567890) /// Check if a phone number is allowed (E.164 format: +1234567890)

View File

@ -4,7 +4,8 @@ pub mod workspace;
#[allow(unused_imports)] #[allow(unused_imports)]
pub use schema::{ pub use schema::{
apply_runtime_proxy_to_builder, build_runtime_proxy_client, apply_channel_proxy_to_builder, apply_runtime_proxy_to_builder, build_channel_proxy_client,
build_channel_proxy_client_with_timeouts, build_runtime_proxy_client,
build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config, build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config,
AgentConfig, AssemblyAiSttConfig, AuditConfig, AutonomyConfig, BackupConfig, AgentConfig, AssemblyAiSttConfig, AuditConfig, AutonomyConfig, BackupConfig,
BrowserComputerUseConfig, BrowserConfig, BuiltinHooksConfig, ChannelsConfig, BrowserComputerUseConfig, BrowserConfig, BuiltinHooksConfig, ChannelsConfig,
@ -58,6 +59,7 @@ mod tests {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}; };
let discord = DiscordConfig { let discord = DiscordConfig {
@ -67,6 +69,7 @@ mod tests {
listen_to_bots: false, listen_to_bots: false,
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
proxy_url: None,
}; };
let lark = LarkConfig { let lark = LarkConfig {
@ -79,6 +82,7 @@ mod tests {
use_feishu: false, use_feishu: false,
receive_mode: crate::config::schema::LarkReceiveMode::Websocket, receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}; };
let feishu = FeishuConfig { let feishu = FeishuConfig {
app_id: "app-id".into(), app_id: "app-id".into(),
@ -88,6 +92,7 @@ mod tests {
allowed_users: vec![], allowed_users: vec![],
receive_mode: crate::config::schema::LarkReceiveMode::Websocket, receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}; };
let nextcloud_talk = NextcloudTalkConfig { let nextcloud_talk = NextcloudTalkConfig {
@ -95,6 +100,7 @@ mod tests {
app_token: "app-token".into(), app_token: "app-token".into(),
webhook_secret: None, webhook_secret: None,
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}; };
assert_eq!(telegram.allowed_users.len(), 1); assert_eq!(telegram.allowed_users.len(), 1);

View File

@ -3381,6 +3381,116 @@ pub fn build_runtime_proxy_client_with_timeouts(
client client
} }
/// Build an HTTP client for a channel, using an explicit per-channel proxy URL
/// when configured. Falls back to the global runtime proxy when `proxy_url` is
/// `None` or empty.
pub fn build_channel_proxy_client(service_key: &str, proxy_url: Option<&str>) -> reqwest::Client {
match normalize_proxy_url_option(proxy_url) {
Some(url) => build_explicit_proxy_client(service_key, &url, None, None),
None => build_runtime_proxy_client(service_key),
}
}
/// Build an HTTP client for a channel with custom timeouts, using an explicit
/// per-channel proxy URL when configured. Falls back to the global runtime
/// proxy when `proxy_url` is `None` or empty.
pub fn build_channel_proxy_client_with_timeouts(
service_key: &str,
proxy_url: Option<&str>,
timeout_secs: u64,
connect_timeout_secs: u64,
) -> reqwest::Client {
match normalize_proxy_url_option(proxy_url) {
Some(url) => build_explicit_proxy_client(
service_key,
&url,
Some(timeout_secs),
Some(connect_timeout_secs),
),
None => build_runtime_proxy_client_with_timeouts(
service_key,
timeout_secs,
connect_timeout_secs,
),
}
}
/// Apply an explicit proxy URL to a `reqwest::ClientBuilder`, returning the
/// modified builder. Used by channels that specify a per-channel `proxy_url`.
pub fn apply_channel_proxy_to_builder(
builder: reqwest::ClientBuilder,
service_key: &str,
proxy_url: Option<&str>,
) -> reqwest::ClientBuilder {
match normalize_proxy_url_option(proxy_url) {
Some(url) => apply_explicit_proxy_to_builder(builder, service_key, &url),
None => apply_runtime_proxy_to_builder(builder, service_key),
}
}
/// Build a client with a single explicit proxy URL (http+https via `Proxy::all`).
fn build_explicit_proxy_client(
service_key: &str,
proxy_url: &str,
timeout_secs: Option<u64>,
connect_timeout_secs: Option<u64>,
) -> reqwest::Client {
let cache_key = format!(
"explicit|{}|{}|timeout={}|connect_timeout={}",
service_key.trim().to_ascii_lowercase(),
proxy_url,
timeout_secs
.map(|v| v.to_string())
.unwrap_or_else(|| "none".to_string()),
connect_timeout_secs
.map(|v| v.to_string())
.unwrap_or_else(|| "none".to_string()),
);
if let Some(client) = runtime_proxy_cached_client(&cache_key) {
return client;
}
let mut builder = reqwest::Client::builder();
if let Some(t) = timeout_secs {
builder = builder.timeout(std::time::Duration::from_secs(t));
}
if let Some(ct) = connect_timeout_secs {
builder = builder.connect_timeout(std::time::Duration::from_secs(ct));
}
builder = apply_explicit_proxy_to_builder(builder, service_key, proxy_url);
let client = builder.build().unwrap_or_else(|error| {
tracing::warn!(
service_key,
proxy_url,
"Failed to build channel proxy client: {error}"
);
reqwest::Client::new()
});
set_runtime_proxy_cached_client(cache_key, client.clone());
client
}
/// Apply a single explicit proxy URL to a builder via `Proxy::all`.
fn apply_explicit_proxy_to_builder(
mut builder: reqwest::ClientBuilder,
service_key: &str,
proxy_url: &str,
) -> reqwest::ClientBuilder {
match reqwest::Proxy::all(proxy_url) {
Ok(proxy) => {
builder = builder.proxy(proxy);
}
Err(error) => {
tracing::warn!(
proxy_url,
service_key,
"Ignoring invalid channel proxy_url: {error}"
);
}
}
builder
}
fn parse_proxy_scope(raw: &str) -> Option<ProxyScope> { fn parse_proxy_scope(raw: &str) -> Option<ProxyScope> {
match raw.trim().to_ascii_lowercase().as_str() { match raw.trim().to_ascii_lowercase().as_str() {
"environment" | "env" => Some(ProxyScope::Environment), "environment" | "env" => Some(ProxyScope::Environment),
@ -4885,6 +4995,10 @@ pub struct TelegramConfig {
/// explicitly, it takes precedence. /// explicitly, it takes precedence.
#[serde(default)] #[serde(default)]
pub ack_reactions: Option<bool>, pub ack_reactions: Option<bool>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for TelegramConfig { impl ChannelConfig for TelegramConfig {
@ -4918,6 +5032,10 @@ pub struct DiscordConfig {
/// Other messages in the guild are silently ignored. /// Other messages in the guild are silently ignored.
#[serde(default)] #[serde(default)]
pub mention_only: bool, pub mention_only: bool,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for DiscordConfig { impl ChannelConfig for DiscordConfig {
@ -4954,6 +5072,10 @@ pub struct SlackConfig {
/// Direct messages remain allowed. /// Direct messages remain allowed.
#[serde(default)] #[serde(default)]
pub mention_only: bool, pub mention_only: bool,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for SlackConfig { impl ChannelConfig for SlackConfig {
@ -4989,6 +5111,10 @@ pub struct MattermostConfig {
/// cancels the in-flight request and starts a fresh response with preserved history. /// cancels the in-flight request and starts a fresh response with preserved history.
#[serde(default)] #[serde(default)]
pub interrupt_on_new_message: bool, pub interrupt_on_new_message: bool,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for MattermostConfig { impl ChannelConfig for MattermostConfig {
@ -5101,6 +5227,10 @@ pub struct SignalConfig {
/// Skip incoming story messages. /// Skip incoming story messages.
#[serde(default)] #[serde(default)]
pub ignore_stories: bool, pub ignore_stories: bool,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for SignalConfig { impl ChannelConfig for SignalConfig {
@ -5195,6 +5325,10 @@ pub struct WhatsAppConfig {
/// user's own self-chat (Notes to Self). Defaults to false. /// user's own self-chat (Notes to Self). Defaults to false.
#[serde(default)] #[serde(default)]
pub self_chat_mode: bool, pub self_chat_mode: bool,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for WhatsAppConfig { impl ChannelConfig for WhatsAppConfig {
@ -5243,6 +5377,10 @@ pub struct WatiConfig {
/// Allowed phone numbers (E.164 format) or "*" for all. /// Allowed phone numbers (E.164 format) or "*" for all.
#[serde(default)] #[serde(default)]
pub allowed_numbers: Vec<String>, pub allowed_numbers: Vec<String>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
fn default_wati_api_url() -> String { fn default_wati_api_url() -> String {
@ -5273,6 +5411,10 @@ pub struct NextcloudTalkConfig {
/// Allowed Nextcloud actor IDs (`[]` = deny all, `"*"` = allow all). /// Allowed Nextcloud actor IDs (`[]` = deny all, `"*"` = allow all).
#[serde(default)] #[serde(default)]
pub allowed_users: Vec<String>, pub allowed_users: Vec<String>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for NextcloudTalkConfig { impl ChannelConfig for NextcloudTalkConfig {
@ -5400,6 +5542,10 @@ pub struct LarkConfig {
/// Not required (and ignored) for websocket mode. /// Not required (and ignored) for websocket mode.
#[serde(default)] #[serde(default)]
pub port: Option<u16>, pub port: Option<u16>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for LarkConfig { impl ChannelConfig for LarkConfig {
@ -5434,6 +5580,10 @@ pub struct FeishuConfig {
/// Not required (and ignored) for websocket mode. /// Not required (and ignored) for websocket mode.
#[serde(default)] #[serde(default)]
pub port: Option<u16>, pub port: Option<u16>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for FeishuConfig { impl ChannelConfig for FeishuConfig {
@ -5895,6 +6045,10 @@ pub struct DingTalkConfig {
/// Allowed user IDs (staff IDs). Empty = deny all, "*" = allow all /// Allowed user IDs (staff IDs). Empty = deny all, "*" = allow all
#[serde(default)] #[serde(default)]
pub allowed_users: Vec<String>, pub allowed_users: Vec<String>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for DingTalkConfig { impl ChannelConfig for DingTalkConfig {
@ -5935,6 +6089,10 @@ pub struct QQConfig {
/// Allowed user IDs. Empty = deny all, "*" = allow all /// Allowed user IDs. Empty = deny all, "*" = allow all
#[serde(default)] #[serde(default)]
pub allowed_users: Vec<String>, pub allowed_users: Vec<String>,
/// Per-channel proxy URL (http, https, socks5, socks5h).
/// Overrides the global `[proxy]` setting for this channel only.
#[serde(default)]
pub proxy_url: Option<String>,
} }
impl ChannelConfig for QQConfig { impl ChannelConfig for QQConfig {
@ -9335,6 +9493,7 @@ default_temperature = 0.7
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}), }),
discord: None, discord: None,
slack: None, slack: None,
@ -9789,6 +9948,7 @@ tool_dispatcher = "xml"
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: LarkReceiveMode::Websocket, receive_mode: LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}); });
config.agents.insert( config.agents.insert(
@ -9930,6 +10090,7 @@ tool_dispatcher = "xml"
interrupt_on_new_message: true, interrupt_on_new_message: true,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}; };
let json = serde_json::to_string(&tc).unwrap(); let json = serde_json::to_string(&tc).unwrap();
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap(); let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
@ -9958,6 +10119,7 @@ tool_dispatcher = "xml"
listen_to_bots: false, listen_to_bots: false,
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
proxy_url: None,
}; };
let json = serde_json::to_string(&dc).unwrap(); let json = serde_json::to_string(&dc).unwrap();
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap(); let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();
@ -9974,6 +10136,7 @@ tool_dispatcher = "xml"
listen_to_bots: false, listen_to_bots: false,
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
proxy_url: None,
}; };
let json = serde_json::to_string(&dc).unwrap(); let json = serde_json::to_string(&dc).unwrap();
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap(); let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();
@ -10075,6 +10238,7 @@ allowed_users = ["@ops:matrix.org"]
allowed_from: vec!["+1111111111".into()], allowed_from: vec!["+1111111111".into()],
ignore_attachments: true, ignore_attachments: true,
ignore_stories: false, ignore_stories: false,
proxy_url: None,
}; };
let json = serde_json::to_string(&sc).unwrap(); let json = serde_json::to_string(&sc).unwrap();
let parsed: SignalConfig = serde_json::from_str(&json).unwrap(); let parsed: SignalConfig = serde_json::from_str(&json).unwrap();
@ -10095,6 +10259,7 @@ allowed_users = ["@ops:matrix.org"]
allowed_from: vec!["*".into()], allowed_from: vec!["*".into()],
ignore_attachments: false, ignore_attachments: false,
ignore_stories: true, ignore_stories: true,
proxy_url: None,
}; };
let toml_str = toml::to_string(&sc).unwrap(); let toml_str = toml::to_string(&sc).unwrap();
let parsed: SignalConfig = toml::from_str(&toml_str).unwrap(); let parsed: SignalConfig = toml::from_str(&toml_str).unwrap();
@ -10325,6 +10490,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}; };
let json = serde_json::to_string(&wc).unwrap(); let json = serde_json::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = serde_json::from_str(&json).unwrap(); let parsed: WhatsAppConfig = serde_json::from_str(&json).unwrap();
@ -10349,6 +10515,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}; };
let toml_str = toml::to_string(&wc).unwrap(); let toml_str = toml::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap(); let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
@ -10378,6 +10545,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}; };
let toml_str = toml::to_string(&wc).unwrap(); let toml_str = toml::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap(); let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
@ -10399,6 +10567,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}; };
assert!(wc.is_ambiguous_config()); assert!(wc.is_ambiguous_config());
assert_eq!(wc.backend_type(), "cloud"); assert_eq!(wc.backend_type(), "cloud");
@ -10419,6 +10588,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}; };
assert!(!wc.is_ambiguous_config()); assert!(!wc.is_ambiguous_config());
assert_eq!(wc.backend_type(), "web"); assert_eq!(wc.backend_type(), "web");
@ -10449,6 +10619,7 @@ channel_id = "C123"
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}), }),
linq: None, linq: None,
wati: None, wati: None,
@ -11453,6 +11624,7 @@ default_model = "legacy-model"
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: LarkReceiveMode::Websocket, receive_mode: LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}); });
config.save().await.unwrap(); config.save().await.unwrap();
@ -12164,6 +12336,7 @@ default_model = "persisted-profile"
use_feishu: true, use_feishu: true,
receive_mode: LarkReceiveMode::Websocket, receive_mode: LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}; };
let json = serde_json::to_string(&lc).unwrap(); let json = serde_json::to_string(&lc).unwrap();
let parsed: LarkConfig = serde_json::from_str(&json).unwrap(); let parsed: LarkConfig = serde_json::from_str(&json).unwrap();
@ -12187,6 +12360,7 @@ default_model = "persisted-profile"
use_feishu: false, use_feishu: false,
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let toml_str = toml::to_string(&lc).unwrap(); let toml_str = toml::to_string(&lc).unwrap();
let parsed: LarkConfig = toml::from_str(&toml_str).unwrap(); let parsed: LarkConfig = toml::from_str(&toml_str).unwrap();
@ -12233,6 +12407,7 @@ default_model = "persisted-profile"
allowed_users: vec!["user_123".into(), "user_456".into()], allowed_users: vec!["user_123".into(), "user_456".into()],
receive_mode: LarkReceiveMode::Websocket, receive_mode: LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}; };
let json = serde_json::to_string(&fc).unwrap(); let json = serde_json::to_string(&fc).unwrap();
let parsed: FeishuConfig = serde_json::from_str(&json).unwrap(); let parsed: FeishuConfig = serde_json::from_str(&json).unwrap();
@ -12253,6 +12428,7 @@ default_model = "persisted-profile"
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: LarkReceiveMode::Webhook, receive_mode: LarkReceiveMode::Webhook,
port: Some(9898), port: Some(9898),
proxy_url: None,
}; };
let toml_str = toml::to_string(&fc).unwrap(); let toml_str = toml::to_string(&fc).unwrap();
let parsed: FeishuConfig = toml::from_str(&toml_str).unwrap(); let parsed: FeishuConfig = toml::from_str(&toml_str).unwrap();
@ -12280,6 +12456,7 @@ default_model = "persisted-profile"
app_token: "app-token".into(), app_token: "app-token".into(),
webhook_secret: Some("webhook-secret".into()), webhook_secret: Some("webhook-secret".into()),
allowed_users: vec!["user_a".into(), "*".into()], allowed_users: vec!["user_a".into(), "*".into()],
proxy_url: None,
}; };
let json = serde_json::to_string(&nc).unwrap(); let json = serde_json::to_string(&nc).unwrap();
@ -12488,6 +12665,7 @@ require_otp_to_resume = true
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
// Save (triggers encryption) // Save (triggers encryption)

View File

@ -646,6 +646,7 @@ mod tests {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
assert!(has_supervised_channels(&config)); assert!(has_supervised_channels(&config));
} }
@ -657,6 +658,7 @@ mod tests {
client_id: "client_id".into(), client_id: "client_id".into(),
client_secret: "client_secret".into(), client_secret: "client_secret".into(),
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}); });
assert!(has_supervised_channels(&config)); assert!(has_supervised_channels(&config));
} }
@ -672,6 +674,7 @@ mod tests {
thread_replies: Some(true), thread_replies: Some(true),
mention_only: Some(false), mention_only: Some(false),
interrupt_on_new_message: false, interrupt_on_new_message: false,
proxy_url: None,
}); });
assert!(has_supervised_channels(&config)); assert!(has_supervised_channels(&config));
} }
@ -683,6 +686,7 @@ mod tests {
app_id: "app-id".into(), app_id: "app-id".into(),
app_secret: "app-secret".into(), app_secret: "app-secret".into(),
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}); });
assert!(has_supervised_channels(&config)); assert!(has_supervised_channels(&config));
} }
@ -695,6 +699,7 @@ mod tests {
app_token: "app-token".into(), app_token: "app-token".into(),
webhook_secret: None, webhook_secret: None,
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}); });
assert!(has_supervised_channels(&config)); assert!(has_supervised_channels(&config));
} }
@ -761,6 +766,7 @@ mod tests {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
let target = resolve_heartbeat_delivery(&config).unwrap(); let target = resolve_heartbeat_delivery(&config).unwrap();
@ -778,6 +784,7 @@ mod tests {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
let target = resolve_heartbeat_delivery(&config).unwrap(); let target = resolve_heartbeat_delivery(&config).unwrap();

View File

@ -1457,6 +1457,7 @@ mod tests {
api_url: "https://live-mt-server.wati.io".to_string(), api_url: "https://live-mt-server.wati.io".to_string(),
tenant_id: None, tenant_id: None,
allowed_numbers: vec![], allowed_numbers: vec![],
proxy_url: None,
}); });
cfg.channels_config.feishu = Some(crate::config::schema::FeishuConfig { cfg.channels_config.feishu = Some(crate::config::schema::FeishuConfig {
app_id: "cli_aabbcc".to_string(), app_id: "cli_aabbcc".to_string(),
@ -1466,6 +1467,7 @@ mod tests {
allowed_users: vec!["*".to_string()], allowed_users: vec!["*".to_string()],
receive_mode: crate::config::schema::LarkReceiveMode::Websocket, receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}); });
cfg.channels_config.email = Some(crate::channels::email_channel::EmailConfig { cfg.channels_config.email = Some(crate::channels::email_channel::EmailConfig {
imap_host: "imap.example.com".to_string(), imap_host: "imap.example.com".to_string(),
@ -1591,6 +1593,7 @@ mod tests {
api_url: "https://live-mt-server.wati.io".to_string(), api_url: "https://live-mt-server.wati.io".to_string(),
tenant_id: None, tenant_id: None,
allowed_numbers: vec![], allowed_numbers: vec![],
proxy_url: None,
}); });
current.channels_config.feishu = Some(crate::config::schema::FeishuConfig { current.channels_config.feishu = Some(crate::config::schema::FeishuConfig {
app_id: "cli_current".to_string(), app_id: "cli_current".to_string(),
@ -1600,6 +1603,7 @@ mod tests {
allowed_users: vec!["*".to_string()], allowed_users: vec!["*".to_string()],
receive_mode: crate::config::schema::LarkReceiveMode::Websocket, receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}); });
current.channels_config.email = Some(crate::channels::email_channel::EmailConfig { current.channels_config.email = Some(crate::channels::email_channel::EmailConfig {
imap_host: "imap.example.com".to_string(), imap_host: "imap.example.com".to_string(),

View File

@ -841,6 +841,7 @@ mod tests {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
let entries = all_integrations(); let entries = all_integrations();
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap(); let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();

View File

@ -3790,6 +3790,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
ack_reactions: None, ack_reactions: None,
proxy_url: None,
}); });
} }
ChannelMenuChoice::Discord => { ChannelMenuChoice::Discord => {
@ -3890,6 +3891,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
listen_to_bots: false, listen_to_bots: false,
interrupt_on_new_message: false, interrupt_on_new_message: false,
mention_only: false, mention_only: false,
proxy_url: None,
}); });
} }
ChannelMenuChoice::Slack => { ChannelMenuChoice::Slack => {
@ -4020,6 +4022,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
interrupt_on_new_message: false, interrupt_on_new_message: false,
thread_replies: None, thread_replies: None,
mention_only: false, mention_only: false,
proxy_url: None,
}); });
} }
ChannelMenuChoice::IMessage => { ChannelMenuChoice::IMessage => {
@ -4271,6 +4274,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
allowed_from, allowed_from,
ignore_attachments, ignore_attachments,
ignore_stories, ignore_stories,
proxy_url: None,
}); });
println!(" {} Signal configured", style("").green().bold()); println!(" {} Signal configured", style("").green().bold());
@ -4372,6 +4376,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}); });
println!( println!(
@ -4477,6 +4482,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
dm_policy: WhatsAppChatPolicy::default(), dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(), group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false, self_chat_mode: false,
proxy_url: None,
}); });
} }
ChannelMenuChoice::Linq => { ChannelMenuChoice::Linq => {
@ -4810,6 +4816,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
Some(webhook_secret.trim().to_string()) Some(webhook_secret.trim().to_string())
}, },
allowed_users, allowed_users,
proxy_url: None,
}); });
println!(" {} Nextcloud Talk configured", style("").green().bold()); println!(" {} Nextcloud Talk configured", style("").green().bold());
@ -4882,6 +4889,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
client_id, client_id,
client_secret, client_secret,
allowed_users, allowed_users,
proxy_url: None,
}); });
} }
ChannelMenuChoice::QqOfficial => { ChannelMenuChoice::QqOfficial => {
@ -4958,6 +4966,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
app_id, app_id,
app_secret, app_secret,
allowed_users, allowed_users,
proxy_url: None,
}); });
} }
ChannelMenuChoice::Lark | ChannelMenuChoice::Feishu => { ChannelMenuChoice::Lark | ChannelMenuChoice::Feishu => {
@ -5147,6 +5156,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
use_feishu: is_feishu, use_feishu: is_feishu,
receive_mode, receive_mode,
port, port,
proxy_url: None,
}); });
} }
#[cfg(feature = "channel-nostr")] #[cfg(feature = "channel-nostr")]
@ -7511,6 +7521,7 @@ mod tests {
allowed_from: vec!["*".into()], allowed_from: vec!["*".into()],
ignore_attachments: false, ignore_attachments: false,
ignore_stories: true, ignore_stories: true,
proxy_url: None,
}); });
assert!(has_launchable_channels(&channels)); assert!(has_launchable_channels(&channels));
@ -7523,6 +7534,7 @@ mod tests {
thread_replies: Some(true), thread_replies: Some(true),
mention_only: Some(false), mention_only: Some(false),
interrupt_on_new_message: false, interrupt_on_new_message: false,
proxy_url: None,
}); });
assert!(has_launchable_channels(&channels)); assert!(has_launchable_channels(&channels));
@ -7531,6 +7543,7 @@ mod tests {
app_id: "app-id".into(), app_id: "app-id".into(),
app_secret: "app-secret".into(), app_secret: "app-secret".into(),
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}); });
assert!(has_launchable_channels(&channels)); assert!(has_launchable_channels(&channels));
@ -7540,6 +7553,7 @@ mod tests {
app_token: "token".into(), app_token: "token".into(),
webhook_secret: Some("secret".into()), webhook_secret: Some("secret".into()),
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
proxy_url: None,
}); });
assert!(has_launchable_channels(&channels)); assert!(has_launchable_channels(&channels));
@ -7552,6 +7566,7 @@ mod tests {
allowed_users: vec!["*".into()], allowed_users: vec!["*".into()],
receive_mode: crate::config::schema::LarkReceiveMode::Websocket, receive_mode: crate::config::schema::LarkReceiveMode::Websocket,
port: None, port: None,
proxy_url: None,
}); });
assert!(has_launchable_channels(&channels)); assert!(has_launchable_channels(&channels));
} }