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    /// message id
99    id: Option<String>,
100    /// delete flag
101    is_deleted: Option<bool>,
102}
103
104
105/// Notification level
106/// 
107/// active: Default value, the system will immediately display the notification on the screen.
108/// 
109/// timeSensitive: Time-sensitive notification, can be displayed while in focus mode.
110/// 
111/// passive: Only adds the notification to the notification list, will not display on the screen.
112#[derive(Clone, Copy)]
113pub enum Level {
114    ACTIVE,
115    TIMESENSITIVE,
116    PASSIVE
117}
118
119impl Level {
120    pub fn from_str(str: &str) -> Option<Self> {
121        match str.to_lowercase().as_str() {
122            "timesensitive" => Some(Self::TIMESENSITIVE),
123            "passive" => Some(Self::PASSIVE),
124            "active" => Some(Self::ACTIVE),
125            _ => None
126        }
127    }
128}
129
130impl Display for Level {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        let str = 
133            match self {
134                Level::ACTIVE => "active",
135                Level::TIMESENSITIVE => "timeSensitive",
136                Level::PASSIVE =>"passive"
137            };
138        write!(f, "{}", str)
139    }
140}
141
142#[derive(Clone, Copy)]
143pub enum EncryptMode {
144    CBC,
145    ECB,
146    GCM,
147}
148
149impl EncryptMode {
150    pub fn from_str(str: &str) -> Option<Self> {
151        if str.is_empty() {
152            return None;
153        }
154        match str.to_lowercase().as_str() {
155            "cbc" => Some(EncryptMode::CBC),
156            "ecb" => Some(EncryptMode::ECB),
157            "gcm" => Some(EncryptMode::GCM),
158            _ => None,
159        }
160    }
161}
162
163#[derive(Clone, Copy)]
164pub enum EncryptType {
165    AES128,
166    AES192,
167    AES256,
168}
169
170impl EncryptType {
171    pub fn from_str(str: &str) -> Option<Self> {
172        if str.is_empty() {
173            return None;
174        }
175        match str.to_lowercase().as_str() {
176            "aes128" => Some(EncryptType::AES128),
177            "aes192" => Some(EncryptType::AES192),
178            "aes256" => Some(EncryptType::AES256),
179            _ => None,
180        }
181    }
182}
183
184impl Msg {
185    /// Creates a new `Msg` instance with a title and body.
186    ///
187    /// # Arguments
188    /// - `title`: The title of the notification.
189    /// - `body`: The content/body of the notification.
190    ///
191    /// # Returns
192    /// A new `Msg` instance.
193    pub fn new(title: &str, body: &str) -> Self {
194        Msg {
195            ..Self::default(Some(title.to_string()), body.to_string())
196        }
197    }
198
199    /// Creates a new `Msg` instance with only a body.
200    ///
201    /// # Arguments
202    /// - `body`: The content/body of the notification.
203    ///
204    /// # Returns
205    /// A new `Msg` instance with the title set to "Notification".
206    pub fn with_body(body: &str) -> Self {
207        Msg {
208            ..Self::default(None, body.to_string())
209        }
210    }
211
212    /// Creates a default `Msg` instance.
213    ///
214    /// # Arguments
215    /// - `title`: An optional title for the notification.
216    /// - `body`: The content/body of the notification.
217    ///
218    /// # Returns
219    /// A new `Msg` instance with default values.
220    fn default(title: Option<String>, body: String) -> Self {
221        Msg {
222            title: title.unwrap_or("Notification".to_string()),
223            body,
224            level: None,
225            badge: None,
226            auto_copy: None,
227            copy: None,
228            sound: Some("chime.caf".to_string()),
229            icon: Some("https://github.com/66f94eae/bark-dev/raw/main/bot.jpg".to_string()),
230            group: None,
231            is_archive: None,
232            url: None,
233            iv: None,
234            enc_type: None,
235            mode: None,
236            key: None,
237            cipher: None,
238            id: None,
239            is_deleted: None,
240        }
241    }
242
243    pub fn get_id(&self) -> Option<String> {
244        self.id.clone()
245    }
246
247    pub fn is_deleted(&self) -> bool {
248        match self.is_deleted {
249            Some(x) => x == true,
250            None => false
251        }
252    }
253
254    /// Sets the interruption level of the notification.
255    ///
256    /// # Arguments
257    /// - `level`: The interruption level [`Level`]
258    /// 
259    /// # Returns
260    /// A mutable reference to `self` for method chaining.
261    pub fn set_level(&mut self, level: Level) -> &mut Self {
262        self.level = Some(level);
263        self
264    }
265
266    /// Sets the badge number.
267    ///
268    /// # Arguments
269    /// - `badge`: The badge number to display on the app icon.
270    ///
271    /// # Returns
272    /// A mutable reference to `self` for method chaining.
273    pub fn set_badge(&mut self, badge: u64) -> &mut Self {
274        if badge > 0 {
275            self.badge = Some(badge);
276        } else {
277            self.badge = None;
278        }
279        self
280    }
281
282    /// Sets whether to automatically copy the notification content.
283    ///
284    /// # Arguments
285    /// - `auto_copy`: false to disable, true to enable.
286    ///
287    /// # Returns
288    /// A mutable reference to `self` for method chaining.
289    pub fn set_auto_copy(&mut self, auto_copy: bool) -> &mut Self {
290        match auto_copy {
291            false => self.auto_copy = Some(0),
292            true => self.auto_copy = None,
293        }
294        self
295    }
296
297    /// Sets specific content to copy when the notification is copied.
298    ///
299    /// # Arguments
300    /// - `copy`: The content to copy.
301    ///
302    /// # Returns
303    /// A mutable reference to `self` for method chaining.
304    pub fn set_copy(&mut self, copy: &str) -> &mut Self {
305        if copy.trim().is_empty() {
306            self.copy = None;
307        } else {
308            self.copy = Some(copy.to_string());
309        }
310        self
311    }
312
313    /// Sets the sound file to play with the notification.
314    ///
315    /// # Arguments
316    /// - `sound`: The sound file name.
317    ///
318    /// # Returns
319    /// A mutable reference to `self` for method chaining.
320    pub fn set_sound(&mut self, sound: &str) -> &mut Self {
321        self.sound = Some(sound.to_string());
322        self
323    }
324
325    /// Sets a custom icon URL for the notification.
326    ///
327    /// # Arguments
328    /// - `icon`: The icon URL.
329    ///
330    /// # Returns
331    /// A mutable reference to `self` for method chaining.
332    pub fn set_icon(&mut self, icon: &str) -> &mut Self {
333        if icon.trim().is_empty() {
334            self.icon = None;
335        } else {
336            self.icon = Some(icon.to_string());
337        }
338        self
339    }
340
341    /// Sets the group identifier for notifications.
342    ///
343    /// # Arguments
344    /// - `group`: The group identifier.
345    ///
346    /// # Returns
347    /// A mutable reference to `self` for method chaining.
348    pub fn set_group(&mut self, group: &str) -> &mut Self {
349        self.group = Some(group.to_string());
350        self
351    }
352
353    /// Sets whether to archive the notification.
354    ///
355    /// # Arguments
356    /// - `is_archive`: true to save, false to not save.
357    ///
358    /// # Returns
359    /// A mutable reference to `self` for method chaining.
360    pub fn set_is_archive(&mut self, is_archive: bool) -> &mut Self {
361        match is_archive {
362            true => self.is_archive = Some(1),
363            false => self.is_archive = None,
364        }
365        self
366    }
367
368    /// Sets the URL to open when the notification is clicked.
369    ///
370    /// # Arguments
371    /// - `url`: The URL.
372    ///
373    /// # Returns
374    /// A mutable reference to `self` for method chaining.
375    pub fn set_url(&mut self, url: &str) -> &mut Self {
376        if url.trim().is_empty() {
377            self.url = None;
378        } else {
379            self.url = Some(url.to_string());
380        }
381        self
382    }
383
384    /// Sets the initialization vector for encryption.
385    ///
386    /// # Arguments
387    /// - `iv`: The initialization vector.
388    ///
389    /// # Returns
390    /// A mutable reference to `self` for method chaining.
391    pub fn set_iv(&mut self, iv: &str) -> &mut Self {
392        if iv.trim().is_empty() {
393            self.iv = None;
394        } else if iv.len() != 12 {
395            panic!("Invalid IV length. IV must be 12 bytes long.");
396        } else {
397            self.iv = Some(iv.to_string());
398        }
399        self
400    }
401
402    /// Generates a random initialization vector.
403    ///
404    /// # Returns
405    /// A mutable reference to `self` for method chaining.
406    pub fn gen_iv(&mut self) -> &mut Self {
407        let mut iv: [u8; 16] = [0u8; 16];
408        openssl::rand::rand_bytes(&mut iv).unwrap();
409        self.set_iv(iv.iter().map(|b| format!("{:02x}", b)).collect::<String>().split_off(16).as_str())
410    }
411
412    fn set_cipher(&mut self) -> &mut Self {
413        if self.enc_type.is_none() || self.mode.is_none() {
414            return self;
415        }
416        let enc_type = self.enc_type.unwrap();
417        let mode = self.mode.unwrap();
418
419        let cipher: Cipher = match enc_type {
420            EncryptType::AES128 => {
421                match mode {
422                    EncryptMode::CBC => {
423                        Cipher::aes_128_cbc()
424                    },
425                    EncryptMode::ECB => {
426                        Cipher::aes_128_ecb()
427                    },
428                    EncryptMode::GCM => {
429                        Cipher::aes_128_gcm()
430                    },
431                }
432            },
433            EncryptType::AES192 => {
434                match mode {
435                    EncryptMode::CBC => {
436                        Cipher::aes_192_cbc()
437                    },
438                    EncryptMode::ECB => {
439                        Cipher::aes_192_ecb()
440                    },
441                    EncryptMode::GCM => {
442                        Cipher::aes_192_gcm()
443                    },
444                }
445            }, 
446            EncryptType::AES256 => {
447                match mode {
448                    EncryptMode::CBC => {
449                        Cipher::aes_256_cbc()
450                    },
451                    EncryptMode::ECB => {
452                        Cipher::aes_256_ecb()
453                    },
454                    EncryptMode::GCM => {
455                        Cipher::aes_256_gcm()
456                    },
457                }
458            },
459        };
460        self.cipher = Some(cipher);
461        self
462    }
463
464    /// Sets the encryption type and updates the cipher.
465    ///
466    /// # Arguments
467    /// - `enc_type`: The encryption type [`EncryptType`].
468    ///
469    /// # Panics
470    /// Panics if the encryption type already set.
471    ///
472    /// # Returns
473    /// A mutable reference to `self` for method chaining.
474    pub fn set_enc_type(&mut self, enc_type: EncryptType) -> &mut Self {
475        if self.enc_type.is_some() {
476            panic!("Encrypt type can only be set once");
477        }
478        self.enc_type = Some(enc_type);
479        self.set_cipher();
480        self
481    }
482
483    /// Sets the encryption mode and updates the cipher.
484    ///
485    /// # Arguments
486    /// - `mode`: The encryption mode [`EncryptMode`].
487    ///
488    /// # Panics
489    /// Panics if the encryption mode already set.
490    ///
491    /// # Returns
492    /// A mutable reference to `self` for method chaining.
493    pub fn set_mode(&mut self, mode: EncryptMode) -> &mut Self {
494        if self.mode.is_some() {
495            panic!("Encrypt mode can only be set once");
496        }
497        self.mode = Some(mode);
498        match mode {
499            EncryptMode::ECB | EncryptMode::GCM => {
500                if self.iv.is_none() {
501                    self.gen_iv();
502                }
503            },
504            _ => {},
505        }
506        self.set_cipher();
507        self
508    }
509
510    /// Sets the encryption key.
511    ///
512    /// # Arguments
513    /// - `key`: The encryption key.
514    ///
515    /// # Returns
516    /// A mutable reference to `self` for method chaining.
517    pub fn set_key(&mut self, key: &str) -> &mut Self {
518        if key.len() != 24 {
519            panic!("Invalid key length. Key must be 24 characters long.");
520        }
521        self.key = Some(key.to_string());
522        self
523    }
524
525    pub fn set_id(&mut self, msg_id: &str) -> &mut Self {
526        if msg_id.as_bytes().len() >= 64 {
527            panic!("Invalid msg_id length.The value of this key must not exceed 64 bytes.");
528        }
529        self.id = Some(msg_id.to_string());
530        self
531    }
532
533    pub fn set_deleted(&mut self) -> &mut Self {
534        self.is_deleted = Some(true);
535        self
536    }
537
538    fn json(&self, encry_body: Option<String>) -> String {
539        let mut body: String = format!("{{\"aps\":{{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"{level}\",", level = self.level.unwrap_or_else(|| Level::ACTIVE));
540
541        if let Some(badge) = self.badge {
542            body += &format!("\"badge\":{badge},", badge = badge);
543        }
544
545        if let Some(sound) = &self.sound {
546            body += &format!("\"sound\":\"{sound}\",", sound = sound);
547        }
548
549        if let Some(group) = &self.group {
550            body += &format!("\"thread-id\":\"{group}\",", group = group);
551        }
552
553        let alert: String = format!(
554            "\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}}}}",
555            title = self.title,
556            body = if encry_body.is_some() {
557                "NoContent"
558            } else {
559                self.body.as_str()
560            }
561        );
562
563        body = body + &alert;
564
565        if let Some(icon) = &self.icon {
566            body += &format!(",\"icon\":\"{icon}\"", icon = icon);
567        }
568
569        if let Some(auto_copy) = self.auto_copy {
570            body += &format!(",\"autoCopy\":{auto_copy}", auto_copy = auto_copy);
571        }
572
573        if let Some(is_archive) = self.is_archive {
574            body += &format!(",\"isArchive\":{is_archive}", is_archive = is_archive);
575        }
576
577        if let Some(copy) = &self.copy {
578            body += &format!(",\"copy\":\"{copy}\"", copy = copy);
579        }
580
581        if let Some(url) = &self.url {
582            body += &format!(",\"url\":\"{url}\"", url = url);
583        }
584
585        if let Some(iv) = &self.iv {
586            body += &format!(",\"iv\":\"{iv}\"", iv = iv);
587        }
588
589        if let Some(encry_body) = encry_body {
590            body += &format!(",\"ciphertext\":\"{encry_body}\"", encry_body = encry_body);
591        }
592
593        body + "}"
594    }
595
596    fn to_json(&self) -> String {
597        // let body: String = format!("{{\"aps\":{{\"interruption-level\":\"critical\",\"mutable-content\":1,\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}},\"category\":\"myNotificationCategory\",\"sound\":\"chime.caf\"}},\"icon\":\"{icon}\"}}",
598        // title = self.title, body = self.body, icon= self.icon
599        //     );
600        self.json(None)
601    }
602
603    /// Encrypts the message using the specified encryption type, mode, and key.
604    /// 
605    /// # Returns
606    /// A `Result` containing the encrypted message as a `String` or an error if the encryption fails.
607    fn encrypt(&self) -> Result<String, Box<dyn std::error::Error>> {
608        if self.enc_type.is_none() || self.mode.is_none() || self.key.is_none() {
609            panic!("Encrypt type, mode, and key must be set");
610        }
611
612        let key: String = self.key.as_ref().unwrap().clone();
613
614        let original: String = format!("{{\"body\":\"{}\"}}", self.body);
615        let original: &[u8] = original.as_bytes();
616
617        let cipher: Cipher = self.cipher.unwrap();
618
619        let mut crypter: Crypter = Crypter::new(
620            cipher,
621            Mode::Encrypt,
622            key.as_bytes(),
623            Some(self.iv.as_ref().unwrap().as_bytes()),
624        )
625        .unwrap();
626        crypter.pad(true); // Enable PKCS7 padding
627        let mut buffer: Vec<u8> = vec![0; original.len() + cipher.block_size()];
628        let count: usize = crypter.update(&original, &mut buffer).unwrap();
629        let rest: usize = crypter.finalize(&mut buffer[count..]).unwrap();
630        buffer.truncate(count + rest);
631        Ok(self.json(Some(openssl::base64::encode_block(&buffer))))
632    }
633
634    /// Serializes the message into a JSON string, encrypting the message if necessary.
635    /// 
636    /// # Returns
637    /// A `String` containing the serialized message.
638    pub fn serialize(&self) -> String {
639        if let Some(id) = &self.id {
640            if self.is_deleted() {
641                return format!("{{\"aps\":{{\"content-available\":1}},\"delete\":\"1\",\"id\":\"{id}\"}}");
642            }
643        }
644        if self.cipher.is_some() {
645            match self.encrypt() {
646                Ok(encrypted) => encrypted,
647                Err(e) => panic!("Error encrypting message: {}", e),
648            }
649        } else {
650            self.to_json()
651        }
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_to_json_all_field() {
661        let mut msg = Msg::new("Test Title", "Test Body");
662        msg.set_level(Level::TIMESENSITIVE);
663        msg.set_badge(1);
664        msg.set_auto_copy(true);
665        msg.set_copy("Test Copy");
666        msg.set_sound("chime.caf");
667        msg.set_icon("icon.png");
668        msg.set_group("Test Group");
669        msg.set_is_archive(true);
670        msg.set_url("https://example.com");
671        let json = msg.to_json();
672        println!("{}", json);
673        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\"}");
674    }
675
676    #[test]
677    fn test_to_json_part_field() {
678        let mut msg = Msg::new("Test Title", "Test Body");
679        msg.set_level(Level::PASSIVE);
680        msg.set_badge(1);
681        msg.set_auto_copy(true);
682        msg.set_copy("");
683        msg.set_sound("chime.caf");
684        msg.set_icon("icon.png");
685        let json = msg.to_json();
686        println!("{}", json);
687        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\"}");
688    }
689
690    #[test]
691    fn test_to_json_default() {
692        let msg = Msg::new("Test Title", "Test Body");
693        let json = msg.to_json();
694        println!("{}", json);
695        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\"}");
696    }
697}