1use std::{
2 fs::File,
3 io::Read,
4 pin::Pin,
5 rc::Rc,
6 sync::{Arc, Mutex},
7 task::Poll,
8 time::Duration,
9};
10
11use anyhow::Result;
12use async_trait::async_trait;
13use derive_builder::Builder;
14use futures::{task::AtomicWaker, FutureExt, StreamExt};
15use lazy_static::lazy_static;
16use lazybar_types::EventResponse;
17use regex::Regex;
18use reqwest::{
19 header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT},
20 Client,
21};
22use serde::Deserialize;
23use tokio::{
24 task::{self, JoinHandle},
25 time::{interval, Interval},
26};
27use tokio_stream::Stream;
28
29use crate::{
30 attrs::Attrs,
31 bar::{Event, PanelDrawInfo},
32 common::{PanelCommon, ShowHide},
33 ipc::ChannelEndpoint,
34 remove_array_from_config, remove_bool_from_config,
35 remove_string_from_config, remove_uint_from_config, Highlight, PanelConfig,
36 PanelStream,
37};
38
39lazy_static! {
40 static ref REGEX: Regex =
41 Regex::new(r#"<(?<url>\S*)>; rel="next""#).unwrap();
42}
43
44#[derive(Debug, Clone, Builder)]
46#[builder_struct_attr(allow(missing_docs))]
47#[builder_impl_attr(allow(missing_docs))]
48pub struct Github {
49 name: &'static str,
50 #[builder(default = "Duration::from_secs(60)")]
51 interval: Duration,
52 #[builder(default)]
53 waker: Arc<AtomicWaker>,
54 token: String,
55 #[builder(default = "Vec::new()")]
56 filter: Vec<String>,
57 #[builder(default)]
58 include: bool,
59 #[builder(default = "true")]
60 show_zero: bool,
61 format: &'static str,
62 attrs: Attrs,
63 #[builder(default, setter(strip_option))]
64 highlight: Option<Highlight>,
65 common: PanelCommon,
66}
67
68impl Github {
69 fn draw(
70 &self,
71 cr: &Rc<cairo::Context>,
72 height: i32,
73 count: usize,
74 paused: Arc<Mutex<bool>>,
75 ) -> Result<PanelDrawInfo> {
76 let mut text = if !self.show_zero && count == 0 {
77 String::new()
78 } else {
79 self.format.replace("%count%", count.to_string().as_str())
80 };
81
82 if count == 50 {
83 text.push('+');
84 }
85
86 self.common.draw(
87 cr,
88 text.as_str(),
89 &self.attrs,
90 self.common.dependence,
91 self.highlight.clone(),
92 self.common.images.clone(),
93 height,
94 ShowHide::Default(paused, self.waker.clone()),
95 format!("{self:?}"),
96 )
97 }
98}
99
100#[async_trait(?Send)]
101impl PanelConfig for Github {
102 fn parse(
129 name: &'static str,
130 table: &mut std::collections::HashMap<String, config::Value>,
131 _global: &config::Config,
132 ) -> anyhow::Result<Self> {
133 let mut builder = GithubBuilder::default();
134
135 builder.name(name);
136
137 if let Some(interval) = remove_uint_from_config("interval", table) {
138 builder.interval(Duration::from_secs(interval.max(1) * 60));
139 }
140
141 if let Some(path) = remove_string_from_config("token", table) {
142 let mut token = String::new();
143 File::open(path)?.read_to_string(&mut token)?;
144
145 builder.token(token);
146 }
147
148 if let Some(filter) = remove_array_from_config("filter", table) {
149 builder.filter(
150 filter
151 .iter()
152 .filter_map(|v| v.clone().into_string().ok())
153 .collect(),
154 );
155 }
156
157 if let Some(include) = remove_bool_from_config("include", table) {
158 builder.include(include);
159 }
160
161 if let Some(show_zero) = remove_bool_from_config("show_zero", table) {
162 builder.show_zero(show_zero);
163 }
164
165 let common = PanelCommon::parse_common(table)?;
166 let format = PanelCommon::parse_format(table, "", "%count%");
167 let attrs = PanelCommon::parse_attr(table, "");
168 let highlight = PanelCommon::parse_highlight(table, "");
169
170 builder.common(common);
171 builder.format(format.leak());
172 builder.attrs(attrs);
173 builder.highlight(highlight);
174
175 Ok(builder.build()?)
176 }
177
178 fn props(&self) -> (&'static str, bool) {
179 (self.name, self.common.visible)
180 }
181
182 async fn run(
183 mut self: Box<Self>,
184 cr: Rc<cairo::Context>,
185 global_attrs: Attrs,
186 height: i32,
187 ) -> Result<(PanelStream, Option<ChannelEndpoint<Event, EventResponse>>)>
188 {
189 self.attrs.apply_to(&global_attrs);
190
191 let paused = Arc::new(Mutex::new(false));
192
193 let stream = GithubStream::new(
194 self.token.as_str(),
195 self.interval,
196 paused.clone(),
197 self.filter.clone(),
198 self.include,
199 )?
200 .map(move |r| self.draw(&cr, height, r?, paused.clone()));
201
202 Ok((Box::pin(stream), None))
203 }
204}
205
206struct GithubStream {
207 handle: Option<JoinHandle<Result<usize>>>,
208 interval: Arc<futures::lock::Mutex<Interval>>,
209 waker: Arc<AtomicWaker>,
210 paused: Arc<Mutex<bool>>,
211 filter: Vec<String>,
212 include: bool,
213 client: Client,
214}
215
216impl GithubStream {
217 pub fn new(
218 token: &str,
219 duration: Duration,
220 paused: Arc<Mutex<bool>>,
221 filter: Vec<String>,
222 include: bool,
223 ) -> Result<Self> {
224 let mut headers = HeaderMap::new();
225 headers.insert(
226 ACCEPT,
227 HeaderValue::from_static("application/vnd.github+json"),
228 );
229 headers.insert(
230 "X-Github-Api-Version",
231 HeaderValue::from_static("2022-11-28"),
232 );
233 headers.insert(USER_AGENT, HeaderValue::from_static("lazybar"));
234 let mut secret =
235 HeaderValue::from_str(format!("Bearer {}", token.trim()).as_str())?;
236 secret.set_sensitive(true);
237 headers.insert(AUTHORIZATION, secret);
238 let client = Client::builder().default_headers(headers).build()?;
239 let interval = Arc::new(futures::lock::Mutex::new(interval(duration)));
240 let waker = Arc::new(AtomicWaker::new());
241 Ok(Self {
242 handle: None,
243 interval,
244 waker,
245 paused,
246 filter,
247 include,
248 client,
249 })
250 }
251}
252
253impl Stream for GithubStream {
254 type Item = Result<usize>;
255
256 fn poll_next(
257 mut self: Pin<&mut Self>,
258 cx: &mut std::task::Context<'_>,
259 ) -> Poll<Option<Self::Item>> {
260 self.waker.register(cx.waker());
261 if *self.paused.lock().unwrap() {
262 Poll::Pending
263 } else if let Some(handle) = &mut self.handle {
264 let val = handle.poll_unpin(cx).map(Result::ok);
265
266 if val.is_ready() {
267 self.handle = None;
268 }
269
270 val
271 } else {
272 let interval = self.interval.clone();
273 let filter = self.filter.clone();
274 let include = self.include;
275 let client = self.client.clone();
276 self.handle = Some(task::spawn(get_notifications(
277 interval, filter, include, client,
278 )));
279
280 Poll::Pending
281 }
282 }
283}
284
285async fn get_notifications(
286 interval: Arc<futures::lock::Mutex<Interval>>,
287 filter: Vec<String>,
288 include: bool,
289 client: Client,
290) -> Result<usize> {
291 interval.lock().await.tick().await;
292
293 let request = client.get("https://api.github.com/notifications").build()?;
294
295 let response = client.execute(request).await?;
296
297 let headers = response.headers().clone();
298 let wait = headers
299 .get("X-Poll-Interval")
300 .and_then(|v| v.to_str().ok())
301 .and_then(|s| s.parse().ok())
302 .unwrap_or(60);
303
304 interval.lock().await.reset_after(Duration::from_secs(wait));
305
306 let body = response.json::<Vec<Thread>>().await?;
307
308 let count = body
309 .into_iter()
310 .filter(|t| !(include ^ filter.contains(&t.reason)))
311 .count();
312
313 Ok(count)
314}
315
316#[derive(Deserialize, Debug)]
317#[non_exhaustive]
318struct Thread {
319 reason: String,
320}