use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelegramConfigFile {
#[serde(default)]
pub bot_token: String,
#[serde(default)]
pub approval_chat_id: Option<String>,
#[serde(default = "default_allow_all")]
pub allowed_users: Vec<String>,
#[serde(default)]
pub mention_only: bool,
#[serde(default)]
pub strict_kb_grounding: bool,
#[serde(default)]
pub model_provider_id: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub style_profile: tandem_channels::config::TelegramStyleProfile,
#[serde(default)]
pub security_profile: tandem_channels::config::ChannelSecurityProfile,
#[serde(default)]
pub webhook_secret_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscordConfigFile {
#[serde(default)]
pub bot_token: String,
#[serde(default)]
pub approval_channel_id: Option<String>,
#[serde(default)]
pub guild_id: Option<String>,
#[serde(default = "default_allow_all")]
pub allowed_users: Vec<String>,
#[serde(default = "default_discord_mention_only")]
pub mention_only: bool,
#[serde(default)]
pub strict_kb_grounding: bool,
#[serde(default)]
pub model_provider_id: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub security_profile: tandem_channels::config::ChannelSecurityProfile,
#[serde(default)]
pub public_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlackConfigFile {
#[serde(default)]
pub bot_token: String,
#[serde(default)]
pub channel_id: String,
#[serde(default = "default_allow_all")]
pub allowed_users: Vec<String>,
#[serde(default)]
pub mention_only: bool,
#[serde(default)]
pub strict_kb_grounding: bool,
#[serde(default)]
pub model_provider_id: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub security_profile: tandem_channels::config::ChannelSecurityProfile,
#[serde(default)]
pub signing_secret: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChannelsConfigFile {
pub telegram: Option<TelegramConfigFile>,
pub discord: Option<DiscordConfigFile>,
pub slack: Option<SlackConfigFile>,
#[serde(default)]
pub tool_policy: tandem_channels::config::ChannelToolPolicy,
}
pub fn normalize_allowed_users_or_wildcard(raw: Vec<String>) -> Vec<String> {
let normalized = normalize_non_empty_list(raw);
if normalized.is_empty() {
return default_allow_all();
}
normalized
}
pub fn normalize_allowed_tools(raw: Vec<String>) -> Vec<String> {
normalize_non_empty_list(raw)
}
fn default_allow_all() -> Vec<String> {
vec!["*".to_string()]
}
fn default_discord_mention_only() -> bool {
true
}
fn normalize_non_empty_list(raw: Vec<String>) -> Vec<String> {
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for item in raw {
let normalized = item.trim().to_string();
if normalized.is_empty() {
continue;
}
if seen.insert(normalized.clone()) {
out.push(normalized);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn partial_channel_entries_without_tokens_still_deserialize() {
let cfg: ChannelsConfigFile = serde_json::from_value(json!({
"telegram": {
"bot_token": "tg-secret",
"allowed_users": ["123456789"],
"security_profile": "trusted_team"
},
"discord": {
"allowed_users": ["*"],
"mention_only": true
},
"slack": {
"channel_id": "C123",
"allowed_users": ["U1"]
}
}))
.expect("partial channel config should deserialize");
assert_eq!(
cfg.telegram
.as_ref()
.map(|telegram| telegram.bot_token.as_str()),
Some("tg-secret")
);
assert_eq!(
cfg.discord
.as_ref()
.map(|discord| discord.bot_token.as_str()),
Some("")
);
assert_eq!(
cfg.slack.as_ref().map(|slack| slack.bot_token.as_str()),
Some("")
);
assert_eq!(
cfg.slack.as_ref().map(|slack| slack.channel_id.as_str()),
Some("C123")
);
}
}