1#![allow(missing_docs)]
2use crate::offtime::{Off, OffDays};
5use crate::utils::parse_from_hmstr;
6use ::structopt::clap::AppSettings;
7use anyhow::{bail, Context, Result};
8use chrono::Local;
9use directories_next::ProjectDirs;
10use figment::{
11 providers::{Format, Serialized, Toml},
12 Figment,
13};
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::fs;
16use std::path::PathBuf;
17use std::process::Command;
18use structopt;
19use structopt::clap::arg_enum;
20use tracing::{debug, info, warn};
21
22arg_enum! {
23#[derive(Serialize, Deserialize,Debug)]
29pub enum SecretType {
30 Token,
31 Password,
32}
33}
34
35#[derive(Debug, PartialEq)]
37pub struct WifiStatusConfig {
38 pub wifi_string: String,
40 pub emoji: String,
43 pub text: String,
45}
46
47impl std::str::FromStr for WifiStatusConfig {
58 type Err = anyhow::Error;
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 let splitted: Vec<&str> = s.split("::").collect();
61 if splitted.len() != 3 {
62 bail!(
63 "Expect status argument to contain two and only two :: separator (in '{}')",
64 &s
65 );
66 }
67 Ok(WifiStatusConfig {
68 wifi_string: splitted[0].to_owned(),
69 emoji: splitted[1].to_owned(),
70 text: splitted[2].to_owned(),
71 })
72 }
73}
74
75#[derive(structopt::StructOpt, Debug, Clone)]
78pub struct QuietVerbose {
79 #[structopt(
83 name = "quietverbose",
84 long = "verbose",
85 short = "v",
86 parse(from_occurrences),
87 conflicts_with = "quietquiet",
88 global = true
89 )]
90 verbosity_level: u8,
91
92 #[structopt(
97 name = "quietquiet",
98 long = "quiet",
99 short = "q",
100 parse(from_occurrences),
101 conflicts_with = "quietverbose",
102 global = true
103 )]
104 quiet_level: u8,
105}
106
107impl Default for QuietVerbose {
108 fn default() -> Self {
109 QuietVerbose {
110 verbosity_level: 1,
111 quiet_level: 0,
112 }
113 }
114}
115
116impl Serialize for QuietVerbose {
117 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118 where
119 S: Serializer,
120 {
121 serializer.serialize_str(self.get_level_filter())
122 }
123}
124
125fn de_from_str<'de, D>(deserializer: D) -> Result<QuietVerbose, D::Error>
126where
127 D: Deserializer<'de>,
128{
129 let s = String::deserialize(deserializer)?;
130 match s.to_ascii_lowercase().as_ref() {
131 "off" => Ok(QuietVerbose {
132 verbosity_level: 0,
133 quiet_level: 2,
134 }),
135 "error" => Ok(QuietVerbose {
136 verbosity_level: 0,
137 quiet_level: 1,
138 }),
139 "warn" => Ok(QuietVerbose {
140 verbosity_level: 0,
141 quiet_level: 0,
142 }),
143 "info" => Ok(QuietVerbose {
144 verbosity_level: 1,
145 quiet_level: 0,
146 }),
147 "debug" => Ok(QuietVerbose {
148 verbosity_level: 2,
149 quiet_level: 0,
150 }),
151 _ => Ok(QuietVerbose {
152 verbosity_level: 3,
153 quiet_level: 0,
154 }),
155 }
156}
157
158impl QuietVerbose {
159 pub fn get_level_filter(&self) -> &str {
161 let quiet: i8 = if self.quiet_level > 1 {
162 2
163 } else {
164 self.quiet_level as i8
165 };
166 let verbose: i8 = if self.verbosity_level > 2 {
167 3
168 } else {
169 self.verbosity_level as i8
170 };
171 match verbose - quiet {
172 -2 => "Off",
173 -1 => "Error",
174 0 => "Warn",
175 1 => "Info",
176 2 => "Debug",
177 _ => "Trace",
178 }
179 }
180}
181
182#[derive(structopt::StructOpt, Serialize, Deserialize, Debug)]
183#[structopt(global_settings(&[AppSettings::ColoredHelp, AppSettings::ColorAuto]))]
190pub struct Args {
191 #[serde(skip_serializing_if = "Option::is_none")]
193 #[structopt(short, long, env, name = "itf_name")]
194 pub interface_name: Option<String>,
195
196 #[serde(skip_serializing_if = "Vec::is_empty")]
202 #[structopt(short, long, name = "wifi_substr::emoji::text")]
203 pub status: Vec<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 #[structopt(short = "u", long, env, name = "url")]
208 pub mm_url: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 #[structopt(long, env, name = "username")]
213 pub mm_user: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 #[structopt(short = "t", long, env, possible_values = &SecretType::variants(), case_insensitive = true)]
218 pub secret_type: Option<SecretType>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
225 #[structopt(long, env, name = "token service name")]
226 pub keyring_service: Option<String>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
236 #[structopt(long, env, hide_env_values = true, name = "token")]
237 pub mm_secret: Option<String>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
244 #[structopt(long, env, name = "command")]
245 pub mm_secret_cmd: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
251 #[structopt(long, env, parse(from_os_str), name = "cache dir")]
252 pub state_dir: Option<PathBuf>,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
258 #[structopt(short, long, env, name = "begin hh:mm")]
259 pub begin: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
265 #[structopt(short, long, env, name = "end hh:mm")]
266 pub end: Option<String>,
267
268 #[serde(skip_serializing_if = "Option::is_none")]
273 #[structopt(long, env, name = "expiry hh:mm")]
274 pub expires_at: Option<String>,
275
276 #[serde(skip_serializing_if = "Option::is_none")]
278 #[structopt(long, env)]
279 pub delay: Option<u32>,
280
281 #[allow(missing_docs)]
282 #[structopt(flatten)]
283 #[serde(deserialize_with = "de_from_str")]
284 pub verbose: QuietVerbose,
285
286 #[structopt(skip)]
287 pub offdays: OffDays,
289}
290
291impl Default for Args {
292 fn default() -> Args {
293 let res = Args {
294 #[cfg(target_os = "linux")]
295 interface_name: Some("wlan0".into()),
296 #[cfg(target_os = "windows")]
297 interface_name: Some("Wireless Network Connection".into()),
298 #[cfg(target_os = "macos")]
299 interface_name: Some("en0".into()),
300 status: ["home::house::working at home".to_string()].to_vec(),
301 delay: Some(60),
302 state_dir: Some(
303 ProjectDirs::from("net", "ams", "automattermostatus")
304 .expect("Unable to find a project dir")
305 .cache_dir()
306 .to_owned(),
307 ),
308 mm_user: None,
309 keyring_service: None,
310 mm_secret: None,
311 mm_secret_cmd: None,
312 secret_type: Some(SecretType::Password),
313 mm_url: Some("https://mattermost.example.com".into()),
314 verbose: QuietVerbose {
315 verbosity_level: 1,
316 quiet_level: 0,
317 },
318 expires_at: Some("19:30".to_string()),
319 begin: Some("8:00".to_string()),
320 end: Some("19:30".to_string()),
321 offdays: OffDays::default(),
322 };
323 res
324 }
325}
326
327impl Off for Args {
328 fn is_off_time(&self) -> bool {
329 self.offdays.is_off_time() || if let Some(begin) = parse_from_hmstr(&self.begin) {
331 Local::now() < begin } else {
333 false }
335 || if let Some(end) = parse_from_hmstr(&self.end) {
336 Local::now() > end } else {
338 false }
340 }
341}
342
343impl Args {
344 pub fn update_secret_with_keyring(mut self) -> Result<Self> {
347 if let Some(user) = &self.mm_user {
348 if let Some(service) = &self.keyring_service {
349 let keyring = keyring::Keyring::new(service, user);
350 let secret = keyring.get_password().with_context(|| {
351 format!("Querying OS keyring (user: {}, service: {})", user, service)
352 })?;
353 self.mm_secret = Some(secret);
354 } else {
355 warn!("User is defined for keyring lookup but service is not");
356 info!("Skipping keyring lookup");
357 }
358 }
359 Ok(self)
360 }
361
362 pub fn update_secret_with_command(mut self) -> Result<Args> {
368 if let Some(command) = &self.mm_secret_cmd {
369 let params =
370 shell_words::split(command).context("Splitting mm_token_cmd into shell words")?;
371 debug!("Running command {}", command);
372 let output = Command::new(¶ms[0])
373 .args(¶ms[1..])
374 .output()
375 .context(format!("Error when running {}", &command))?;
376 let secret = String::from_utf8_lossy(&output.stdout);
377 if secret.len() == 0 {
378 bail!("command '{}' returns nothing", &command);
379 }
380 self.mm_secret = Some(secret.to_string());
383 }
384 Ok(self)
385 }
386
387 pub fn merge_config_and_params(&self) -> Result<Args> {
389 let default_args = Args::default();
390 debug!("default Args : {:#?}", default_args);
391 let conf_dir = ProjectDirs::from("net", "ams", "automattermostatus")
392 .expect("Unable to find a project dir")
393 .config_dir()
394 .to_owned();
395 fs::create_dir_all(&conf_dir)
396 .with_context(|| format!("Creating conf dir {:?}", &conf_dir))?;
397 let conf_file = conf_dir.join("automattermostatus.toml");
398 if !conf_file.exists() {
399 info!("Write {:?} default config file", &conf_file);
400 fs::write(&conf_file, toml::to_string(&Args::default())?)
401 .unwrap_or_else(|_| panic!("Unable to write default config file {:?}", conf_file));
402 }
403
404 let config_args: Args = Figment::from(Toml::file(&conf_file)).extract()?;
405 debug!("config Args : {:#?}", config_args);
406 debug!("parameter Args : {:#?}", self);
407 let res = Figment::from(Serialized::defaults(Args::default()))
409 .merge(Toml::file(&conf_file))
410 .merge(Serialized::defaults(self))
411 .extract()
412 .context("Merging configuration file and parameters")?;
413 debug!("Merged config and parameters : {:#?}", res);
414 Ok(res)
415 }
416}