1use 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 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(¶ms).send() {
204 warn!("Failed to send Twilio message: {:?}", err);
205 }
206 }
207 NotificationType::Log(level) => {
208 log!(*level, "{}", msg)
209 }
210 }
211 }
212 }
213}