use super::prelude::*;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
#[default(60.into())]
pub interval: Seconds,
pub format: FormatConfig,
pub token: Option<String>,
pub hide_if_total_is_zero: bool,
pub good: Option<Vec<String>>,
pub info: Option<Vec<String>>,
pub warning: Option<Vec<String>>,
pub critical: Option<Vec<String>>,
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let format = config.format.with_default(" $icon $total.eng(w:1) ")?;
let mut interval = config.interval.timer();
let token = config
.token
.clone()
.or_else(|| std::env::var("I3RS_GITHUB_TOKEN").ok())
.error("Github token not found")?;
loop {
let stats = get_stats(&token).await?;
if stats.get("total").is_some_and(|x| *x > 0) || !config.hide_if_total_is_zero {
let mut widget = Widget::new().with_format(format.clone());
'outer: for (list_opt, ret) in [
(&config.critical, State::Critical),
(&config.warning, State::Warning),
(&config.info, State::Info),
(&config.good, State::Good),
] {
if let Some(list) = list_opt {
for val in list {
if stats.get(val).is_some_and(|x| *x > 0) {
widget.state = ret;
break 'outer;
}
}
}
}
let mut values: HashMap<_, _> = stats
.into_iter()
.map(|(k, v)| (k.into(), Value::number(v)))
.collect();
values.insert("icon".into(), Value::icon("github"));
widget.set_values(values);
api.set_widget(widget)?;
} else {
api.hide()?;
}
select! {
_ = interval.tick() => (),
_ = api.wait_for_update_request() => (),
}
}
}
#[derive(Deserialize, Debug)]
struct Notification {
reason: String,
}
async fn get_stats(token: &str) -> Result<HashMap<String, usize>> {
let mut stats = HashMap::new();
let mut total = 0;
for page in 1..100 {
let fetch = || get_on_page(token, page);
let on_page = fetch.retry(ExponentialBuilder::default()).await?;
if on_page.is_empty() {
break;
}
total += on_page.len();
for n in on_page {
stats.entry(n.reason).and_modify(|x| *x += 1).or_insert(1);
}
}
stats.insert("total".into(), total);
stats.entry("total".into()).or_insert(0);
stats.entry("assign".into()).or_insert(0);
stats.entry("author".into()).or_insert(0);
stats.entry("comment".into()).or_insert(0);
stats.entry("ci_activity".into()).or_insert(0);
stats.entry("invitation".into()).or_insert(0);
stats.entry("manual".into()).or_insert(0);
stats.entry("mention".into()).or_insert(0);
stats.entry("review_requested".into()).or_insert(0);
stats.entry("security_alert".into()).or_insert(0);
stats.entry("state_change".into()).or_insert(0);
stats.entry("subscribed".into()).or_insert(0);
stats.entry("team_mention".into()).or_insert(0);
Ok(stats)
}
async fn get_on_page(token: &str, page: usize) -> Result<Vec<Notification>> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Response {
Notifications(Vec<Notification>),
ErrorMessage { message: String },
}
let request = REQWEST_CLIENT
.get(format!(
"https://api.github.com/notifications?per_page=100&page={page}",
))
.header("Authorization", format!("token {token}"));
let response = request
.send()
.await
.error("Failed to send request")?
.json::<Response>()
.await
.error("Failed to get JSON")?;
match response {
Response::Notifications(n) => Ok(n),
Response::ErrorMessage { message } => Err(Error::new(format!("API error: {message}"))),
}
}