schwab_cli/notify/
telegram.rs1use 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}