1use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::error::CredentialError;
11use crate::handle::{Channel, CredentialHandle, TELEGRAM};
12use crate::store::{CredentialStore, ValidationReport};
13
14#[derive(Clone)]
15pub struct TelegramAccount {
16 pub instance: String,
17 pub token: String,
18 pub allow_agents: Vec<String>,
19 pub allowed_chat_ids: Vec<i64>,
20}
21
22impl std::fmt::Debug for TelegramAccount {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.debug_struct("TelegramAccount")
26 .field("instance", &self.instance)
27 .field("allow_agents", &self.allow_agents)
28 .field("allowed_chat_ids", &self.allowed_chat_ids)
29 .field("token", &"<redacted>")
30 .finish()
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct TelegramCredentialStore {
36 accounts: Arc<HashMap<String, TelegramAccount>>,
37}
38
39impl TelegramCredentialStore {
40 pub fn new(accounts: Vec<TelegramAccount>) -> Self {
41 let mut map = HashMap::with_capacity(accounts.len());
42 for a in accounts {
43 map.insert(a.instance.clone(), a);
44 }
45 Self {
46 accounts: Arc::new(map),
47 }
48 }
49
50 pub fn empty() -> Self {
51 Self {
52 accounts: Arc::new(HashMap::new()),
53 }
54 }
55
56 pub fn account(&self, instance: &str) -> Option<&TelegramAccount> {
57 self.accounts.get(instance)
58 }
59}
60
61impl CredentialStore for TelegramCredentialStore {
62 type Account = TelegramAccount;
63
64 fn channel(&self) -> Channel {
65 TELEGRAM
66 }
67
68 fn get(&self, handle: &CredentialHandle) -> Result<Self::Account, CredentialError> {
69 let id = handle.account_id_raw();
70 self.accounts
71 .get(id)
72 .cloned()
73 .ok_or_else(|| CredentialError::NotFound {
74 channel: TELEGRAM,
75 account: id.to_string(),
76 })
77 }
78
79 fn issue(&self, account_id: &str, agent_id: &str) -> Result<CredentialHandle, CredentialError> {
80 let account = self
81 .accounts
82 .get(account_id)
83 .ok_or_else(|| CredentialError::NotFound {
84 channel: TELEGRAM,
85 account: account_id.to_string(),
86 })?;
87 if !account.allow_agents.is_empty() && !account.allow_agents.iter().any(|a| a == agent_id) {
88 let handle = CredentialHandle::new(TELEGRAM, account_id, agent_id);
89 return Err(CredentialError::NotPermitted {
90 channel: TELEGRAM,
91 agent: agent_id.to_string(),
92 fp: handle.fingerprint(),
93 });
94 }
95 Ok(CredentialHandle::new(TELEGRAM, account_id, agent_id))
96 }
97
98 fn list(&self) -> Vec<String> {
99 let mut ids: Vec<_> = self.accounts.keys().cloned().collect();
100 ids.sort();
101 ids
102 }
103
104 fn allow_agents(&self, account_id: &str) -> Vec<String> {
105 self.accounts
106 .get(account_id)
107 .map(|a| a.allow_agents.clone())
108 .unwrap_or_default()
109 }
110
111 fn validate(&self) -> ValidationReport {
112 let mut report = ValidationReport::default();
113 for (id, a) in self.accounts.iter() {
114 if a.token.trim().is_empty() {
115 report.warnings.push(format!(
116 "telegram instance '{id}' has an empty token; bot will 401 on every call"
117 ));
118 } else {
119 report.accounts_ok += 1;
120 }
121 }
122 report
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn mk(instance: &str, allow: &[&str]) -> TelegramAccount {
131 TelegramAccount {
132 instance: instance.into(),
133 token: "123:ABC".into(),
134 allow_agents: allow.iter().map(|s| s.to_string()).collect(),
135 allowed_chat_ids: vec![],
136 }
137 }
138
139 #[test]
140 fn token_is_redacted_in_debug() {
141 let a = mk("ana", &["ana"]);
142 let rendered = format!("{a:?}");
143 assert!(!rendered.contains("123:ABC"));
144 assert!(rendered.contains("<redacted>"));
145 }
146
147 #[test]
148 fn issue_and_list() {
149 let store = TelegramCredentialStore::new(vec![mk("a", &["ana"]), mk("b", &[])]);
150 assert_eq!(store.list(), vec!["a", "b"]);
151 assert!(store.issue("a", "ana").is_ok());
152 assert!(matches!(
153 store.issue("a", "kate").unwrap_err(),
154 CredentialError::NotPermitted { .. }
155 ));
156 assert!(store.issue("b", "kate").is_ok());
157 }
158
159 #[test]
160 fn empty_token_warning() {
161 let account = TelegramAccount {
162 token: " ".into(),
163 ..mk("a", &[])
164 };
165 let store = TelegramCredentialStore::new(vec![account]);
166 let report = store.validate();
167 assert_eq!(report.warnings.len(), 1);
168 assert_eq!(report.accounts_ok, 0);
169 }
170}