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}