webserver_base/telegram/
settings.rs1use std::time::Duration;
2
3use super::error::TelegramError;
4use super::token::BotToken;
5
6pub const TELEGRAM_API_BASE_URL: &str = "https://api.telegram.org";
8
9pub const MAX_TEXT_LENGTH: usize = 4096;
11
12pub const MAX_CAPTION_LENGTH: usize = 1024;
17
18pub const DEFAULT_MAX_CHUNKS: usize = 5;
20
21pub const DEFAULT_MAX_INPUT_BYTES: usize = 1024 * 1024;
26
27pub const DEFAULT_QUEUE_CAPACITY: usize = 1024;
29
30pub const DEFAULT_PER_CHAT_INTERVAL: Duration = Duration::from_secs(1);
35
36pub const DEFAULT_GLOBAL_PER_SECOND: u32 = 30;
41
42pub const DEFAULT_MAX_RETRIES: u32 = 3;
44
45pub const DEFAULT_MAX_RETRY_AFTER: Duration = Duration::from_mins(1);
51
52pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
54
55pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
57
58#[derive(Debug, Clone)]
64pub struct TelegramSettings {
65 pub(crate) token: BotToken,
66 pub(crate) base_url: String,
67 pub(crate) connect_timeout: Duration,
68 pub(crate) request_timeout: Duration,
69 pub(crate) queue_capacity: usize,
70 pub(crate) max_chunks: usize,
71 pub(crate) max_input_bytes: usize,
72 pub(crate) max_retries: u32,
73 pub(crate) max_retry_after: Duration,
74 pub(crate) per_chat_interval: Duration,
75 pub(crate) global_per_second: u32,
76}
77
78impl TelegramSettings {
79 #[must_use]
84 pub fn builder(token: impl Into<String>) -> TelegramSettingsBuilder {
85 TelegramSettingsBuilder::new(token)
86 }
87
88 #[must_use]
90 pub fn base_url(&self) -> &str {
91 &self.base_url
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct TelegramSettingsBuilder {
98 token: String,
99 base_url: String,
100 connect_timeout: Duration,
101 request_timeout: Duration,
102 queue_capacity: usize,
103 max_chunks: usize,
104 max_input_bytes: usize,
105 max_retries: u32,
106 max_retry_after: Duration,
107 per_chat_interval: Duration,
108 global_per_second: u32,
109}
110
111impl TelegramSettingsBuilder {
112 #[must_use]
114 pub fn new(token: impl Into<String>) -> Self {
115 Self {
116 token: token.into(),
117 base_url: String::from(TELEGRAM_API_BASE_URL),
118 connect_timeout: DEFAULT_CONNECT_TIMEOUT,
119 request_timeout: DEFAULT_REQUEST_TIMEOUT,
120 queue_capacity: DEFAULT_QUEUE_CAPACITY,
121 max_chunks: DEFAULT_MAX_CHUNKS,
122 max_input_bytes: DEFAULT_MAX_INPUT_BYTES,
123 max_retries: DEFAULT_MAX_RETRIES,
124 max_retry_after: DEFAULT_MAX_RETRY_AFTER,
125 per_chat_interval: DEFAULT_PER_CHAT_INTERVAL,
126 global_per_second: DEFAULT_GLOBAL_PER_SECOND,
127 }
128 }
129
130 #[must_use]
135 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
136 self.base_url = base_url.into().trim_end_matches('/').to_string();
137 self
138 }
139
140 #[must_use]
142 pub const fn connect_timeout(mut self, timeout: Duration) -> Self {
143 self.connect_timeout = timeout;
144 self
145 }
146
147 #[must_use]
149 pub const fn request_timeout(mut self, timeout: Duration) -> Self {
150 self.request_timeout = timeout;
151 self
152 }
153
154 #[must_use]
156 pub const fn queue_capacity(mut self, capacity: usize) -> Self {
157 self.queue_capacity = capacity;
158 self
159 }
160
161 #[must_use]
163 pub const fn max_chunks(mut self, max_chunks: usize) -> Self {
164 self.max_chunks = max_chunks;
165 self
166 }
167
168 #[must_use]
170 pub const fn max_input_bytes(mut self, max_bytes: usize) -> Self {
171 self.max_input_bytes = max_bytes;
172 self
173 }
174
175 #[must_use]
177 pub const fn max_retries(mut self, retries: u32) -> Self {
178 self.max_retries = retries;
179 self
180 }
181
182 #[must_use]
184 pub const fn max_retry_after(mut self, max_retry_after: Duration) -> Self {
185 self.max_retry_after = max_retry_after;
186 self
187 }
188
189 #[must_use]
191 pub const fn per_chat_interval(mut self, interval: Duration) -> Self {
192 self.per_chat_interval = interval;
193 self
194 }
195
196 #[must_use]
198 pub const fn global_per_second(mut self, per_second: u32) -> Self {
199 self.global_per_second = per_second;
200 self
201 }
202
203 pub fn build(self) -> Result<TelegramSettings, TelegramError> {
211 Ok(TelegramSettings {
212 token: BotToken::new(self.token)?,
213 base_url: self.base_url,
214 connect_timeout: self.connect_timeout,
215 request_timeout: self.request_timeout,
216 queue_capacity: self.queue_capacity.max(1),
217 max_chunks: self.max_chunks.max(1),
218 max_input_bytes: self.max_input_bytes.max(1),
219 max_retries: self.max_retries,
220 max_retry_after: self.max_retry_after,
221 per_chat_interval: self.per_chat_interval,
222 global_per_second: self.global_per_second.max(1),
223 })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use std::time::Duration;
230
231 use super::{
232 DEFAULT_GLOBAL_PER_SECOND, DEFAULT_MAX_CHUNKS, DEFAULT_QUEUE_CAPACITY,
233 TELEGRAM_API_BASE_URL, TelegramSettings,
234 };
235 use crate::telegram::error::TelegramError;
236
237 const VALID: &str = "123456789:AAFNpHzr6wq4YimAMwIjqVrFU8TO5kcayEI";
238
239 #[test]
240 fn defaults_match_telegrams_documented_limits() {
241 let settings: TelegramSettings = TelegramSettings::builder(VALID)
242 .build()
243 .expect("valid token should build");
244
245 let expected_base: String = String::from(TELEGRAM_API_BASE_URL);
246 let actual_base: String = settings.base_url().to_string();
247 assert_eq!(expected_base, actual_base);
248
249 let expected_global: u32 = DEFAULT_GLOBAL_PER_SECOND;
250 let actual_global: u32 = settings.global_per_second;
251 assert_eq!(expected_global, actual_global);
252
253 let expected_interval: Duration = Duration::from_secs(1);
254 let actual_interval: Duration = settings.per_chat_interval;
255 assert_eq!(expected_interval, actual_interval);
256
257 let expected_chunks: usize = DEFAULT_MAX_CHUNKS;
258 let actual_chunks: usize = settings.max_chunks;
259 assert_eq!(expected_chunks, actual_chunks);
260
261 let expected_capacity: usize = DEFAULT_QUEUE_CAPACITY;
262 let actual_capacity: usize = settings.queue_capacity;
263 assert_eq!(expected_capacity, actual_capacity);
264 }
265
266 #[test]
267 fn blank_token_is_rejected_at_build_time() {
268 let error: TelegramError = TelegramSettings::builder(" ")
269 .build()
270 .expect_err("blank token should be rejected");
271
272 assert!(matches!(error, TelegramError::InvalidToken(_)));
273 }
274
275 #[test]
276 fn overrides_are_applied() {
277 let settings: TelegramSettings = TelegramSettings::builder(VALID)
278 .base_url("http://127.0.0.1:8080/")
279 .queue_capacity(16)
280 .max_chunks(2)
281 .global_per_second(5)
282 .per_chat_interval(Duration::from_millis(10))
283 .build()
284 .expect("valid token should build");
285
286 let expected_base: String = String::from("http://127.0.0.1:8080");
287 let actual_base: String = settings.base_url().to_string();
288 assert_eq!(expected_base, actual_base);
289
290 let expected_capacity: usize = 16;
291 let actual_capacity: usize = settings.queue_capacity;
292 assert_eq!(expected_capacity, actual_capacity);
293
294 let expected_chunks: usize = 2;
295 let actual_chunks: usize = settings.max_chunks;
296 assert_eq!(expected_chunks, actual_chunks);
297 }
298
299 #[test]
300 fn trailing_slash_is_stripped_from_the_base_url() {
301 let settings: TelegramSettings = TelegramSettings::builder(VALID)
302 .base_url("https://example.com///")
303 .build()
304 .expect("valid token should build");
305
306 let expected: String = String::from("https://example.com");
307 let actual: String = settings.base_url().to_string();
308 assert_eq!(expected, actual);
309 }
310
311 #[test]
312 fn degenerate_values_are_clamped_to_something_workable() {
313 let settings: TelegramSettings = TelegramSettings::builder(VALID)
314 .queue_capacity(0)
315 .max_chunks(0)
316 .global_per_second(0)
317 .build()
318 .expect("valid token should build");
319
320 let expected_capacity: usize = 1;
321 let actual_capacity: usize = settings.queue_capacity;
322 assert_eq!(expected_capacity, actual_capacity);
323
324 let expected_chunks: usize = 1;
325 let actual_chunks: usize = settings.max_chunks;
326 assert_eq!(expected_chunks, actual_chunks);
327
328 let expected_global: u32 = 1;
329 let actual_global: u32 = settings.global_per_second;
330 assert_eq!(expected_global, actual_global);
331 }
332
333 #[test]
334 fn debug_output_does_not_leak_the_token() {
335 let settings: TelegramSettings = TelegramSettings::builder(VALID)
336 .build()
337 .expect("valid token should build");
338
339 let actual: String = format!("{settings:?}");
340 assert!(!actual.contains("AAFNpHzr"));
341 assert!(actual.contains("***"));
342 }
343}