use crate::offtime::{Off, OffDays};
use crate::utils::parse_from_hmstr;
use ::structopt::clap::AppSettings;
use anyhow;
use anyhow::{bail, Context, Result};
use chrono::Local;
use directories_next::ProjectDirs;
use figment::{
providers::{Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use structopt;
use tracing::{debug, info, warn};
#[derive(Debug, PartialEq)]
pub struct WifiStatusConfig {
pub wifi_string: String,
pub emoji: String,
pub text: String,
}
impl std::str::FromStr for WifiStatusConfig {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let splitted: Vec<&str> = s.split("::").collect();
if splitted.len() != 3 {
bail!(
"Expect status argument to contain two and only two :: separator (in '{}')",
&s
);
}
Ok(WifiStatusConfig {
wifi_string: splitted[0].to_owned(),
emoji: splitted[1].to_owned(),
text: splitted[2].to_owned(),
})
}
}
#[derive(structopt::StructOpt, Debug, Clone)]
pub struct QuietVerbose {
#[structopt(
name = "quietverbose",
long = "verbose",
short = "v",
parse(from_occurrences),
conflicts_with = "quietquiet",
global = true
)]
verbosity_level: u8,
#[structopt(
name = "quietquiet",
long = "quiet",
short = "q",
parse(from_occurrences),
conflicts_with = "quietverbose",
global = true
)]
quiet_level: u8,
}
impl Default for QuietVerbose {
fn default() -> Self {
QuietVerbose {
verbosity_level: 1,
quiet_level: 0,
}
}
}
impl Serialize for QuietVerbose {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.get_level_filter())
}
}
fn de_from_str<'de, D>(deserializer: D) -> Result<QuietVerbose, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_ascii_lowercase().as_ref() {
"off" => Ok(QuietVerbose {
verbosity_level: 0,
quiet_level: 2,
}),
"error" => Ok(QuietVerbose {
verbosity_level: 0,
quiet_level: 1,
}),
"warn" => Ok(QuietVerbose {
verbosity_level: 0,
quiet_level: 0,
}),
"info" => Ok(QuietVerbose {
verbosity_level: 1,
quiet_level: 0,
}),
"debug" => Ok(QuietVerbose {
verbosity_level: 2,
quiet_level: 0,
}),
_ => Ok(QuietVerbose {
verbosity_level: 3,
quiet_level: 0,
}),
}
}
impl QuietVerbose {
pub fn get_level_filter(&self) -> &str {
let quiet: i8 = if self.quiet_level > 1 {
2
} else {
self.quiet_level as i8
};
let verbose: i8 = if self.verbosity_level > 2 {
3
} else {
self.verbosity_level as i8
};
match verbose - quiet {
-2 => "Off",
-1 => "Error",
0 => "Warn",
1 => "Info",
2 => "Debug",
_ => "Trace",
}
}
}
#[derive(structopt::StructOpt, Serialize, Deserialize, Debug)]
#[structopt(global_settings(&[AppSettings::ColoredHelp, AppSettings::ColorAuto]))]
pub struct Args {
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(short, long, env)]
pub interface_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[structopt(short, long)]
pub status: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(short = "u", long, env)]
pub mm_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env)]
pub keyring_user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env)]
pub keyring_service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env, hide_env_values = true)]
pub mm_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env)]
pub mm_token_cmd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env, parse(from_os_str))]
pub state_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(short, long, env)]
pub begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(short, long, env)]
pub end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env)]
pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[structopt(long, env)]
pub delay: Option<u8>,
#[allow(missing_docs)]
#[structopt(flatten)]
#[serde(deserialize_with = "de_from_str")]
pub verbose: QuietVerbose,
#[structopt(skip)]
pub offdays: OffDays,
}
impl Default for Args {
fn default() -> Args {
let res = Args {
#[cfg(target_os = "linux")]
interface_name: Some("wlan0".into()),
#[cfg(target_os = "windows")]
interface_name: Some("Wireless Network Connection".into()),
#[cfg(target_os = "macos")]
interface_name: Some("en0".into()),
status: ["home::house::working at home".to_string()].to_vec(),
delay: Some(60),
state_dir: Some(
ProjectDirs::from("net", "clabaut", "automattermostatus")
.expect("Unable to find a project dir")
.cache_dir()
.to_owned(),
),
keyring_user: None,
keyring_service: None,
mm_token: None,
mm_token_cmd: None,
mm_url: Some("https://mattermost.example.com".into()),
verbose: QuietVerbose {
verbosity_level: 1,
quiet_level: 0,
},
expires_at: Some("19:30".to_string()),
begin: Some("8:00".to_string()),
end: Some("19:30".to_string()),
offdays: OffDays::default(),
};
res
}
}
impl Off for Args {
fn is_off_time(&self) -> bool {
self.offdays.is_off_time() || if let Some(begin) = parse_from_hmstr(&self.begin) {
Local::now() < begin } else {
false }
|| if let Some(end) = parse_from_hmstr(&self.end) {
Local::now() > end } else {
false }
}
}
impl Args {
pub fn update_token_with_keyring(mut self) -> Result<Self> {
if let Some(user) = &self.keyring_user {
if let Some(service) = &self.keyring_service {
let keyring = keyring::Keyring::new(service, user);
let token = keyring.get_password().with_context(|| {
format!("Querying OS keyring (user: {}, service: {})", user, service)
})?;
self.mm_token = Some(token);
} else {
warn!("User is defined for keyring lookup but service is not");
info!("Skipping keyring lookup");
}
}
Ok(self)
}
pub fn update_token_with_command(mut self) -> Result<Args> {
if let Some(command) = &self.mm_token_cmd {
let params =
shell_words::split(command).context("Splitting mm_token_cmd into shell words")?;
debug!("Running command {}", command);
let output = Command::new(¶ms[0])
.args(¶ms[1..])
.output()
.context(format!("Error when running {}", &command))?;
let token = String::from_utf8_lossy(&output.stdout);
if token.len() == 0 {
bail!("command '{}' returns nothing", &command);
}
self.mm_token = Some(token.to_string());
}
Ok(self)
}
pub fn merge_config_and_params(&self) -> Result<Args> {
let default_args = Args::default();
debug!("default Args : {:#?}", default_args);
let conf_dir = ProjectDirs::from("net", "clabaut", "automattermostatus")
.expect("Unable to find a project dir")
.config_dir()
.to_owned();
fs::create_dir_all(&conf_dir)
.with_context(|| format!("Creating conf dir {:?}", &conf_dir))?;
let conf_file = conf_dir.join("automattermostatus.toml");
if !conf_file.exists() {
info!("Write {:?} default config file", &conf_file);
fs::write(&conf_file, toml::to_string(&Args::default())?)
.unwrap_or_else(|_| panic!("Unable to write default config file {:?}", conf_file));
}
let config_args: Args = Figment::from(Toml::file(&conf_file)).extract()?;
debug!("config Args : {:#?}", config_args);
debug!("parameter Args : {:#?}", self);
let res = Figment::from(Serialized::defaults(Args::default()))
.merge(Toml::file(&conf_file))
.merge(Serialized::defaults(self))
.extract()
.context("Merging configuration file and parameters")?;
debug!("Merged config and parameters : {:#?}", res);
Ok(res)
}
}