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
84 iv: Option<String>,
85 /// encrypt type
86 enc_type: Option<EncryptType>,
87 /// encrypt mode
88 mode: Option<EncryptMode>,
89 /// encrypt key
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 {
340 self.iv = Some(iv.to_string());
341 }
342 self
343 }
344
345 /// Generates a random initialization vector.
346 ///
347 /// # Returns
348 /// A mutable reference to `self` for method chaining.
349 pub fn gen_iv(&mut self) -> &mut Self {
350 let mut iv: [u8; 16] = [0u8; 16];
351 openssl::rand::rand_bytes(&mut iv).unwrap();
352 self.set_iv(iv.iter().map(|b| format!("{:02x}", b)).collect::<String>().split_off(16).as_str())
353 }
354
355 fn set_cipher(&mut self) -> &mut Self {
356 if self.enc_type.is_none() || self.mode.is_none() {
357 return self;
358 }
359 let enc_type = self.enc_type.unwrap();
360 let mode = self.mode.unwrap();
361
362 let cipher: Cipher = match enc_type {
363 EncryptType::AES128 => {
364 match mode {
365 EncryptMode::CBC => {
366 Cipher::aes_128_cbc()
367 },
368 EncryptMode::ECB => {
369 Cipher::aes_128_ecb()
370 },
371 EncryptMode::GCM => {
372 Cipher::aes_128_gcm()
373 },
374 }
375 },
376 EncryptType::AES192 => {
377 match mode {
378 EncryptMode::CBC => {
379 Cipher::aes_192_cbc()
380 },
381 EncryptMode::ECB => {
382 Cipher::aes_192_ecb()
383 },
384 EncryptMode::GCM => {
385 Cipher::aes_192_gcm()
386 },
387 }
388 },
389 EncryptType::AES256 => {
390 match mode {
391 EncryptMode::CBC => {
392 Cipher::aes_256_cbc()
393 },
394 EncryptMode::ECB => {
395 Cipher::aes_256_ecb()
396 },
397 EncryptMode::GCM => {
398 Cipher::aes_256_gcm()
399 },
400 }
401 },
402 };
403 self.cipher = Some(cipher);
404 self
405 }
406
407 /// Sets the encryption type and updates the cipher.
408 ///
409 /// # Arguments
410 /// - `enc_type`: The encryption type [`EncryptType`].
411 ///
412 /// # Panics
413 /// Panics if the encryption type already set.
414 ///
415 /// # Returns
416 /// A mutable reference to `self` for method chaining.
417 pub fn set_enc_type(&mut self, enc_type: EncryptType) -> &mut Self {
418 if self.enc_type.is_some() {
419 panic!("Encrypt type can only be set once");
420 }
421 self.enc_type = Some(enc_type);
422 self.set_cipher();
423 self
424 }
425
426 /// Sets the encryption mode and updates the cipher.
427 ///
428 /// # Arguments
429 /// - `mode`: The encryption mode [`EncryptMode`].
430 ///
431 /// # Panics
432 /// Panics if the encryption mode already set.
433 ///
434 /// # Returns
435 /// A mutable reference to `self` for method chaining.
436 pub fn set_mode(&mut self, mode: EncryptMode) -> &mut Self {
437 if self.mode.is_some() {
438 panic!("Encrypt mode can only be set once");
439 }
440 self.mode = Some(mode);
441 match mode {
442 EncryptMode::ECB | EncryptMode::GCM => {
443 if self.iv.is_none() {
444 println!("ensure iv is set, generate a random iv standby");
445 self.gen_iv();
446 }
447 },
448 _ => {},
449 }
450 self.set_cipher();
451 self
452 }
453
454 /// Sets the encryption key.
455 ///
456 /// # Arguments
457 /// - `key`: The encryption key.
458 ///
459 /// # Returns
460 /// A mutable reference to `self` for method chaining.
461 pub fn set_key(&mut self, key: &str) -> &mut Self {
462 self.key = Some(key.to_string());
463 self
464 }
465
466 fn json(&self, encry_body: Option<String>) -> String {
467 let mut body: String = format!("{{\"aps\":{{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"{level}\",", level = self.level.as_ref().unwrap_or(&"active".to_string()));
468
469 if let Some(badge) = self.badge {
470 body += &format!("\"badge\":{badge},", badge = badge);
471 }
472
473 if let Some(sound) = &self.sound {
474 body += &format!("\"sound\":\"{sound}\",", sound = sound);
475 }
476
477 if let Some(group) = &self.group {
478 body += &format!("\"thread-id\":\"{group}\",", group = group);
479 }
480
481 let alert: String = format!(
482 "\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}}}}",
483 title = self.title,
484 body = if encry_body.is_some() {
485 "NoContent"
486 } else {
487 self.body.as_str()
488 }
489 );
490
491 body = body + &alert;
492
493 if let Some(icon) = &self.icon {
494 body += &format!(",\"icon\":\"{icon}\"", icon = icon);
495 }
496
497 if let Some(auto_copy) = self.auto_copy {
498 body += &format!(",\"autoCopy\":{auto_copy}", auto_copy = auto_copy);
499 }
500
501 if let Some(is_archive) = self.is_archive {
502 body += &format!(",\"isArchive\":{is_archive}", is_archive = is_archive);
503 }
504
505 if let Some(copy) = &self.copy {
506 body += &format!(",\"copy\":\"{copy}\"", copy = copy);
507 }
508
509 if let Some(url) = &self.url {
510 body += &format!(",\"url\":\"{url}\"", url = url);
511 }
512
513 if let Some(iv) = &self.iv {
514 body += &format!(",\"iv\":\"{iv}\"", iv = iv);
515 }
516
517 if let Some(encry_body) = encry_body {
518 body += &format!(",\"ciphertext\":\"{encry_body}\"", encry_body = encry_body);
519 }
520
521 body + "}"
522 }
523
524 fn to_json(&self) -> String {
525 // let body: String = format!("{{\"aps\":{{\"interruption-level\":\"critical\",\"mutable-content\":1,\"alert\":{{\"title\":\"{title}\",\"body\":\"{body}\"}},\"category\":\"myNotificationCategory\",\"sound\":\"chime.caf\"}},\"icon\":\"{icon}\"}}",
526 // title = self.title, body = self.body, icon= self.icon
527 // );
528 self.json(None)
529 }
530
531 /// Encrypts the message using the specified encryption type, mode, and key.
532 ///
533 /// # Returns
534 /// A `Result` containing the encrypted message as a `String` or an error if the encryption fails.
535 fn encrypt(&self) -> Result<String, Box<dyn std::error::Error>> {
536 if self.enc_type.is_none() || self.mode.is_none() || self.key.is_none() {
537 panic!("Encrypt type, mode, and key must be set");
538 }
539
540 let key: String = self.key.as_ref().unwrap().clone();
541
542 let original: String = format!("{{\"body\":\"{}\"}}", self.body);
543 let original: &[u8] = original.as_bytes();
544
545 let cipher: Cipher = self.cipher.unwrap();
546
547 let mut crypter: Crypter = Crypter::new(
548 cipher,
549 Mode::Encrypt,
550 key.as_bytes(),
551 Some(self.iv.as_ref().unwrap().as_bytes()),
552 )
553 .unwrap();
554 crypter.pad(true); // Enable PKCS7 padding
555 let mut buffer: Vec<u8> = vec![0; original.len() + cipher.block_size()];
556 let count: usize = crypter.update(&original, &mut buffer).unwrap();
557 let rest: usize = crypter.finalize(&mut buffer[count..]).unwrap();
558 buffer.truncate(count + rest);
559 Ok(self.json(Some(openssl::base64::encode_block(&buffer))))
560 }
561
562 /// Serializes the message into a JSON string, encrypting the message if necessary.
563 ///
564 /// # Returns
565 /// A `String` containing the serialized message.
566 pub fn serialize(&self) -> String {
567 if self.cipher.is_some() {
568 match self.encrypt() {
569 Ok(encrypted) => encrypted,
570 Err(e) => panic!("Error encrypting message: {}", e),
571 }
572 } else {
573 self.to_json()
574 }
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn test_to_json_all_field() {
584 let mut msg = Msg::new("Test Title", "Test Body");
585 msg.set_level("timeSensitive");
586 msg.set_badge(1);
587 msg.set_auto_copy(1);
588 msg.set_copy("Test Copy");
589 msg.set_sound("chime.caf");
590 msg.set_icon("icon.png");
591 msg.set_group("Test Group");
592 msg.set_is_archive(1);
593 msg.set_url("https://example.com");
594 let json = msg.to_json();
595 println!("{}", json);
596 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\"}");
597 }
598
599 #[test]
600 fn test_to_json_part_field() {
601 let mut msg = Msg::new("Test Title", "Test Body");
602 msg.set_level("passive");
603 msg.set_badge(1);
604 msg.set_auto_copy(1);
605 msg.set_copy("");
606 msg.set_sound("chime.caf");
607 msg.set_icon("icon.png");
608 let json = msg.to_json();
609 println!("{}", json);
610 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\"}}");
611 }
612
613 #[test]
614 fn test_to_json_default() {
615 let msg = Msg::new("Test Title", "Test Body");
616 let json = msg.to_json();
617 println!("{}", json);
618 assert_eq!(json, "{\"aps\":{\"mutable-content\":1,\"category\":\"myNotificationCategory\",\"interruption-level\":\"active\",\"alert\":{\"title\":\"Test Title\",\"body\":\"Test Body\"}}}");
619 }
620}