use zbus::MatchRule;
use super::prelude::*;
use crate::util::{country_flag_from_iso_code, new_system_dbus_connection};
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub format: FormatConfig,
#[default(300.into())]
pub interval: Seconds,
#[default(true)]
pub with_network_manager: bool,
#[default(false)]
pub use_ipv4: bool,
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let format = config.format.with_default(" $ip $country_flag ")?;
type UpdatesStream = Pin<Box<dyn Stream<Item = ()>>>;
let mut stream: UpdatesStream = if config.with_network_manager {
let dbus = new_system_dbus_connection().await?;
let proxy = zbus::fdo::DBusProxy::new(&dbus)
.await
.error("Failed to create DBusProxy")?;
proxy
.add_match_rule(
MatchRule::builder()
.msg_type(zbus::message::Type::Signal)
.path("/org/freedesktop/NetworkManager")
.and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
.and_then(|x| x.member("PropertiesChanged"))
.unwrap()
.build(),
)
.await
.error("Failed to add match")?;
proxy
.add_match_rule(
MatchRule::builder()
.msg_type(zbus::message::Type::Signal)
.path_namespace("/org/freedesktop/NetworkManager/ActiveConnection")
.and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
.and_then(|x| x.member("PropertiesChanged"))
.unwrap()
.build(),
)
.await
.error("Failed to add match")?;
proxy
.add_match_rule(
MatchRule::builder()
.msg_type(zbus::message::Type::Signal)
.path_namespace("/org/freedesktop/NetworkManager/IP4Config")
.and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
.and_then(|x| x.member("PropertiesChanged"))
.unwrap()
.build(),
)
.await
.error("Failed to add match")?;
let stream: zbus::MessageStream = dbus.into();
Box::pin(stream.map(|_| ()))
} else {
Box::pin(futures::stream::empty())
};
let client = if config.use_ipv4 {
&REQWEST_CLIENT_IPV4
} else {
&REQWEST_CLIENT
};
loop {
let fetch_info = || api.find_ip_location(client, Duration::from_secs(0));
let info = fetch_info.retry(ExponentialBuilder::default()).await?;
let mut values = map! {
"ip" => Value::text(info.ip),
"city" => Value::text(info.city),
"latitude" => Value::number(info.latitude),
"longitude" => Value::number(info.longitude),
};
macro_rules! map_push_if_some { ($($key:ident: $type:ident),* $(,)?) => {
$({
let key = stringify!($key);
if let Some(value) = info.$key {
values.insert(key.into(), Value::$type(value));
} else if format.contains_key(key) {
return Err(Error::new(format!(
"The format string contains '{key}', but the {key} field is not provided by {} (an api key may be required)",
api.locator_name()
)));
}
})*
} }
map_push_if_some!(
version: text,
region: text,
region_code: text,
country: text,
country_name: text,
country_code_iso3: text,
country_capital: text,
country_tld: text,
continent_code: text,
postal: text,
timezone: text,
utc_offset: text,
country_calling_code: text,
currency: text,
currency_name: text,
languages: text,
country_area: number,
country_population: number,
asn: text,
org: text,
);
if let Some(country_code) = info.country_code {
values.insert(
"country_flag".into(),
Value::text(country_flag_from_iso_code(&country_code)),
);
values.insert("country_code".into(), Value::text(country_code));
} else if format.contains_key("country_code") || format.contains_key("country_flag") {
return Err(Error::new(format!(
"The format string contains 'country_code' or 'country_flag', but the country_code field is not provided by {}",
api.locator_name()
)));
}
if let Some(in_eu) = info.in_eu {
if in_eu {
values.insert("in_eu".into(), Value::flag());
}
} else if format.contains_key("in_eu") {
return Err(Error::new(format!(
"The format string contains 'in_eu', but the in_eu field is not provided by {}",
api.locator_name()
)));
}
let mut widget = Widget::new().with_format(format.clone());
widget.set_values(values);
api.set_widget(widget)?;
select! {
_ = sleep(config.interval.0) => (),
_ = api.wait_for_update_request() => (),
_ = stream.next_debounced() => ()
}
}
}