#![feature(async_stream)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::nonstandard_macro_braces)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::tabs_in_doc_comments)]
#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod spreadsheets;
#[cfg(test)]
mod tests;
pub mod traits;
pub mod types;
#[doc(hidden)]
pub mod utils;
use std::io::Write;
use anyhow::{anyhow, Error, Result};
pub const DEFAULT_HOST: &str = "https://sheets.googleapis.com";
mod progenitor_support {
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
const PATH_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'`')
.add(b'{')
.add(b'}');
#[allow(dead_code)]
pub(crate) fn encode_path(pc: &str) -> String {
utf8_percent_encode(pc, PATH_SET).to_string()
}
}
use std::env;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token";
const USER_CONSENT_ENDPOINT: &str = "https://";
#[derive(Clone)]
pub struct Client {
token: String,
refresh_token: String,
client_id: String,
client_secret: String,
redirect_uri: String,
client: reqwest::Client,
}
#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
pub struct AccessToken {
#[serde(
default,
skip_serializing_if = "String::is_empty",
deserialize_with = "crate::utils::deserialize_null_string::deserialize"
)]
pub token_type: String,
#[serde(
default,
skip_serializing_if = "String::is_empty",
deserialize_with = "crate::utils::deserialize_null_string::deserialize"
)]
pub access_token: String,
#[serde(default)]
pub expires_in: i64,
#[serde(
default,
skip_serializing_if = "String::is_empty",
deserialize_with = "crate::utils::deserialize_null_string::deserialize"
)]
pub refresh_token: String,
#[serde(default, alias = "x_refresh_token_expires_in")]
pub refresh_token_expires_in: i64,
#[serde(
default,
skip_serializing_if = "String::is_empty",
deserialize_with = "crate::utils::deserialize_null_string::deserialize"
)]
pub scope: String,
}
impl Client {
pub fn new<I, K, R, T, Q>(
client_id: I,
client_secret: K,
redirect_uri: R,
token: T,
refresh_token: Q,
) -> Self
where
I: ToString,
K: ToString,
R: ToString,
T: ToString,
Q: ToString,
{
let client = reqwest::Client::builder().build();
match client {
Ok(c) => {
Client {
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
redirect_uri: redirect_uri.to_string(),
token: token.to_string(),
refresh_token: refresh_token.to_string(),
client: c,
}
}
Err(e) => panic!("creating reqwest client failed: {:?}", e),
}
}
pub async fn new_from_env<T, R>(token: T, refresh_token: R) -> Self
where
T: ToString,
R: ToString,
{
let google_key = env::var("GOOGLE_KEY_ENCODED").unwrap_or_default();
let b = base64::decode(google_key).unwrap();
let mut file_path = env::temp_dir();
file_path.push("google_key.json");
let mut file = std::fs::File::create(file_path.clone()).unwrap();
file.write_all(&b).unwrap();
let google_credential_file = file_path.to_str().unwrap().to_string();
let secret = yup_oauth2::read_application_secret(google_credential_file)
.await
.expect("failed to read google credential file");
let client = reqwest::Client::builder().build();
match client {
Ok(c) => {
Client {
client_id: secret.client_id.to_string(),
client_secret: secret.client_secret.to_string(),
redirect_uri: secret.redirect_uris[0].to_string(),
token: token.to_string(),
refresh_token: refresh_token.to_string(),
client: c,
}
}
Err(e) => panic!("creating reqwest client failed: {:?}", e),
}
}
async fn url_and_auth(&self, uri: &str) -> Result<(reqwest::Url, Option<String>)> {
let parsed_url = uri.parse::<reqwest::Url>();
let auth = format!("Bearer {}", self.token);
parsed_url.map(|u| (u, Some(auth))).map_err(Error::from)
}
async fn request_raw(
&self,
method: reqwest::Method,
uri: &str,
body: Option<reqwest::Body>,
) -> Result<reqwest::Response> {
let u = if uri.starts_with("https://") {
uri.to_string()
} else {
(DEFAULT_HOST.to_string() + uri).to_string()
};
let (url, auth) = self.url_and_auth(&u).await?;
let instance = <&Client>::clone(&self);
let mut req = instance.client.request(method, url);
req = req.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
req = req.header(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
);
if let Some(auth_str) = auth {
req = req.header(http::header::AUTHORIZATION, &*auth_str);
}
if let Some(body) = body {
req = req.body(body);
}
Ok(req.send().await?)
}
async fn request<Out>(
&self,
method: reqwest::Method,
uri: &str,
body: Option<reqwest::Body>,
) -> Result<Out>
where
Out: serde::de::DeserializeOwned + 'static + Send,
{
let response = self.request_raw(method, uri, body).await?;
let status = response.status();
let response_body = response.bytes().await?;
if status.is_success() {
let parsed_response = if status == http::StatusCode::NO_CONTENT {
serde_json::from_str("null")
} else {
serde_json::from_slice::<Out>(&response_body)
};
parsed_response.map_err(Error::from)
} else {
let error = if response_body.is_empty() {
anyhow!("code: {}, empty response", status)
} else {
anyhow!(
"code: {}, error: {:?}",
status,
String::from_utf8_lossy(&response_body),
)
};
Err(error)
}
}
#[allow(dead_code)]
async fn request_with_mime<Out>(
&self,
method: reqwest::Method,
uri: &str,
content: &[u8],
mime_type: &str,
) -> Result<Out>
where
Out: serde::de::DeserializeOwned + 'static + Send,
{
let u = if uri.starts_with("https://") {
uri.to_string()
} else {
(DEFAULT_HOST.to_string() + uri).to_string()
};
let (url, auth) = self.url_and_auth(&u).await?;
let instance = <&Client>::clone(&self);
let mut req = instance.client.request(method, url);
req = req.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
req = req.header(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_bytes(mime_type.as_bytes()).unwrap(),
);
req = req.header(
reqwest::header::HeaderName::from_static("x-upload-content-type"),
reqwest::header::HeaderValue::from_static("application/octet-stream"),
);
req = req.header(
reqwest::header::HeaderName::from_static("x-upload-content-length"),
reqwest::header::HeaderValue::from_bytes(format!("{}", content.len()).as_bytes())
.unwrap(),
);
if let Some(auth_str) = auth {
req = req.header(http::header::AUTHORIZATION, &*auth_str);
}
if content.len() > 1 {
let b = bytes::Bytes::copy_from_slice(content);
req = req.body(b);
}
let response = req.send().await?;
let status = response.status();
let response_body = response.bytes().await?;
if status.is_success() {
let parsed_response = if status == http::StatusCode::NO_CONTENT {
serde_json::from_str("null")
} else {
serde_json::from_slice::<Out>(&response_body)
};
parsed_response.map_err(Error::from)
} else {
let error = if response_body.is_empty() {
anyhow!("code: {}, empty response", status)
} else {
anyhow!(
"code: {}, error: {:?}",
status,
String::from_utf8_lossy(&response_body),
)
};
Err(error)
}
}
async fn request_entity<D>(
&self,
method: http::Method,
uri: &str,
body: Option<reqwest::Body>,
) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
let r = self.request(method, uri, body).await?;
Ok(r)
}
pub fn user_consent_url(&self, scopes: &[String]) -> String {
let state = uuid::Uuid::new_v4();
let url = format!(
"{}?client_id={}&response_type=code&redirect_uri={}&state={}",
USER_CONSENT_ENDPOINT, self.client_id, self.redirect_uri, state
);
if scopes.is_empty() {
return url;
}
format!("{}&scope={}", url, scopes.join(" "))
}
pub async fn refresh_access_token(&mut self) -> Result<AccessToken> {
if self.refresh_token.is_empty() {
anyhow!("refresh token cannot be empty");
}
let mut headers = reqwest::header::HeaderMap::new();
headers.append(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let params = [
("grant_type", "refresh_token"),
("refresh_token", &self.refresh_token),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
("redirect_uri", &self.redirect_uri),
];
let client = reqwest::Client::new();
let resp = client
.post(TOKEN_ENDPOINT)
.headers(headers)
.form(¶ms)
.basic_auth(&self.client_id, Some(&self.client_secret))
.send()
.await?;
let t: AccessToken = resp.json().await?;
self.token = t.access_token.to_string();
self.refresh_token = t.refresh_token.to_string();
Ok(t)
}
pub async fn get_access_token(&mut self, code: &str, state: &str) -> Result<AccessToken> {
let mut headers = reqwest::header::HeaderMap::new();
headers.append(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let params = [
("grant_type", "authorization_code"),
("code", code),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
("redirect_uri", &self.redirect_uri),
("state", state),
];
let client = reqwest::Client::new();
let resp = client
.post(TOKEN_ENDPOINT)
.headers(headers)
.form(¶ms)
.basic_auth(&self.client_id, Some(&self.client_secret))
.send()
.await?;
let t: AccessToken = resp.json().await?;
self.token = t.access_token.to_string();
self.refresh_token = t.refresh_token.to_string();
Ok(t)
}
#[allow(dead_code)]
async fn get<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::GET,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
#[allow(dead_code)]
async fn get_all_pages<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<Vec<D>>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::GET,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
#[allow(dead_code)]
async fn post<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::POST,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
#[allow(dead_code)]
async fn patch<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::PATCH,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
#[allow(dead_code)]
async fn put<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::PUT,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
#[allow(dead_code)]
async fn delete<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
where
D: serde::de::DeserializeOwned + 'static + Send,
{
self.request_entity(
http::Method::DELETE,
&(DEFAULT_HOST.to_string() + uri),
message,
)
.await
}
pub fn spreadsheets(&self) -> spreadsheets::Spreadsheets {
spreadsheets::Spreadsheets::new(self.clone())
}
}