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///
27/// Push Notification Message
28/// 
29/// # Example
30/// ```rust
31/// use bark::msg::Msg;
32/// 
33/// // new a simple message with title and body
34/// let msg = Msg::new("title", "body");
35/// 
36/// // new a message with title = Notification and body
37/// let mut msg = Msg::with_body("body");
38/// 
39/// // set some fields
40/// msg.set_level("active");
41/// msg.set_badge(1);
42/// // and so on
43/// ```
44pub struct Msg {
45    /// Push Title
46    title: String,
47    
48    /// Push Content
49    body: String,
50    
51    /// Push Interruption Level
52    /// active: Default value, the system will immediately display the notification on the screen.
53    /// timeSensitive: Time-sensitive notification, can be displayed while in focus mode.
54    /// passive: Only adds the notification to the notification list, will not display on the screen.
55    level: Option<String>,
56    
57    /// Push Badge, can be any number
58    badge: Option<u64>,
59    
60    /// 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
61    auto_copy: Option<u8>,
62    
63    /// When copying the push, specify the content to copy; if this parameter is not provided, the entire push content will be copied
64    copy: Option<String>,
65    
66    /// You can set different ringtones for the push
67    sound: Option<String>,
68    
69    /// Set a custom icon for the push; the set icon will replace the default Bark icon
70    icon: Option<String>,
71    
72    /// Group messages; pushes will be displayed in groups in the notification center
73    group: Option<String>,
74    
75    /// 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
76    is_archive: Option<u8>,
77    
78    /// The URL to jump to when clicking the push, supports URL Scheme and Universal Link
79    url: Option<String>,
80
81    /// iv
82    iv: Option<String>,
83    /// encrypt type
84    enc_type: Option<String>,
85    /// encrypt mode
86    mode: Option<String>,
87    /// encrypt key
88    key: Option<String>,
89    /// cipher
90    cipher: Option<Cipher>,
91}
92
93impl Msg {
94    pub fn new(title: &str, body: &str) -> Self {
95        Msg {
96            ..Self::default(Some(title.to_string()), body.to_string())
97        }
98    }
99
100    pub fn with_body(body: &str) -> Self {
101        Msg {
102            ..Self::default(None, body.to_string())
103        }
104    }
105
106    fn default(title: Option<String>, body: String) -> Self {
107        Msg {
108            title: title.unwrap_or("Notification".to_string()),
109            body,
110            level: None,
111            badge: None,
112            auto_copy: None,
113            copy: None,
114            sound: Some("chime.caf".to_string()),
115            icon: Some("https://github.com/66f94eae/bark-dev/raw/main/bot.jpg".to_string()),
116            group: None,
117            is_archive: None,
118            url: None,
119            iv: None,
120            enc_type: None,
121            mode: None,
122            key: None,
123            cipher: None,
124        }
125    }
126
127    pub fn set_level(&mut self, level: &str) -> &mut Self {
128        match level.to_lowercase().as_str() {
129            "active" => self.level = Some("active".to_string()),
130            "timesensitive" => self.level = Some("timeSensitive".to_string()),
131            "passive" => self.level = Some("passive".to_string()),
132            _ => self.level = None,
133        }
134        self
135    }
136
137    pub fn set_badge(&mut self, badge: u64) -> &mut  Self {
138        if badge > 0 {
139            self.badge = Some(badge);
140        } else {
141            self.badge = None;
142        }
143        self
144    }
145
146    pub fn set_auto_copy(&mut self, auto_copy: u8) -> &mut Self {
147        match auto_copy {
148            0 => self.auto_copy = Some(0),
149            _ => self.auto_copy = None,
150        }
151        self
152    }
153
154    pub fn set_copy(&mut self, copy: &str) -> &mut Self {
155        if copy.trim().is_empty() {
156            self.copy = None;
157        } else {
158            self.copy = Some(copy.to_string());
159        }
160        self
161    }
162
163    pub fn set_sound(&mut self, sound: &str) -> &mut Self {
164        self.sound = Some(sound.to_string());
165        self
166    }
167
168    pub fn set_icon(&mut self, icon: &str) -> &mut Self {
169        if icon.trim().is_empty() {
170            self.icon = None;
171        } else {
172            self.icon = Some(icon.to_string());
173        }
174        self
175    }
176
177    pub fn set_group(&mut self, group: &str) -> &mut Self {
178        self.group = Some(group.to_string());
179        self
180    }
181
182    pub fn set_is_archive(&mut self, is_archive: u8) -> &mut Self {
183        match is_archive {
184            1 => self.is_archive = Some(1),
185            _ => self.is_archive = None,
186        }
187        self
188    }
189
190    pub fn set_url(&mut self, url: &str) -> &mut Self {
191        if url.trim().is_empty() {
192            self.url = None;
193        } else {
194            self.url = Some(url.to_string());
195        }
196        self
197    }
198
199    pub fn set_iv(&mut self, iv: &str) -> &mut Self {
200        if iv.trim().is_empty() {
201            self.iv = None;
202        } else {
203            self.iv = Some(iv.to_string());
204        }
205        self
206    }
207
208    pub fn gen_iv(&mut self) -> &mut Self {
209        let mut iv: [u8; 16] = [0u8; 16];
210        openssl::rand::rand_bytes(&mut iv).unwrap();
211        self.set_iv(iv.iter().map(|b| format!("{:02x}", b)).collect::<String>().split_off(16).as_str())
212    }
213
214    fn set_cipher(&mut self) -> &mut Self {
215        if self.enc_type.is_none() || self.mode.is_none() {
216            return self;
217        }
218        let enc_type = self.enc_type.as_ref().unwrap().clone();
219        let mode = self.mode.as_ref().unwrap().clone();
220
221        let cipher: Cipher = match enc_type.to_lowercase().as_str() {
222            "aes128" => {
223                match mode.to_lowercase().as_str() {
224                    "cbc" => {
225                        Cipher::aes_128_cbc()
226                    },
227                    "ecb" => {
228                        Cipher::aes_128_ecb()
229                    },
230                    "gcm" => {
231                        Cipher::aes_128_gcm()
232                    },
233                    _ => panic!("Invalid mode"),
234                }
235            },
236            "aes192" => {
237                match mode.to_lowercase().as_str() {
238                    "cbc" => {
239                        Cipher::aes_192_cbc()
240                    },
241                    "ecb" => {
242                        Cipher::aes_192_ecb()
243                    },
244                    "gcm" => {
245                        Cipher::aes_192_gcm()
246                    },
247                    _ => panic!("Invalid mode"),
248                }
249            }, 
250            "aes256" => {
251                match mode.to_lowercase().as_str() {
252                    "cbc" => {
253                        Cipher::aes_256_cbc()
254                    },
255                    "ecb" => {
256                        Cipher::aes_256_ecb()
257                    },
258                    "gcm" => {
259                        Cipher::aes_256_gcm()
260                    },
261                    _ => panic!("Invalid mode"),
262                }
263            },
264            _ => panic!("Invalid type"),
265        };
266        self.cipher = Some(cipher);
267        self
268    }
269
270    pub fn set_enc_type(&mut self, enc_type: &str) -> &mut Self {
271       if self.enc_type.is_some() {
272            panic!("Encrypt type can only be set once");
273        }
274        match enc_type.to_lowercase().as_str() {
275            "aes128" => self.enc_type = Some("aes128".to_string()),
276            "aes192" => self.enc_type = Some("aes192".to_string()),
277            "aes256" => self.enc_type = Some("aes256".to_string()),
278            _ => panic!("Invalid encrypt type"),
279        }
280        self.set_cipher();
281        self
282    }
283
284    pub fn set_mode(&mut self, mode: &str) -> &mut Self {
285        if self.mode.is_some() {
286            panic!("Encrypt mode can only be set once");
287        }
288        match mode.to_lowercase().as_str() {
289            "cbc" => self.mode = Some("cbc".to_string()),
290            "ecb" => self.mode = Some("ecb".to_string()),
291            "gcm" => self.mode = Some("gcm".to_string()),
292            _ => panic!("Invalid encrypt mode"),
293        }
294        self.set_cipher();
295        self
296    }
297
298    pub fn set_key(&mut self, key: &str) -> &mut Self {
299        self.key = Some(key.to_string());
300        self
301    }
302    
303    fn json(&self, encry_body: Option<String>) -> String {
304        let mut body: String = format!("{{\"aps\":{{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"{level}\",", level = self.level.as_ref().unwrap_or(&"active".to_string()));
305
306        if let Some(badge) = self.badge {
307            body += &format!("\"badge\":{badge},", badge = badge);
308        }
309
310        if let Some(sound) = &self.sound {
311            body += &format!("\"sound\":\"{sound}\",", sound = sound);
312        }
313
314        if let Some(group) = &self.group {
315            body += &format!("\"thread-id\":\"{group}\",", group = group);
316        }
317
318        let alert: String = format!("\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}}}}",
319            title = self.title, body = if encry_body.is_some() {"NoContent"} else {self.body.as_str()}
320        );
321
322        body = body + &alert;
323
324        if let Some(icon) = &self.icon {
325            body += &format!(",\"icon\":\"{icon}\"", icon = icon);
326        }
327
328        if let Some(auto_copy) = self.auto_copy {
329            body += &format!(",\"autoCopy\":{auto_copy}", auto_copy = auto_copy);
330        }
331
332        if let Some(is_archive) = self.is_archive {
333            body += &format!(",\"isArchive\":{is_archive}", is_archive = is_archive);
334        }
335
336        if let Some(copy) = &self.copy {
337            body += &format!(",\"copy\":\"{copy}\"", copy = copy);
338        }
339
340        if let Some(url) = &self.url {
341            body += &format!(",\"url\":\"{url}\"", url = url);
342        }
343
344        if let Some(iv) = &self.iv {
345            body += &format!(",\"iv\":\"{iv}\"", iv = iv);
346        }
347        
348        if let Some(encry_body) = encry_body {
349            body += &format!(",\"ciphertext\":\"{encry_body}\"", encry_body = encry_body);
350        }
351
352        body + "}"
353    }
354
355    pub fn to_json(&self) -> String {
356        // let body: String = format!("{{\"aps\":{{\"interruption-level\":\"critical\",\"mutable-content\":1,\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}},\"category\":\"myNotificationCategory\",\"sound\":\"chime.caf\"}},\"icon\":\"{icon}\"}}",
357        // title = self.title, body = self.body, icon= self.icon
358        //     );
359        self.json(None)
360    }
361
362
363    pub fn encrypt(&self) -> Result<String, Box<dyn std::error::Error>> {
364        if self.enc_type.is_none() || self.mode.is_none() || self.key.is_none() {
365            panic!("Encrypt type, mode, and key must be set");
366        }
367
368        let key: String = self.key.as_ref().unwrap().clone();
369
370        let original: String = format!("{{\"body\":\"{}\"}}", self.body);
371        let original: &[u8] = original.as_bytes();
372
373        let cipher: Cipher = self.cipher.unwrap();
374        
375        let mut crypter: Crypter = Crypter::new(cipher, Mode::Encrypt, key.as_bytes(), Some(self.iv.as_ref().unwrap().as_bytes())).unwrap();
376        crypter.pad(true); // Enable PKCS7 padding
377        let mut buffer: Vec<u8> = vec![0; original.len() + cipher.block_size()];
378        let count: usize = crypter.update(&original, &mut buffer).unwrap();
379        let rest: usize = crypter.finalize(&mut buffer[count..]).unwrap();
380        buffer.truncate(count + rest);
381        Ok(self.json(Some(openssl::base64::encode_block(&buffer))))
382    }
383
384    pub fn serialize(&self) -> String {
385        if self.cipher.is_some() {
386            match self.encrypt() {
387                Ok(encrypted) => {
388                    encrypted
389                },
390                Err(e) => panic!("Error encrypting message: {}", e),
391             }
392        } else {
393            self.to_json()
394        }
395    }
396    
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_to_json_all_field() {
405        let mut msg = Msg::new("Test Title", "Test Body");
406        msg.set_level("timeSensitive");
407        msg.set_badge(1);
408        msg.set_auto_copy(1);
409        msg.set_copy("Test Copy");
410        msg.set_sound("chime.caf");
411        msg.set_icon("icon.png");
412        msg.set_group("Test Group");
413        msg.set_is_archive(1);
414        msg.set_url("https://example.com");
415        let json = msg.to_json();
416        println!("{}", json);
417        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\"}");
418    }
419
420    #[test]
421    fn test_to_json_part_field() {
422        let mut msg = Msg::new("Test Title", "Test Body");
423        msg.set_level("passive");
424        msg.set_badge(1);
425        msg.set_auto_copy(1);
426        msg.set_copy("");
427        msg.set_sound("chime.caf");
428        msg.set_icon("icon.png");
429        let json = msg.to_json();
430        println!("{}", json);
431        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\"}}");
432    }
433
434    #[test]
435    fn test_to_json_default() {
436        let msg = Msg::new("Test Title", "Test Body");
437        let json = msg.to_json();
438        println!("{}", json);
439        assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"active\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"}}}");
440    }
441}