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