1#![cfg_attr(test, recursion_limit = "512")]
211#![cfg_attr(docsrs, feature(doc_cfg))]
212
213mod api;
214mod body;
215mod error;
216mod from_response;
217mod page;
218
219pub mod auth;
220pub mod etag;
221pub mod models;
222pub mod params;
223pub mod service;
224
225use api::repos::RepoRef;
226use api::users::UserRef;
227pub use body::OctoBody;
228use chrono::{DateTime, Utc};
229use http::{HeaderMap, HeaderValue, Method, Uri};
230use http_body_util::combinators::BoxBody;
231use http_body_util::BodyExt;
232use service::middleware::auth_header::AuthHeaderLayer;
233use service::middleware::cache::{CacheStorage, HttpCacheLayer};
234use std::convert::{Infallible, TryInto};
235use std::fmt;
236use std::future::Future;
237use std::io::Write;
238use std::marker::PhantomData;
239use std::pin::Pin;
240use std::str::FromStr;
241use std::sync::{Arc, RwLock};
242use web_time::Duration;
243
244use http::{header::HeaderName, StatusCode};
245use hyper::{Request, Response};
246
247use secrecy::{ExposeSecret, SecretString};
248use serde::{Deserialize, Serialize};
249use snafu::*;
250use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceExt};
251
252use bytes::Bytes;
253use http::header::USER_AGENT;
254use http::request::Builder;
255#[cfg(feature = "opentls")]
256use hyper_tls::HttpsConnector;
257
258#[cfg(feature = "rustls")]
259use hyper_rustls::HttpsConnectorBuilder;
260
261#[cfg(feature = "retry")]
262use tower::retry::{Retry, RetryLayer};
263
264#[cfg(feature = "timeout")]
265use hyper_timeout::TimeoutConnector;
266
267use tower_http::{classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer};
268
269#[cfg(feature = "tracing")]
270use {tower_http::trace::TraceLayer, tracing::Span};
271
272use crate::api::codes_of_conduct;
273use crate::error::{
274 HttpSnafu, HyperSnafu, InvalidUtf8Snafu, SerdeSnafu, SerdeUrlEncodedSnafu, ServiceSnafu,
275 UriParseError, UriParseSnafu, UriSnafu,
276};
277
278use crate::service::middleware::base_uri::BaseUriLayer;
279use crate::service::middleware::extra_headers::ExtraHeadersLayer;
280
281#[cfg(feature = "retry")]
282use crate::service::middleware::retry::RetryConfig;
283
284use auth::{AppAuth, Auth};
285use models::{AppId, InstallationId, InstallationToken, RepositoryId, UserId};
286
287pub use self::{
288 api::{
289 actions, activity, apps, checks, classroom, code_scannings, commits, current, events,
290 gists, gitignore, hooks, issues, licenses, markdown, orgs, projects, pulls, ratelimit,
291 repos, search, teams, users, workflows,
292 },
293 error::{Error, GitHubError},
294 from_response::FromResponse,
295 page::Page,
296};
297
298#[cfg(all(feature = "jwt-rust-crypto", feature = "jwt-aws-lc-rs"))]
299compile_error!(
300 "feature \"jwt-rust-crypto\" and feature \"jwt-aws-lc-rs\" cannot be enabled at the same time"
301);
302
303#[cfg(not(any(feature = "jwt-rust-crypto", feature = "jwt-aws-lc-rs")))]
304compile_error!("at least one of the features \"jwt-rust-crypto\" and feature \"jwt-aws-lc-rs\" must be enabled");
305
306pub type Result<T, E = error::Error> = std::result::Result<T, E>;
308
309const GITHUB_BASE_URI: &str = "https://api.github.com";
310const GITHUB_BASE_UPLOAD_URI: &str = "https://uploads.github.com";
311
312include!(concat!(env!("OUT_DIR"), "/headers_metadata.rs"));
320
321#[cfg(feature = "default-client")]
322static STATIC_INSTANCE: std::sync::LazyLock<arc_swap::ArcSwap<Octocrab>> =
323 std::sync::LazyLock::new(|| arc_swap::ArcSwap::from_pointee(Octocrab::default()));
324
325pub fn format_preview(preview: impl AsRef<str>) -> String {
331 format!("application/vnd.github.{}-preview", preview.as_ref())
332}
333
334pub fn format_media_type(media_type: impl AsRef<str>) -> String {
342 let media_type = media_type.as_ref();
343 let json_suffix = match media_type {
344 "raw" | "text" | "html" | "full" => "+json",
345 _ => "",
346 };
347
348 format!("application/vnd.github.v3.{media_type}{json_suffix}")
349}
350
351#[derive(Debug, Deserialize)]
352struct GitHubErrorBody {
353 pub documentation_url: Option<String>,
354 pub errors: Option<Vec<serde_json::Value>>,
355 pub message: String,
356}
357
358pub async fn map_github_error(
361 response: http::Response<BoxBody<Bytes, crate::Error>>,
362) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
363 if response.status().is_success() {
364 Ok(response)
365 } else {
366 let (parts, body) = response.into_parts();
367 let GitHubErrorBody {
368 documentation_url,
369 errors,
370 message,
371 } = serde_json::from_slice(body.collect().await?.to_bytes().as_ref())
372 .context(error::SerdeSnafu)?;
373
374 Err(error::Error::GitHub {
375 source: Box::new(GitHubError {
376 status_code: parts.status,
377 documentation_url,
378 errors,
379 message,
380 }),
381 backtrace: Backtrace::capture(),
382 })
383 }
384}
385
386#[cfg(feature = "default-client")]
396#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
397pub fn initialise(crab: Octocrab) -> Arc<Octocrab> {
398 STATIC_INSTANCE.swap(Arc::from(crab))
399}
400
401#[cfg(feature = "default-client")]
410#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
411pub fn instance() -> Arc<Octocrab> {
412 STATIC_INSTANCE.load().clone()
413}
414
415type Executor = Box<dyn Fn(Pin<Box<dyn Future<Output = ()>>>)>;
416
417pub struct OctocrabBuilder<Svc, Config, Auth, LayerReady> {
433 service: Svc,
434 auth: Auth,
435 config: Config,
436 _layer_ready: PhantomData<LayerReady>,
437 executor: Option<Executor>,
438}
439
440pub struct NoConfig {}
442
443pub struct NoSvc {}
445
446pub struct NotLayerReady {}
448pub struct LayerReady {}
449
450pub struct NoAuth {}
452
453impl OctocrabBuilder<NoSvc, NoConfig, NoAuth, NotLayerReady> {
454 pub fn new_empty() -> Self {
455 OctocrabBuilder {
456 service: NoSvc {},
457 auth: NoAuth {},
458 config: NoConfig {},
459 _layer_ready: PhantomData,
460 executor: None,
461 }
462 }
463}
464
465impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
466 pub fn new() -> Self {
467 OctocrabBuilder::default()
468 }
469}
470
471impl<Config, Auth> OctocrabBuilder<NoSvc, Config, Auth, NotLayerReady> {
472 pub fn with_service<Svc>(self, service: Svc) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
473 OctocrabBuilder {
474 service,
475 auth: self.auth,
476 config: self.config,
477 _layer_ready: PhantomData,
478 executor: None,
479 }
480 }
481}
482
483impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
484where
485 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
486 Svc::Future: Send + 'static,
487 Svc::Error: Into<BoxError>,
488 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
489 B::Error: Into<BoxError>,
490{
491 pub fn with_executor(
492 self,
493 executor: Executor,
494 ) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
495 OctocrabBuilder {
496 service: self.service,
497 auth: self.auth,
498 config: self.config,
499 _layer_ready: PhantomData,
500 executor: Some(executor),
501 }
502 }
503}
504
505impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
506where
507 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
508 Svc::Future: Send + 'static,
509 Svc::Error: Into<BoxError>,
510 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
511 B::Error: Into<BoxError>,
512{
513 pub fn with_layer<L: Layer<Svc>>(
515 self,
516 layer: &L,
517 ) -> OctocrabBuilder<L::Service, Config, Auth, LayerReady> {
518 let Self {
519 service: stack,
520 auth,
521 config,
522 executor,
523 ..
524 } = self;
525 OctocrabBuilder {
526 service: layer.layer(stack),
527 auth,
528 config,
529 executor,
530 _layer_ready: PhantomData,
531 }
532 }
533}
534
535impl Default for OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
536 fn default() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
537 OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
538 }
539}
540
541impl<Svc, Auth, LayerState> OctocrabBuilder<Svc, NoConfig, Auth, LayerState> {
542 fn with_config<Config>(self, config: Config) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
543 OctocrabBuilder {
544 service: self.service,
545 auth: self.auth,
546 executor: self.executor,
547 config,
548 _layer_ready: PhantomData,
549 }
550 }
551}
552
553impl<Svc, B, LayerState> OctocrabBuilder<Svc, NoConfig, AuthState, LayerState>
554where
555 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
556 Svc::Future: Send + 'static,
557 Svc::Error: Into<BoxError>,
558 B: http_body::Body<Data = bytes::Bytes> + Send + Sync + 'static,
559 B::Error: Into<BoxError>,
560{
561 pub fn build(self) -> Result<Octocrab, Infallible> {
563 let service = MapResponseBodyLayer::new(|b: B| {
565 b.map_err(|e| ServiceSnafu.into_error(e.into())).boxed()
566 })
567 .layer(self.service)
568 .map_err(|e| e.into());
569
570 if let Some(executor) = self.executor {
571 return Ok(Octocrab::new_with_executor(service, self.auth, executor));
572 }
573
574 Ok(Octocrab::new(service, self.auth))
575 }
576}
577
578impl<Svc, Config, LayerState> OctocrabBuilder<Svc, Config, NoAuth, LayerState> {
579 pub fn with_auth<Auth>(self, auth: Auth) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
580 OctocrabBuilder {
581 service: self.service,
582 auth,
583 config: self.config,
584 executor: self.executor,
585 _layer_ready: PhantomData,
586 }
587 }
588}
589
590#[cfg(all(feature = "rustls", not(feature = "opentls")))]
591fn default_rustls_crypto_provider() -> Arc<rustls::crypto::CryptoProvider> {
592 #[cfg(feature = "rustls-aws-lc-rs")]
593 {
594 Arc::new(rustls::crypto::aws_lc_rs::default_provider())
595 }
596 #[cfg(all(feature = "rustls-ring", not(feature = "rustls-aws-lc-rs")))]
597 {
598 Arc::new(rustls::crypto::ring::default_provider())
599 }
600 #[cfg(not(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring")))]
601 {
602 compile_error!(
603 "the `rustls` feature requires one of the `rustls-ring` or `rustls-aws-lc-rs` features to be enabled"
604 )
605 }
606}
607
608impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
609 #[cfg(feature = "retry")]
611 #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
612 pub fn add_retry_config(mut self, retry_config: RetryConfig) -> Self {
613 self.config.retry_config = retry_config;
614 self
615 }
616
617 #[cfg(feature = "timeout")]
619 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
620 pub fn set_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
621 self.config.connect_timeout = timeout;
622 self
623 }
624
625 #[cfg(feature = "timeout")]
627 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
628 pub fn set_read_timeout(mut self, timeout: Option<Duration>) -> Self {
629 self.config.read_timeout = timeout;
630 self
631 }
632
633 #[cfg(feature = "timeout")]
635 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
636 pub fn set_write_timeout(mut self, timeout: Option<Duration>) -> Self {
637 self.config.write_timeout = timeout;
638 self
639 }
640
641 pub fn add_preview(mut self, preview: &'static str) -> Self {
643 self.config.previews.push(preview);
644 self
645 }
646
647 pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
649 self.config.extra_headers.push((key, value));
650 self
651 }
652
653 pub fn personal_token<S: Into<SecretString>>(mut self, token: S) -> Self {
655 self.config.auth = Auth::PersonalToken(token.into());
656 self
657 }
658
659 pub fn app(mut self, app_id: AppId, key: jsonwebtoken::EncodingKey) -> Self {
662 self.config.auth = Auth::App(AppAuth { app_id, key });
663 self
664 }
665
666 pub fn basic_auth(mut self, username: String, password: String) -> Self {
669 self.config.auth = Auth::Basic { username, password };
670 self
671 }
672
673 pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
675 self.config.auth = Auth::OAuth(oauth);
676 self
677 }
678
679 pub fn user_access_token<S: Into<SecretString>>(mut self, token: S) -> Self {
681 self.config.auth = Auth::UserAccessToken(token.into());
682 self
683 }
684
685 pub fn base_uri(mut self, base_uri: impl TryInto<Uri>) -> Result<Self> {
687 self.config.base_uri = Some(
688 base_uri
689 .try_into()
690 .map_err(|_| UriParseError {})
691 .context(UriParseSnafu)?,
692 );
693 Ok(self)
694 }
695
696 pub fn upload_uri(mut self, upload_uri: impl TryInto<Uri>) -> Result<Self> {
698 self.config.upload_uri = Some(
699 upload_uri
700 .try_into()
701 .map_err(|_| UriParseError {})
702 .context(UriParseSnafu)?,
703 );
704 Ok(self)
705 }
706
707 pub fn cache<C>(mut self, cache: C) -> Self
708 where
709 C: CacheStorage + 'static,
710 {
711 self.config.cache_storage = Some(Arc::new(cache));
712 self
713 }
714
715 #[cfg(feature = "retry")]
716 #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
717 pub fn set_connector_retry_service<S>(
718 &self,
719 connector: hyper_util::client::legacy::Client<S, OctoBody>,
720 ) -> Retry<RetryConfig, hyper_util::client::legacy::Client<S, OctoBody>> {
721 let retry_layer = RetryLayer::new(self.config.retry_config.clone());
722
723 retry_layer.layer(connector)
724 }
725
726 #[cfg(feature = "timeout")]
727 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
728 pub fn set_connect_timeout_service<T>(&self, connector: T) -> TimeoutConnector<T>
729 where
730 T: Service<Uri> + Send,
731 T::Response: hyper::rt::Read + hyper::rt::Write + Send + Unpin,
732 T::Future: Send + 'static,
733 T::Error: Into<BoxError>,
734 {
735 let mut connector = TimeoutConnector::new(connector);
736 connector.set_connect_timeout(self.config.connect_timeout);
738 connector.set_read_timeout(self.config.read_timeout);
739 connector.set_write_timeout(self.config.write_timeout);
740 connector
741 }
742
743 #[cfg(feature = "default-client")]
745 #[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
746 pub fn build(self) -> Result<Octocrab> {
747 let client: hyper_util::client::legacy::Client<_, OctoBody> = {
748 #[cfg(all(not(feature = "opentls"), not(feature = "rustls")))]
749 let mut connector = hyper::client::conn::http1::HttpConnector::new();
750
751 #[cfg(all(feature = "rustls", not(feature = "opentls")))]
752 let connector = {
753 let builder = HttpsConnectorBuilder::new();
754 let provider = rustls::crypto::CryptoProvider::get_default()
757 .map(|arc| arc.clone())
758 .unwrap_or_else(default_rustls_crypto_provider);
759 #[cfg(feature = "rustls-webpki-tokio")]
760 let builder = builder
761 .with_provider_and_webpki_roots(provider)
762 .map_err(Into::into)
763 .context(error::OtherSnafu)?;
764 #[cfg(not(feature = "rustls-webpki-tokio"))]
765 let builder = builder
766 .with_provider_and_native_roots(provider)
767 .map_err(Into::into)
768 .context(error::OtherSnafu)?; builder
771 .https_or_http() .enable_http1()
773 .build()
774 };
775
776 #[cfg(all(feature = "opentls", not(feature = "rustls")))]
777 let connector = HttpsConnector::new();
778
779 #[cfg(feature = "timeout")]
780 let connector = self.set_connect_timeout_service(connector);
781
782 hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
783 .build(connector)
784 };
785
786 #[cfg(feature = "retry")]
787 let client = self.set_connector_retry_service(client);
788
789 #[cfg(feature = "tracing")]
790 let client = TraceLayer::new_for_http()
791 .make_span_with(|req: &Request<OctoBody>| {
792 tracing::debug_span!(
793 "HTTP",
794 http.method = %req.method(),
795 http.url = %req.uri(),
796 http.status_code = tracing::field::Empty,
797 otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
798 otel.kind = "client",
799 otel.status_code = tracing::field::Empty,
800 )
801 })
802 .on_request(|_req: &Request<OctoBody>, _span: &Span| {
803 tracing::debug!("requesting");
804 })
805 .on_response(
806 |res: &Response<hyper::body::Incoming>, _latency: Duration, span: &Span| {
807 let status = res.status();
808 span.record("http.status_code", status.as_u16());
809 if status.is_client_error() || status.is_server_error() {
810 span.record("otel.status_code", "ERROR");
811 }
812 },
813 )
814 .on_body_chunk(())
816 .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
817 tracing::debug!("stream closed");
818 })
819 .on_failure(
820 |ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
821 span.record("otel.status_code", "ERROR");
827 match ec {
828 ServerErrorsFailureClass::StatusCode(status) => {
829 span.record("http.status_code", status.as_u16());
830 tracing::error!("failed with status {}", status)
831 }
832 ServerErrorsFailureClass::Error(err) => {
833 tracing::error!("failed with error {}", err)
834 }
835 }
836 },
837 )
838 .layer(client);
839
840 #[cfg(feature = "follow-redirect")]
841 let client = tower_http::follow_redirect::FollowRedirectLayer::new().layer(client);
842
843 let mut hmap: Vec<(HeaderName, HeaderValue)> = vec![];
844
845 hmap.push((USER_AGENT, HeaderValue::from_str("octocrab").unwrap()));
847
848 for preview in &self.config.previews {
849 hmap.push((
850 http::header::ACCEPT,
851 HeaderValue::from_str(crate::format_preview(preview).as_str()).unwrap(),
852 ));
853 }
854
855 let (auth_header, auth_state): (Option<HeaderValue>, _) = match self.config.auth {
856 Auth::None => (None, AuthState::None),
857 Auth::Basic { username, password } => {
858 (None, AuthState::BasicAuth { username, password })
859 }
860 Auth::PersonalToken(token) => (
861 Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
862 AuthState::None,
863 ),
864 Auth::UserAccessToken(token) => (
865 Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
866 AuthState::None,
867 ),
868 Auth::App(app_auth) => (None, AuthState::App(app_auth)),
869 Auth::OAuth(device) => (
870 Some(
871 format!(
872 "{} {}",
873 device.token_type,
874 &device.access_token.expose_secret()
875 )
876 .parse()
877 .unwrap(),
878 ),
879 AuthState::None,
880 ),
881 };
882
883 for (key, value) in self.config.extra_headers.iter() {
884 hmap.push((
885 key.clone(),
886 HeaderValue::from_str(value.as_str())
887 .map_err(http::Error::from)
888 .context(HttpSnafu)?,
889 ));
890 }
891
892 let client = ExtraHeadersLayer::new(Arc::new(hmap)).layer(client);
893
894 let client = MapResponseBodyLayer::new(|body| {
895 BodyExt::map_err(body, |e| HyperSnafu.into_error(e)).boxed()
896 })
897 .layer(client);
898
899 let base_uri = self
900 .config
901 .base_uri
902 .clone()
903 .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_URI).unwrap());
904
905 let upload_uri = self
906 .config
907 .upload_uri
908 .clone()
909 .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_UPLOAD_URI).unwrap());
910
911 let client = BaseUriLayer::new(base_uri.clone()).layer(client);
912
913 let client = AuthHeaderLayer::new(auth_header, base_uri, upload_uri).layer(client);
914
915 let client = HttpCacheLayer::new(self.config.cache_storage.clone()).layer(client);
916
917 if let Some(executor) = self.executor {
918 return Ok(Octocrab::new_with_executor(client, auth_state, executor));
919 }
920
921 Ok(Octocrab::new(client, auth_state))
922 }
923}
924
925pub struct DefaultOctocrabBuilderConfig {
926 auth: Auth,
927 previews: Vec<&'static str>,
928 extra_headers: Vec<(HeaderName, String)>,
929 #[cfg(feature = "timeout")]
930 connect_timeout: Option<Duration>,
931 #[cfg(feature = "timeout")]
932 read_timeout: Option<Duration>,
933 #[cfg(feature = "timeout")]
934 write_timeout: Option<Duration>,
935 base_uri: Option<Uri>,
936 upload_uri: Option<Uri>,
937 #[cfg(feature = "retry")]
938 retry_config: RetryConfig,
939 cache_storage: Option<Arc<dyn CacheStorage>>,
940}
941
942impl Default for DefaultOctocrabBuilderConfig {
943 fn default() -> Self {
944 Self {
945 auth: Auth::None,
946 previews: Vec::new(),
947 extra_headers: Vec::new(),
948 #[cfg(feature = "timeout")]
949 connect_timeout: None,
950 #[cfg(feature = "timeout")]
951 read_timeout: None,
952 #[cfg(feature = "timeout")]
953 write_timeout: None,
954 base_uri: None,
955 upload_uri: None,
956 #[cfg(feature = "retry")]
957 retry_config: RetryConfig::Simple(3),
958 cache_storage: None,
959 }
960 }
961}
962
963impl DefaultOctocrabBuilderConfig {
964 pub fn new() -> Self {
965 Self::default()
966 }
967}
968
969#[derive(Debug, Clone)]
970struct CachedTokenInner {
971 expiration: Option<DateTime<Utc>>,
972 secret: SecretString,
973}
974
975impl CachedTokenInner {
976 fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self {
977 Self { secret, expiration }
978 }
979
980 fn expose_secret(&self) -> &str {
981 self.secret.expose_secret()
982 }
983}
984
985pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
987
988impl CachedToken {
989 fn clear(&self) {
990 *self.0.write().unwrap() = None;
991 }
992
993 fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> {
995 let inner = self.0.read().unwrap();
996
997 if let Some(token) = inner.as_ref() {
998 if let Some(exp) = token.expiration {
999 if exp - Utc::now() > buffer {
1000 return Some(token.secret.clone());
1001 }
1002 } else {
1003 return Some(token.secret.clone());
1004 }
1005 }
1006
1007 None
1008 }
1009
1010 fn valid_token(&self) -> Option<SecretString> {
1011 self.valid_token_with_buffer(chrono::Duration::seconds(30))
1012 }
1013
1014 fn set<S: Into<SecretString>>(&self, token: S, expiration: Option<DateTime<Utc>>) {
1015 *self.0.write().unwrap() = Some(CachedTokenInner::new(token.into(), expiration));
1016 }
1017}
1018
1019impl fmt::Debug for CachedToken {
1020 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021 self.0.read().unwrap().fmt(f)
1022 }
1023}
1024
1025impl fmt::Display for CachedToken {
1026 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1027 let option = self.0.read().unwrap();
1028 option
1029 .as_ref()
1030 .map(|s| s.expose_secret().fmt(f))
1031 .unwrap_or_else(|| write!(f, "<none>"))
1032 }
1033}
1034
1035impl Clone for CachedToken {
1036 fn clone(&self) -> CachedToken {
1037 CachedToken(RwLock::new(self.0.read().unwrap().clone()))
1038 }
1039}
1040
1041impl Default for CachedToken {
1042 fn default() -> CachedToken {
1043 CachedToken(RwLock::new(None))
1044 }
1045}
1046
1047#[derive(Debug, Clone)]
1049pub enum AuthState {
1050 None,
1053 BasicAuth {
1055 username: String,
1057 password: String,
1059 },
1060 App(AppAuth),
1062 Installation {
1064 app: AppAuth,
1066 installation: InstallationId,
1068 token: CachedToken,
1070 },
1071 AccessToken {
1073 token: SecretString,
1075 },
1076}
1077
1078pub type OctocrabService = Buffer<
1079 http::Request<OctoBody>,
1080 <BoxService<http::Request<OctoBody>, http::Response<BoxBody<Bytes, Error>>, BoxError> as tower::Service<http::Request<OctoBody>>>::Future
1081>;
1082
1083#[derive(Clone)]
1085pub struct Octocrab {
1086 client: OctocrabService,
1087 auth_state: AuthState,
1088}
1089
1090impl fmt::Debug for Octocrab {
1091 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092 f.debug_struct("Octocrab")
1093 .field("auth_state", &self.auth_state)
1094 .finish()
1095 }
1096}
1097
1098#[cfg(feature = "default-client")]
1103#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
1104impl Default for Octocrab {
1105 fn default() -> Self {
1106 OctocrabBuilder::default().build().unwrap()
1107 }
1108}
1109
1110impl Octocrab {
1112 pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1114 {
1115 OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1116 }
1117
1118 fn new<S>(service: S, auth_state: AuthState) -> Self
1120 where
1121 S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1122 + Send
1123 + 'static,
1124 S::Future: Send + 'static,
1125 S::Error: Into<BoxError>,
1126 {
1127 let service = Buffer::new(BoxService::new(service.map_err(Into::into)), 1024);
1128
1129 Self {
1130 client: service,
1131 auth_state,
1132 }
1133 }
1134
1135 fn new_with_executor<S>(service: S, auth_state: AuthState, executor: Executor) -> Self
1137 where
1138 S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1139 + Send
1140 + 'static,
1141 S::Future: Send + 'static,
1142 S::Error: Into<BoxError>,
1143 {
1144 let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1146
1147 executor(Box::pin(worker));
1149
1150 Self {
1151 client: service,
1152 auth_state,
1153 }
1154 }
1155
1156 pub fn installation(&self, id: InstallationId) -> Result<Octocrab> {
1164 let app_auth = if let AuthState::App(ref app_auth) = self.auth_state {
1165 app_auth.clone()
1166 } else {
1167 return Err(Error::Installation {
1168 backtrace: Backtrace::capture(),
1169 });
1170 };
1171 Ok(Octocrab {
1172 client: self.client.clone(),
1173 auth_state: AuthState::Installation {
1174 app: app_auth,
1175 installation: id,
1176 token: CachedToken::default(),
1177 },
1178 })
1179 }
1180
1181 pub async fn installation_and_token(
1188 &self,
1189 id: InstallationId,
1190 ) -> Result<(Octocrab, SecretString)> {
1191 let crab = self.installation(id)?;
1192 let token = crab.request_installation_auth_token().await?;
1193 Ok((crab, token))
1194 }
1195
1196 pub async fn installation_token(&self) -> Result<SecretString> {
1201 self.installation_token_with_buffer(chrono::Duration::seconds(30))
1202 .await
1203 }
1204
1205 pub async fn installation_token_with_buffer(
1210 &self,
1211 buffer: chrono::Duration,
1212 ) -> Result<SecretString> {
1213 let token = if let AuthState::Installation { ref token, .. } = self.auth_state {
1214 token
1215 } else {
1216 return Err(Error::InstallationTokenInvalidAuth {
1217 backtrace: Backtrace::capture(),
1218 });
1219 };
1220
1221 let token = match token.valid_token_with_buffer(buffer) {
1222 Some(token) => token,
1223 None => self.request_installation_auth_token().await?,
1224 };
1225
1226 Ok(token)
1227 }
1228
1229 pub fn user_access_token<S: Into<SecretString>>(&self, token: S) -> Result<Self> {
1234 Ok(Octocrab {
1235 client: self.client.clone(),
1236 auth_state: AuthState::AccessToken {
1237 token: token.into(),
1238 },
1239 })
1240 }
1241}
1242
1243impl Octocrab {
1245 pub fn actions(&self) -> actions::ActionsHandler<'_> {
1248 actions::ActionsHandler::new(self)
1249 }
1250
1251 pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1254 current::CurrentAuthHandler::new(self)
1255 }
1256
1257 pub fn activity(&self) -> activity::ActivityHandler<'_> {
1259 activity::ActivityHandler::new(self)
1260 }
1261
1262 pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1264 apps::AppsRequestHandler::new(self)
1265 }
1266
1267 pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1270 gitignore::GitignoreHandler::new(self)
1271 }
1272
1273 pub fn issues(
1276 &self,
1277 owner: impl Into<String>,
1278 repo: impl Into<String>,
1279 ) -> issues::IssueHandler<'_> {
1280 issues::IssueHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1281 }
1282
1283 pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1286 issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1287 }
1288
1289 pub fn code_scannings(
1292 &self,
1293 owner: impl Into<String>,
1294 repo: impl Into<String>,
1295 ) -> code_scannings::CodeScanningHandler<'_> {
1296 code_scannings::CodeScanningHandler::new(self, owner.into(), Option::from(repo.into()))
1297 }
1298
1299 pub fn code_scannings_organisation(
1302 &self,
1303 owner: impl Into<String>,
1304 ) -> code_scannings::CodeScanningHandler<'_> {
1305 code_scannings::CodeScanningHandler::new(self, owner.into(), None)
1306 }
1307
1308 pub fn commits(
1310 &self,
1311 owner: impl Into<String>,
1312 repo: impl Into<String>,
1313 ) -> commits::CommitHandler<'_> {
1314 commits::CommitHandler::new(self, owner.into(), repo.into())
1315 }
1316
1317 pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1319 licenses::LicenseHandler::new(self)
1320 }
1321
1322 pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1324 markdown::MarkdownHandler::new(self)
1325 }
1326
1327 pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1330 orgs::OrgHandler::new(self, owner.into())
1331 }
1332
1333 pub fn pulls(
1336 &self,
1337 owner: impl Into<String>,
1338 repo: impl Into<String>,
1339 ) -> pulls::PullRequestHandler<'_> {
1340 pulls::PullRequestHandler::new(self, owner.into(), repo.into())
1341 }
1342
1343 pub fn repos(
1346 &self,
1347 owner: impl Into<String>,
1348 repo: impl Into<String>,
1349 ) -> repos::RepoHandler<'_> {
1350 repos::RepoHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1351 }
1352
1353 pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1356 repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1357 }
1358
1359 pub fn projects(&self) -> projects::ProjectHandler<'_> {
1362 projects::ProjectHandler::new(self)
1363 }
1364
1365 pub fn search(&self) -> search::SearchHandler<'_> {
1368 search::SearchHandler::new(self)
1369 }
1370
1371 pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1374 teams::TeamHandler::new(self, owner.into())
1375 }
1376
1377 pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1379 users::UserHandler::new(self, UserRef::ByString(user.into()))
1380 }
1381
1382 pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1384 users::UserHandler::new(self, UserRef::ById(user.into()))
1385 }
1386
1387 pub fn workflows(
1390 &self,
1391 owner: impl Into<String>,
1392 repo: impl Into<String>,
1393 ) -> workflows::WorkflowsHandler<'_> {
1394 workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
1395 }
1396
1397 pub fn events(&self) -> events::EventsBuilder<'_> {
1400 events::EventsBuilder::new(self)
1401 }
1402
1403 pub fn gists(&self) -> gists::GistsHandler<'_> {
1406 gists::GistsHandler::new(self)
1407 }
1408
1409 pub fn checks(
1411 &self,
1412 owner: impl Into<String>,
1413 repo: impl Into<String>,
1414 ) -> checks::ChecksHandler<'_> {
1415 checks::ChecksHandler::new(self, owner.into(), repo.into())
1416 }
1417
1418 pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1420 ratelimit::RateLimitHandler::new(self)
1421 }
1422
1423 pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1425 hooks::HooksHandler::new(self, owner.into())
1426 }
1427
1428 pub fn assignments(&self) -> classroom::AssignmentsHandler<'_> {
1430 classroom::AssignmentsHandler::new(self)
1431 }
1432
1433 pub fn classrooms(&self) -> classroom::ClassroomHandler<'_> {
1435 classroom::ClassroomHandler::new(self)
1436 }
1437
1438 pub fn codes_of_conduct(&self) -> codes_of_conduct::CodesOfConductHandler<'_> {
1440 codes_of_conduct::CodesOfConductHandler::new(self)
1441 }
1442}
1443
1444impl Octocrab {
1446 pub async fn graphql<R: serde::de::DeserializeOwned>(
1457 &self,
1458 payload: &(impl serde::Serialize + ?Sized),
1459 ) -> crate::Result<R> {
1460 let response: GraphqlResponse<R> = self
1461 .post("/graphql", Some(&serde_json::json!(payload)))
1462 .await?;
1463
1464 match response {
1465 GraphqlResponse::Ok(res) => Ok(res.data),
1466 GraphqlResponse::Err(errors) => Err(error::Error::Graphql {
1467 source: errors.errors.into(),
1468 backtrace: Backtrace::capture(),
1469 }),
1470 }
1471 }
1472}
1473
1474#[derive(Serialize, Deserialize, Debug)]
1477#[serde(untagged)]
1478pub enum GraphqlResponse<T> {
1479 Err(GraphqlErrorResponse<T>),
1481 Ok(GraphqlOkResponse<T>),
1483}
1484
1485#[derive(Serialize, Deserialize, Debug)]
1486pub struct GraphqlOkResponse<T> {
1487 pub data: T,
1488}
1489
1490#[derive(Serialize, Deserialize, Debug)]
1491pub struct GraphqlErrorResponse<T> {
1492 pub data: Option<T>,
1494 pub errors: Vec<GraphqlError>,
1496}
1497
1498#[derive(Serialize, Deserialize, Debug)]
1501pub struct GraphqlError {
1502 pub message: String,
1504 pub locations: Option<Vec<GraphqlErrorLocation>>,
1507 pub path: Option<Vec<GraphqlPathSegment>>,
1510 pub extensions: Option<serde_json::Value>,
1512}
1513
1514#[derive(Serialize, Deserialize, Debug)]
1515pub struct GraphqlErrorLocation {
1516 pub line: u32,
1517 pub column: u32,
1518}
1519
1520#[derive(Serialize, Deserialize, Debug)]
1522#[serde(untagged)]
1523pub enum GraphqlPathSegment {
1524 Path(String),
1525 Position(usize),
1526}
1527
1528impl Octocrab {
1540 pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
1543 &self,
1544 route: impl AsRef<str>,
1545 body: Option<&P>,
1546 ) -> Result<R> {
1547 let response = self
1548 ._post(self.parameterized_uri(route, None::<&()>)?, body)
1549 .await?;
1550 R::from_response(crate::map_github_error(response).await?).await
1551 }
1552
1553 pub async fn _post<P: Serialize + ?Sized>(
1555 &self,
1556 uri: impl TryInto<http::Uri>,
1557 body: Option<&P>,
1558 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1559 let uri = uri
1560 .try_into()
1561 .map_err(|_| UriParseError {})
1562 .context(UriParseSnafu)?;
1563 let request = Builder::new().method(Method::POST).uri(uri);
1564 let request = self.build_request(request, body)?;
1565 self.execute(request).await
1566 }
1567
1568 pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
1571 where
1572 A: AsRef<str>,
1573 P: Serialize + ?Sized,
1574 R: FromResponse,
1575 {
1576 self.get_with_headers(route, parameters, None).await
1577 }
1578
1579 pub async fn _get(
1581 &self,
1582 uri: impl TryInto<Uri>,
1583 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1584 self._get_with_headers(uri, None).await
1585 }
1586
1587 pub(crate) fn parameterized_uri<A, P>(&self, uri: A, parameters: Option<&P>) -> Result<Uri>
1590 where
1591 A: AsRef<str>,
1592 P: Serialize + ?Sized,
1593 {
1594 let mut uri = uri.as_ref().to_string();
1595 if let Some(parameters) = parameters {
1596 if uri.contains('?') {
1597 uri = format!("{uri}&");
1598 } else {
1599 uri = format!("{uri}?");
1600 }
1601 uri = format!(
1602 "{}{}",
1603 uri,
1604 serde_urlencoded::to_string(parameters)
1605 .context(SerdeUrlEncodedSnafu)?
1606 .as_str()
1607 );
1608 }
1609 let uri = Uri::from_str(uri.as_str()).context(UriSnafu);
1610 uri
1611 }
1612
1613 pub async fn body_to_string(
1614 &self,
1615 res: http::Response<BoxBody<Bytes, crate::Error>>,
1616 ) -> Result<String> {
1617 let body_bytes = res.into_body().collect().await?.to_bytes();
1618 String::from_utf8(body_bytes.to_vec()).context(InvalidUtf8Snafu)
1619 }
1620
1621 pub async fn get_with_headers<R, A, P>(
1624 &self,
1625 route: A,
1626 parameters: Option<&P>,
1627 headers: Option<http::header::HeaderMap>,
1628 ) -> Result<R>
1629 where
1630 A: AsRef<str>,
1631 P: Serialize + ?Sized,
1632 R: FromResponse,
1633 {
1634 let response = self
1635 ._get_with_headers(self.parameterized_uri(route, parameters)?, headers)
1636 .await?;
1637 R::from_response(crate::map_github_error(response).await?).await
1638 }
1639
1640 pub async fn _get_with_headers(
1642 &self,
1643 uri: impl TryInto<Uri>,
1644 headers: Option<http::header::HeaderMap>,
1645 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1646 let uri = uri
1647 .try_into()
1648 .map_err(|_| UriParseError {})
1649 .context(UriParseSnafu)?;
1650 let mut request = Builder::new().method(Method::GET).uri(uri);
1651 if let Some(headers) = headers {
1652 for (key, value) in headers.iter() {
1653 request = request.header(key, value);
1654 }
1655 }
1656 let request = self.build_request(request, None::<&()>)?;
1657 self.execute(request).await
1658 }
1659
1660 pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1663 where
1664 A: AsRef<str>,
1665 B: Serialize + ?Sized,
1666 R: FromResponse,
1667 {
1668 let response = self
1669 ._patch(self.parameterized_uri(route, None::<&()>)?, body)
1670 .await?;
1671 R::from_response(crate::map_github_error(response).await?).await
1672 }
1673
1674 pub async fn _patch<B: Serialize + ?Sized>(
1676 &self,
1677 uri: impl TryInto<Uri>,
1678 body: Option<&B>,
1679 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1680 let uri = uri
1681 .try_into()
1682 .map_err(|_| UriParseError {})
1683 .context(UriParseSnafu)?;
1684 let request = Builder::new().method(Method::PATCH).uri(uri);
1685 let request = self.build_request(request, body)?;
1686 self.execute(request).await
1687 }
1688
1689 pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1692 where
1693 A: AsRef<str>,
1694 B: Serialize + ?Sized,
1695 R: FromResponse,
1696 {
1697 let response = self
1698 ._put(self.parameterized_uri(route, None::<&()>)?, body)
1699 .await?;
1700 R::from_response(crate::map_github_error(response).await?).await
1701 }
1702
1703 pub async fn _put<B: Serialize + ?Sized>(
1705 &self,
1706 uri: impl TryInto<Uri>,
1707 body: Option<&B>,
1708 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1709 let uri = uri
1710 .try_into()
1711 .map_err(|_| UriParseError {})
1712 .context(UriParseSnafu)?;
1713 let request = Builder::new().method(Method::PUT).uri(uri);
1714 let request = self.build_request(request, body)?;
1715 self.execute(request).await
1716 }
1717
1718 pub fn build_request<B: Serialize + ?Sized>(
1719 &self,
1720 mut builder: Builder,
1721 body: Option<&B>,
1722 ) -> Result<http::Request<OctoBody>> {
1723 for kv in _SET_HEADERS_MAP {
1732 builder = builder.header(kv.0, kv.1);
1733 }
1734
1735 if let Some(body) = body {
1736 builder = builder.header(http::header::CONTENT_TYPE, "application/json");
1737 let serialized = serde_json::to_string(body).context(SerdeSnafu)?;
1738 let body: OctoBody = serialized.into();
1739 let request = builder.body(body).context(HttpSnafu)?;
1740 Ok(request)
1741 } else {
1742 Ok(builder
1743 .header(http::header::CONTENT_LENGTH, "0")
1744 .body(OctoBody::empty())
1745 .context(HttpSnafu)?)
1746 }
1747 }
1748
1749 pub async fn delete<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1752 where
1753 A: AsRef<str>,
1754 B: Serialize + ?Sized,
1755 R: FromResponse,
1756 {
1757 let response = self
1758 ._delete(self.parameterized_uri(route, None::<&()>)?, body)
1759 .await?;
1760 R::from_response(crate::map_github_error(response).await?).await
1761 }
1762
1763 pub async fn _delete<B: Serialize + ?Sized>(
1765 &self,
1766 uri: impl TryInto<Uri>,
1767 body: Option<&B>,
1768 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1769 let uri = uri
1770 .try_into()
1771 .map_err(|_| UriParseError {})
1772 .context(UriParseSnafu)?;
1773 let request = self.build_request(Builder::new().method(Method::DELETE).uri(uri), body)?;
1774
1775 self.execute(request).await
1776 }
1777
1778 async fn request_installation_auth_token(&self) -> Result<SecretString> {
1780 let (app, installation, token) = if let AuthState::Installation {
1781 ref app,
1782 installation,
1783 ref token,
1784 } = self.auth_state
1785 {
1786 (app, installation, token)
1787 } else {
1788 return Err(Error::Installation {
1789 backtrace: Backtrace::capture(),
1790 });
1791 };
1792 let mut request = Builder::new();
1793 let mut sensitive_value =
1794 HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1795 .map_err(http::Error::from)
1796 .context(HttpSnafu)?;
1797
1798 let uri = http::Uri::builder()
1799 .path_and_query(format!("/app/installations/{installation}/access_tokens"))
1800 .build()
1801 .context(HttpSnafu)?;
1802
1803 sensitive_value.set_sensitive(true);
1804 request = request
1805 .header(http::header::AUTHORIZATION, sensitive_value)
1806 .method(http::Method::POST)
1807 .uri(uri);
1808 let response = self
1809 .send(request.body("{}".into()).context(HttpSnafu)?)
1810 .await?;
1811 let _status = response.status();
1812
1813 let token_object =
1814 InstallationToken::from_response(crate::map_github_error(response).await?).await?;
1815
1816 let expiration = token_object
1817 .expires_at
1818 .map(|time| {
1819 DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other {
1820 source: Box::new(e),
1821 backtrace: snafu::Backtrace::capture(),
1822 })
1823 })
1824 .transpose()?;
1825
1826 #[cfg(feature = "tracing")]
1827 tracing::debug!("Token expires at: {:?}", expiration);
1828
1829 token.set(token_object.token.clone(), expiration);
1830
1831 Ok(SecretString::from(token_object.token))
1832 }
1833
1834 pub async fn send(
1836 &self,
1837 request: Request<OctoBody>,
1838 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1839 let mut svc = self.client.clone();
1840 let response: Response<BoxBody<Bytes, crate::Error>> = svc
1841 .ready()
1842 .await
1843 .context(ServiceSnafu)?
1844 .call(request)
1845 .await
1846 .context(ServiceSnafu)?;
1847 Ok(response)
1848 }
1859
1860 pub async fn execute(
1862 &self,
1863 request: http::Request<impl Into<OctoBody>>,
1864 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1865 let (mut parts, body) = request.into_parts();
1866 let body: OctoBody = body.into();
1867 let auth_header: Option<HeaderValue> = match self.auth_state {
1869 AuthState::None => None,
1870 AuthState::App(ref app) => Some(
1871 HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1872 .map_err(http::Error::from)
1873 .context(HttpSnafu)?,
1874 ),
1875 AuthState::BasicAuth {
1876 ref username,
1877 ref password,
1878 } => {
1879 use base64::prelude::BASE64_STANDARD;
1881 use base64::write::EncoderWriter;
1882
1883 let mut buf = b"Basic ".to_vec();
1884 {
1885 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
1886 write!(encoder, "{username}:{password}").expect("writing to a Vec never fails");
1887 }
1888 Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"))
1889 }
1890 AuthState::Installation { ref token, .. } => {
1891 let token = if let Some(token) = token.valid_token() {
1892 token
1893 } else {
1894 self.request_installation_auth_token().await?
1895 };
1896
1897 Some(
1898 HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1899 .map_err(http::Error::from)
1900 .context(HttpSnafu)?,
1901 )
1902 }
1903 AuthState::AccessToken { ref token } => Some(
1904 HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1905 .map_err(http::Error::from)
1906 .context(HttpSnafu)?,
1907 ),
1908 };
1909
1910 if let Some(mut auth_header) = auth_header {
1911 match parts.uri.authority() {
1916 None => {
1917 auth_header.set_sensitive(true);
1918 parts
1919 .headers
1920 .insert(http::header::AUTHORIZATION, auth_header);
1921 }
1922 Some(authority) if authority == "api.github.com" => {
1923 auth_header.set_sensitive(true);
1924 parts
1925 .headers
1926 .insert(http::header::AUTHORIZATION, auth_header);
1927 }
1928 Some(_) => {
1929 }
1931 }
1932 }
1933
1934 let request = http::Request::from_parts(parts, body);
1935
1936 let response = self.send(request).await?;
1937
1938 let status = response.status();
1939 if StatusCode::UNAUTHORIZED == status {
1940 if let AuthState::Installation { ref token, .. } = self.auth_state {
1941 token.clear();
1942 }
1943 }
1944 Ok(response)
1945 }
1946
1947 pub async fn follow_location_to_data(
1948 &self,
1949 response: http::Response<BoxBody<Bytes, Error>>,
1950 ) -> crate::Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1951 if let Some(redirect) = response.headers().get(http::header::LOCATION) {
1952 let location = redirect.to_str().expect("Location URL not valid str");
1953
1954 self._get(location).await
1955 } else {
1956 Ok(response)
1957 }
1958 }
1959
1960 pub async fn download(
1965 &self,
1966 uri: impl TryInto<Uri>,
1967 content_type: impl TryInto<http::HeaderValue>,
1968 ) -> crate::Result<Vec<u8>> {
1969 let uri = uri
1970 .try_into()
1971 .map_err(|_| UriParseError {})
1972 .context(UriParseSnafu)?;
1973 let content_type = content_type
1974 .try_into()
1975 .map_err(|_| UriParseError {})
1976 .context(UriParseSnafu)?;
1977
1978 let mut request = Builder::new().method(Method::GET).uri(uri);
1979 request = request.header(http::header::ACCEPT, content_type);
1980
1981 let request = self.build_request(request, None::<&()>)?;
1982 let response = self.execute(request).await?;
1983
1984 let bytes = response.into_body().collect().await?.to_bytes();
1985 Ok(bytes.to_vec())
1986 }
1987
1988 pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1990 self.download(uri, "application/zip").await
1991 }
1992}
1993
1994impl Octocrab {
1996 pub async fn get_page<R: serde::de::DeserializeOwned>(
1998 &self,
1999 uri: &Option<Uri>,
2000 ) -> crate::Result<Option<Page<R>>> {
2001 match uri {
2002 Some(uri) => self.get(uri.to_string(), None::<&()>).await.map(Some),
2003 None => Ok(None),
2004 }
2005 }
2006
2007 pub async fn all_pages<R: serde::de::DeserializeOwned>(
2010 &self,
2011 mut page: Page<R>,
2012 ) -> crate::Result<Vec<R>> {
2013 let mut ret = page.take_items();
2014 while let Some(mut next_page) = self.get_page(&page.next).await? {
2015 ret.append(&mut next_page.take_items());
2016 page = next_page;
2017 }
2018 Ok(ret)
2019 }
2020}
2021
2022#[cfg(test)]
2023mod tests {
2024 #[tokio::test]
2026 async fn parametrize_uri_valid() {
2027 let uri = crate::instance()
2030 .parameterized_uri("/help%20world", None::<&()>)
2031 .unwrap();
2032 assert_eq!(uri.path(), "/help%20world");
2033 }
2034
2035 #[tokio::test]
2036 async fn extra_headers() {
2037 use http::header::HeaderName;
2038 use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
2039 let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
2040 let mock_server = MockServer::start().await;
2041 Mock::given(matchers::method("GET"))
2042 .and(matchers::path_regex(".*"))
2043 .and(matchers::header("x-test1", "hello"))
2044 .and(matchers::header("x-test2", "goodbye"))
2045 .respond_with(response)
2046 .expect(1)
2047 .mount(&mock_server)
2048 .await;
2049 crate::OctocrabBuilder::default()
2050 .base_uri(mock_server.uri())
2051 .unwrap()
2052 .add_header(HeaderName::from_static("x-test1"), "hello".to_string())
2053 .add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
2054 .build()
2055 .unwrap()
2056 .repos("XAMPPRocky", "octocrab")
2057 .events()
2058 .send()
2059 .await
2060 .unwrap();
2061 }
2062
2063 use super::*;
2064 use chrono::Duration;
2065
2066 #[test]
2067 fn clear_token() {
2068 let cache = CachedToken(RwLock::new(None));
2069 cache.set("secret".to_string(), None);
2070 cache.clear();
2071
2072 assert!(cache.valid_token().is_none(), "Token was not cleared.");
2073 }
2074
2075 #[test]
2076 fn no_token_when_expired() {
2077 let cache = CachedToken(RwLock::new(None));
2078 let expiration = Utc::now() + Duration::seconds(9);
2079 cache.set("secret".to_string(), Some(expiration));
2080
2081 assert!(
2082 cache
2083 .valid_token_with_buffer(Duration::seconds(10))
2084 .is_none(),
2085 "Token should be considered expired due to buffer."
2086 );
2087 }
2088
2089 #[test]
2090 fn get_valid_token_outside_buffer() {
2091 let cache = CachedToken(RwLock::new(None));
2092 let expiration = Utc::now() + Duration::seconds(12);
2093 cache.set("secret".to_string(), Some(expiration));
2094
2095 assert!(
2096 cache
2097 .valid_token_with_buffer(Duration::seconds(10))
2098 .is_some(),
2099 "Token should still be valid outside of buffer."
2100 );
2101 }
2102
2103 #[test]
2104 fn get_valid_token_without_expiration() {
2105 let cache = CachedToken(RwLock::new(None));
2106 cache.set("secret".to_string(), None);
2107
2108 assert!(
2109 cache
2110 .valid_token_with_buffer(Duration::seconds(10))
2111 .is_some(),
2112 "Token with no expiration should always be considered valid."
2113 );
2114 }
2115}