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}