From 39f77530412701af1f180ad53cf31317066b62f5 Mon Sep 17 00:00:00 2001 From: Zuhair Parvez Date: Thu, 19 Oct 2023 08:36:05 -0700 Subject: [PATCH] Add wrapper crate for some Google APIs --- Cargo.lock | 336 ++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 2 +- src/google/Cargo.toml | 21 +++ src/google/src/lib.rs | 178 ++++++++++++++++++++++ src/tmdb/src/client.rs | 14 +- 5 files changed, 515 insertions(+), 36 deletions(-) create mode 100644 src/google/Cargo.toml create mode 100644 src/google/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 23fc11e..c5e8432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,7 @@ dependencies = [ "matchit", "memchr", "mime", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project-lite", "rustversion", "serde", @@ -236,7 +236,7 @@ dependencies = [ "serde", "serde_json", "time", - "url", + "url 2.4.1", "uuid", ] @@ -257,7 +257,7 @@ dependencies = [ "serde_json", "time", "tz-rs", - "url", + "url 2.4.1", "uuid", ] @@ -273,7 +273,7 @@ dependencies = [ "serde", "serde_json", "time", - "url", + "url 2.4.1", ] [[package]] @@ -626,7 +626,7 @@ dependencies = [ "color-eyre", "hex", "reqwest", - "ring", + "ring 0.17.3", "serde", "serde_json", "tempfile", @@ -773,7 +773,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.0", ] [[package]] @@ -904,7 +904,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "url", + "url 2.4.1", ] [[package]] @@ -937,6 +937,82 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "google" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "dotenvy", + "google-books1", + "google-calendar3", + "serde", + "serde_json", + "thiserror", + "tokio", + "yup-oauth2", +] + +[[package]] +name = "google-apis-common" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ee789438087de4e6aa14e8505f661d4f45b161f2d21807c1fee0d68b54732d" +dependencies = [ + "base64 0.13.1", + "chrono", + "http", + "hyper", + "itertools 0.10.5", + "mime", + "serde", + "serde_json", + "serde_with 2.3.3", + "tokio", + "tower-service", + "url 1.7.2", + "yup-oauth2", +] + +[[package]] +name = "google-books1" +version = "5.0.3+20230117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09338da32f2865caa804dfc350fde22200ef5617538e292e324d8a5bc0800d79" +dependencies = [ + "anyhow", + "google-apis-common", + "http", + "hyper", + "hyper-rustls", + "itertools 0.10.5", + "mime", + "serde", + "serde_json", + "tokio", + "tower-service", + "url 1.7.2", +] + +[[package]] +name = "google-calendar3" +version = "5.0.3+20221229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34ab87fa230d2b54e29863a60fa8150f232300cdcf0e2fba860fd5a5c2f90ed" +dependencies = [ + "anyhow", + "google-apis-common", + "http", + "hyper", + "hyper-rustls", + "itertools 0.10.5", + "mime", + "serde", + "serde_json", + "tokio", + "tower-service", + "url 1.7.2", +] + [[package]] name = "h2" version = "0.3.21" @@ -1011,7 +1087,7 @@ dependencies = [ "hash32", "rustc_version", "serde", - "spin", + "spin 0.9.8", "stable_deref_trait", ] @@ -1090,7 +1166,7 @@ dependencies = [ "serde_json", "serde_qs", "serde_urlencoded", - "url", + "url 2.4.1", ] [[package]] @@ -1129,6 +1205,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1171,6 +1263,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -1253,6 +1356,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1320,6 +1432,12 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.7.3" @@ -1436,7 +1554,7 @@ dependencies = [ "serde_path_to_error", "sha2", "thiserror", - "url", + "url 2.4.1", ] [[package]] @@ -1568,6 +1686,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1665,7 +1789,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -1686,7 +1810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", "syn", @@ -1841,7 +1965,7 @@ dependencies = [ "mime", "native-tls", "once_cell", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project-lite", "serde", "serde_json", @@ -1851,7 +1975,7 @@ dependencies = [ "tokio-native-tls", "tokio-util", "tower-service", - "url", + "url 2.4.1", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", @@ -1859,6 +1983,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.3" @@ -1868,8 +2007,8 @@ dependencies = [ "cc", "getrandom 0.2.10", "libc", - "spin", - "untrusted", + "spin 0.9.8", + "untrusted 0.9.0", "windows-sys", ] @@ -1901,6 +2040,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring 0.16.20", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1928,6 +2110,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.9.2" @@ -1959,18 +2157,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -2004,7 +2202,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.0", "serde", "thiserror", ] @@ -2032,6 +2230,22 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros 2.3.3", + "time", +] + [[package]] name = "serde_with" version = "3.3.0" @@ -2045,10 +2259,22 @@ dependencies = [ "indexmap 2.0.2", "serde", "serde_json", - "serde_with_macros", + "serde_with_macros 3.3.0", "time", ] +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_with_macros" version = "3.3.0" @@ -2136,6 +2362,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -2302,7 +2534,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "serde_with", + "serde_with 3.3.0", "thiserror", "tokio", ] @@ -2347,6 +2579,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -2500,12 +2742,29 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.4.1" @@ -2513,8 +2772,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna", - "percent-encoding", + "idna 0.4.0", + "percent-encoding 2.3.0", "serde", ] @@ -2786,6 +3045,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "yup-oauth2" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364ca376b5c04d9b2be9693054e3e0d2d146b363819d0f9a10c6ee66e4c8406b" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "futures", + "http", + "hyper", + "hyper-rustls", + "itertools 0.10.5", + "log", + "percent-encoding 2.3.0", + "rustls", + "rustls-pemfile", + "seahash", + "serde", + "serde_json", + "time", + "tokio", + "tower-service", + "url 2.4.1", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index fe5c6a9..bee8094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] resolver = "2" -members = ["src/igdb", "src/get-config", "src/tmdb", "tools/deploy-agent"] +members = ["src/igdb", "src/get-config", "src/tmdb", "src/google", "tools/deploy-agent"] diff --git a/src/google/Cargo.toml b/src/google/Cargo.toml new file mode 100644 index 0000000..e36a821 --- /dev/null +++ b/src/google/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "google" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +google-calendar3 = "5.0.3" +google-books1 = "5.0.3" +serde = "1.0.189" +serde_json = "1.0.107" +yup-oauth2 = "8.3.0" +thiserror = "1.0.49" +tokio = { version = "1.33.0", features = ["full"] } + +[dev-dependencies] +dotenvy = "0.15.7" +base64 = "0.21.4" + + diff --git a/src/google/src/lib.rs b/src/google/src/lib.rs new file mode 100644 index 0000000..0b6c48d --- /dev/null +++ b/src/google/src/lib.rs @@ -0,0 +1,178 @@ +use std::io; + +use google_books1::api::{Volume, VolumeVolumeInfo}; +use google_books1::Books; +use google_calendar3::api::Event; +use google_calendar3::chrono::{DateTime, Utc}; +use google_calendar3::{hyper, hyper_rustls, CalendarHub}; +use thiserror::Error; +use yup_oauth2::hyper::client::HttpConnector; +use yup_oauth2::hyper_rustls::HttpsConnector; +use yup_oauth2::ServiceAccountAuthenticator; + +type GoogleCalendar = CalendarHub>; +type GoogleBooks = Books>; + +pub struct Google { + calendar_hub: GoogleCalendar, + books_hub: GoogleBooks, +} + +#[derive(Error, Debug)] +pub enum GoogleError { + #[error("unable to parse service account key")] + ServiceAccountKeyParseError(#[from] io::Error), + + #[error("error with google api")] + GoogleApiError(#[from] google_calendar3::Error), + + #[error("expected data for field '{0}'")] + MissingDataError(&'static str), +} + +impl Google { + pub async fn new(service_account_key: &str) -> Result { + let creds = yup_oauth2::parse_service_account_key(service_account_key)?; + let auth = ServiceAccountAuthenticator::builder(creds).build().await?; + + let calendar_hub = CalendarHub::new( + hyper::Client::builder().build( + hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .build(), + ), + auth.clone(), + ); + + let books_hub = Books::new( + hyper::Client::builder().build( + hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .build(), + ), + auth.clone(), + ); + + Ok(Self { + calendar_hub, + books_hub, + }) + } + + pub async fn get_events( + &self, + calendar_id: &str, + min_time: DateTime, + max_time: DateTime, + ) -> Result>, GoogleError> { + let (_, events) = self + .calendar_hub + .events() + .list(calendar_id) + .single_events(true) + .show_deleted(true) + .order_by("startTime") + .time_min(min_time) + .time_max(max_time) + .doit() + .await?; + + Ok(events.items) + } + + pub async fn books_search(&self, query: &str) -> Result>, GoogleError> { + let (_, books) = self.books_hub.volumes().list(query).doit().await?; + Ok(books.items) + } + + pub async fn isbn_lookup(&self, isbn: &str) -> Result, GoogleError> { + let query = format!("isbn:{}", isbn.replace("-", "")); + let (_, volumes) = self.books_hub.volumes().list(&query).doit().await?; + if 0 == volumes + .total_items + .ok_or_else(|| GoogleError::MissingDataError("totalItems"))? + { + return Ok(None); + } + + let items = volumes + .items + .ok_or_else(|| GoogleError::MissingDataError("items"))?; + let result = items + .first() + .ok_or_else(|| GoogleError::MissingDataError("items[0]"))?; + let info = result + .volume_info + .as_ref() + .ok_or_else(|| GoogleError::MissingDataError("items[0]['volumeInfo']"))?; + + Ok(Some(info.to_owned())) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use base64::prelude::BASE64_STANDARD; + use base64::Engine; + use dotenvy::dotenv; + use google_calendar3::chrono::{Duration, Utc}; + + use crate::Google; + + #[tokio::test] + async fn google_calendar_test() { + dotenv().ok(); + let service_key = read_service_key_from_env(); + let google = Google::new(&service_key).await.unwrap(); + + let summer_game_fest_calendar = + "s71id26u0afr69leltrq0us0b97jp35k@import.calendar.google.com"; + let events = google + .get_events( + summer_game_fest_calendar, + Utc::now() - Duration::days(365), + Utc::now() + Duration::days(365), + ) + .await + .unwrap(); + + assert_ne!(events.unwrap().len(), 0); + } + + #[tokio::test] + async fn google_books_search_test() { + dotenv().unwrap(); + let service_key = read_service_key_from_env(); + let google = Google::new(&service_key).await.unwrap(); + let books = google.books_search("Jade City").await.unwrap(); + + assert_ne!(books.unwrap().len(), 0); + } + + #[tokio::test] + async fn google_books_isbn_test() { + dotenv().ok(); + let service_key = read_service_key_from_env(); + let google = Google::new(&service_key).await.unwrap(); + let book = google.isbn_lookup("9780316440882").await.unwrap().unwrap(); + + assert_eq!("Jade City", book.title.unwrap()); + } + + fn read_service_key_from_env() -> String { + let service_account_key_64 = + env::var("GOOGLE_CREDENTIALS").expect("GOOGLE_CREDENTIALS not found in envvar"); + + let decoded = BASE64_STANDARD + .decode(service_account_key_64) + .expect("Unable to decode service account key"); + + String::from_utf8(decoded).unwrap() + } +} diff --git a/src/tmdb/src/client.rs b/src/tmdb/src/client.rs index 682210e..f413b2d 100644 --- a/src/tmdb/src/client.rs +++ b/src/tmdb/src/client.rs @@ -130,16 +130,10 @@ mod tests { let api_token = env::var("TMDB_TOKEN").expect("couldn't find TMDB token"); let client = TMDBClient::new(&api_token).expect("couldn't create client"); - let movie = match client.get_movie(24428.into()).await { - Ok(m) => m, - Err(e) => match e { - TMDBClientError::ClientDeserializationError(e) => { - println!("{}:{} {:?}", e.line(), e.column(), e.classify()); - panic!("Boo") - } - _ => panic!("boo"), - }, - }; + let movie = client + .get_movie(24428.into()) + .await + .expect("movie should exist"); let british_release_date = movie .release_dates()