1use std::{collections::HashMap, fmt::Display};
6
7use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
8
9use url::Url;
10
11#[derive(Debug, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Attachment {
14 id: String,
15 url: Url,
16}
17
18impl Attachment {
19 pub fn new(id: impl Into<String>, url: Url) -> Self {
20 Self { id: id.into(), url }
21 }
22}
23
24#[derive(Debug, Default, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct ScheduleInterval {
27 pub year: Option<u8>,
28 pub month: Option<u8>,
29 pub day: Option<u8>,
30 pub weekday: Option<u8>,
31 pub hour: Option<u8>,
32 pub minute: Option<u8>,
33 pub second: Option<u8>,
34}
35
36#[derive(Debug)]
37pub enum ScheduleEvery {
38 Year,
39 Month,
40 TwoWeeks,
41 Week,
42 Day,
43 Hour,
44 Minute,
45 Second,
46}
47
48impl Display for ScheduleEvery {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(
51 f,
52 "{}",
53 match self {
54 Self::Year => "year",
55 Self::Month => "month",
56 Self::TwoWeeks => "twoWeeks",
57 Self::Week => "week",
58 Self::Day => "day",
59 Self::Hour => "hour",
60 Self::Minute => "minute",
61 Self::Second => "second",
62 }
63 )
64 }
65}
66
67impl Serialize for ScheduleEvery {
68 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
69 where
70 S: Serializer,
71 {
72 serializer.serialize_str(self.to_string().as_ref())
73 }
74}
75
76impl<'de> Deserialize<'de> for ScheduleEvery {
77 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
78 where
79 D: Deserializer<'de>,
80 {
81 let s = String::deserialize(deserializer)?;
82 match s.to_lowercase().as_str() {
83 "year" => Ok(Self::Year),
84 "month" => Ok(Self::Month),
85 "twoweeks" => Ok(Self::TwoWeeks),
86 "week" => Ok(Self::Week),
87 "day" => Ok(Self::Day),
88 "hour" => Ok(Self::Hour),
89 "minute" => Ok(Self::Minute),
90 "second" => Ok(Self::Second),
91 _ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
92 }
93 }
94}
95
96#[derive(Debug, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub enum Schedule {
99 #[serde(rename_all = "camelCase")]
100 At {
101 #[serde(
102 serialize_with = "iso8601::serialize",
103 deserialize_with = "time::serde::iso8601::deserialize"
104 )]
105 date: time::OffsetDateTime,
106 #[serde(default)]
107 repeating: bool,
108 #[serde(default)]
109 allow_while_idle: bool,
110 },
111 #[serde(rename_all = "camelCase")]
112 Interval {
113 interval: ScheduleInterval,
114 #[serde(default)]
115 allow_while_idle: bool,
116 },
117 #[serde(rename_all = "camelCase")]
118 Every {
119 interval: ScheduleEvery,
120 count: u8,
121 #[serde(default)]
122 allow_while_idle: bool,
123 },
124}
125
126mod iso8601 {
128 use serde::{ser::Error as _, Serialize, Serializer};
129 use time::{
130 format_description::well_known::iso8601::{Config, EncodedConfig},
131 format_description::well_known::Iso8601,
132 OffsetDateTime,
133 };
134
135 const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
136
137 pub fn serialize<S: Serializer>(
138 datetime: &OffsetDateTime,
139 serializer: S,
140 ) -> Result<S::Ok, S::Error> {
141 datetime
142 .format(&Iso8601::<SERDE_CONFIG>)
143 .map_err(S::Error::custom)?
144 .serialize(serializer)
145 }
146}
147
148#[derive(Debug, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct NotificationData {
151 #[serde(default = "default_id")]
152 pub(crate) id: i32,
153 pub(crate) channel_id: Option<String>,
154 pub(crate) title: Option<String>,
155 pub(crate) body: Option<String>,
156 pub(crate) schedule: Option<Schedule>,
157 pub(crate) large_body: Option<String>,
158 pub(crate) summary: Option<String>,
159 pub(crate) action_type_id: Option<String>,
160 pub(crate) group: Option<String>,
161 #[serde(default)]
162 pub(crate) group_summary: bool,
163 pub(crate) sound: Option<String>,
164 #[serde(default)]
165 pub(crate) inbox_lines: Vec<String>,
166 pub(crate) icon: Option<String>,
167 pub(crate) large_icon: Option<String>,
168 pub(crate) icon_color: Option<String>,
169 #[serde(default)]
170 pub(crate) attachments: Vec<Attachment>,
171 #[serde(default)]
172 pub(crate) extra: HashMap<String, serde_json::Value>,
173 #[serde(default)]
174 pub(crate) ongoing: bool,
175 #[serde(default)]
176 pub(crate) auto_cancel: bool,
177 #[serde(default)]
178 pub(crate) silent: bool,
179}
180
181fn default_id() -> i32 {
182 rand::random()
183}
184
185impl Default for NotificationData {
186 fn default() -> Self {
187 Self {
188 id: default_id(),
189 channel_id: None,
190 title: None,
191 body: None,
192 schedule: None,
193 large_body: None,
194 summary: None,
195 action_type_id: None,
196 group: None,
197 group_summary: false,
198 sound: None,
199 inbox_lines: Vec::new(),
200 icon: None,
201 large_icon: None,
202 icon_color: None,
203 attachments: Vec::new(),
204 extra: Default::default(),
205 ongoing: false,
206 auto_cancel: false,
207 silent: false,
208 }
209 }
210}
211
212#[derive(Debug, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct PendingNotification {
215 id: i32,
216 title: Option<String>,
217 body: Option<String>,
218 schedule: Schedule,
219}
220
221impl PendingNotification {
222 pub fn id(&self) -> i32 {
223 self.id
224 }
225
226 pub fn title(&self) -> Option<&str> {
227 self.title.as_deref()
228 }
229
230 pub fn body(&self) -> Option<&str> {
231 self.body.as_deref()
232 }
233
234 pub fn schedule(&self) -> &Schedule {
235 &self.schedule
236 }
237}
238
239#[derive(Debug, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct ActiveNotification {
242 id: i32,
243 tag: Option<String>,
244 title: Option<String>,
245 body: Option<String>,
246 group: Option<String>,
247 #[serde(default)]
248 group_summary: bool,
249 #[serde(default)]
250 data: HashMap<String, String>,
251 #[serde(default)]
252 extra: HashMap<String, serde_json::Value>,
253 #[serde(default)]
254 attachments: Vec<Attachment>,
255 action_type_id: Option<String>,
256 schedule: Option<Schedule>,
257 sound: Option<String>,
258}
259
260impl ActiveNotification {
261 pub fn id(&self) -> i32 {
262 self.id
263 }
264
265 pub fn tag(&self) -> Option<&str> {
266 self.tag.as_deref()
267 }
268
269 pub fn title(&self) -> Option<&str> {
270 self.title.as_deref()
271 }
272
273 pub fn body(&self) -> Option<&str> {
274 self.body.as_deref()
275 }
276
277 pub fn group(&self) -> Option<&str> {
278 self.group.as_deref()
279 }
280
281 pub fn group_summary(&self) -> bool {
282 self.group_summary
283 }
284
285 pub fn data(&self) -> &HashMap<String, String> {
286 &self.data
287 }
288
289 pub fn extra(&self) -> &HashMap<String, serde_json::Value> {
290 &self.extra
291 }
292
293 pub fn attachments(&self) -> &[Attachment] {
294 &self.attachments
295 }
296
297 pub fn action_type_id(&self) -> Option<&str> {
298 self.action_type_id.as_deref()
299 }
300
301 pub fn schedule(&self) -> Option<&Schedule> {
302 self.schedule.as_ref()
303 }
304
305 pub fn sound(&self) -> Option<&str> {
306 self.sound.as_deref()
307 }
308}
309
310#[cfg(mobile)]
311#[derive(Debug, Serialize)]
312#[serde(rename_all = "camelCase")]
313pub struct ActionType {
314 id: String,
315 actions: Vec<Action>,
316 hidden_previews_body_placeholder: Option<String>,
317 custom_dismiss_action: bool,
318 allow_in_car_play: bool,
319 hidden_previews_show_title: bool,
320 hidden_previews_show_subtitle: bool,
321}
322
323#[cfg(mobile)]
324#[derive(Debug)]
325pub struct ActionTypeBuilder(ActionType);
326
327#[cfg(mobile)]
328impl ActionType {
329 pub fn builder(id: impl Into<String>) -> ActionTypeBuilder {
330 ActionTypeBuilder(Self {
331 id: id.into(),
332 actions: Vec::new(),
333 hidden_previews_body_placeholder: None,
334 custom_dismiss_action: false,
335 allow_in_car_play: false,
336 hidden_previews_show_title: false,
337 hidden_previews_show_subtitle: false,
338 })
339 }
340
341 pub fn id(&self) -> &str {
342 &self.id
343 }
344
345 pub fn actions(&self) -> &[Action] {
346 &self.actions
347 }
348
349 pub fn hidden_previews_body_placeholder(&self) -> Option<&str> {
350 self.hidden_previews_body_placeholder.as_deref()
351 }
352
353 pub fn custom_dismiss_action(&self) -> bool {
354 self.custom_dismiss_action
355 }
356
357 pub fn allow_in_car_play(&self) -> bool {
358 self.allow_in_car_play
359 }
360
361 pub fn hidden_previews_show_title(&self) -> bool {
362 self.hidden_previews_show_title
363 }
364
365 pub fn hidden_previews_show_subtitle(&self) -> bool {
366 self.hidden_previews_show_subtitle
367 }
368}
369
370#[cfg(mobile)]
371impl ActionTypeBuilder {
372 pub fn actions(mut self, actions: Vec<Action>) -> Self {
373 self.0.actions = actions;
374 self
375 }
376
377 pub fn hidden_previews_body_placeholder(
378 mut self,
379 hidden_previews_body_placeholder: impl Into<String>,
380 ) -> Self {
381 self.0
382 .hidden_previews_body_placeholder
383 .replace(hidden_previews_body_placeholder.into());
384 self
385 }
386
387 pub fn custom_dismiss_action(mut self, custom_dismiss_action: bool) -> Self {
388 self.0.custom_dismiss_action = custom_dismiss_action;
389 self
390 }
391
392 pub fn allow_in_car_play(mut self, allow_in_car_play: bool) -> Self {
393 self.0.allow_in_car_play = allow_in_car_play;
394 self
395 }
396
397 pub fn hidden_previews_show_title(mut self, hidden_previews_show_title: bool) -> Self {
398 self.0.hidden_previews_show_title = hidden_previews_show_title;
399 self
400 }
401
402 pub fn hidden_previews_show_subtitle(mut self, hidden_previews_show_subtitle: bool) -> Self {
403 self.0.hidden_previews_show_subtitle = hidden_previews_show_subtitle;
404 self
405 }
406
407 pub fn build(self) -> ActionType {
408 self.0
409 }
410}
411
412#[cfg(mobile)]
413#[derive(Debug, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub struct Action {
416 id: String,
417 title: String,
418 requires_authentication: bool,
419 foreground: bool,
420 destructive: bool,
421 input: bool,
422 input_button_title: Option<String>,
423 input_placeholder: Option<String>,
424}
425
426#[cfg(mobile)]
427#[derive(Debug)]
428pub struct ActionBuilder(Action);
429
430#[cfg(mobile)]
431impl Action {
432 pub fn builder(id: impl Into<String>, title: impl Into<String>) -> ActionBuilder {
433 ActionBuilder(Self {
434 id: id.into(),
435 title: title.into(),
436 requires_authentication: false,
437 foreground: false,
438 destructive: false,
439 input: false,
440 input_button_title: None,
441 input_placeholder: None,
442 })
443 }
444
445 pub fn id(&self) -> &str {
446 &self.id
447 }
448
449 pub fn title(&self) -> &str {
450 &self.title
451 }
452
453 pub fn requires_authentication(&self) -> bool {
454 self.requires_authentication
455 }
456
457 pub fn foreground(&self) -> bool {
458 self.foreground
459 }
460
461 pub fn destructive(&self) -> bool {
462 self.destructive
463 }
464
465 pub fn input(&self) -> bool {
466 self.input
467 }
468
469 pub fn input_button_title(&self) -> Option<&str> {
470 self.input_button_title.as_deref()
471 }
472
473 pub fn input_placeholder(&self) -> Option<&str> {
474 self.input_placeholder.as_deref()
475 }
476}
477
478#[cfg(mobile)]
479impl ActionBuilder {
480 pub fn requires_authentication(mut self, requires_authentication: bool) -> Self {
481 self.0.requires_authentication = requires_authentication;
482 self
483 }
484
485 pub fn foreground(mut self, foreground: bool) -> Self {
486 self.0.foreground = foreground;
487 self
488 }
489
490 pub fn destructive(mut self, destructive: bool) -> Self {
491 self.0.destructive = destructive;
492 self
493 }
494
495 pub fn input(mut self, input: bool) -> Self {
496 self.0.input = input;
497 self
498 }
499
500 pub fn input_button_title(mut self, input_button_title: impl Into<String>) -> Self {
501 self.0.input_button_title.replace(input_button_title.into());
502 self
503 }
504
505 pub fn input_placeholder(mut self, input_placeholder: impl Into<String>) -> Self {
506 self.0.input_placeholder.replace(input_placeholder.into());
507 self
508 }
509
510 pub fn build(self) -> Action {
511 self.0
512 }
513}
514
515#[cfg(target_os = "android")]
516pub use android::*;
517
518#[cfg(target_os = "android")]
519mod android {
520 use serde::{Deserialize, Serialize};
521 use serde_repr::{Deserialize_repr, Serialize_repr};
522
523 #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
524 #[repr(u8)]
525 pub enum Importance {
526 None = 0,
527 Min = 1,
528 Low = 2,
529 Default = 3,
530 High = 4,
531 }
532
533 impl Default for Importance {
534 fn default() -> Self {
535 Self::Default
536 }
537 }
538
539 #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
540 #[repr(i8)]
541 pub enum Visibility {
542 Secret = -1,
543 Private = 0,
544 Public = 1,
545 }
546
547 #[derive(Debug, Serialize, Deserialize)]
548 #[serde(rename_all = "camelCase")]
549 pub struct Channel {
550 id: String,
551 name: String,
552 description: Option<String>,
553 sound: Option<String>,
554 lights: bool,
555 light_color: Option<String>,
556 vibration: bool,
557 importance: Importance,
558 visibility: Option<Visibility>,
559 }
560
561 #[derive(Debug)]
562 pub struct ChannelBuilder(Channel);
563
564 impl Channel {
565 pub fn builder(id: impl Into<String>, name: impl Into<String>) -> ChannelBuilder {
566 ChannelBuilder(Self {
567 id: id.into(),
568 name: name.into(),
569 description: None,
570 sound: None,
571 lights: false,
572 light_color: None,
573 vibration: false,
574 importance: Default::default(),
575 visibility: None,
576 })
577 }
578
579 pub fn id(&self) -> &str {
580 &self.id
581 }
582
583 pub fn name(&self) -> &str {
584 &self.name
585 }
586
587 pub fn description(&self) -> Option<&str> {
588 self.description.as_deref()
589 }
590
591 pub fn sound(&self) -> Option<&str> {
592 self.sound.as_deref()
593 }
594
595 pub fn lights(&self) -> bool {
596 self.lights
597 }
598
599 pub fn light_color(&self) -> Option<&str> {
600 self.light_color.as_deref()
601 }
602
603 pub fn vibration(&self) -> bool {
604 self.vibration
605 }
606
607 pub fn importance(&self) -> Importance {
608 self.importance
609 }
610
611 pub fn visibility(&self) -> Option<Visibility> {
612 self.visibility
613 }
614 }
615
616 impl ChannelBuilder {
617 pub fn description(mut self, description: impl Into<String>) -> Self {
618 self.0.description.replace(description.into());
619 self
620 }
621
622 pub fn sound(mut self, sound: impl Into<String>) -> Self {
623 self.0.sound.replace(sound.into());
624 self
625 }
626
627 pub fn lights(mut self, lights: bool) -> Self {
628 self.0.lights = lights;
629 self
630 }
631
632 pub fn light_color(mut self, color: impl Into<String>) -> Self {
633 self.0.light_color.replace(color.into());
634 self
635 }
636
637 pub fn vibration(mut self, vibration: bool) -> Self {
638 self.0.vibration = vibration;
639 self
640 }
641
642 pub fn importance(mut self, importance: Importance) -> Self {
643 self.0.importance = importance;
644 self
645 }
646
647 pub fn visibility(mut self, visibility: Visibility) -> Self {
648 self.0.visibility.replace(visibility);
649 self
650 }
651
652 pub fn build(self) -> Channel {
653 self.0
654 }
655 }
656}