265 lines · 7.3 KB · Rust
RawDownload
use crate::expiring::Expiring;use anyhow::{anyhow, Context};use serde::{de::DeserializeOwned, Deserialize, Serialize};use std::{    future::Future,    sync::Arc,    time::{Duration, Instant},};use surf::{http::Method, RequestBuilder, Url}; #[derive(Debug, Deserialize, Serialize)]pub struct Release {    pub tag_name: String,    pub name: String,    pub body: String,    pub draft: bool,    pub assets: Vec<Asset>,} #[derive(Debug, Deserialize, Serialize)]pub struct Asset {    pub name: String,    pub url: String,} pub struct AppClient {    id: usize,    private_key: String,    jwt_bearer_header: Expiring<String>,} #[derive(Deserialize)]struct Installation {    #[allow(unused)]    id: usize,} impl AppClient {    #[cfg(test)]    pub fn test() -> Arc<Self> {        Arc::new(Self {            id: Default::default(),            private_key: Default::default(),            jwt_bearer_header: Default::default(),        })    }     pub fn new(id: usize, private_key: String) -> Arc<Self> {        Arc::new(Self {            id,            private_key,            jwt_bearer_header: Default::default(),        })    }     pub async fn repo(self: &Arc<Self>, nwo: String) -> tide::Result<RepoClient> {        let installation: Installation = self            .request(                Method::Get,                &format!("/repos/{}/installation", &nwo),                |refresh| self.bearer_header(refresh),            )            .await?;         Ok(RepoClient {            app: self.clone(),            nwo,            installation_id: installation.id,            installation_token_header: Default::default(),        })    }     pub fn user(self: &Arc<Self>, access_token: String) -> UserClient {        UserClient {            app: self.clone(),            access_token,        }    }     async fn request<T, F, G>(        &self,        method: Method,        path: &str,        get_auth_header: F,    ) -> tide::Result<T>    where        T: DeserializeOwned,        F: Fn(bool) -> G,        G: Future<Output = tide::Result<String>>,    {        let mut retried = false;         loop {            let response = RequestBuilder::new(                method,                Url::parse(&format!("https://api.github.com{}", path))?,            )            .header("Accept", "application/vnd.github.v3+json")            .header("Authorization", get_auth_header(retried).await?)            .recv_json()            .await;             if let Err(error) = response.as_ref() {                if error.status() == 401 && !retried {                    retried = true;                    continue;                }            }             return response;        }    }     async fn bearer_header(&self, refresh: bool) -> tide::Result<String> {        if refresh {            self.jwt_bearer_header.clear().await;        }         self.jwt_bearer_header            .get_or_refresh(|| async {                use jwt_simple::{algorithms::RS256KeyPair, prelude::*};                use std::time;                 let key_pair = RS256KeyPair::from_pem(&self.private_key)                    .with_context(|| format!("invalid private key {:?}", self.private_key))?;                let mut claims = Claims::create(Duration::from_mins(10));                claims.issued_at = Some(Clock::now_since_epoch() - Duration::from_mins(1));                claims.issuer = Some(self.id.to_string());                let token = key_pair.sign(claims).context("failed to sign claims")?;                let expires_at = time::Instant::now() + time::Duration::from_secs(9 * 60);                 Ok((format!("Bearer {}", token), expires_at))            })            .await    }     async fn installation_token_header(        &self,        header: &Expiring<String>,        installation_id: usize,        refresh: bool,    ) -> tide::Result<String> {        if refresh {            header.clear().await;        }         header            .get_or_refresh(|| async {                #[derive(Debug, Deserialize)]                struct AccessToken {                    token: String,                }                 let access_token: AccessToken = self                    .request(                        Method::Post,                        &format!("/app/installations/{}/access_tokens", installation_id),                        |refresh| self.bearer_header(refresh),                    )                    .await?;                 let header = format!("Token {}", access_token.token);                let expires_at = Instant::now() + Duration::from_secs(60 * 30);                 Ok((header, expires_at))            })            .await    }} pub struct RepoClient {    app: Arc<AppClient>,    nwo: String,    installation_id: usize,    installation_token_header: Expiring<String>,} impl RepoClient {    #[cfg(test)]    pub fn test(app_client: &Arc<AppClient>) -> Self {        Self {            app: app_client.clone(),            nwo: String::new(),            installation_id: 0,            installation_token_header: Default::default(),        }    }     pub async fn releases(&self) -> tide::Result<Vec<Release>> {        self.get(&format!("/repos/{}/releases?per_page=100", self.nwo))            .await    }     pub async fn release_asset(&self, tag: &str, name: &str) -> tide::Result<surf::Body> {        let release: Release = self            .get(&format!("/repos/{}/releases/tags/{}", self.nwo, tag))            .await?;         let asset = release            .assets            .iter()            .find(|asset| asset.name == name)            .ok_or_else(|| anyhow!("no asset found with name {}", name))?;         let request = surf::get(&asset.url)            .header("Accept", "application/octet-stream'")            .header(                "Authorization",                self.installation_token_header(false).await?,            );        let client = surf::client().with(surf::middleware::Redirect::new(5));        let mut response = client.send(request).await?;         Ok(response.take_body())    }     async fn get<T: DeserializeOwned>(&self, path: &str) -> tide::Result<T> {        self.request::<T>(Method::Get, path).await    }     async fn request<T: DeserializeOwned>(&self, method: Method, path: &str) -> tide::Result<T> {        Ok(self            .app            .request(method, path, |refresh| {                self.installation_token_header(refresh)            })            .await?)    }     async fn installation_token_header(&self, refresh: bool) -> tide::Result<String> {        self.app            .installation_token_header(                &self.installation_token_header,                self.installation_id,                refresh,            )            .await    }} pub struct UserClient {    app: Arc<AppClient>,    access_token: String,} #[derive(Clone, Debug, Deserialize, Serialize)]pub struct User {    pub login: String,    pub avatar_url: String,} impl UserClient {    pub async fn details(&self) -> tide::Result<User> {        Ok(self            .app            .request(Method::Get, "/user", |_| async {                Ok(self.access_token_header())            })            .await?)    }     fn access_token_header(&self) -> String {        format!("Token {}", self.access_token)    }}