1use openssl::symm::{Cipher, Crypter, Mode};
25
26pub struct Msg {
45 title: String,
47
48 body: String,
50
51 level: Option<String>,
56
57 badge: Option<u64>,
59
60 auto_copy: Option<u8>,
62
63 copy: Option<String>,
65
66 sound: Option<String>,
68
69 icon: Option<String>,
71
72 group: Option<String>,
74
75 is_archive: Option<u8>,
77
78 url: Option<String>,
80
81 iv: Option<String>,
83 enc_type: Option<String>,
85 mode: Option<String>,
87 key: Option<String>,
89 cipher: Option<Cipher>,
91}
92
93impl Msg {
94 pub fn new(title: &str, body: &str) -> Self {
95 Msg {
96 ..Self::default(Some(title.to_string()), body.to_string())
97 }
98 }
99
100 pub fn with_body(body: &str) -> Self {
101 Msg {
102 ..Self::default(None, body.to_string())
103 }
104 }
105
106 fn default(title: Option<String>, body: String) -> Self {
107 Msg {
108 title: title.unwrap_or("Notification".to_string()),
109 body,
110 level: None,
111 badge: None,
112 auto_copy: None,
113 copy: None,
114 sound: Some("chime.caf".to_string()),
115 icon: Some("https://github.com/66f94eae/bark-dev/raw/main/bot.jpg".to_string()),
116 group: None,
117 is_archive: None,
118 url: None,
119 iv: None,
120 enc_type: None,
121 mode: None,
122 key: None,
123 cipher: None,
124 }
125 }
126
127 pub fn set_level(&mut self, level: &str) -> &mut Self {
128 match level.to_lowercase().as_str() {
129 "active" => self.level = Some("active".to_string()),
130 "timesensitive" => self.level = Some("timeSensitive".to_string()),
131 "passive" => self.level = Some("passive".to_string()),
132 _ => self.level = None,
133 }
134 self
135 }
136
137 pub fn set_badge(&mut self, badge: u64) -> &mut Self {
138 if badge > 0 {
139 self.badge = Some(badge);
140 } else {
141 self.badge = None;
142 }
143 self
144 }
145
146 pub fn set_auto_copy(&mut self, auto_copy: u8) -> &mut Self {
147 match auto_copy {
148 0 => self.auto_copy = Some(0),
149 _ => self.auto_copy = None,
150 }
151 self
152 }
153
154 pub fn set_copy(&mut self, copy: &str) -> &mut Self {
155 if copy.trim().is_empty() {
156 self.copy = None;
157 } else {
158 self.copy = Some(copy.to_string());
159 }
160 self
161 }
162
163 pub fn set_sound(&mut self, sound: &str) -> &mut Self {
164 self.sound = Some(sound.to_string());
165 self
166 }
167
168 pub fn set_icon(&mut self, icon: &str) -> &mut Self {
169 if icon.trim().is_empty() {
170 self.icon = None;
171 } else {
172 self.icon = Some(icon.to_string());
173 }
174 self
175 }
176
177 pub fn set_group(&mut self, group: &str) -> &mut Self {
178 self.group = Some(group.to_string());
179 self
180 }
181
182 pub fn set_is_archive(&mut self, is_archive: u8) -> &mut Self {
183 match is_archive {
184 1 => self.is_archive = Some(1),
185 _ => self.is_archive = None,
186 }
187 self
188 }
189
190 pub fn set_url(&mut self, url: &str) -> &mut Self {
191 if url.trim().is_empty() {
192 self.url = None;
193 } else {
194 self.url = Some(url.to_string());
195 }
196 self
197 }
198
199 pub fn set_iv(&mut self, iv: &str) -> &mut Self {
200 if iv.trim().is_empty() {
201 self.iv = None;
202 } else {
203 self.iv = Some(iv.to_string());
204 }
205 self
206 }
207
208 pub fn gen_iv(&mut self) -> &mut Self {
209 let mut iv: [u8; 16] = [0u8; 16];
210 openssl::rand::rand_bytes(&mut iv).unwrap();
211 self.set_iv(iv.iter().map(|b| format!("{:02x}", b)).collect::<String>().split_off(16).as_str())
212 }
213
214 fn set_cipher(&mut self) -> &mut Self {
215 if self.enc_type.is_none() || self.mode.is_none() {
216 return self;
217 }
218 let enc_type = self.enc_type.as_ref().unwrap().clone();
219 let mode = self.mode.as_ref().unwrap().clone();
220
221 let cipher: Cipher = match enc_type.to_lowercase().as_str() {
222 "aes128" => {
223 match mode.to_lowercase().as_str() {
224 "cbc" => {
225 Cipher::aes_128_cbc()
226 },
227 "ecb" => {
228 Cipher::aes_128_ecb()
229 },
230 "gcm" => {
231 Cipher::aes_128_gcm()
232 },
233 _ => panic!("Invalid mode"),
234 }
235 },
236 "aes192" => {
237 match mode.to_lowercase().as_str() {
238 "cbc" => {
239 Cipher::aes_192_cbc()
240 },
241 "ecb" => {
242 Cipher::aes_192_ecb()
243 },
244 "gcm" => {
245 Cipher::aes_192_gcm()
246 },
247 _ => panic!("Invalid mode"),
248 }
249 },
250 "aes256" => {
251 match mode.to_lowercase().as_str() {
252 "cbc" => {
253 Cipher::aes_256_cbc()
254 },
255 "ecb" => {
256 Cipher::aes_256_ecb()
257 },
258 "gcm" => {
259 Cipher::aes_256_gcm()
260 },
261 _ => panic!("Invalid mode"),
262 }
263 },
264 _ => panic!("Invalid type"),
265 };
266 self.cipher = Some(cipher);
267 self
268 }
269
270 pub fn set_enc_type(&mut self, enc_type: &str) -> &mut Self {
271 if self.enc_type.is_some() {
272 panic!("Encrypt type can only be set once");
273 }
274 match enc_type.to_lowercase().as_str() {
275 "aes128" => self.enc_type = Some("aes128".to_string()),
276 "aes192" => self.enc_type = Some("aes192".to_string()),
277 "aes256" => self.enc_type = Some("aes256".to_string()),
278 _ => panic!("Invalid encrypt type"),
279 }
280 self.set_cipher();
281 self
282 }
283
284 pub fn set_mode(&mut self, mode: &str) -> &mut Self {
285 if self.mode.is_some() {
286 panic!("Encrypt mode can only be set once");
287 }
288 match mode.to_lowercase().as_str() {
289 "cbc" => self.mode = Some("cbc".to_string()),
290 "ecb" => self.mode = Some("ecb".to_string()),
291 "gcm" => self.mode = Some("gcm".to_string()),
292 _ => panic!("Invalid encrypt mode"),
293 }
294 self.set_cipher();
295 self
296 }
297
298 pub fn set_key(&mut self, key: &str) -> &mut Self {
299 self.key = Some(key.to_string());
300 self
301 }
302
303 fn json(&self, encry_body: Option<String>) -> String {
304 let mut body: String = format!("{{\"aps\":{{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"{level}\",", level = self.level.as_ref().unwrap_or(&"active".to_string()));
305
306 if let Some(badge) = self.badge {
307 body += &format!("\"badge\":{badge},", badge = badge);
308 }
309
310 if let Some(sound) = &self.sound {
311 body += &format!("\"sound\":\"{sound}\",", sound = sound);
312 }
313
314 if let Some(group) = &self.group {
315 body += &format!("\"thread-id\":\"{group}\",", group = group);
316 }
317
318 let alert: String = format!("\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}}}}",
319 title = self.title, body = if encry_body.is_some() {"NoContent"} else {self.body.as_str()}
320 );
321
322 body = body + &alert;
323
324 if let Some(icon) = &self.icon {
325 body += &format!(",\"icon\":\"{icon}\"", icon = icon);
326 }
327
328 if let Some(auto_copy) = self.auto_copy {
329 body += &format!(",\"autoCopy\":{auto_copy}", auto_copy = auto_copy);
330 }
331
332 if let Some(is_archive) = self.is_archive {
333 body += &format!(",\"isArchive\":{is_archive}", is_archive = is_archive);
334 }
335
336 if let Some(copy) = &self.copy {
337 body += &format!(",\"copy\":\"{copy}\"", copy = copy);
338 }
339
340 if let Some(url) = &self.url {
341 body += &format!(",\"url\":\"{url}\"", url = url);
342 }
343
344 if let Some(iv) = &self.iv {
345 body += &format!(",\"iv\":\"{iv}\"", iv = iv);
346 }
347
348 if let Some(encry_body) = encry_body {
349 body += &format!(",\"ciphertext\":\"{encry_body}\"", encry_body = encry_body);
350 }
351
352 body + "}"
353 }
354
355 pub fn to_json(&self) -> String {
356 self.json(None)
360 }
361
362
363 pub fn encrypt(&self) -> Result<String, Box<dyn std::error::Error>> {
364 if self.enc_type.is_none() || self.mode.is_none() || self.key.is_none() {
365 panic!("Encrypt type, mode, and key must be set");
366 }
367
368 let key: String = self.key.as_ref().unwrap().clone();
369
370 let original: String = format!("{{\"body\":\"{}\"}}", self.body);
371 let original: &[u8] = original.as_bytes();
372
373 let cipher: Cipher = self.cipher.unwrap();
374
375 let mut crypter: Crypter = Crypter::new(cipher, Mode::Encrypt, key.as_bytes(), Some(self.iv.as_ref().unwrap().as_bytes())).unwrap();
376 crypter.pad(true); let mut buffer: Vec<u8> = vec![0; original.len() + cipher.block_size()];
378 let count: usize = crypter.update(&original, &mut buffer).unwrap();
379 let rest: usize = crypter.finalize(&mut buffer[count..]).unwrap();
380 buffer.truncate(count + rest);
381 Ok(self.json(Some(openssl::base64::encode_block(&buffer))))
382 }
383
384 pub fn serialize(&self) -> String {
385 if self.cipher.is_some() {
386 match self.encrypt() {
387 Ok(encrypted) => {
388 encrypted
389 },
390 Err(e) => panic!("Error encrypting message: {}", e),
391 }
392 } else {
393 self.to_json()
394 }
395 }
396
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_to_json_all_field() {
405 let mut msg = Msg::new("Test Title", "Test Body");
406 msg.set_level("timeSensitive");
407 msg.set_badge(1);
408 msg.set_auto_copy(1);
409 msg.set_copy("Test Copy");
410 msg.set_sound("chime.caf");
411 msg.set_icon("icon.png");
412 msg.set_group("Test Group");
413 msg.set_is_archive(1);
414 msg.set_url("https://example.com");
415 let json = msg.to_json();
416 println!("{}", json);
417 assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"timeSensitive\",\"badge\":1,\"sound\":\"chime.caf\",\"thread-id\":\"Test Group\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"},\"icon\":\"icon.png\"},\"isArchive\":1,\"copy\":\"Test Copy\",\"url\":\"https://example.com\"}");
418 }
419
420 #[test]
421 fn test_to_json_part_field() {
422 let mut msg = Msg::new("Test Title", "Test Body");
423 msg.set_level("passive");
424 msg.set_badge(1);
425 msg.set_auto_copy(1);
426 msg.set_copy("");
427 msg.set_sound("chime.caf");
428 msg.set_icon("icon.png");
429 let json = msg.to_json();
430 println!("{}", json);
431 assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"passive\",\"badge\":1,\"sound\":\"chime.caf\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"},\"icon\":\"icon.png\"}}");
432 }
433
434 #[test]
435 fn test_to_json_default() {
436 let msg = Msg::new("Test Title", "Test Body");
437 let json = msg.to_json();
438 println!("{}", json);
439 assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"active\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"}}}");
440 }
441}