github.rs

  1use crate::expiring::Expiring;
  2use anyhow::{anyhow, Context};
  3use serde::{de::DeserializeOwned, Deserialize, Serialize};
  4use std::{
  5    future::Future,
  6    sync::Arc,
  7    time::{Duration, Instant},
  8};
  9use surf::{http::Method, RequestBuilder, Url};
 10
 11#[derive(Debug, Deserialize, Serialize)]
 12pub struct Release {
 13    pub tag_name: String,
 14    pub name: String,
 15    pub body: String,
 16    pub draft: bool,
 17    pub assets: Vec<Asset>,
 18}
 19
 20#[derive(Debug, Deserialize, Serialize)]
 21pub struct Asset {
 22    pub name: String,
 23    pub url: String,
 24}
 25
 26pub struct AppClient {
 27    id: usize,
 28    private_key: String,
 29    jwt_bearer_header: Expiring<String>,
 30}
 31
 32#[derive(Deserialize)]
 33struct Installation {
 34    #[allow(unused)]
 35    id: usize,
 36}
 37
 38impl AppClient {
 39    #[cfg(test)]
 40    pub fn test() -> Arc<Self> {
 41        Arc::new(Self {
 42            id: Default::default(),
 43            private_key: Default::default(),
 44            jwt_bearer_header: Default::default(),
 45        })
 46    }
 47
 48    pub fn new(id: usize, private_key: String) -> Arc<Self> {
 49        Arc::new(Self {
 50            id,
 51            private_key,
 52            jwt_bearer_header: Default::default(),
 53        })
 54    }
 55
 56    pub async fn repo(self: &Arc<Self>, nwo: String) -> tide::Result<RepoClient> {
 57        let installation: Installation = self
 58            .request(
 59                Method::Get,
 60                &format!("/repos/{}/installation", &nwo),
 61                |refresh| self.bearer_header(refresh),
 62            )
 63            .await?;
 64
 65        Ok(RepoClient {
 66            app: self.clone(),
 67            nwo,
 68            installation_id: installation.id,
 69            installation_token_header: Default::default(),
 70        })
 71    }
 72
 73    pub fn user(self: &Arc<Self>, access_token: String) -> UserClient {
 74        UserClient {
 75            app: self.clone(),
 76            access_token,
 77        }
 78    }
 79
 80    async fn request<T, F, G>(
 81        &self,
 82        method: Method,
 83        path: &str,
 84        get_auth_header: F,
 85    ) -> tide::Result<T>
 86    where
 87        T: DeserializeOwned,
 88        F: Fn(bool) -> G,
 89        G: Future<Output = tide::Result<String>>,
 90    {
 91        let mut retried = false;
 92
 93        loop {
 94            let response = RequestBuilder::new(
 95                method,
 96                Url::parse(&format!("https://api.github.com{}", path))?,
 97            )
 98            .header("Accept", "application/vnd.github.v3+json")
 99            .header("Authorization", get_auth_header(retried).await?)
100            .recv_json()
101            .await;
102
103            if let Err(error) = response.as_ref() {
104                if error.status() == 401 && !retried {
105                    retried = true;
106                    continue;
107                }
108            }
109
110            return response;
111        }
112    }
113
114    async fn bearer_header(&self, refresh: bool) -> tide::Result<String> {
115        if refresh {
116            self.jwt_bearer_header.clear().await;
117        }
118
119        self.jwt_bearer_header
120            .get_or_refresh(|| async {
121                use jwt_simple::{algorithms::RS256KeyPair, prelude::*};
122                use std::time;
123
124                let key_pair = RS256KeyPair::from_pem(&self.private_key)
125                    .with_context(|| format!("invalid private key {:?}", self.private_key))?;
126                let mut claims = Claims::create(Duration::from_mins(10));
127                claims.issued_at = Some(Clock::now_since_epoch() - Duration::from_mins(1));
128                claims.issuer = Some(self.id.to_string());
129                let token = key_pair.sign(claims).context("failed to sign claims")?;
130                let expires_at = time::Instant::now() + time::Duration::from_secs(9 * 60);
131
132                Ok((format!("Bearer {}", token), expires_at))
133            })
134            .await
135    }
136
137    async fn installation_token_header(
138        &self,
139        header: &Expiring<String>,
140        installation_id: usize,
141        refresh: bool,
142    ) -> tide::Result<String> {
143        if refresh {
144            header.clear().await;
145        }
146
147        header
148            .get_or_refresh(|| async {
149                #[derive(Debug, Deserialize)]
150                struct AccessToken {
151                    token: String,
152                }
153
154                let access_token: AccessToken = self
155                    .request(
156                        Method::Post,
157                        &format!("/app/installations/{}/access_tokens", installation_id),
158                        |refresh| self.bearer_header(refresh),
159                    )
160                    .await?;
161
162                let header = format!("Token {}", access_token.token);
163                let expires_at = Instant::now() + Duration::from_secs(60 * 30);
164
165                Ok((header, expires_at))
166            })
167            .await
168    }
169}
170
171pub struct RepoClient {
172    app: Arc<AppClient>,
173    nwo: String,
174    installation_id: usize,
175    installation_token_header: Expiring<String>,
176}
177
178impl RepoClient {
179    #[cfg(test)]
180    pub fn test(app_client: &Arc<AppClient>) -> Self {
181        Self {
182            app: app_client.clone(),
183            nwo: String::new(),
184            installation_id: 0,
185            installation_token_header: Default::default(),
186        }
187    }
188
189    pub async fn releases(&self) -> tide::Result<Vec<Release>> {
190        self.get(&format!("/repos/{}/releases?per_page=100", self.nwo))
191            .await
192    }
193
194    pub async fn release_asset(&self, tag: &str, name: &str) -> tide::Result<surf::Body> {
195        let release: Release = self
196            .get(&format!("/repos/{}/releases/tags/{}", self.nwo, tag))
197            .await?;
198
199        let asset = release
200            .assets
201            .iter()
202            .find(|asset| asset.name == name)
203            .ok_or_else(|| anyhow!("no asset found with name {}", name))?;
204
205        let request = surf::get(&asset.url)
206            .header("Accept", "application/octet-stream'")
207            .header(
208                "Authorization",
209                self.installation_token_header(false).await?,
210            );
211        let client = surf::client().with(surf::middleware::Redirect::new(5));
212        let mut response = client.send(request).await?;
213
214        Ok(response.take_body())
215    }
216
217    async fn get<T: DeserializeOwned>(&self, path: &str) -> tide::Result<T> {
218        self.request::<T>(Method::Get, path).await
219    }
220
221    async fn request<T: DeserializeOwned>(&self, method: Method, path: &str) -> tide::Result<T> {
222        Ok(self
223            .app
224            .request(method, path, |refresh| {
225                self.installation_token_header(refresh)
226            })
227            .await?)
228    }
229
230    async fn installation_token_header(&self, refresh: bool) -> tide::Result<String> {
231        self.app
232            .installation_token_header(
233                &self.installation_token_header,
234                self.installation_id,
235                refresh,
236            )
237            .await
238    }
239}
240
241pub struct UserClient {
242    app: Arc<AppClient>,
243    access_token: String,
244}
245
246#[derive(Clone, Debug, Deserialize, Serialize)]
247pub struct User {
248    pub login: String,
249    pub avatar_url: String,
250}
251
252impl UserClient {
253    pub async fn details(&self) -> tide::Result<User> {
254        Ok(self
255            .app
256            .request(Method::Get, "/user", |_| async {
257                Ok(self.access_token_header())
258            })
259            .await?)
260    }
261
262    fn access_token_header(&self) -> String {
263        format!("Token {}", self.access_token)
264    }
265}