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