1use std::path::PathBuf;
2
3use anyhow::{anyhow, Result};
4use apple_apns::{Endpoint, InterruptionLevel, Priority, PushType};
5use clap::{ArgGroup, Parser};
6use humantime::parse_duration;
7use time::{format_description::well_known::Iso8601, OffsetDateTime};
8use uuid::Uuid;
9
10#[derive(Parser)]
12#[command(author, version, about)]
13#[command(group(
14 ArgGroup::new("authentication")
15 .args(["client_pem_file", "key_pem_file"])
16 .required(true)
17), group(
18 ArgGroup::new("certificate")
19 .conflicts_with("token")
20 .arg("client_pem_file")
21 .requires("client_pem_file")
22), group(
23 ArgGroup::new("token")
24 .conflicts_with("certificate")
25 .args(["key_id", "key_pem_file", "team_id"])
26 .requires_all(["key_id", "key_pem_file", "team_id"])
27 .multiple(true)
28))]
29pub struct Cli {
30 #[arg(long, env)]
31 pub ca_pem_file: Option<PathBuf>,
32
33 #[arg(long, env)]
34 pub client_pem_file: Option<PathBuf>,
35
36 #[arg(long, env)]
37 pub key_id: Option<String>,
38
39 #[arg(long, env)]
40 pub key_pem_file: Option<String>,
41
42 #[arg(long, env)]
43 pub team_id: Option<String>,
44
45 #[arg(long, env)]
46 pub endpoint: Option<Endpoint>,
47
48 #[arg(long, env)]
49 pub user_agent: Option<String>,
50
51 #[arg(long, env)]
53 pub device_token: String,
54
55 #[arg(long, env, default_value_t = PushType::Alert)]
57 pub push_type: PushType,
58
59 #[arg(long, env)]
61 pub id: Option<Uuid>,
62
63 #[arg(long, env, value_parser = parse_timestamp)]
65 pub expiration: Option<OffsetDateTime>,
66
67 #[arg(long, env, default_value_t = Default::default())]
69 pub priority: Priority,
70
71 #[arg(long, env)]
73 pub topic: Option<String>,
74
75 #[arg(long, env)]
78 pub collapse_id: Option<String>,
79
80 #[arg(long, env)]
82 pub title: Option<String>,
83
84 #[arg(long, env)]
87 pub subtitle: Option<String>,
88
89 #[arg(long, env)]
91 pub body: Option<String>,
92
93 #[arg(long, env)]
95 pub launch_image: Option<String>,
96
97 #[arg(long, env)]
100 pub badge: Option<u32>,
101
102 #[arg(long, env)]
105 pub sound: Option<String>,
106
107 #[arg(long, env)]
110 pub volume: Option<f64>,
111
112 #[arg(long, env)]
114 pub thread_id: Option<String>,
115
116 #[arg(long, env)]
118 pub category: Option<String>,
119
120 #[arg(long, env, default_value_t = false)]
122 pub content_available: bool,
123
124 #[arg(long, env, default_value_t = false)]
126 pub mutable_content: bool,
127
128 #[arg(long, env)]
130 pub target_content_id: Option<String>,
131
132 #[arg(long, env)]
134 pub interruption_level: Option<InterruptionLevel>,
135
136 #[arg(long, env)]
138 pub relevance_score: Option<f64>,
139
140 #[arg(long, env)]
142 pub user_info: Option<serde_json::Value>,
143}
144
145fn parse_timestamp(arg: &str) -> Result<OffsetDateTime> {
146 match OffsetDateTime::parse(arg, &Iso8601::DEFAULT) {
147 Ok(timestamp) => Ok(timestamp),
148 Err(duration_err) => match parse_duration(arg) {
149 Ok(duration) => Ok(OffsetDateTime::now_utc() + duration),
150 Err(timestamp_err) => Err(anyhow!("Invalid expiration; invalid timestamp: {timestamp_err}; invalid duration: {duration_err}")),
151 },
152 }
153}