bark_dev/msg.rs
1// MIT License
2//
3// Copyright (c) 2025 66f94eae
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23
24use std::fmt::Display;
25
26use openssl::symm::{Cipher, Crypter, Mode};
27
28/// Push Notification Message structure.
29///
30/// This struct represents a push notification message that can be sent to devices.
31/// It contains various fields to customize the notification's behavior and appearance.
32///
33/// # Example
34/// ```rust
35/// use bark::msg::Msg;
36///
37/// // new a simple message with title and body
38/// let msg = Msg::new("title", "body");
39///
40/// // new a message with title = Notification and body
41/// let mut msg = Msg::with_body("body");
42///
43/// // set some fields
44/// msg.set_level(Level::ACTIVE);
45/// msg.set_badge(1);
46/// // and so on
47/// ```
48pub struct Msg {
49 /// Push Title
50 title: String,
51
52 /// Push Content
53 body: String,
54
55 /// Push Interruption Level
56 ///
57 /// active: Default value, the system will immediately display the notification on the screen.
58 ///
59 /// timeSensitive: Time-sensitive notification, can be displayed while in focus mode.
60 ///
61 /// passive: Only adds the notification to the notification list, will not display on the screen.
62 level: Option<Level>,
63
64 /// Push Badge, can be any number
65 badge: Option<u64>,
66
67 /// Pass 0 to disable; Automatically copy push content below iOS 14.5; above iOS 14.5, you need to manually long-press the push or pull down the push
68 auto_copy: Option<u8>,
69
70 /// When copying the push, specify the content to copy; if this parameter is not provided, the entire push content will be copied
71 copy: Option<String>,
72
73 /// You can set different ringtones for the push
74 sound: Option<String>,
75
76 /// Set a custom icon for the push; the set icon will replace the default Bark icon
77 icon: Option<String>,
78
79 /// Group messages; pushes will be displayed in groups in the notification center
80 group: Option<String>,
81
82 /// Pass 1 to save the push; passing anything else will not save the push; if not passed, it will be decided according to the app's internal settings
83 is_archive: Option<u8>,
84
85 /// The URL to jump to when clicking the push, supports URL Scheme and Universal Link
86 url: Option<String>,
87
88 /// iv, 12 Bytes
89 iv: Option<String>,
90 /// encrypt type
91 enc_type: Option<EncryptType>,
92 /// encrypt mode
93 mode: Option<EncryptMode>,
94 /// encrypt key, 24 Bytes
95 key: Option<String>,
96 /// cipher
97 cipher: Option<Cipher>,
98}
99
100
101/// Notification level
102///
103/// active: Default value, the system will immediately display the notification on the screen.
104///
105/// timeSensitive: Time-sensitive notification, can be displayed while in focus mode.
106///
107/// passive: Only adds the notification to the notification list, will not display on the screen.
108#[derive(Clone, Copy)]
109pub enum Level {
110 ACTIVE,
111 TIMESENSITIVE,
112 PASSIVE
113}
114
115impl Level {
116 pub fn from_str(str: &str) -> Option<Self> {
117 match str.to_lowercase().as_str() {
118 "timesensitive" => Some(Self::TIMESENSITIVE),
119 "passive" => Some(Self::PASSIVE),
120 "active" => Some(Self::ACTIVE),
121 _ => None
122 }
123 }
124}
125
126impl Display for Level {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 let str =
129 match self {
130 Level::ACTIVE => "active",
131 Level::TIMESENSITIVE => "timeSensitive",
132 Level::PASSIVE =>"passive"
133 };
134 write!(f, "{}", str)
135 }
136}
137
138#[derive(Clone, Copy)]
139pub enum EncryptMode {
140 CBC,
141 ECB,
142 GCM,
143}
144
145impl EncryptMode {
146 pub fn from_str(str: &str) -> Option<Self> {
147 if str.is_empty() {
148 return None;
149 }
150 match str.to_lowercase().as_str() {
151 "cbc" => Some(EncryptMode::CBC),
152 "ecb" => Some(EncryptMode::ECB),
153 "gcm" => Some(EncryptMode::GCM),
154 _ => None,
155 }
156 }
157}
158
159#[derive(Clone, Copy)]
160pub enum EncryptType {
161 AES128,
162 AES192,
163 AES256,
164}
165
166impl EncryptType {
167 pub fn from_str(str: &str) -> Option<Self> {
168 if str.is_empty() {
169 return None;
170 }
171 match str.to_lowercase().as_str() {
172 "aes128" => Some(EncryptType::AES128),
173 "aes192" => Some(EncryptType::AES192),
174 "aes256" => Some(EncryptType::AES256),
175 _ => None,
176 }
177 }
178}
179
180impl Msg {
181 /// Creates a new `Msg` instance with a title and body.
182 ///
183 /// # Arguments
184 /// - `title`: The title of the notification.
185 /// - `body`: The content/body of the notification.
186 ///
187 /// # Returns
188 /// A new `Msg` instance.
189 pub fn new(title: &str, body: &str) -> Self {
190 Msg {
191 ..Self::default(Some(title.to_string()), body.to_string())
192 }
193 }
194
195 /// Creates a new `Msg` instance with only a body.
196 ///
197 /// # Arguments
198 /// - `body`: The content/body of the notification.
199 ///
200 /// # Returns
201 /// A new `Msg` instance with the title set to "Notification".
202 pub fn with_body(body: &str) -> Self {
203 Msg {
204 ..Self::default(None, body.to_string())
205 }
206 }
207
208 /// Creates a default `Msg` instance.
209 ///
210 /// # Arguments
211 /// - `title`: An optional title for the notification.
212 /// - `body`: The content/body of the notification.
213 ///
214 /// # Returns
215 /// A new `Msg` instance with default values.
216 fn default(title: Option<String>, body: String) -> Self {
217 Msg {
218 title: title.unwrap_or("Notification".to_string()),
219 body,
220 level: None,
221 badge: None,
222 auto_copy: None,
223 copy: None,
224 sound: Some("chime.caf".to_string()),
225 icon: Some("https://github.com/66f94eae/bark-dev/raw/main/bot.jpg".to_string()),
226 group: None,
227 is_archive: None,
228 url: None,
229 iv: None,
230 enc_type: None,
231 mode: None,
232 key: None,
233 cipher: None,
234 }
235 }
236
237 /// Sets the interruption level of the notification.
238 ///
239 /// # Arguments
240 /// - `level`: The interruption level [`Level`]
241 ///
242 /// # Returns
243 /// A mutable reference to `self` for method chaining.
244 pub fn set_level(&mut self, level: Level) -> &mut Self {
245 self.level = Some(level);
246 self
247 }
248
249 /// Sets the badge number.
250 ///
251 /// # Arguments
252 /// - `badge`: The badge number to display on the app icon.
253 ///
254 /// # Returns
255 /// A mutable reference to `self` for method chaining.
256 pub fn set_badge(&mut self, badge: u64) -> &mut Self {
257 if badge > 0 {
258 self.badge = Some(badge);
259 } else {
260 self.badge = None;
261 }
262 self
263 }
264
265 /// Sets whether to automatically copy the notification content.
266 ///
267 /// # Arguments
268 /// - `auto_copy`: false to disable, true to enable.
269 ///
270 /// # Returns
271 /// A mutable reference to `self` for method chaining.
272 pub fn set_auto_copy(&mut self, auto_copy: bool) -> &mut Self {
273 match auto_copy {
274 false => self.auto_copy = Some(0),
275 true => self.auto_copy = None,
276 }
277 self
278 }
279
280 /// Sets specific content to copy when the notification is copied.
281 ///
282 /// # Arguments
283 /// - `copy`: The content to copy.
284 ///
285 /// # Returns
286 /// A mutable reference to `self` for method chaining.
287 pub fn set_copy(&mut self, copy: &str) -> &mut Self {
288 if copy.trim().is_empty() {
289 self.copy = None;
290 } else {
291 self.copy = Some(copy.to_string());
292 }
293 self
294 }
295
296 /// Sets the sound file to play with the notification.
297 ///
298 /// # Arguments
299 /// - `sound`: The sound file name.
300 ///
301 /// # Returns
302 /// A mutable reference to `self` for method chaining.
303 pub fn set_sound(&mut self, sound: &str) -> &mut Self {
304 self.sound = Some(sound.to_string());
305 self
306 }
307
308 /// Sets a custom icon URL for the notification.
309 ///
310 /// # Arguments
311 /// - `icon`: The icon URL.
312 ///
313 /// # Returns
314 /// A mutable reference to `self` for method chaining.
315 pub fn set_icon(&mut self, icon: &str) -> &mut Self {
316 if icon.trim().is_empty() {
317 self.icon = None;
318 } else {
319 self.icon = Some(icon.to_string());
320 }
321 self
322 }
323
324 /// Sets the group identifier for notifications.
325 ///
326 /// # Arguments
327 /// - `group`: The group identifier.
328 ///
329 /// # Returns
330 /// A mutable reference to `self` for method chaining.
331 pub fn set_group(&mut self, group: &str) -> &mut Self {
332 self.group = Some(group.to_string());
333 self
334 }
335
336 /// Sets whether to archive the notification.
337 ///
338 /// # Arguments
339 /// - `is_archive`: true to save, false to not save.
340 ///
341 /// # Returns
342 /// A mutable reference to `self` for method chaining.
343 pub fn set_is_archive(&mut self, is_archive: bool) -> &mut Self {
344 match is_archive {
345 true => self.is_archive = Some(1),
346 false => self.is_archive = None,
347 }
348 self
349 }
350
351 /// Sets the URL to open when the notification is clicked.
352 ///
353 /// # Arguments
354 /// - `url`: The URL.
355 ///
356 /// # Returns
357 /// A mutable reference to `self` for method chaining.
358 pub fn set_url(&mut self, url: &str) -> &mut Self {
359 if url.trim().is_empty() {
360 self.url = None;
361 } else {
362 self.url = Some(url.to_string());
363 }
364 self
365 }
366
367 /// Sets the initialization vector for encryption.
368 ///
369 /// # Arguments
370 /// - `iv`: The initialization vector.
371 ///
372 /// # Returns
373 /// A mutable reference to `self` for method chaining.
374 pub fn set_iv(&mut self, iv: &str) -> &mut Self {
375 if iv.trim().is_empty() {
376 self.iv = None;
377 } else if iv.len() != 12 {
378 panic!("Invalid IV length. IV must be 12 bytes long.");
379 } else {
380 self.iv = Some(iv.to_string());
381 }
382 self
383 }
384
385 /// Generates a random initialization vector.
386 ///
387 /// # Returns
388 /// A mutable reference to `self` for method chaining.
389 pub fn gen_iv(&mut self) -> &mut Self {
390 let mut iv: [u8; 16] = [0u8; 16];
391 openssl::rand::rand_bytes(&mut iv).unwrap();
392 self.set_iv(iv.iter().map(|b| format!("{:02x}", b)).collect::<String>().split_off(16).as_str())
393 }
394
395 fn set_cipher(&mut self) -> &mut Self {
396 if self.enc_type.is_none() || self.mode.is_none() {
397 return self;
398 }
399 let enc_type = self.enc_type.unwrap();
400 let mode = self.mode.unwrap();
401
402 let cipher: Cipher = match enc_type {
403 EncryptType::AES128 => {
404 match mode {
405 EncryptMode::CBC => {
406 Cipher::aes_128_cbc()
407 },
408 EncryptMode::ECB => {
409 Cipher::aes_128_ecb()
410 },
411 EncryptMode::GCM => {
412 Cipher::aes_128_gcm()
413 },
414 }
415 },
416 EncryptType::AES192 => {
417 match mode {
418 EncryptMode::CBC => {
419 Cipher::aes_192_cbc()
420 },
421 EncryptMode::ECB => {
422 Cipher::aes_192_ecb()
423 },
424 EncryptMode::GCM => {
425 Cipher::aes_192_gcm()
426 },
427 }
428 },
429 EncryptType::AES256 => {
430 match mode {
431 EncryptMode::CBC => {
432 Cipher::aes_256_cbc()
433 },
434 EncryptMode::ECB => {
435 Cipher::aes_256_ecb()
436 },
437 EncryptMode::GCM => {
438 Cipher::aes_256_gcm()
439 },
440 }
441 },
442 };
443 self.cipher = Some(cipher);
444 self
445 }
446
447 /// Sets the encryption type and updates the cipher.
448 ///
449 /// # Arguments
450 /// - `enc_type`: The encryption type [`EncryptType`].
451 ///
452 /// # Panics
453 /// Panics if the encryption type already set.
454 ///
455 /// # Returns
456 /// A mutable reference to `self` for method chaining.
457 pub fn set_enc_type(&mut self, enc_type: EncryptType) -> &mut Self {
458 if self.enc_type.is_some() {
459 panic!("Encrypt type can only be set once");
460 }
461 self.enc_type = Some(enc_type);
462 self.set_cipher();
463 self
464 }
465
466 /// Sets the encryption mode and updates the cipher.
467 ///
468 /// # Arguments
469 /// - `mode`: The encryption mode [`EncryptMode`].
470 ///
471 /// # Panics
472 /// Panics if the encryption mode already set.
473 ///
474 /// # Returns
475 /// A mutable reference to `self` for method chaining.
476 pub fn set_mode(&mut self, mode: EncryptMode) -> &mut Self {
477 if self.mode.is_some() {
478 panic!("Encrypt mode can only be set once");
479 }
480 self.mode = Some(mode);
481 match mode {
482 EncryptMode::ECB | EncryptMode::GCM => {
483 if self.iv.is_none() {
484 self.gen_iv();
485 }
486 },
487 _ => {},
488 }
489 self.set_cipher();
490 self
491 }
492
493 /// Sets the encryption key.
494 ///
495 /// # Arguments
496 /// - `key`: The encryption key.
497 ///
498 /// # Returns
499 /// A mutable reference to `self` for method chaining.
500 pub fn set_key(&mut self, key: &str) -> &mut Self {
501 if key.len() != 24 {
502 panic!("Invalid key length. Key must be 24 characters long.");
503 }
504 self.key = Some(key.to_string());
505 self
506 }
507
508 fn json(&self, encry_body: Option<String>) -> String {
509 let mut body: String = format!("{{\"aps\":{{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"{level}\",", level = self.level.unwrap_or_else(|| Level::ACTIVE));
510
511 if let Some(badge) = self.badge {
512 body += &format!("\"badge\":{badge},", badge = badge);
513 }
514
515 if let Some(sound) = &self.sound {
516 body += &format!("\"sound\":\"{sound}\",", sound = sound);
517 }
518
519 if let Some(group) = &self.group {
520 body += &format!("\"thread-id\":\"{group}\",", group = group);
521 }
522
523 let alert: String = format!(
524 "\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}}}}",
525 title = self.title,
526 body = if encry_body.is_some() {
527 "NoContent"
528 } else {
529 self.body.as_str()
530 }
531 );
532
533 body = body + &alert;
534
535 if let Some(icon) = &self.icon {
536 body += &format!(",\"icon\":\"{icon}\"", icon = icon);
537 }
538
539 if let Some(auto_copy) = self.auto_copy {
540 body += &format!(",\"autoCopy\":{auto_copy}", auto_copy = auto_copy);
541 }
542
543 if let Some(is_archive) = self.is_archive {
544 body += &format!(",\"isArchive\":{is_archive}", is_archive = is_archive);
545 }
546
547 if let Some(copy) = &self.copy {
548 body += &format!(",\"copy\":\"{copy}\"", copy = copy);
549 }
550
551 if let Some(url) = &self.url {
552 body += &format!(",\"url\":\"{url}\"", url = url);
553 }
554
555 if let Some(iv) = &self.iv {
556 body += &format!(",\"iv\":\"{iv}\"", iv = iv);
557 }
558
559 if let Some(encry_body) = encry_body {
560 body += &format!(",\"ciphertext\":\"{encry_body}\"", encry_body = encry_body);
561 }
562
563 body + "}"
564 }
565
566 fn to_json(&self) -> String {
567 // let body: String = format!("{{\"aps\":{{\"interruption-level\":\"critical\",\"mutable-content\":1,\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}},\"category\":\"myNotificationCategory\",\"sound\":\"chime.caf\"}},\"icon\":\"{icon}\"}}",
568 // title = self.title, body = self.body, icon= self.icon
569 // );
570 self.json(None)
571 }
572
573 /// Encrypts the message using the specified encryption type, mode, and key.
574 ///
575 /// # Returns
576 /// A `Result` containing the encrypted message as a `String` or an error if the encryption fails.
577 fn encrypt(&self) -> Result<String, Box<dyn std::error::Error>> {
578 if self.enc_type.is_none() || self.mode.is_none() || self.key.is_none() {
579 panic!("Encrypt type, mode, and key must be set");
580 }
581
582 let key: String = self.key.as_ref().unwrap().clone();
583
584 let original: String = format!("{{\"body\":\"{}\"}}", self.body);
585 let original: &[u8] = original.as_bytes();
586
587 let cipher: Cipher = self.cipher.unwrap();
588
589 let mut crypter: Crypter = Crypter::new(
590 cipher,
591 Mode::Encrypt,
592 key.as_bytes(),
593 Some(self.iv.as_ref().unwrap().as_bytes()),
594 )
595 .unwrap();
596 crypter.pad(true); // Enable PKCS7 padding
597 let mut buffer: Vec<u8> = vec![0; original.len() + cipher.block_size()];
598 let count: usize = crypter.update(&original, &mut buffer).unwrap();
599 let rest: usize = crypter.finalize(&mut buffer[count..]).unwrap();
600 buffer.truncate(count + rest);
601 Ok(self.json(Some(openssl::base64::encode_block(&buffer))))
602 }
603
604 /// Serializes the message into a JSON string, encrypting the message if necessary.
605 ///
606 /// # Returns
607 /// A `String` containing the serialized message.
608 pub fn serialize(&self) -> String {
609 if self.cipher.is_some() {
610 match self.encrypt() {
611 Ok(encrypted) => encrypted,
612 Err(e) => panic!("Error encrypting message: {}", e),
613 }
614 } else {
615 self.to_json()
616 }
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_to_json_all_field() {
626 let mut msg = Msg::new("Test Title", "Test Body");
627 msg.set_level(Level::TIMESENSITIVE);
628 msg.set_badge(1);
629 msg.set_auto_copy(true);
630 msg.set_copy("Test Copy");
631 msg.set_sound("chime.caf");
632 msg.set_icon("icon.png");
633 msg.set_group("Test Group");
634 msg.set_is_archive(true);
635 msg.set_url("https://example.com");
636 let json = msg.to_json();
637 println!("{}", json);
638 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\"}");
639 }
640
641 #[test]
642 fn test_to_json_part_field() {
643 let mut msg = Msg::new("Test Title", "Test Body");
644 msg.set_level(Level::PASSIVE);
645 msg.set_badge(1);
646 msg.set_auto_copy(true);
647 msg.set_copy("");
648 msg.set_sound("chime.caf");
649 msg.set_icon("icon.png");
650 let json = msg.to_json();
651 println!("{}", json);
652 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\"}");
653 }
654
655 #[test]
656 fn test_to_json_default() {
657 let msg = Msg::new("Test Title", "Test Body");
658 let json = msg.to_json();
659 println!("{}", json);
660 assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"active\",\"sound\":\"chime.caf\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"}},\"icon\":\"https://github.com/66f94eae/bark-dev/raw/main/bot.jpg\"}");
661 }
662}