Skip to main content

tandem_server/config/
channels.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct TelegramConfigFile {
5    pub bot_token: String,
6    #[serde(default = "default_allow_all")]
7    pub allowed_users: Vec<String>,
8    #[serde(default)]
9    pub mention_only: bool,
10    #[serde(default)]
11    pub strict_kb_grounding: bool,
12    #[serde(default)]
13    pub model_provider_id: Option<String>,
14    #[serde(default)]
15    pub model_id: Option<String>,
16    #[serde(default)]
17    pub style_profile: tandem_channels::config::TelegramStyleProfile,
18    #[serde(default)]
19    pub security_profile: tandem_channels::config::ChannelSecurityProfile,
20    /// Telegram webhook secret token. When the bot's webhook is registered
21    /// (via `setWebhook`) with a `secret_token` parameter, every callback
22    /// POST from Telegram includes that exact value in the
23    /// `x-telegram-bot-api-secret-token` header. Tandem rejects callback
24    /// POSTs whose header does not match this value, preventing a third
25    /// party from spoofing button clicks at the engine. Required when the
26    /// Telegram interactions endpoint (`POST /channels/telegram/interactions`)
27    /// is enabled.
28    #[serde(default)]
29    pub webhook_secret_token: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DiscordConfigFile {
34    pub bot_token: String,
35    #[serde(default)]
36    pub guild_id: Option<String>,
37    #[serde(default = "default_allow_all")]
38    pub allowed_users: Vec<String>,
39    #[serde(default = "default_discord_mention_only")]
40    pub mention_only: bool,
41    #[serde(default)]
42    pub strict_kb_grounding: bool,
43    #[serde(default)]
44    pub model_provider_id: Option<String>,
45    #[serde(default)]
46    pub model_id: Option<String>,
47    #[serde(default)]
48    pub security_profile: tandem_channels::config::ChannelSecurityProfile,
49    /// Discord application public key (32-byte hex). Required when the
50    /// Discord interactions endpoint (`POST /channels/discord/interactions`)
51    /// is enabled — every interaction POST from Discord is Ed25519-signed
52    /// using this key. Discord disables the endpoint if even a single
53    /// inbound interaction is unverified, so this is mandatory for any
54    /// channel that wants approval cards. Configurable via
55    /// `channels.discord.public_key` in `config.json`.
56    #[serde(default)]
57    pub public_key: Option<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SlackConfigFile {
62    pub bot_token: String,
63    pub channel_id: String,
64    #[serde(default = "default_allow_all")]
65    pub allowed_users: Vec<String>,
66    #[serde(default)]
67    pub mention_only: bool,
68    #[serde(default)]
69    pub strict_kb_grounding: bool,
70    #[serde(default)]
71    pub model_provider_id: Option<String>,
72    #[serde(default)]
73    pub model_id: Option<String>,
74    #[serde(default)]
75    pub security_profile: tandem_channels::config::ChannelSecurityProfile,
76    /// Slack app signing secret. Required when the Slack interactions endpoint
77    /// (`POST /channels/slack/interactions`) is enabled — every interaction
78    /// payload from Slack is HMAC-SHA256 signed using this secret. Stored in
79    /// the OS keystore in production; this field is the in-memory copy.
80    #[serde(default)]
81    pub signing_secret: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85pub struct ChannelsConfigFile {
86    pub telegram: Option<TelegramConfigFile>,
87    pub discord: Option<DiscordConfigFile>,
88    pub slack: Option<SlackConfigFile>,
89    #[serde(default)]
90    pub tool_policy: tandem_channels::config::ChannelToolPolicy,
91}
92
93pub fn normalize_allowed_users_or_wildcard(raw: Vec<String>) -> Vec<String> {
94    let normalized = normalize_non_empty_list(raw);
95    if normalized.is_empty() {
96        return default_allow_all();
97    }
98    normalized
99}
100
101pub fn normalize_allowed_tools(raw: Vec<String>) -> Vec<String> {
102    normalize_non_empty_list(raw)
103}
104
105fn default_allow_all() -> Vec<String> {
106    vec!["*".to_string()]
107}
108
109fn default_discord_mention_only() -> bool {
110    true
111}
112
113fn normalize_non_empty_list(raw: Vec<String>) -> Vec<String> {
114    let mut out = Vec::new();
115    let mut seen = std::collections::HashSet::new();
116    for item in raw {
117        let normalized = item.trim().to_string();
118        if normalized.is_empty() {
119            continue;
120        }
121        if seen.insert(normalized.clone()) {
122            out.push(normalized);
123        }
124    }
125    out
126}