Skip to main content

schwab_cli/notify/
telegram.rs

1use anyhow::{Context, Result};
2use serde_json::json;
3
4use crate::rules::TelegramNotifyConfig;
5
6const TELEGRAM_API: &str = "https://api.telegram.org";
7
8pub struct TelegramNotifier {
9    http: reqwest::Client,
10    token: String,
11    chat_id: String,
12    config: TelegramNotifyConfig,
13}
14
15impl TelegramNotifier {
16    pub fn from_env(config: &TelegramNotifyConfig) -> Result<Option<Self>> {
17        if !config.enabled {
18            return Ok(None);
19        }
20        let token = std::env::var("TELEGRAM_BOT_TOKEN")
21            .context("TELEGRAM_BOT_TOKEN required when notify.telegram.enabled is true")?;
22        let chat_id = std::env::var("TELEGRAM_CHAT_ID")
23            .context("TELEGRAM_CHAT_ID required when notify.telegram.enabled is true")?;
24        Ok(Some(Self {
25            http: reqwest::Client::new(),
26            token,
27            chat_id,
28            config: config.clone(),
29        }))
30    }
31
32    pub async fn send(&self, text: &str) -> Result<()> {
33        let url = format!("{TELEGRAM_API}/bot{}/sendMessage", self.token);
34        let resp = self
35            .http
36            .post(&url)
37            .json(&json!({
38                "chat_id": self.chat_id,
39                "text": truncate_message(text, 4000),
40                "disable_web_page_preview": true,
41            }))
42            .send()
43            .await
44            .context("Telegram send failed")?;
45
46        if !resp.status().is_success() {
47            let body = resp.text().await.unwrap_or_default();
48            anyhow::bail!("Telegram API error: {body}");
49        }
50        Ok(())
51    }
52
53    pub fn wants_tick_summary(&self) -> bool {
54        self.config.notify_every_tick
55    }
56
57    pub fn wants_actions(&self) -> bool {
58        self.config.notify_on_actions
59    }
60}
61
62fn truncate_message(text: &str, max: usize) -> String {
63    if text.chars().count() <= max {
64        return text.to_string();
65    }
66    let mut out = String::new();
67    for ch in text.chars().take(max.saturating_sub(1)) {
68        out.push(ch);
69    }
70    out.push('…');
71    out
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn truncates_long_messages() {
80        let msg = "a".repeat(5000);
81        assert_eq!(truncate_message(&msg, 4000).chars().count(), 4000);
82    }
83}