use failure::{bail, Error}; use reqwest::header::{self, HeaderValue}; use reqwest::{Client, Method, RequestBuilder}; use std::borrow::Cow; use std::collections::HashMap; static API_BASE: &str = "https://api.github.com/"; static TOKEN_VAR: &str = "GITHUB_TOKEN"; #[derive(serde::Deserialize)] pub(crate) struct User { pub(crate) id: usize, pub(crate) login: String, pub(crate) name: Option, pub(crate) email: Option, } #[derive(serde::Deserialize)] struct GraphResult { data: Option, #[serde(default)] errors: Vec, } #[derive(serde::Deserialize)] struct GraphError { message: String, } #[derive(serde::Deserialize)] struct GraphNodes { nodes: Vec>, } pub(crate) struct GitHubApi { http: Client, token: Option, } impl GitHubApi { pub(crate) fn new() -> Self { GitHubApi { http: Client::new(), token: std::env::var(TOKEN_VAR).ok(), } } fn prepare( &self, require_auth: bool, method: Method, url: &str, ) -> Result { let url = if url.starts_with("https://") { Cow::Borrowed(url) } else { Cow::Owned(format!("{}{}", API_BASE, url)) }; if require_auth { self.require_auth()?; } let mut req = self.http.request(method, url.as_ref()); if let Some(token) = &self.token { req = req.header( header::AUTHORIZATION, HeaderValue::from_str(&format!("token {}", token))?, ); } Ok(req) } fn graphql(&self, query: &str, variables: V) -> Result where R: serde::de::DeserializeOwned, V: serde::Serialize, { #[derive(serde::Serialize)] struct Request<'a, V> { query: &'a str, variables: V, } let res: GraphResult = self .prepare(true, Method::POST, "graphql")? .json(&Request { query, variables }) .send()? .error_for_status()? .json()?; if let Some(error) = res.errors.iter().next() { bail!("graphql error: {}", error.message); } else if let Some(data) = res.data { Ok(data) } else { bail!("missing graphql data"); } } pub(crate) fn require_auth(&self) -> Result<(), Error> { if self.token.is_none() { bail!("missing environment variable {}", TOKEN_VAR); } Ok(()) } pub(crate) fn user(&self, login: &str) -> Result { Ok(self .prepare(false, Method::GET, &format!("users/{}", login))? .send()? .error_for_status()? .json()?) } pub(crate) fn usernames(&self, ids: &[usize]) -> Result, Error> { #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct Usernames { database_id: usize, login: String, } #[derive(serde::Serialize)] struct Params { ids: Vec, } static QUERY: &str = " query($ids: [ID!]!) { nodes(ids: $ids) { ... on User { databaseId login } } } "; let mut result = HashMap::new(); for chunk in ids.chunks(100) { let res: GraphNodes = self.graphql( QUERY, Params { ids: chunk.iter().map(|id| user_node_id(*id)).collect(), }, )?; for node in res.nodes.into_iter().filter_map(|n| n) { result.insert(node.database_id, node.login); } } Ok(result) } } fn user_node_id(id: usize) -> String { base64::encode(&format!("04:User{}", id)) }