astrid_telegram/
config.rs1use std::path::Path;
7
8use tracing::{debug, warn};
9
10use crate::error::{TelegramBotError, TelegramResult};
11
12#[derive(Clone)]
14pub struct TelegramConfig {
15 pub bot_token: String,
17 pub daemon_url: Option<String>,
20 pub allowed_user_ids: Vec<u64>,
23 pub workspace_path: Option<String>,
25}
26
27impl std::fmt::Debug for TelegramConfig {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.debug_struct("TelegramConfig")
30 .field("bot_token", &"[REDACTED]")
31 .field("daemon_url", &self.daemon_url)
32 .field("allowed_user_ids", &self.allowed_user_ids)
33 .field("workspace_path", &self.workspace_path)
34 .finish()
35 }
36}
37
38impl TelegramConfig {
39 pub fn load(workspace_root: Option<&Path>) -> TelegramResult<Self> {
53 let telegram_section = match astrid_config::Config::load(workspace_root) {
55 Ok(resolved) => {
56 debug!(
57 files = ?resolved.loaded_files,
58 "loaded config from files"
59 );
60 resolved.config.telegram
61 },
62 Err(e) => {
63 warn!("failed to load config files, using env vars only: {e}");
64 let mut section = astrid_config::TelegramSection::default();
65 if let Ok(val) = std::env::var("TELEGRAM_BOT_TOKEN")
66 && !val.is_empty()
67 {
68 section.bot_token = Some(val);
69 }
70 if let Ok(val) = std::env::var("ASTRID_DAEMON_URL")
71 && !val.is_empty()
72 {
73 section.daemon_url = Some(val);
74 }
75 if let Ok(val) = std::env::var("ASTRID_WORKSPACE")
76 && !val.is_empty()
77 {
78 section.workspace_path = Some(val);
79 }
80 section
81 },
82 };
83
84 let bot_token = telegram_section.bot_token.ok_or_else(|| {
88 TelegramBotError::Config(
89 "bot_token is required — set [telegram] bot_token in \
90 ~/.astrid/config.toml or TELEGRAM_BOT_TOKEN env var"
91 .to_owned(),
92 )
93 })?;
94
95 let mut allowed_user_ids = telegram_section.allowed_user_ids;
96 if allowed_user_ids.is_empty()
97 && let Ok(val) = std::env::var("TELEGRAM_ALLOWED_USERS")
98 {
99 allowed_user_ids = val
100 .split(',')
101 .filter_map(|entry| {
102 let trimmed = entry.trim();
103 if trimmed.is_empty() {
104 return None;
105 }
106 match trimmed.parse::<u64>() {
107 Ok(id) => Some(id),
108 Err(e) => {
109 warn!(
110 value = trimmed,
111 error = %e,
112 "ignoring unparseable entry in TELEGRAM_ALLOWED_USERS"
113 );
114 None
115 },
116 }
117 })
118 .collect();
119 }
120
121 Ok(Self {
122 bot_token,
123 daemon_url: telegram_section.daemon_url,
124 allowed_user_ids,
125 workspace_path: telegram_section.workspace_path,
126 })
127 }
128
129 pub fn is_user_allowed(&self, user_id: u64) -> bool {
131 self.allowed_user_ids.is_empty() || self.allowed_user_ids.contains(&user_id)
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn test_config(allowed: Vec<u64>) -> TelegramConfig {
141 TelegramConfig {
142 bot_token: "test-token".to_owned(),
143 daemon_url: None,
144 allowed_user_ids: allowed,
145 workspace_path: None,
146 }
147 }
148
149 #[test]
150 fn empty_allowlist_permits_everyone() {
151 let cfg = test_config(vec![]);
152 assert!(cfg.is_user_allowed(12345));
153 assert!(cfg.is_user_allowed(99999));
154 }
155
156 #[test]
157 fn allowlist_permits_listed_users() {
158 let cfg = test_config(vec![100, 200, 300]);
159 assert!(cfg.is_user_allowed(100));
160 assert!(cfg.is_user_allowed(200));
161 assert!(cfg.is_user_allowed(300));
162 }
163
164 #[test]
165 fn allowlist_denies_unlisted_users() {
166 let cfg = test_config(vec![100, 200]);
167 assert!(!cfg.is_user_allowed(999));
168 assert!(!cfg.is_user_allowed(0));
169 }
170}