/// To activate Slack, Discord and/or Telegram notifications, define these environment variables /// before using the `Notifier` /// ```bash /// export SLACK_WEBHOOK=... /// export DISCORD_WEBHOOK=... /// ``` /// /// Telegram requires the following two variables: /// ```bash /// export TELEGRAM_BOT_TOKEN=... /// export TELEGRAM_CHAT_ID=... /// ``` /// /// To receive a Twilio SMS notification on failure, having a Twilio account, /// and a sending number owned by that account, /// define environment variable before running `solana-watchtower`: /// ```bash /// export TWILIO_CONFIG='ACCOUNT=,TOKEN=,TO=,FROM=' /// ``` use log::*; use reqwest::{blocking::Client, StatusCode}; use serde_json::json; use std::{env, thread::sleep, time::Duration}; struct TelegramWebHook { bot_token: String, chat_id: String, } #[derive(Debug, Default)] struct TwilioWebHook { account: String, token: String, to: String, from: String, } impl TwilioWebHook { fn complete(&self) -> bool { !(self.account.is_empty() || self.token.is_empty() || self.to.is_empty() || self.from.is_empty()) } } fn get_twilio_config() -> Result, String> { let config_var = env::var("TWILIO_CONFIG"); if config_var.is_err() { info!("Twilio notifications disabled"); return Ok(None); } let mut config = TwilioWebHook::default(); for pair in config_var.unwrap().split(',') { let nv: Vec<_> = pair.split('=').collect(); if nv.len() != 2 { return Err(format!("TWILIO_CONFIG is invalid: '{}'", pair)); } let v = nv[1].to_string(); match nv[0] { "ACCOUNT" => config.account = v, "TOKEN" => config.token = v, "TO" => config.to = v, "FROM" => config.from = v, _ => return Err(format!("TWILIO_CONFIG is invalid: '{}'", pair)), } } if !config.complete() { return Err("TWILIO_CONFIG is incomplete".to_string()); } Ok(Some(config)) } pub struct Notifier { client: Client, discord_webhook: Option, slack_webhook: Option, telegram_webhook: Option, twilio_webhook: Option, } impl Notifier { pub fn default() -> Self { Self::new("") } pub fn new(env_prefix: &str) -> Self { info!("Initializing {}Notifier", env_prefix); let discord_webhook = env::var(format!("{}DISCORD_WEBHOOK", env_prefix)) .map_err(|_| { info!("Discord notifications disabled"); }) .ok(); let slack_webhook = env::var(format!("{}SLACK_WEBHOOK", env_prefix)) .map_err(|_| { info!("Slack notifications disabled"); }) .ok(); let telegram_webhook = if let (Ok(bot_token), Ok(chat_id)) = ( env::var(format!("{}TELEGRAM_BOT_TOKEN", env_prefix)), env::var(format!("{}TELEGRAM_CHAT_ID", env_prefix)), ) { Some(TelegramWebHook { bot_token, chat_id }) } else { info!("Telegram notifications disabled"); None }; let twilio_webhook = get_twilio_config() .map_err(|err| panic!("Twilio config error: {}", err)) .unwrap(); Notifier { client: Client::new(), discord_webhook, slack_webhook, telegram_webhook, twilio_webhook, } } pub fn send(&self, msg: &str) { if let Some(webhook) = &self.discord_webhook { for line in msg.split('\n') { // Discord rate limiting is aggressive, limit to 1 message a second sleep(Duration::from_millis(1000)); info!("Sending {}", line); let data = json!({ "content": line }); loop { let response = self.client.post(webhook).json(&data).send(); if let Err(err) = response { warn!("Failed to send Discord message: \"{}\": {:?}", line, err); break; } else if let Ok(response) = response { info!("response status: {}", response.status()); if response.status() == StatusCode::TOO_MANY_REQUESTS { warn!("rate limited!..."); warn!("response text: {:?}", response.text()); sleep(Duration::from_secs(2)); } else { break; } } } } } if let Some(webhook) = &self.slack_webhook { let data = json!({ "text": msg }); if let Err(err) = self.client.post(webhook).json(&data).send() { warn!("Failed to send Slack message: {:?}", err); } } if let Some(TelegramWebHook { chat_id, bot_token }) = &self.telegram_webhook { let data = json!({ "chat_id": chat_id, "text": msg }); let url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token); if let Err(err) = self.client.post(&url).json(&data).send() { warn!("Failed to send Telegram message: {:?}", err); } } if let Some(TwilioWebHook { account, token, to, from, }) = &self.twilio_webhook { let url = format!( "https://{}:{}@api.twilio.com/2010-04-01/Accounts/{}/Messages.json", account, token, account ); let params = [("To", to), ("From", from), ("Body", &msg.to_string())]; if let Err(err) = self.client.post(&url).form(¶ms).send() { warn!("Failed to send Twilio message: {:?}", err); } } } }