Skip to main content

bbachain_notifier/
lib.rs

1/// To activate Slack, Discord and/or Telegram notifications, define these environment variables
2/// before using the `Notifier`
3/// ```bash
4/// export SLACK_WEBHOOK=...
5/// export DISCORD_WEBHOOK=...
6/// ```
7///
8/// Telegram requires the following two variables:
9/// ```bash
10/// export TELEGRAM_BOT_TOKEN=...
11/// export TELEGRAM_CHAT_ID=...
12/// ```
13///
14/// To receive a Twilio SMS notification on failure, having a Twilio account,
15/// and a sending number owned by that account,
16/// define environment variable before running `bbachain-watchtower`:
17/// ```bash
18/// export TWILIO_CONFIG='ACCOUNT=<account>,TOKEN=<securityToken>,TO=<receivingNumber>,FROM=<sendingNumber>'
19/// ```
20use log::*;
21use {
22    reqwest::{blocking::Client, StatusCode},
23    serde_json::json,
24    std::{env, str::FromStr, thread::sleep, time::Duration},
25};
26
27struct TelegramWebHook {
28    bot_token: String,
29    chat_id: String,
30}
31
32#[derive(Debug, Default)]
33struct TwilioWebHook {
34    account: String,
35    token: String,
36    to: String,
37    from: String,
38}
39
40impl TwilioWebHook {
41    fn complete(&self) -> bool {
42        !(self.account.is_empty()
43            || self.token.is_empty()
44            || self.to.is_empty()
45            || self.from.is_empty())
46    }
47}
48
49fn get_twilio_config() -> Result<Option<TwilioWebHook>, String> {
50    let config_var = env::var("TWILIO_CONFIG");
51
52    if config_var.is_err() {
53        info!("Twilio notifications disabled");
54        return Ok(None);
55    }
56
57    let mut config = TwilioWebHook::default();
58
59    for pair in config_var.unwrap().split(',') {
60        let nv: Vec<_> = pair.split('=').collect();
61        if nv.len() != 2 {
62            return Err(format!("TWILIO_CONFIG is invalid: '{}'", pair));
63        }
64        let v = nv[1].to_string();
65        match nv[0] {
66            "ACCOUNT" => config.account = v,
67            "TOKEN" => config.token = v,
68            "TO" => config.to = v,
69            "FROM" => config.from = v,
70            _ => return Err(format!("TWILIO_CONFIG is invalid: '{}'", pair)),
71        }
72    }
73
74    if !config.complete() {
75        return Err("TWILIO_CONFIG is incomplete".to_string());
76    }
77    Ok(Some(config))
78}
79
80enum NotificationType {
81    Discord(String),
82    Slack(String),
83    Telegram(TelegramWebHook),
84    Twilio(TwilioWebHook),
85    Log(Level),
86}
87
88pub struct Notifier {
89    client: Client,
90    notifiers: Vec<NotificationType>,
91}
92
93impl Notifier {
94    pub fn default() -> Self {
95        Self::new("")
96    }
97
98    pub fn new(env_prefix: &str) -> Self {
99        info!("Initializing {}Notifier", env_prefix);
100
101        let mut notifiers = vec![];
102
103        if let Ok(webhook) = env::var(format!("{}DISCORD_WEBHOOK", env_prefix)) {
104            notifiers.push(NotificationType::Discord(webhook));
105        }
106        if let Ok(webhook) = env::var(format!("{}SLACK_WEBHOOK", env_prefix)) {
107            notifiers.push(NotificationType::Slack(webhook));
108        }
109
110        if let (Ok(bot_token), Ok(chat_id)) = (
111            env::var(format!("{}TELEGRAM_BOT_TOKEN", env_prefix)),
112            env::var(format!("{}TELEGRAM_CHAT_ID", env_prefix)),
113        ) {
114            notifiers.push(NotificationType::Telegram(TelegramWebHook {
115                bot_token,
116                chat_id,
117            }));
118        }
119
120        if let Ok(Some(webhook)) = get_twilio_config() {
121            notifiers.push(NotificationType::Twilio(webhook));
122        }
123
124        if let Ok(log_level) = env::var(format!("{}LOG_NOTIFIER_LEVEL", env_prefix)) {
125            match Level::from_str(&log_level) {
126                Ok(level) => notifiers.push(NotificationType::Log(level)),
127                Err(e) => warn!(
128                    "could not parse specified log notifier level string ({}): {}",
129                    log_level, e
130                ),
131            }
132        }
133
134        info!("{} notifiers", notifiers.len());
135
136        Notifier {
137            client: Client::new(),
138            notifiers,
139        }
140    }
141
142    pub fn is_empty(&self) -> bool {
143        self.notifiers.is_empty()
144    }
145
146    pub fn send(&self, msg: &str) {
147        for notifier in &self.notifiers {
148            match notifier {
149                NotificationType::Discord(webhook) => {
150                    for line in msg.split('\n') {
151                        // Discord rate limiting is aggressive, limit to 1 message a second
152                        sleep(Duration::from_millis(1000));
153
154                        info!("Sending {}", line);
155                        let data = json!({ "content": line });
156
157                        loop {
158                            let response = self.client.post(webhook).json(&data).send();
159
160                            if let Err(err) = response {
161                                warn!("Failed to send Discord message: \"{}\": {:?}", line, err);
162                                break;
163                            } else if let Ok(response) = response {
164                                info!("response status: {}", response.status());
165                                if response.status() == StatusCode::TOO_MANY_REQUESTS {
166                                    warn!("rate limited!...");
167                                    warn!("response text: {:?}", response.text());
168                                    sleep(Duration::from_secs(2));
169                                } else {
170                                    break;
171                                }
172                            }
173                        }
174                    }
175                }
176                NotificationType::Slack(webhook) => {
177                    let data = json!({ "text": msg });
178                    if let Err(err) = self.client.post(webhook).json(&data).send() {
179                        warn!("Failed to send Slack message: {:?}", err);
180                    }
181                }
182
183                NotificationType::Telegram(TelegramWebHook { chat_id, bot_token }) => {
184                    let data = json!({ "chat_id": chat_id, "text": msg });
185                    let url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token);
186
187                    if let Err(err) = self.client.post(&url).json(&data).send() {
188                        warn!("Failed to send Telegram message: {:?}", err);
189                    }
190                }
191
192                NotificationType::Twilio(TwilioWebHook {
193                    account,
194                    token,
195                    to,
196                    from,
197                }) => {
198                    let url = format!(
199                        "https://{}:{}@api.twilio.com/2010-04-01/Accounts/{}/Messages.json",
200                        account, token, account
201                    );
202                    let params = [("To", to), ("From", from), ("Body", &msg.to_string())];
203                    if let Err(err) = self.client.post(&url).form(&params).send() {
204                        warn!("Failed to send Twilio message: {:?}", err);
205                    }
206                }
207                NotificationType::Log(level) => {
208                    log!(*level, "{}", msg)
209                }
210            }
211        }
212    }
213}