i3status_rs/blocks/github.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
//! The number of GitHub notifications
//!
//! This block shows the unread notification count for a GitHub account. A GitHub [personal access token](https://github.com/settings/tokens/new) with the "notifications" scope is required, and must be passed using the `I3RS_GITHUB_TOKEN` environment variable or `token` configuration option. Optionally the colour of the block is determined by the highest notification in the following lists from highest to lowest: `critical`,`warning`,`info`,`good`
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $total.eng(w:1) "`
//! `interval` | Update interval in seconds | `30`
//! `token` | A GitHub personal access token with the "notifications" scope | `None`
//! `hide_if_total_is_zero` | Hide this block if the total count of notifications is zero | `false`
//! `critical` | List of notification types that change the block to the critical colour | `None`
//! `warning` | List of notification types that change the block to the warning colour | `None`
//! `info` | List of notification types that change the block to the info colour | `None`
//! `good` | List of notification types that change the block to the good colour | `None`
//!
//!
//! All the placeholders are numbers without a unit.
//!
//! Placeholder | Value
//! -------------------|------
//! `icon` | A static icon
//! `total` | The total number of notifications
//! `assign` | You were assigned to the issue
//! `author` | You created the thread
//! `comment` | You commented on the thread
//! `ci_activity` | A GitHub Actions workflow run that you triggered was completed
//! `invitation` | You accepted an invitation to contribute to the repository
//! `manual` | You subscribed to the thread (via an issue or pull request)
//! `mention` | You were specifically @mentioned in the content
//! `review_requested` | You, or a team you're a member of, were requested to review a pull request
//! `security_alert` | GitHub discovered a security vulnerability in your repository
//! `state_change` | You changed the thread state (for example, closing an issue or merging a pull request)
//! `subscribed` | You're watching the repository
//! `team_mention` | You were on a team that was mentioned
//!
//! # Examples
//!
//! ```toml
//! [[block]]
//! block = "github"
//! format = " $icon $total.eng(w:1)|$mention.eng(w:1) "
//! interval = 60
//! token = "..."
//! ```
//!
//! ```toml
//! [[block]]
//! block = "github"
//! token = "..."
//! format = " $icon $total.eng(w:1) "
//! info = ["total"]
//! warning = ["mention","review_requested"]
//! hide_if_total_is_zero = true
//! ```
//!
//! # Icons Used
//! - `github`
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 },
}
// https://docs.github.com/en/rest/reference/activity#notifications
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}"))),
}
}