i3status_rs/blocks/
external_ip.rs1use zbus::MatchRule;
64
65use super::prelude::*;
66use crate::util::{country_flag_from_iso_code, new_system_dbus_connection};
67
68const API_ENDPOINT: &str = "https://ipapi.co/json/";
69
70#[derive(Deserialize, Debug, SmartDefault)]
71#[serde(deny_unknown_fields, default)]
72pub struct Config {
73 pub format: FormatConfig,
74 #[default(300.into())]
75 pub interval: Seconds,
76 #[default(true)]
77 pub with_network_manager: bool,
78 #[default(false)]
79 pub use_ipv4: bool,
80}
81
82pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
83 let format = config.format.with_default(" $ip $country_flag ")?;
84
85 type UpdatesStream = Pin<Box<dyn Stream<Item = ()>>>;
86 let mut stream: UpdatesStream = if config.with_network_manager {
87 let dbus = new_system_dbus_connection().await?;
88 let proxy = zbus::fdo::DBusProxy::new(&dbus)
89 .await
90 .error("Failed to create DBusProxy")?;
91 proxy
92 .add_match_rule(
93 MatchRule::builder()
94 .msg_type(zbus::message::Type::Signal)
95 .path("/org/freedesktop/NetworkManager")
96 .and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
97 .and_then(|x| x.member("PropertiesChanged"))
98 .unwrap()
99 .build(),
100 )
101 .await
102 .error("Failed to add match")?;
103 proxy
104 .add_match_rule(
105 MatchRule::builder()
106 .msg_type(zbus::message::Type::Signal)
107 .path_namespace("/org/freedesktop/NetworkManager/ActiveConnection")
108 .and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
109 .and_then(|x| x.member("PropertiesChanged"))
110 .unwrap()
111 .build(),
112 )
113 .await
114 .error("Failed to add match")?;
115 proxy
116 .add_match_rule(
117 MatchRule::builder()
118 .msg_type(zbus::message::Type::Signal)
119 .path_namespace("/org/freedesktop/NetworkManager/IP4Config")
120 .and_then(|x| x.interface("org.freedesktop.DBus.Properties"))
121 .and_then(|x| x.member("PropertiesChanged"))
122 .unwrap()
123 .build(),
124 )
125 .await
126 .error("Failed to add match")?;
127 let stream: zbus::MessageStream = dbus.into();
128 Box::pin(stream.map(|_| ()))
129 } else {
130 Box::pin(futures::stream::empty())
131 };
132
133 let client = if config.use_ipv4 {
134 &REQWEST_CLIENT_IPV4
135 } else {
136 &REQWEST_CLIENT
137 };
138
139 loop {
140 let fetch_info = || IPAddressInfo::new(client);
141 let info = fetch_info.retry(ExponentialBuilder::default()).await?;
142
143 let mut values = map! {
144 "ip" => Value::text(info.ip),
145 "version" => Value::text(info.version),
146 "city" => Value::text(info.city),
147 "region" => Value::text(info.region),
148 "region_code" => Value::text(info.region_code),
149 "country" => Value::text(info.country),
150 "country_name" => Value::text(info.country_name),
151 "country_flag" => Value::text(country_flag_from_iso_code(&info.country_code)),
152 "country_code" => Value::text(info.country_code),
153 "country_code_iso3" => Value::text(info.country_code_iso3),
154 "country_capital" => Value::text(info.country_capital),
155 "country_tld" => Value::text(info.country_tld),
156 "continent_code" => Value::text(info.continent_code),
157 "latitude" => Value::number(info.latitude),
158 "longitude" => Value::number(info.longitude),
159 "timezone" => Value::text(info.timezone),
160 "utc_offset" => Value::text(info.utc_offset),
161 "country_calling_code" => Value::text(info.country_calling_code),
162 "currency" => Value::text(info.currency),
163 "currency_name" => Value::text(info.currency_name),
164 "languages" => Value::text(info.languages),
165 "country_area" => Value::number(info.country_area),
166 "country_population" => Value::number(info.country_population),
167 "asn" => Value::text(info.asn),
168 "org" => Value::text(info.org),
169 };
170 info.postal
171 .map(|x| values.insert("postal".into(), Value::text(x)));
172 if info.in_eu {
173 values.insert("in_eu".into(), Value::flag());
174 }
175
176 let mut widget = Widget::new().with_format(format.clone());
177 widget.set_values(values);
178 api.set_widget(widget)?;
179
180 select! {
181 _ = sleep(config.interval.0) => (),
182 _ = api.wait_for_update_request() => (),
183 _ = stream.next_debounced() => ()
184 }
185 }
186}
187
188#[derive(Deserialize, Default)]
189#[serde(default)]
190struct IPAddressInfo {
191 error: bool,
192 reason: String,
193 ip: String,
194 version: String,
195 city: String,
196 region: String,
197 region_code: String,
198 country: String,
199 country_name: String,
200 country_code: String,
201 country_code_iso3: String,
202 country_capital: String,
203 country_tld: String,
204 continent_code: String,
205 in_eu: bool,
206 postal: Option<String>,
207 latitude: f64,
208 longitude: f64,
209 timezone: String,
210 utc_offset: String,
211 country_calling_code: String,
212 currency: String,
213 currency_name: String,
214 languages: String,
215 country_area: f64,
216 country_population: f64,
217 asn: String,
218 org: String,
219}
220
221impl IPAddressInfo {
222 async fn new(client: &reqwest::Client) -> Result<Self> {
223 let info: Self = client
224 .get(API_ENDPOINT)
225 .send()
226 .await
227 .error("Failed to request current location")?
228 .json::<Self>()
229 .await
230 .error("Failed to parse JSON")?;
231 if info.error {
232 Err(Error::new(info.reason))
233 } else {
234 Ok(info)
235 }
236 }
237}