From 775936c7731b6a8cae4f0a2d4628d1d03e341e24 Mon Sep 17 00:00:00 2001 From: Marek Counts Date: Sat, 8 Apr 2023 14:31:49 -0400 Subject: [PATCH] added poll org events (#325) --- examples/poll_org_events.rs | 41 ++++++++++++++++ src/api/orgs.rs | 33 +++++++++++++ src/api/orgs/events.rs | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 examples/poll_org_events.rs create mode 100644 src/api/orgs/events.rs diff --git a/examples/poll_org_events.rs b/examples/poll_org_events.rs new file mode 100644 index 00000000..6df373d9 --- /dev/null +++ b/examples/poll_org_events.rs @@ -0,0 +1,41 @@ +use octocrab::{etag::Etagged, models::events::Event, Page}; +use std::collections::VecDeque; + +const DELAY_MS: u64 = 500; +const TRACKING_CAPACITY: usize = 20; + +#[tokio::main] +async fn main() -> octocrab::Result<()> { + let mut etag = None; + let mut seen = VecDeque::with_capacity(TRACKING_CAPACITY); + let octo = octocrab::instance(); + loop { + let response: Etagged> = octo + .orgs("nixos") + .events() + .etag(etag) + .per_page(10) + .send() + .await?; + if let Some(page) = response.value { + for event in page { + // If an etag changes and we get a new page, this page may contain events we have + // already seen along with new events. So, keep track of the ones we have seen for + // each page, this will be at most 20 events - the current page of 10 events and + // the last page. + if !seen.contains(&event.id) { + println!( + "New event : id = {:?}, repo = {:?}, type = {:?}, time = {:?}", + event.id, event.repo.name, event.r#type, event.created_at + ); + if seen.len() == TRACKING_CAPACITY { + seen.pop_back(); + } + seen.push_front(event.id); + } + } + } + etag = response.etag; + tokio::time::sleep(tokio::time::Duration::from_millis(DELAY_MS)).await; + } +} diff --git a/src/api/orgs.rs b/src/api/orgs.rs index 6bede459..1e72656c 100644 --- a/src/api/orgs.rs +++ b/src/api/orgs.rs @@ -2,6 +2,7 @@ mod list_members; mod list_repos; +mod events; use crate::error::HttpSnafu; use crate::Octocrab; @@ -10,6 +11,7 @@ use snafu::ResultExt; pub use self::list_members::ListOrgMembersBuilder; pub use self::list_repos::ListReposBuilder; +pub use self::events::ListOrgEventsBuilder; /// A client to GitHub's organization API. /// @@ -134,6 +136,37 @@ impl<'octo> OrgHandler<'octo> { list_repos::ListReposBuilder::new(self) } + /// List events on this organization. + /// + /// Takes an optional etag which allows for efficient polling. Here is a quick example to poll a + /// organization's events. + /// ```no_run + /// # use std::convert::TryFrom; + /// # use octocrab::{models::events::Event, etag::{Etagged,EntityTag}, Page}; + /// # async fn run() -> octocrab::Result<()> { + /// let mut etag = None; + /// loop { + /// let response: Etagged> = octocrab::instance() + /// .orgs("owner") + /// .events() + /// .etag(etag) + /// .send() + /// .await?; + /// if let Some(page) = response.value { + /// // do something with the page ... + /// } else { + /// println!("No new data received, trying again soon"); + /// } + /// etag = response.etag; + /// // add a delay before the next iteration + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn events(&self) -> events::ListOrgEventsBuilder<'_, '_> { + events::ListOrgEventsBuilder::new(self) + } + /// Creates a new webhook for the specified organization. /// /// # Notes diff --git a/src/api/orgs/events.rs b/src/api/orgs/events.rs new file mode 100644 index 00000000..6729e119 --- /dev/null +++ b/src/api/orgs/events.rs @@ -0,0 +1,95 @@ +//! GitHub Organization Events +use crate::{ + etag::{EntityTag, Etagged}, + models::events, + orgs::OrgHandler, + FromResponse, Page, +}; +use http::request::Builder; +use http::{header::HeaderMap, Method, StatusCode}; + +pub struct ListOrgEventsBuilder<'octo, 'handler> { + handler: &'handler OrgHandler<'octo>, + headers: Headers, + params: Params, +} + +struct Headers { + etag: Option, +} + +#[derive(serde::Serialize)] +struct Params { + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'handler> ListOrgEventsBuilder<'octo, 'handler> { + pub(crate) fn new(handler: &'handler OrgHandler<'octo>) -> Self { + Self { + handler, + headers: Headers { etag: None }, + params: Params { + per_page: None, + page: None, + }, + } + } + + /// Etag for this request. + pub fn etag(mut self, etag: Option) -> Self { + self.headers.etag = etag; + self + } + + /// Results per page (max 100). + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.params.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. + pub fn page(mut self, page: impl Into) -> Self { + self.params.page = Some(page.into()); + self + } + + /// Sends the actual request. + pub async fn send(self) -> crate::Result>> { + let route = format!( + "/orgs/{owner}/events", + owner = self.handler.owner + ); + + let uri = self + .handler + .crab + .parameterized_uri(route, Some(&self.params))?; + + let mut headers = HeaderMap::new(); + if let Some(etag) = self.headers.etag { + EntityTag::insert_if_none_match_header(&mut headers, etag)?; + } + + let mut request = Builder::new().uri(uri).method(Method::GET); + for (key, value) in headers.iter() { + request = request.header(key, value); + } + + let request = self.handler.crab.build_request(request, None::<&()>)?; + let response = self.handler.crab.execute(request).await?; + let etag = EntityTag::extract_from_response(&response); + if response.status() == StatusCode::NOT_MODIFIED { + Ok(Etagged { etag, value: None }) + } else { + >::from_response(crate::map_github_error(response).await?) + .await + .map(|page| Etagged { + etag, + value: Some(page), + }) + } + } +}