octocrab/
lib.rs

1//! # Octocrab: A modern, extensible GitHub API client.
2//! Octocrab is an third party GitHub API client, allowing you to easily build
3//! your own GitHub integrations or bots. `octocrab` comes with two primary
4//! set of APIs for communicating with GitHub, a high level strongly typed
5//! semantic API, and a lower level HTTP API for extending behaviour.
6//!
7//! ## Semantic API
8//! The semantic API provides strong typing around GitHub's API, as well as a
9//! set of [`models`] that maps to GitHub's types. Currently the following
10//! modules are available.
11//!
12//! - [`actions`] GitHub Actions
13//! - [`activity`] GitHub Activity
14//! - [`apps`] GitHub Apps
15//! - [`checks`] GitHub Checks
16//! - [`code_scannings`] Code Scanning
17//! - [`commits`] GitHub Commits
18//! - [`current`] Information about the current user.
19//! - [`events`] GitHub Events
20//! - [`gists`] Gists
21//! - [`gitignore`] Gitignore templates
22//! - [`Octocrab::graphql`] GraphQL.
23//! - [`issues`] Issues and related items, e.g. comments, labels, etc.
24//! - [`licenses`] License Metadata.
25//! - [`markdown`] Rendering Markdown with GitHub
26//! - [`orgs`] GitHub Organisations
27//! - [`projects`] GitHub Projects
28//! - [`pulls`] Pull Requests
29//! - [`ratelimit`] Rate Limiting
30//! - [`repos`] Repositories
31//! - [`repos::forks`] Repository forks
32//! - [`repos::releases`] Repository releases
33//! - [`search`] Using GitHub's search.
34//! - [`teams`] Teams
35//! - [`users`] Users
36//!
37//! #### Getting a Pull Request
38//! ```no_run
39//! # async fn run() -> octocrab::Result<()> {
40//! // Get pull request #404 from `octocrab/repo`.
41//! let pr = octocrab::instance().pulls("octocrab", "repo").get(404).await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! All methods with multiple optional parameters are built as `Builder`
47//! structs, allowing you to easily specify parameters.
48//!
49//! #### Listing issues
50//! ```no_run
51//! # async fn run() -> octocrab::Result<()> {
52//! use octocrab::{models, params};
53//!
54//! let octocrab = octocrab::instance();
55//! // Returns the first page of all issues.
56//! let mut page = octocrab.issues("octocrab", "repo")
57//!     .list()
58//!     // Optional Parameters
59//!     .creator("octocrab")
60//!     .state(params::State::All)
61//!     .per_page(50)
62//!     .send()
63//!     .await?;
64//!
65//! // Go through every page of issues. Warning: There's no rate limiting so
66//! // be careful.
67//! let results = octocrab.all_pages::<models::issues::Issue>(page).await?;
68//!
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! ## HTTP API
74//! The typed API currently doesn't cover all of GitHub's API at this time, and
75//! even if it did GitHub is in active development and this library will
76//! likely always be somewhat behind GitHub at some points in time. However that
77//! shouldn't mean that in order to use those features that you have to now fork
78//! or replace `octocrab` with your own solution.
79//!
80//! Instead `octocrab` exposes a suite of HTTP methods allowing you to easily
81//! extend `Octocrab`'s existing behaviour. Using these HTTP methods allows you
82//! to keep using the same authentication and configuration, while having
83//! control over the request and response. There is a method for each HTTP
84//! method `get`, `post`, `patch`, `put`, `delete`, all of which accept a
85//! relative route and a optional body.
86//!
87//! ```no_run
88//! # async fn run() -> octocrab::Result<()> {
89//! let user: octocrab::models::Author = octocrab::instance()
90//!     .get("/user", None::<&()>)
91//!     .await?;
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! Each of the HTTP methods expects a body, formats the URL with the base
97//! URL, and errors if GitHub doesn't return a successful status, but this isn't
98//! always desired when working with GitHub's API, sometimes you need to check
99//! the response status or headers. As such there are companion methods `_get`,
100//! `_post`, etc. that perform no additional pre or post-processing to
101//! the request.
102//!
103//! ```no_run
104//! # use http::Uri;
105//! # async fn run() -> octocrab::Result<()> {
106//! let octocrab = octocrab::instance();
107//! let response = octocrab
108//!     ._get("https://api.github.com/organizations")
109//!     .await?;
110//!
111//! // You can also use `Uri::builder().authority("<my custom base>").path_and_query("<my custom path>")` if you want to customize the base uri and path.
112//! let response =  octocrab
113//!     ._get(Uri::builder().path_and_query("/organizations").build().expect("valid uri"))
114//!     .await?;
115//! # Ok(())
116//! # }
117//! ```
118//!
119//! You can use the those HTTP methods to easily create your own extensions to
120//! `Octocrab`'s typed API. (Requires `async_trait`).
121//! ```
122//! use octocrab::{Octocrab, Page, Result, models};
123//!
124//! #[async_trait::async_trait]
125//! trait OrganisationExt {
126//!   async fn list_every_organisation(&self) -> Result<Page<models::orgs::Organization>>;
127//! }
128//!
129//! #[async_trait::async_trait]
130//! impl OrganisationExt for Octocrab {
131//!   async fn list_every_organisation(&self) -> Result<Page<models::orgs::Organization>> {
132//!     self.get("organizations", None::<&()>).await
133//!   }
134//! }
135//! ```
136//!
137//! You can also easily access new properties that aren't available in the
138//! current models using `serde`.
139//!
140//! ```no_run
141//! use serde::Deserialize;
142//!
143//! #[derive(Deserialize)]
144//! struct RepositoryWithVisibility {
145//!     #[serde(flatten)]
146//!     inner: octocrab::models::Repository,
147//!     visibility: String,
148//! }
149//!
150//!
151//! # async fn run() -> octocrab::Result<()> {
152//! let my_repo = octocrab::instance()
153//!     .get::<RepositoryWithVisibility, _, _>("https://api.github.com/repos/XAMPPRocky/octocrab", None::<&()>)
154//!     .await?;
155//! # Ok(())
156//! # }
157//! ```
158//!
159//!
160//!
161//! ## Static API
162//! `octocrab` also provides a statically reference count version of its API,
163//! allowing you to easily plug it into existing systems without worrying
164//! about having to integrate and pass around the client.
165//!
166//! ```
167//! // Initialises the static instance with your configuration and returns an
168//! // instance of the client.
169//! # use octocrab::Octocrab;
170//! tokio_test::block_on(async {
171//! octocrab::initialise(Octocrab::default());
172//! // Gets a instance of `Octocrab` from the static API. If you call this
173//! // without first calling `octocrab::initialise` a default client will be
174//! // initialised and returned instead.
175//! octocrab::instance();
176//! # })
177//! ```
178//!
179//! ## GitHub webhook application support
180//!
181//! `octocrab` provides [deserializable datatypes](crate::models::webhook_events)
182//! for the payloads received by a GitHub application [responding to
183//! webhooks](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-github-app-that-responds-to-webhook-events).
184//! This allows you to write a typesafe application using Rust with
185//! pattern-matching/enum-dispatch to respond to events.
186//!
187//! **Note**: Webhook support in `octocrab` is still beta, not all known webhook events are
188//! strongly typed.
189//!
190//! ```no_run
191//! # use http::request::Request;
192//! # use tracing::{warn, info};
193//! # use octocrab::models::webhook_events::*;
194//! # let request_from_github = Request::post("https://my-webhook-url.com").body(vec![0_u8]).unwrap();
195//! // request_from_github is the HTTP request your webhook handler received
196//! let (parts, body) = request_from_github.into_parts();
197//! let header = parts.headers.get("X-GitHub-Event").unwrap().to_str().unwrap();
198//!
199//! let event = WebhookEvent::try_from_header_and_body(header, &body).unwrap();
200//! // Now you can match on event type and call any specific handling logic
201//! match event.kind {
202//!     WebhookEventType::Ping => info!("Received a ping"),
203//!     WebhookEventType::PullRequest => info!("Received a pull request event"),
204//!     // ...
205//!     _ => warn!("Ignored event"),
206//! };
207//! ```
208#![cfg_attr(test, recursion_limit = "512")]
209#![cfg_attr(docsrs, feature(doc_cfg))]
210
211mod api;
212mod body;
213mod error;
214mod from_response;
215mod page;
216
217pub mod auth;
218pub mod etag;
219pub mod models;
220pub mod params;
221pub mod service;
222
223use api::repos::RepoRef;
224use api::users::UserRef;
225use body::OctoBody;
226use chrono::{DateTime, Utc};
227use http::{HeaderMap, HeaderValue, Method, Uri};
228use http_body_util::combinators::BoxBody;
229use http_body_util::BodyExt;
230use service::middleware::auth_header::AuthHeaderLayer;
231use std::convert::{Infallible, TryInto};
232use std::fmt;
233use std::future::Future;
234use std::io::Write;
235use std::marker::PhantomData;
236use std::pin::Pin;
237use std::str::FromStr;
238use std::sync::{Arc, RwLock};
239use web_time::Duration;
240
241use http::{header::HeaderName, StatusCode};
242use hyper::{Request, Response};
243
244use once_cell::sync::Lazy;
245use secrecy::{ExposeSecret, SecretString};
246use serde::{Deserialize, Serialize};
247use snafu::*;
248use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceExt};
249
250use bytes::Bytes;
251use http::header::USER_AGENT;
252use http::request::Builder;
253#[cfg(feature = "opentls")]
254use hyper_tls::HttpsConnector;
255
256#[cfg(feature = "rustls")]
257use hyper_rustls::HttpsConnectorBuilder;
258
259#[cfg(feature = "retry")]
260use tower::retry::{Retry, RetryLayer};
261
262#[cfg(feature = "timeout")]
263use hyper_timeout::TimeoutConnector;
264
265use tower_http::{classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer};
266
267#[cfg(feature = "tracing")]
268use {tower_http::trace::TraceLayer, tracing::Span};
269
270use crate::error::{
271    HttpSnafu, HyperSnafu, InvalidUtf8Snafu, SerdeSnafu, SerdeUrlEncodedSnafu, ServiceSnafu,
272    UriParseError, UriParseSnafu, UriSnafu,
273};
274
275use crate::service::middleware::base_uri::BaseUriLayer;
276use crate::service::middleware::extra_headers::ExtraHeadersLayer;
277
278#[cfg(feature = "retry")]
279use crate::service::middleware::retry::RetryConfig;
280
281use auth::{AppAuth, Auth};
282use models::{AppId, InstallationId, InstallationToken, RepositoryId, UserId};
283
284pub use self::{
285    api::{
286        actions, activity, apps, checks, code_scannings, commits, current, events, gists,
287        gitignore, hooks, issues, licenses, markdown, orgs, projects, pulls, ratelimit, repos,
288        search, teams, users, workflows,
289    },
290    error::{Error, GitHubError},
291    from_response::FromResponse,
292    page::Page,
293};
294
295/// A convenience type with a default error type of [`Error`].
296pub type Result<T, E = error::Error> = std::result::Result<T, E>;
297
298const GITHUB_BASE_URI: &str = "https://api.github.com";
299const GITHUB_BASE_UPLOAD_URI: &str = "https://uploads.github.com";
300
301#[cfg(feature = "default-client")]
302static STATIC_INSTANCE: Lazy<arc_swap::ArcSwap<Octocrab>> =
303    Lazy::new(|| arc_swap::ArcSwap::from_pointee(Octocrab::default()));
304
305/// Formats a GitHub preview from it's name into the full value for the
306/// `Accept` header.
307/// ```
308/// assert_eq!(octocrab::format_preview("machine-man"), "application/vnd.github.machine-man-preview");
309/// ```
310pub fn format_preview(preview: impl AsRef<str>) -> String {
311    format!("application/vnd.github.{}-preview", preview.as_ref())
312}
313
314/// Formats a media type from it's name into the full value for the
315/// `Accept` header.
316/// ```
317/// assert_eq!(octocrab::format_media_type("html"), "application/vnd.github.v3.html+json");
318/// assert_eq!(octocrab::format_media_type("json"), "application/vnd.github.v3.json");
319/// assert_eq!(octocrab::format_media_type("patch"), "application/vnd.github.v3.patch");
320/// ```
321pub fn format_media_type(media_type: impl AsRef<str>) -> String {
322    let media_type = media_type.as_ref();
323    let json_suffix = match media_type {
324        "raw" | "text" | "html" | "full" => "+json",
325        _ => "",
326    };
327
328    format!("application/vnd.github.v3.{media_type}{json_suffix}")
329}
330
331#[derive(Debug, Deserialize)]
332struct GitHubErrorBody {
333    pub documentation_url: Option<String>,
334    pub errors: Option<Vec<serde_json::Value>>,
335    pub message: String,
336}
337
338/// Maps a GitHub error response into and `Err()` variant if the status is
339/// not a success.
340pub async fn map_github_error(
341    response: http::Response<BoxBody<Bytes, crate::Error>>,
342) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
343    if response.status().is_success() {
344        Ok(response)
345    } else {
346        let (parts, body) = response.into_parts();
347        let GitHubErrorBody {
348            documentation_url,
349            errors,
350            message,
351        } = serde_json::from_slice(body.collect().await?.to_bytes().as_ref())
352            .context(error::SerdeSnafu)?;
353
354        Err(error::Error::GitHub {
355            source: Box::new(GitHubError {
356                status_code: parts.status,
357                documentation_url,
358                errors,
359                message,
360            }),
361            backtrace: Backtrace::capture(),
362        })
363    }
364}
365
366/// Initialises the static instance using the configuration set by
367/// `builder`.
368/// ```
369/// # #[tokio::main]
370/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
371/// let octocrab = octocrab::initialise(octocrab::Octocrab::default());
372/// # Ok(())
373/// # }
374/// ```
375#[cfg(feature = "default-client")]
376#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
377pub fn initialise(crab: Octocrab) -> Arc<Octocrab> {
378    STATIC_INSTANCE.swap(Arc::from(crab))
379}
380
381/// Returns a new instance of [`Octocrab`]. If it hasn't been previously
382/// initialised it returns a default instance with no authentication set.
383/// ```
384/// #[tokio::main]
385/// async fn main() -> () {
386/// let octocrab = octocrab::instance();
387/// }
388/// ```
389#[cfg(feature = "default-client")]
390#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
391pub fn instance() -> Arc<Octocrab> {
392    STATIC_INSTANCE.load().clone()
393}
394
395type Executor = Box<dyn Fn(Pin<Box<dyn Future<Output = ()>>>)>;
396
397/// A builder struct for `Octocrab`, allowing you to configure the client, such
398/// as using GitHub previews, the github instance, authentication, etc.
399///
400/// ```
401/// # #[tokio::main]
402/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
403/// let octocrab = octocrab::OctocrabBuilder::default()
404///     .add_preview("machine-man")
405///     .base_uri("https://github.example.com")?
406///     .build()?;
407/// # Ok(())
408/// # }
409/// ```
410///
411/// OctocrabBuilder can be extended with a custom config, see [DefaultOctocrabBuilderConfig] for an example
412pub struct OctocrabBuilder<Svc, Config, Auth, LayerReady> {
413    service: Svc,
414    auth: Auth,
415    config: Config,
416    _layer_ready: PhantomData<LayerReady>,
417    executor: Option<Executor>,
418}
419
420//Indicates weather the builder supports config
421pub struct NoConfig {}
422
423//Indicates weather the builder supports service that is already inside builder
424pub struct NoSvc {}
425
426//Indicates weather builder supports with_layer(This is somewhat redundant given NoSvc exists, but we have to use this until specialization is stable)
427pub struct NotLayerReady {}
428pub struct LayerReady {}
429
430//Indicates weather the builder supports auth
431pub struct NoAuth {}
432
433impl OctocrabBuilder<NoSvc, NoConfig, NoAuth, NotLayerReady> {
434    pub fn new_empty() -> Self {
435        OctocrabBuilder {
436            service: NoSvc {},
437            auth: NoAuth {},
438            config: NoConfig {},
439            _layer_ready: PhantomData,
440            executor: None,
441        }
442    }
443}
444
445impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
446    pub fn new() -> Self {
447        OctocrabBuilder::default()
448    }
449}
450
451impl<Config, Auth> OctocrabBuilder<NoSvc, Config, Auth, NotLayerReady> {
452    pub fn with_service<Svc>(self, service: Svc) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
453        OctocrabBuilder {
454            service,
455            auth: self.auth,
456            config: self.config,
457            _layer_ready: PhantomData,
458            executor: None,
459        }
460    }
461}
462
463impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
464where
465    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
466    Svc::Future: Send + 'static,
467    Svc::Error: Into<BoxError>,
468    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
469    B::Error: Into<BoxError>,
470{
471    pub fn with_executor(
472        self,
473        executor: Executor,
474    ) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
475        OctocrabBuilder {
476            service: self.service,
477            auth: self.auth,
478            config: self.config,
479            _layer_ready: PhantomData,
480            executor: Some(executor),
481        }
482    }
483}
484
485impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
486where
487    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
488    Svc::Future: Send + 'static,
489    Svc::Error: Into<BoxError>,
490    B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
491    B::Error: Into<BoxError>,
492{
493    /// Add a [`Layer`] to the current [`Service`] stack.
494    pub fn with_layer<L: Layer<Svc>>(
495        self,
496        layer: &L,
497    ) -> OctocrabBuilder<L::Service, Config, Auth, LayerReady> {
498        let Self {
499            service: stack,
500            auth,
501            config,
502            executor,
503            ..
504        } = self;
505        OctocrabBuilder {
506            service: layer.layer(stack),
507            auth,
508            config,
509            executor,
510            _layer_ready: PhantomData,
511        }
512    }
513}
514
515impl Default for OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
516    fn default() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
517        OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
518    }
519}
520
521impl<Svc, Auth, LayerState> OctocrabBuilder<Svc, NoConfig, Auth, LayerState> {
522    fn with_config<Config>(self, config: Config) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
523        OctocrabBuilder {
524            service: self.service,
525            auth: self.auth,
526            executor: self.executor,
527            config,
528            _layer_ready: PhantomData,
529        }
530    }
531}
532
533impl<Svc, B, LayerState> OctocrabBuilder<Svc, NoConfig, AuthState, LayerState>
534where
535    Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
536    Svc::Future: Send + 'static,
537    Svc::Error: Into<BoxError>,
538    B: http_body::Body<Data = bytes::Bytes> + Send + Sync + 'static,
539    B::Error: Into<BoxError>,
540{
541    /// Build a [`Client`](OctocrabService) instance with the current [`Service`] stack.
542    pub fn build(self) -> Result<Octocrab, Infallible> {
543        // Transform response body to `BoxBody<Bytes, crate::Error>` and use type erased error to avoid type parameters.
544        let service = MapResponseBodyLayer::new(|b: B| {
545            b.map_err(|e| ServiceSnafu.into_error(e.into())).boxed()
546        })
547        .layer(self.service)
548        .map_err(|e| e.into());
549
550        if let Some(executor) = self.executor {
551            return Ok(Octocrab::new_with_executor(service, self.auth, executor));
552        }
553
554        Ok(Octocrab::new(service, self.auth))
555    }
556}
557
558impl<Svc, Config, LayerState> OctocrabBuilder<Svc, Config, NoAuth, LayerState> {
559    pub fn with_auth<Auth>(self, auth: Auth) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
560        OctocrabBuilder {
561            service: self.service,
562            auth,
563            config: self.config,
564            executor: self.executor,
565            _layer_ready: PhantomData,
566        }
567    }
568}
569
570impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
571    /// Set the retry configuration
572    #[cfg(feature = "retry")]
573    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
574    pub fn add_retry_config(mut self, retry_config: RetryConfig) -> Self {
575        self.config.retry_config = retry_config;
576        self
577    }
578
579    /// Set the connect timeout.
580    #[cfg(feature = "timeout")]
581    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
582    pub fn set_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
583        self.config.connect_timeout = timeout;
584        self
585    }
586
587    /// Set the read timeout.
588    #[cfg(feature = "timeout")]
589    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
590    pub fn set_read_timeout(mut self, timeout: Option<Duration>) -> Self {
591        self.config.read_timeout = timeout;
592        self
593    }
594
595    /// Set the write timeout.
596    #[cfg(feature = "timeout")]
597    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
598    pub fn set_write_timeout(mut self, timeout: Option<Duration>) -> Self {
599        self.config.write_timeout = timeout;
600        self
601    }
602
603    /// Enable a GitHub preview.
604    pub fn add_preview(mut self, preview: &'static str) -> Self {
605        self.config.previews.push(preview);
606        self
607    }
608
609    /// Add an additional header to include with every request.
610    pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
611        self.config.extra_headers.push((key, value));
612        self
613    }
614
615    /// Add a personal token to use for authentication.
616    pub fn personal_token<S: Into<SecretString>>(mut self, token: S) -> Self {
617        self.config.auth = Auth::PersonalToken(token.into());
618        self
619    }
620
621    /// Authenticate as a Github App.
622    /// `key`: RSA private key in DER or PEM formats.
623    pub fn app(mut self, app_id: AppId, key: jsonwebtoken::EncodingKey) -> Self {
624        self.config.auth = Auth::App(AppAuth { app_id, key });
625        self
626    }
627
628    /// Authenticate as a Basic Auth
629    /// username and password
630    pub fn basic_auth(mut self, username: String, password: String) -> Self {
631        self.config.auth = Auth::Basic { username, password };
632        self
633    }
634
635    /// Authenticate with an OAuth token.
636    pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
637        self.config.auth = Auth::OAuth(oauth);
638        self
639    }
640
641    /// Authenticate with a user access token.
642    pub fn user_access_token<S: Into<SecretString>>(mut self, token: S) -> Self {
643        self.config.auth = Auth::UserAccessToken(token.into());
644        self
645    }
646
647    /// Set the base url for `Octocrab`.
648    pub fn base_uri(mut self, base_uri: impl TryInto<Uri>) -> Result<Self> {
649        self.config.base_uri = Some(
650            base_uri
651                .try_into()
652                .map_err(|_| UriParseError {})
653                .context(UriParseSnafu)?,
654        );
655        Ok(self)
656    }
657
658    /// Set the base upload url for `Octocrab`.
659    pub fn upload_uri(mut self, upload_uri: impl TryInto<Uri>) -> Result<Self> {
660        self.config.upload_uri = Some(
661            upload_uri
662                .try_into()
663                .map_err(|_| UriParseError {})
664                .context(UriParseSnafu)?,
665        );
666        Ok(self)
667    }
668
669    #[cfg(feature = "retry")]
670    #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
671    pub fn set_connector_retry_service<S>(
672        &self,
673        connector: hyper_util::client::legacy::Client<S, OctoBody>,
674    ) -> Retry<RetryConfig, hyper_util::client::legacy::Client<S, OctoBody>> {
675        let retry_layer = RetryLayer::new(self.config.retry_config.clone());
676
677        retry_layer.layer(connector)
678    }
679
680    #[cfg(feature = "timeout")]
681    #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
682    pub fn set_connect_timeout_service<T>(&self, connector: T) -> TimeoutConnector<T>
683    where
684        T: Service<Uri> + Send,
685        T::Response: hyper::rt::Read + hyper::rt::Write + Send + Unpin,
686        T::Future: Send + 'static,
687        T::Error: Into<BoxError>,
688    {
689        let mut connector = TimeoutConnector::new(connector);
690        // Set the timeouts for the client
691        connector.set_connect_timeout(self.config.connect_timeout);
692        connector.set_read_timeout(self.config.read_timeout);
693        connector.set_write_timeout(self.config.write_timeout);
694        connector
695    }
696
697    /// Build a [`Client`](hyper_util::client::legacy::Client) instance with the current [`Service`] stack.
698    #[cfg(feature = "default-client")]
699    #[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
700    pub fn build(self) -> Result<Octocrab> {
701        let client: hyper_util::client::legacy::Client<_, OctoBody> = {
702            #[cfg(all(not(feature = "opentls"), not(feature = "rustls")))]
703            let mut connector = hyper::client::conn::http1::HttpConnector::new();
704
705            #[cfg(all(feature = "rustls", not(feature = "opentls")))]
706            let connector = {
707                let builder = HttpsConnectorBuilder::new();
708                #[cfg(feature = "rustls-webpki-tokio")]
709                let builder = builder.with_webpki_roots();
710                #[cfg(not(feature = "rustls-webpki-tokio"))]
711                let builder = builder
712                    .with_native_roots()
713                    .map_err(Into::into)
714                    .context(error::OtherSnafu)?; // enabled the `rustls-native-certs` feature in hyper-rustls
715
716                builder
717                    .https_or_http() //  Disable .https_only() during tests until: https://github.com/LukeMathWalker/wiremock-rs/issues/58 is resolved. Alternatively we can use conditional compilation to only enable this feature in tests, but it becomes rather ugly with integration tests.
718                    .enable_http1()
719                    .build()
720            };
721
722            #[cfg(all(feature = "opentls", not(feature = "rustls")))]
723            let connector = HttpsConnector::new();
724
725            #[cfg(feature = "timeout")]
726            let connector = self.set_connect_timeout_service(connector);
727
728            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
729                .build(connector)
730        };
731
732        #[cfg(feature = "retry")]
733        let client = self.set_connector_retry_service(client);
734
735        #[cfg(feature = "tracing")]
736        let client = TraceLayer::new_for_http()
737            .make_span_with(|req: &Request<OctoBody>| {
738                tracing::debug_span!(
739                    "HTTP",
740                     http.method = %req.method(),
741                     http.url = %req.uri(),
742                     http.status_code = tracing::field::Empty,
743                     otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
744                     otel.kind = "client",
745                     otel.status_code = tracing::field::Empty,
746                )
747            })
748            .on_request(|_req: &Request<OctoBody>, _span: &Span| {
749                tracing::debug!("requesting");
750            })
751            .on_response(
752                |res: &Response<hyper::body::Incoming>, _latency: Duration, span: &Span| {
753                    let status = res.status();
754                    span.record("http.status_code", status.as_u16());
755                    if status.is_client_error() || status.is_server_error() {
756                        span.record("otel.status_code", "ERROR");
757                    }
758                },
759            )
760            // Explicitly disable `on_body_chunk`. The default does nothing.
761            .on_body_chunk(())
762            .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
763                tracing::debug!("stream closed");
764            })
765            .on_failure(
766                |ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
767                    // Called when
768                    // - Calling the inner service errored
769                    // - Polling `Body` errored
770                    // - the response was classified as failure (5xx)
771                    // - End of stream was classified as failure
772                    span.record("otel.status_code", "ERROR");
773                    match ec {
774                        ServerErrorsFailureClass::StatusCode(status) => {
775                            span.record("http.status_code", status.as_u16());
776                            tracing::error!("failed with status {}", status)
777                        }
778                        ServerErrorsFailureClass::Error(err) => {
779                            tracing::error!("failed with error {}", err)
780                        }
781                    }
782                },
783            )
784            .layer(client);
785
786        #[cfg(feature = "follow-redirect")]
787        let client = tower_http::follow_redirect::FollowRedirectLayer::new().layer(client);
788
789        let mut hmap: Vec<(HeaderName, HeaderValue)> = vec![];
790
791        // Add the user agent header required by GitHub
792        hmap.push((USER_AGENT, HeaderValue::from_str("octocrab").unwrap()));
793
794        for preview in &self.config.previews {
795            hmap.push((
796                http::header::ACCEPT,
797                HeaderValue::from_str(crate::format_preview(preview).as_str()).unwrap(),
798            ));
799        }
800
801        let (auth_header, auth_state): (Option<HeaderValue>, _) = match self.config.auth {
802            Auth::None => (None, AuthState::None),
803            Auth::Basic { username, password } => {
804                (None, AuthState::BasicAuth { username, password })
805            }
806            Auth::PersonalToken(token) => (
807                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
808                AuthState::None,
809            ),
810            Auth::UserAccessToken(token) => (
811                Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
812                AuthState::None,
813            ),
814            Auth::App(app_auth) => (None, AuthState::App(app_auth)),
815            Auth::OAuth(device) => (
816                Some(
817                    format!(
818                        "{} {}",
819                        device.token_type,
820                        &device.access_token.expose_secret()
821                    )
822                    .parse()
823                    .unwrap(),
824                ),
825                AuthState::None,
826            ),
827        };
828
829        for (key, value) in self.config.extra_headers.iter() {
830            hmap.push((
831                key.clone(),
832                HeaderValue::from_str(value.as_str())
833                    .map_err(http::Error::from)
834                    .context(HttpSnafu)?,
835            ));
836        }
837
838        let client = ExtraHeadersLayer::new(Arc::new(hmap)).layer(client);
839
840        let client = MapResponseBodyLayer::new(|body| {
841            BodyExt::map_err(body, |e| HyperSnafu.into_error(e)).boxed()
842        })
843        .layer(client);
844
845        let base_uri = self
846            .config
847            .base_uri
848            .clone()
849            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_URI).unwrap());
850
851        let upload_uri = self
852            .config
853            .upload_uri
854            .clone()
855            .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_UPLOAD_URI).unwrap());
856
857        let client = BaseUriLayer::new(base_uri.clone()).layer(client);
858
859        let client = AuthHeaderLayer::new(auth_header, base_uri, upload_uri).layer(client);
860
861        if let Some(executor) = self.executor {
862            return Ok(Octocrab::new_with_executor(client, auth_state, executor));
863        }
864
865        Ok(Octocrab::new(client, auth_state))
866    }
867}
868
869pub struct DefaultOctocrabBuilderConfig {
870    auth: Auth,
871    previews: Vec<&'static str>,
872    extra_headers: Vec<(HeaderName, String)>,
873    #[cfg(feature = "timeout")]
874    connect_timeout: Option<Duration>,
875    #[cfg(feature = "timeout")]
876    read_timeout: Option<Duration>,
877    #[cfg(feature = "timeout")]
878    write_timeout: Option<Duration>,
879    base_uri: Option<Uri>,
880    upload_uri: Option<Uri>,
881    #[cfg(feature = "retry")]
882    retry_config: RetryConfig,
883}
884
885impl Default for DefaultOctocrabBuilderConfig {
886    fn default() -> Self {
887        Self {
888            auth: Auth::None,
889            previews: Vec::new(),
890            extra_headers: Vec::new(),
891            #[cfg(feature = "timeout")]
892            connect_timeout: None,
893            #[cfg(feature = "timeout")]
894            read_timeout: None,
895            #[cfg(feature = "timeout")]
896            write_timeout: None,
897            base_uri: None,
898            upload_uri: None,
899            #[cfg(feature = "retry")]
900            retry_config: RetryConfig::Simple(3),
901        }
902    }
903}
904
905impl DefaultOctocrabBuilderConfig {
906    pub fn new() -> Self {
907        Self::default()
908    }
909}
910
911#[derive(Debug, Clone)]
912struct CachedTokenInner {
913    expiration: Option<DateTime<Utc>>,
914    secret: SecretString,
915}
916
917impl CachedTokenInner {
918    fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self {
919        Self { secret, expiration }
920    }
921
922    fn expose_secret(&self) -> &str {
923        self.secret.expose_secret()
924    }
925}
926
927/// A cached API access token (which may be None)
928pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
929
930impl CachedToken {
931    fn clear(&self) {
932        *self.0.write().unwrap() = None;
933    }
934
935    /// Returns a valid token if it exists and is not expired or if there is no expiration date.
936    fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> {
937        let inner = self.0.read().unwrap();
938
939        if let Some(token) = inner.as_ref() {
940            if let Some(exp) = token.expiration {
941                if exp - Utc::now() > buffer {
942                    return Some(token.secret.clone());
943                }
944            } else {
945                return Some(token.secret.clone());
946            }
947        }
948
949        None
950    }
951
952    fn valid_token(&self) -> Option<SecretString> {
953        self.valid_token_with_buffer(chrono::Duration::seconds(30))
954    }
955
956    fn set<S: Into<SecretString>>(&self, token: S, expiration: Option<DateTime<Utc>>) {
957        *self.0.write().unwrap() = Some(CachedTokenInner::new(token.into(), expiration));
958    }
959}
960
961impl fmt::Debug for CachedToken {
962    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963        self.0.read().unwrap().fmt(f)
964    }
965}
966
967impl fmt::Display for CachedToken {
968    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969        let option = self.0.read().unwrap();
970        option
971            .as_ref()
972            .map(|s| s.expose_secret().fmt(f))
973            .unwrap_or_else(|| write!(f, "<none>"))
974    }
975}
976
977impl Clone for CachedToken {
978    fn clone(&self) -> CachedToken {
979        CachedToken(RwLock::new(self.0.read().unwrap().clone()))
980    }
981}
982
983impl Default for CachedToken {
984    fn default() -> CachedToken {
985        CachedToken(RwLock::new(None))
986    }
987}
988
989/// State used for authenticate to Github
990#[derive(Debug, Clone)]
991pub enum AuthState {
992    /// No state, although Auth::PersonalToken may have caused
993    /// an Authorization HTTP header to be set to provide authentication.
994    None,
995    /// Basic Auth HTTP. (username:password)
996    BasicAuth {
997        /// The username
998        username: String,
999        /// The password
1000        password: String,
1001    },
1002    /// Github App authentication with the given app data
1003    App(AppAuth),
1004    /// Authentication via a Github App repo-specific installation
1005    Installation {
1006        /// The app authentication data (app ID and private key)
1007        app: AppAuth,
1008        /// The installation ID
1009        installation: InstallationId,
1010        /// The cached access token, if any
1011        token: CachedToken,
1012    },
1013    /// Access token based authentication.
1014    AccessToken {
1015        /// The access token
1016        token: SecretString,
1017    },
1018}
1019
1020pub type OctocrabService = Buffer<
1021    http::Request<OctoBody>,
1022    <BoxService<http::Request<OctoBody>, http::Response<BoxBody<Bytes, Error>>, BoxError> as tower::Service<http::Request<OctoBody>>>::Future
1023>;
1024
1025/// The GitHub API client.
1026#[derive(Clone)]
1027pub struct Octocrab {
1028    client: OctocrabService,
1029    auth_state: AuthState,
1030}
1031
1032impl fmt::Debug for Octocrab {
1033    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034        f.debug_struct("Octocrab")
1035            .field("auth_state", &self.auth_state)
1036            .finish()
1037    }
1038}
1039
1040/// Defaults for Octocrab:
1041/// - `base_uri`: `https://api.github.com`
1042/// - `auth`: `None`
1043/// - `client`: http client with the `octocrab` user agent.
1044#[cfg(feature = "default-client")]
1045#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
1046impl Default for Octocrab {
1047    fn default() -> Self {
1048        OctocrabBuilder::default().build().unwrap()
1049    }
1050}
1051
1052/// # Constructors
1053impl Octocrab {
1054    /// Returns a new `OctocrabBuilder`.
1055    pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1056    {
1057        OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1058    }
1059
1060    /// Creates a new `Octocrab`.
1061    fn new<S>(service: S, auth_state: AuthState) -> Self
1062    where
1063        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1064            + Send
1065            + 'static,
1066        S::Future: Send + 'static,
1067        S::Error: Into<BoxError>,
1068    {
1069        let service = Buffer::new(BoxService::new(service.map_err(Into::into)), 1024);
1070
1071        Self {
1072            client: service,
1073            auth_state,
1074        }
1075    }
1076
1077    /// Creates a new `Octocrab` with a custom executor
1078    fn new_with_executor<S>(service: S, auth_state: AuthState, executor: Executor) -> Self
1079    where
1080        S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1081            + Send
1082            + 'static,
1083        S::Future: Send + 'static,
1084        S::Error: Into<BoxError>,
1085    {
1086        // Use Buffer pair to return the background worker
1087        let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1088
1089        // Execute the background worker with the custom executor
1090        executor(Box::pin(worker));
1091
1092        Self {
1093            client: service,
1094            auth_state,
1095        }
1096    }
1097
1098    /// Returns a new `Octocrab` based on the current builder but
1099    /// authorizing via a specific installation ID.
1100    /// Typically you will first construct an `Octocrab` using
1101    /// `OctocrabBuilder::app` to authenticate as your Github App,
1102    /// then obtain an installation ID, and then pass that here to
1103    /// obtain a new `Octocrab` with which you can make API calls
1104    /// with the permissions of that installation.
1105    pub fn installation(&self, id: InstallationId) -> Result<Octocrab> {
1106        let app_auth = if let AuthState::App(ref app_auth) = self.auth_state {
1107            app_auth.clone()
1108        } else {
1109            return Err(Error::Installation {
1110                backtrace: Backtrace::capture(),
1111            });
1112        };
1113        Ok(Octocrab {
1114            client: self.client.clone(),
1115            auth_state: AuthState::Installation {
1116                app: app_auth,
1117                installation: id,
1118                token: CachedToken::default(),
1119            },
1120        })
1121    }
1122
1123    /// Similar to `installation`, but also eagerly caches the installation
1124    /// token and returns the token. The returned token can be used to make
1125    /// https git requests to e.g. clone repositories that the installation
1126    /// has access to.
1127    ///
1128    /// See also <https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#http-based-git-access-by-an-installation>
1129    pub async fn installation_and_token(
1130        &self,
1131        id: InstallationId,
1132    ) -> Result<(Octocrab, SecretString)> {
1133        let crab = self.installation(id)?;
1134        let token = crab.request_installation_auth_token().await?;
1135        Ok((crab, token))
1136    }
1137
1138    /// Returns a new `Octocrab` based on the current builder but
1139    /// authorizing via an access token.
1140    ///
1141    /// See also <https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app>
1142    pub fn user_access_token<S: Into<SecretString>>(&self, token: S) -> Result<Self> {
1143        Ok(Octocrab {
1144            client: self.client.clone(),
1145            auth_state: AuthState::AccessToken {
1146                token: token.into(),
1147            },
1148        })
1149    }
1150}
1151
1152/// # GitHub API Methods
1153impl Octocrab {
1154    /// Creates a new [`actions::ActionsHandler`] for accessing information from
1155    /// GitHub Actions.
1156    pub fn actions(&self) -> actions::ActionsHandler<'_> {
1157        actions::ActionsHandler::new(self)
1158    }
1159
1160    /// Creates a [`current::CurrentAuthHandler`] that allows you to access
1161    /// information about the current authenticated user.
1162    pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1163        current::CurrentAuthHandler::new(self)
1164    }
1165
1166    /// Creates a [`activity::ActivityHandler`] for the current authenticated user.
1167    pub fn activity(&self) -> activity::ActivityHandler<'_> {
1168        activity::ActivityHandler::new(self)
1169    }
1170
1171    /// Creates a new [`apps::AppsRequestHandler`] for the currently authenticated app.
1172    pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1173        apps::AppsRequestHandler::new(self)
1174    }
1175
1176    /// Creates a [`gitignore::GitignoreHandler`] for accessing information
1177    /// about `gitignore`.
1178    pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1179        gitignore::GitignoreHandler::new(self)
1180    }
1181
1182    /// Creates a [`issues::IssueHandler`] for the repo specified at `owner/repo`,
1183    /// that allows you to access GitHub's issues API.
1184    pub fn issues(
1185        &self,
1186        owner: impl Into<String>,
1187        repo: impl Into<String>,
1188    ) -> issues::IssueHandler<'_> {
1189        issues::IssueHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1190    }
1191
1192    /// Creates a [`issues::IssueHandler`] for the repo specified at repository ID,
1193    /// that allows you to access GitHub's issues API.
1194    pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1195        issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1196    }
1197
1198    /// Creates a [`code_scannings::CodeScanningHandler`] for the repo specified at `owner/repo`,
1199    /// that allows you to access GitHub's Code scanning API.
1200    pub fn code_scannings(
1201        &self,
1202        owner: impl Into<String>,
1203        repo: impl Into<String>,
1204    ) -> code_scannings::CodeScanningHandler<'_> {
1205        code_scannings::CodeScanningHandler::new(self, owner.into(), Option::from(repo.into()))
1206    }
1207
1208    /// Creates a [`code_scannings::CodeScanningHandler`] for the org specified at `owner`,
1209    /// that allows you to access GitHub's Code scanning API.
1210    pub fn code_scannings_organisation(
1211        &self,
1212        owner: impl Into<String>,
1213    ) -> code_scannings::CodeScanningHandler<'_> {
1214        code_scannings::CodeScanningHandler::new(self, owner.into(), None)
1215    }
1216
1217    /// Creates a [`commits::CommitHandler`] for the repo specified at `owner/repo`,
1218    pub fn commits(
1219        &self,
1220        owner: impl Into<String>,
1221        repo: impl Into<String>,
1222    ) -> commits::CommitHandler<'_> {
1223        commits::CommitHandler::new(self, owner.into(), repo.into())
1224    }
1225
1226    /// Creates a [`licenses::LicenseHandler`].
1227    pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1228        licenses::LicenseHandler::new(self)
1229    }
1230
1231    /// Creates a [`markdown::MarkdownHandler`].
1232    pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1233        markdown::MarkdownHandler::new(self)
1234    }
1235
1236    /// Creates an [`orgs::OrgHandler`] for the specified organization,
1237    /// that allows you to access GitHub's organization API.
1238    pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1239        orgs::OrgHandler::new(self, owner.into())
1240    }
1241
1242    /// Creates a [`pulls::PullRequestHandler`] for the repo specified at
1243    /// `owner/repo`, that allows you to access GitHub's pull request API.
1244    pub fn pulls(
1245        &self,
1246        owner: impl Into<String>,
1247        repo: impl Into<String>,
1248    ) -> pulls::PullRequestHandler<'_> {
1249        pulls::PullRequestHandler::new(self, owner.into(), repo.into())
1250    }
1251
1252    /// Creates a [`repos::RepoHandler`] for the repo specified at `owner/repo`,
1253    /// that allows you to access GitHub's repository API.
1254    pub fn repos(
1255        &self,
1256        owner: impl Into<String>,
1257        repo: impl Into<String>,
1258    ) -> repos::RepoHandler<'_> {
1259        repos::RepoHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1260    }
1261
1262    /// Creates a [`repos::RepoHandler`] for the repo specified at repository ID,
1263    /// that allows you to access GitHub's repository API.
1264    pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1265        repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1266    }
1267
1268    /// Creates a [`projects::ProjectHandler`] that allows you to access GitHub's
1269    /// projects API (classic).
1270    pub fn projects(&self) -> projects::ProjectHandler<'_> {
1271        projects::ProjectHandler::new(self)
1272    }
1273
1274    /// Creates a [`search::SearchHandler`] that allows you to construct general queries
1275    /// to GitHub's API.
1276    pub fn search(&self) -> search::SearchHandler<'_> {
1277        search::SearchHandler::new(self)
1278    }
1279
1280    /// Creates a [`teams::TeamHandler`] for the specified organization that allows
1281    /// you to access GitHub's teams API.
1282    pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1283        teams::TeamHandler::new(self, owner.into())
1284    }
1285
1286    /// Creates a [`users::UserHandler`] for the specified user using the user name
1287    pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1288        users::UserHandler::new(self, UserRef::ByString(user.into()))
1289    }
1290
1291    /// Creates a [`users::UserHandler`] for the specified user using the user ID
1292    pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1293        users::UserHandler::new(self, UserRef::ById(user.into()))
1294    }
1295
1296    /// Creates a [`workflows::WorkflowsHandler`] for the specified repository that allows
1297    /// you to access GitHub's workflows API.
1298    pub fn workflows(
1299        &self,
1300        owner: impl Into<String>,
1301        repo: impl Into<String>,
1302    ) -> workflows::WorkflowsHandler<'_> {
1303        workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
1304    }
1305
1306    /// Creates an [`events::EventsBuilder`] that allows you to access
1307    /// GitHub's events API.
1308    pub fn events(&self) -> events::EventsBuilder<'_> {
1309        events::EventsBuilder::new(self)
1310    }
1311
1312    /// Creates a [`gists::GistsHandler`] that allows you to access
1313    /// GitHub's Gists API.
1314    pub fn gists(&self) -> gists::GistsHandler<'_> {
1315        gists::GistsHandler::new(self)
1316    }
1317
1318    /// Creates a [`checks::ChecksHandler`] that allows to access the Checks API.
1319    pub fn checks(
1320        &self,
1321        owner: impl Into<String>,
1322        repo: impl Into<String>,
1323    ) -> checks::ChecksHandler<'_> {
1324        checks::ChecksHandler::new(self, owner.into(), repo.into())
1325    }
1326
1327    /// Creates a [`ratelimit::RateLimitHandler`] that returns the API rate limit.
1328    pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1329        ratelimit::RateLimitHandler::new(self)
1330    }
1331
1332    /// Creates a [`hooks::HooksHandler`] that returns the API hooks
1333    pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1334        hooks::HooksHandler::new(self, owner.into())
1335    }
1336}
1337
1338/// # GraphQL API.
1339impl Octocrab {
1340    /// Sends a graphql query to GitHub, and deserialises the response
1341    /// from JSON.
1342    /// ```no_run
1343    ///# async fn run() -> octocrab::Result<()> {
1344    /// let response: serde_json::Value = octocrab::instance()
1345    ///     .graphql(&serde_json::json!({ "query": "{ viewer { login }}" }))
1346    ///     .await?;
1347    ///# Ok(())
1348    ///# }
1349    /// ```
1350    pub async fn graphql<R: crate::FromResponse>(
1351        &self,
1352        payload: &(impl serde::Serialize + ?Sized),
1353    ) -> crate::Result<R> {
1354        self.post("/graphql", Some(&serde_json::json!(payload)))
1355            .await
1356    }
1357}
1358
1359/// # HTTP Methods
1360/// A collection of different of HTTP methods to use with Octocrab's
1361/// configuration (Authenication, etc.). All of the HTTP methods (`get`, `post`,
1362/// etc.) perform some amount of pre-processing such as making relative urls
1363/// absolute, and post processing such as mapping any potential GitHub errors
1364/// into `Err()` variants, and deserializing the response body.
1365///
1366/// This isn't always ideal when working with GitHub's API and as such there are
1367/// additional methods available prefixed with `_` (e.g.  `_get`, `_post`,
1368/// etc.) that perform no pre or post processing and directly return the
1369/// `http::Response` struct.
1370impl Octocrab {
1371    /// Send a `POST` request to `route` with an optional body, returning the body
1372    /// of the response.
1373    pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
1374        &self,
1375        route: impl AsRef<str>,
1376        body: Option<&P>,
1377    ) -> Result<R> {
1378        let response = self
1379            ._post(self.parameterized_uri(route, None::<&()>)?, body)
1380            .await?;
1381        R::from_response(crate::map_github_error(response).await?).await
1382    }
1383
1384    /// Send a `POST` request with no additional pre/post-processing.
1385    pub async fn _post<P: Serialize + ?Sized>(
1386        &self,
1387        uri: impl TryInto<http::Uri>,
1388        body: Option<&P>,
1389    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1390        let uri = uri
1391            .try_into()
1392            .map_err(|_| UriParseError {})
1393            .context(UriParseSnafu)?;
1394        let request = Builder::new().method(Method::POST).uri(uri);
1395        let request = self.build_request(request, body)?;
1396        self.execute(request).await
1397    }
1398
1399    /// Send a `GET` request to `route` with optional query parameters, returning
1400    /// the body of the response.
1401    pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
1402    where
1403        A: AsRef<str>,
1404        P: Serialize + ?Sized,
1405        R: FromResponse,
1406    {
1407        self.get_with_headers(route, parameters, None).await
1408    }
1409
1410    /// Send a `GET` request with no additional post-processing.
1411    pub async fn _get(
1412        &self,
1413        uri: impl TryInto<Uri>,
1414    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1415        self._get_with_headers(uri, None).await
1416    }
1417
1418    /// Convenience method to accept any &str, and attempt to convert it to a Uri.
1419    /// the method also attempts to serialize any parameters into a query string, and append it to the uri.
1420    fn parameterized_uri<A, P>(&self, uri: A, parameters: Option<&P>) -> Result<Uri>
1421    where
1422        A: AsRef<str>,
1423        P: Serialize + ?Sized,
1424    {
1425        let mut uri = uri.as_ref().to_string();
1426        if let Some(parameters) = parameters {
1427            if uri.contains('?') {
1428                uri = format!("{uri}&");
1429            } else {
1430                uri = format!("{uri}?");
1431            }
1432            uri = format!(
1433                "{}{}",
1434                uri,
1435                serde_urlencoded::to_string(parameters)
1436                    .context(SerdeUrlEncodedSnafu)?
1437                    .as_str()
1438            );
1439        }
1440        let uri = Uri::from_str(uri.as_str()).context(UriSnafu);
1441        uri
1442    }
1443
1444    pub async fn body_to_string(
1445        &self,
1446        res: http::Response<BoxBody<Bytes, crate::Error>>,
1447    ) -> Result<String> {
1448        let body_bytes = res.into_body().collect().await?.to_bytes();
1449        String::from_utf8(body_bytes.to_vec()).context(InvalidUtf8Snafu)
1450    }
1451
1452    /// Send a `GET` request to `route` with optional query parameters and headers, returning
1453    /// the body of the response.
1454    pub async fn get_with_headers<R, A, P>(
1455        &self,
1456        route: A,
1457        parameters: Option<&P>,
1458        headers: Option<http::header::HeaderMap>,
1459    ) -> Result<R>
1460    where
1461        A: AsRef<str>,
1462        P: Serialize + ?Sized,
1463        R: FromResponse,
1464    {
1465        let response = self
1466            ._get_with_headers(self.parameterized_uri(route, parameters)?, headers)
1467            .await?;
1468        R::from_response(crate::map_github_error(response).await?).await
1469    }
1470
1471    /// Send a `GET` request including option to set headers, with no additional post-processing.
1472    pub async fn _get_with_headers(
1473        &self,
1474        uri: impl TryInto<Uri>,
1475        headers: Option<http::header::HeaderMap>,
1476    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1477        let uri = uri
1478            .try_into()
1479            .map_err(|_| UriParseError {})
1480            .context(UriParseSnafu)?;
1481        let mut request = Builder::new().method(Method::GET).uri(uri);
1482        if let Some(headers) = headers {
1483            for (key, value) in headers.iter() {
1484                request = request.header(key, value);
1485            }
1486        }
1487        let request = self.build_request(request, None::<&()>)?;
1488        self.execute(request).await
1489    }
1490
1491    /// Send a `PATCH` request to `route` with optional query parameters,
1492    /// returning the body of the response.
1493    pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1494    where
1495        A: AsRef<str>,
1496        B: Serialize + ?Sized,
1497        R: FromResponse,
1498    {
1499        let response = self
1500            ._patch(self.parameterized_uri(route, None::<&()>)?, body)
1501            .await?;
1502        R::from_response(crate::map_github_error(response).await?).await
1503    }
1504
1505    /// Send a `PATCH` request with no additional post-processing.
1506    pub async fn _patch<B: Serialize + ?Sized>(
1507        &self,
1508        uri: impl TryInto<Uri>,
1509        body: Option<&B>,
1510    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1511        let uri = uri
1512            .try_into()
1513            .map_err(|_| UriParseError {})
1514            .context(UriParseSnafu)?;
1515        let request = Builder::new().method(Method::PATCH).uri(uri);
1516        let request = self.build_request(request, body)?;
1517        self.execute(request).await
1518    }
1519
1520    /// Send a `PUT` request to `route` with optional query parameters,
1521    /// returning the body of the response.
1522    pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1523    where
1524        A: AsRef<str>,
1525        B: Serialize + ?Sized,
1526        R: FromResponse,
1527    {
1528        let response = self
1529            ._put(self.parameterized_uri(route, None::<&()>)?, body)
1530            .await?;
1531        R::from_response(crate::map_github_error(response).await?).await
1532    }
1533
1534    /// Send a `PATCH` request with no additional post-processing.
1535    pub async fn _put<B: Serialize + ?Sized>(
1536        &self,
1537        uri: impl TryInto<Uri>,
1538        body: Option<&B>,
1539    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1540        let uri = uri
1541            .try_into()
1542            .map_err(|_| UriParseError {})
1543            .context(UriParseSnafu)?;
1544        let request = Builder::new().method(Method::PUT).uri(uri);
1545        let request = self.build_request(request, body)?;
1546        self.execute(request).await
1547    }
1548
1549    pub fn build_request<B: Serialize + ?Sized>(
1550        &self,
1551        mut builder: Builder,
1552        body: Option<&B>,
1553    ) -> Result<http::Request<OctoBody>> {
1554        // Since Octocrab doesn't require streamable bodies(aka, file upload) because it is serde::Serialize),
1555        // we can just use String body, since it is both http_body::Body(required by Hyper::Client), and Clone(required by BoxService).
1556
1557        // In case octocrab needs to support cases where body is strictly streamable, it should use something like reqwest::Body,
1558        // since it differentiates between retryable bodies, and streams(aka, it implements try_clone(), which is needed for middlewares like retry).
1559
1560        if let Some(body) = body {
1561            builder = builder.header(http::header::CONTENT_TYPE, "application/json");
1562            let serialized = serde_json::to_string(body).context(SerdeSnafu)?;
1563            let body: OctoBody = serialized.into();
1564            let request = builder.body(body).context(HttpSnafu)?;
1565            Ok(request)
1566        } else {
1567            Ok(builder
1568                .header(http::header::CONTENT_LENGTH, "0")
1569                .body(OctoBody::empty())
1570                .context(HttpSnafu)?)
1571        }
1572    }
1573
1574    /// Send a `DELETE` request to `route` with optional query body,
1575    /// returning the body of the response.
1576    pub async fn delete<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1577    where
1578        A: AsRef<str>,
1579        B: Serialize + ?Sized,
1580        R: FromResponse,
1581    {
1582        let response = self
1583            ._delete(self.parameterized_uri(route, None::<&()>)?, body)
1584            .await?;
1585        R::from_response(crate::map_github_error(response).await?).await
1586    }
1587
1588    /// Send a `DELETE` request with no additional post-processing.
1589    pub async fn _delete<B: Serialize + ?Sized>(
1590        &self,
1591        uri: impl TryInto<Uri>,
1592        body: Option<&B>,
1593    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1594        let uri = uri
1595            .try_into()
1596            .map_err(|_| UriParseError {})
1597            .context(UriParseSnafu)?;
1598        let request = self.build_request(Builder::new().method(Method::DELETE).uri(uri), body)?;
1599
1600        self.execute(request).await
1601    }
1602
1603    /// Requests a fresh installation auth token and caches it. Returns the token.
1604    async fn request_installation_auth_token(&self) -> Result<SecretString> {
1605        let (app, installation, token) = if let AuthState::Installation {
1606            ref app,
1607            installation,
1608            ref token,
1609        } = self.auth_state
1610        {
1611            (app, installation, token)
1612        } else {
1613            return Err(Error::Installation {
1614                backtrace: Backtrace::capture(),
1615            });
1616        };
1617        let mut request = Builder::new();
1618        let mut sensitive_value =
1619            HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1620                .map_err(http::Error::from)
1621                .context(HttpSnafu)?;
1622
1623        let uri = http::Uri::builder()
1624            .path_and_query(format!("/app/installations/{installation}/access_tokens"))
1625            .build()
1626            .context(HttpSnafu)?;
1627
1628        sensitive_value.set_sensitive(true);
1629        request = request
1630            .header(http::header::AUTHORIZATION, sensitive_value)
1631            .method(http::Method::POST)
1632            .uri(uri);
1633        let response = self
1634            .send(request.body("{}".into()).context(HttpSnafu)?)
1635            .await?;
1636        let _status = response.status();
1637
1638        let token_object =
1639            InstallationToken::from_response(crate::map_github_error(response).await?).await?;
1640
1641        let expiration = token_object
1642            .expires_at
1643            .map(|time| {
1644                DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other {
1645                    source: Box::new(e),
1646                    backtrace: snafu::Backtrace::capture(),
1647                })
1648            })
1649            .transpose()?;
1650
1651        #[cfg(feature = "tracing")]
1652        tracing::debug!("Token expires at: {:?}", expiration);
1653
1654        token.set(token_object.token.clone(), expiration);
1655
1656        Ok(SecretString::from(token_object.token))
1657    }
1658
1659    /// Send the given request to the underlying service
1660    pub async fn send(
1661        &self,
1662        request: Request<OctoBody>,
1663    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1664        let mut svc = self.client.clone();
1665        let response: Response<BoxBody<Bytes, crate::Error>> = svc
1666            .ready()
1667            .await
1668            .context(ServiceSnafu)?
1669            .call(request)
1670            .await
1671            .context(ServiceSnafu)?;
1672        Ok(response)
1673        //todo: attempt to downcast error to something more specific before returning. (Currently having trouble with this because I am not accustomed with snafu)
1674        // map_err(|err| {
1675        //     // Error decorating request
1676        //     err.downcast::<Error>()
1677        //         .map(|e| *e)
1678        //         // Error requesting
1679        //         .or_else(|err| err.downcast::<hyper::Error>().map(|err| Error::HyperError(*err)))
1680        //         // Error from another middleware
1681        //         .unwrap_or_else(|err| Error::Service(err))
1682        // })?;
1683    }
1684
1685    /// Execute the given `request` using octocrab's Client.
1686    pub async fn execute(
1687        &self,
1688        request: http::Request<impl Into<OctoBody>>,
1689    ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1690        let (mut parts, body) = request.into_parts();
1691        let body: OctoBody = body.into();
1692        // Saved request that we can retry later if necessary
1693        let auth_header: Option<HeaderValue> = match self.auth_state {
1694            AuthState::None => None,
1695            AuthState::App(ref app) => Some(
1696                HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1697                    .map_err(http::Error::from)
1698                    .context(HttpSnafu)?,
1699            ),
1700            AuthState::BasicAuth {
1701                ref username,
1702                ref password,
1703            } => {
1704                // Equivalent implementation of: https://github.com/seanmonstar/reqwest/blob/df2b3baadc1eade54b1c22415792b778442673a4/src/util.rs#L3-L23
1705                use base64::prelude::BASE64_STANDARD;
1706                use base64::write::EncoderWriter;
1707
1708                let mut buf = b"Basic ".to_vec();
1709                {
1710                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
1711                    write!(encoder, "{username}:{password}").expect("writing to a Vec never fails");
1712                }
1713                Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"))
1714            }
1715            AuthState::Installation { ref token, .. } => {
1716                let token = if let Some(token) = token.valid_token() {
1717                    token
1718                } else {
1719                    self.request_installation_auth_token().await?
1720                };
1721
1722                Some(
1723                    HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1724                        .map_err(http::Error::from)
1725                        .context(HttpSnafu)?,
1726                )
1727            }
1728            AuthState::AccessToken { ref token } => Some(
1729                HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1730                    .map_err(http::Error::from)
1731                    .context(HttpSnafu)?,
1732            ),
1733        };
1734
1735        if let Some(mut auth_header) = auth_header {
1736            // Only set the auth_header if the authority (host) is api.github.com or empty (destined for
1737            // GitHub). Otherwise, leave it off as we could have been redirected
1738            // away from GitHub (via follow_location_to_data()), and we don't
1739            // want to give our credentials to third-party services.
1740            match parts.uri.authority() {
1741                None => {
1742                    auth_header.set_sensitive(true);
1743                    parts
1744                        .headers
1745                        .insert(http::header::AUTHORIZATION, auth_header);
1746                }
1747                Some(authority) if authority == "api.github.com" => {
1748                    auth_header.set_sensitive(true);
1749                    parts
1750                        .headers
1751                        .insert(http::header::AUTHORIZATION, auth_header);
1752                }
1753                Some(_) => {
1754                    // Don't insert auth header.
1755                }
1756            }
1757        }
1758
1759        let request = http::Request::from_parts(parts, body);
1760
1761        let response = self.send(request).await?;
1762
1763        let status = response.status();
1764        if StatusCode::UNAUTHORIZED == status {
1765            if let AuthState::Installation { ref token, .. } = self.auth_state {
1766                token.clear();
1767            }
1768        }
1769        Ok(response)
1770    }
1771
1772    pub async fn follow_location_to_data(
1773        &self,
1774        response: http::Response<BoxBody<Bytes, Error>>,
1775    ) -> crate::Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1776        if let Some(redirect) = response.headers().get(http::header::LOCATION) {
1777            let location = redirect.to_str().expect("Location URL not valid str");
1778
1779            self._get(location).await
1780        } else {
1781            Ok(response)
1782        }
1783    }
1784
1785    /// Download a file from the given URL with the given content type
1786    ///
1787    /// This is a convenience method that sets the `Accept` header to the given
1788    /// content type and downloads the file into a `Vec<u8>`.
1789    pub async fn download(
1790        &self,
1791        uri: impl TryInto<Uri>,
1792        content_type: impl TryInto<http::HeaderValue>,
1793    ) -> crate::Result<Vec<u8>> {
1794        let uri = uri
1795            .try_into()
1796            .map_err(|_| UriParseError {})
1797            .context(UriParseSnafu)?;
1798        let content_type = content_type
1799            .try_into()
1800            .map_err(|_| UriParseError {})
1801            .context(UriParseSnafu)?;
1802
1803        let mut request = Builder::new().method(Method::GET).uri(uri);
1804        request = request.header(http::header::ACCEPT, content_type);
1805
1806        let request = self.build_request(request, None::<&()>)?;
1807        let response = self.execute(request).await?;
1808
1809        let bytes = response.into_body().collect().await?.to_bytes();
1810        Ok(bytes.to_vec())
1811    }
1812
1813    /// Download a zip file from the given URL into a `Vec<u8>`.
1814    pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1815        self.download(uri, "application/zip").await
1816    }
1817}
1818
1819/// # Utility Methods
1820impl Octocrab {
1821    /// A convenience method to get a page of results (if present).
1822    pub async fn get_page<R: serde::de::DeserializeOwned>(
1823        &self,
1824        uri: &Option<Uri>,
1825    ) -> crate::Result<Option<Page<R>>> {
1826        match uri {
1827            Some(uri) => self.get(uri.to_string(), None::<&()>).await.map(Some),
1828            None => Ok(None),
1829        }
1830    }
1831
1832    /// A convenience method to get all the results starting at a given
1833    /// page.
1834    pub async fn all_pages<R: serde::de::DeserializeOwned>(
1835        &self,
1836        mut page: Page<R>,
1837    ) -> crate::Result<Vec<R>> {
1838        let mut ret = page.take_items();
1839        while let Some(mut next_page) = self.get_page(&page.next).await? {
1840            ret.append(&mut next_page.take_items());
1841            page = next_page;
1842        }
1843        Ok(ret)
1844    }
1845}
1846
1847#[cfg(test)]
1848mod tests {
1849    // tokio runtime seems to be needed for tower: https://users.rust-lang.org/t/no-reactor-running-when-calling-runtime-spawn/81256
1850    #[tokio::test]
1851    async fn parametrize_uri_valid() {
1852        //Previously, invalid characters were handled by url lib's parse function.
1853        //Todo: should we handle encoding of uri routes ourselves?
1854        let uri = crate::instance()
1855            .parameterized_uri("/help%20world", None::<&()>)
1856            .unwrap();
1857        assert_eq!(uri.path(), "/help%20world");
1858    }
1859
1860    #[tokio::test]
1861    async fn extra_headers() {
1862        use http::header::HeaderName;
1863        use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
1864        let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
1865        let mock_server = MockServer::start().await;
1866        Mock::given(matchers::method("GET"))
1867            .and(matchers::path_regex(".*"))
1868            .and(matchers::header("x-test1", "hello"))
1869            .and(matchers::header("x-test2", "goodbye"))
1870            .respond_with(response)
1871            .expect(1)
1872            .mount(&mock_server)
1873            .await;
1874        crate::OctocrabBuilder::default()
1875            .base_uri(mock_server.uri())
1876            .unwrap()
1877            .add_header(HeaderName::from_static("x-test1"), "hello".to_string())
1878            .add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
1879            .build()
1880            .unwrap()
1881            .repos("XAMPPRocky", "octocrab")
1882            .events()
1883            .send()
1884            .await
1885            .unwrap();
1886    }
1887
1888    use super::*;
1889    use chrono::Duration;
1890
1891    #[test]
1892    fn clear_token() {
1893        let cache = CachedToken(RwLock::new(None));
1894        cache.set("secret".to_string(), None);
1895        cache.clear();
1896
1897        assert!(cache.valid_token().is_none(), "Token was not cleared.");
1898    }
1899
1900    #[test]
1901    fn no_token_when_expired() {
1902        let cache = CachedToken(RwLock::new(None));
1903        let expiration = Utc::now() + Duration::seconds(9);
1904        cache.set("secret".to_string(), Some(expiration));
1905
1906        assert!(
1907            cache
1908                .valid_token_with_buffer(Duration::seconds(10))
1909                .is_none(),
1910            "Token should be considered expired due to buffer."
1911        );
1912    }
1913
1914    #[test]
1915    fn get_valid_token_outside_buffer() {
1916        let cache = CachedToken(RwLock::new(None));
1917        let expiration = Utc::now() + Duration::seconds(12);
1918        cache.set("secret".to_string(), Some(expiration));
1919
1920        assert!(
1921            cache
1922                .valid_token_with_buffer(Duration::seconds(10))
1923                .is_some(),
1924            "Token should still be valid outside of buffer."
1925        );
1926    }
1927
1928    #[test]
1929    fn get_valid_token_without_expiration() {
1930        let cache = CachedToken(RwLock::new(None));
1931        cache.set("secret".to_string(), None);
1932
1933        assert!(
1934            cache
1935                .valid_token_with_buffer(Duration::seconds(10))
1936                .is_some(),
1937            "Token with no expiration should always be considered valid."
1938        );
1939    }
1940}