From 4e6f819cd229288e46651dac190fb782ea23663a Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 15 Mar 2025 20:21:50 +0900 Subject: [PATCH 1/6] rough sketch on outlook calendar api --- Cargo.lock | 78 ++++++++++++++++++++++++++- crates/calendar-google/Cargo.toml | 1 + crates/calendar-outlook/Cargo.toml | 7 ++- crates/calendar-outlook/src/errors.rs | 18 +++++++ crates/calendar-outlook/src/lib.rs | 39 ++++++++++++-- crates/calendar-outlook/src/models.rs | 64 ++++++++++++++++++++++ 6 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 crates/calendar-outlook/src/errors.rs create mode 100644 crates/calendar-outlook/src/models.rs diff --git a/Cargo.lock b/Cargo.lock index 684140c4c..52f288837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1818,6 +1818,7 @@ dependencies = [ "calendar-interface", "chrono", "google-calendar", + "google-calendar3", ] [[package]] @@ -1835,7 +1836,12 @@ name = "calendar-outlook" version = "0.1.0" dependencies = [ "calendar-interface", + "chrono", "graph-rs-sdk", + "reqwest 0.12.12", + "serde", + "serde_json", + "thiserror 2.0.12", ] [[package]] @@ -4640,6 +4646,29 @@ dependencies = [ "scroll", ] +[[package]] +name = "google-apis-common" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7530ee92a7e9247c3294ae1b84ea98474dbc27563c49a14d3938e816499bf38f" +dependencies = [ + "base64 0.22.1", + "chrono", + "http 1.2.0", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "itertools 0.13.0", + "mime", + "percent-encoding", + "serde", + "serde_json", + "serde_with 3.12.0", + "tokio", + "url", + "yup-oauth2 11.0.0", +] + [[package]] name = "google-calendar" version = "0.7.0" @@ -4672,7 +4701,27 @@ dependencies = [ "tokio", "url", "uuid", - "yup-oauth2", + "yup-oauth2 8.3.2", +] + +[[package]] +name = "google-calendar3" +version = "6.0.0+20240523" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eecfc108ae1dc04a710b8db3a79e15ab3e87bda7735e0f84df9bf4d4e1496cf6" +dependencies = [ + "chrono", + "google-apis-common", + "hyper 1.6.0", + "hyper-rustls 0.27.5", + "hyper-util", + "mime", + "serde", + "serde_json", + "serde_with 3.12.0", + "tokio", + "url", + "yup-oauth2 11.0.0", ] [[package]] @@ -14862,6 +14911,33 @@ dependencies = [ "url", ] +[[package]] +name = "yup-oauth2" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed5f19242090128c5809f6535cc7b8d4e2c32433f6c6005800bbc20a644a7f0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "futures", + "http 1.2.0", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls 0.27.5", + "hyper-util", + "log", + "percent-encoding", + "rustls 0.23.23", + "rustls-pemfile 2.2.0", + "seahash", + "serde", + "serde_json", + "time", + "tokio", + "url", +] + [[package]] name = "zbus" version = "5.5.0" diff --git a/crates/calendar-google/Cargo.toml b/crates/calendar-google/Cargo.toml index 751a5bee4..8c2eb4cb3 100644 --- a/crates/calendar-google/Cargo.toml +++ b/crates/calendar-google/Cargo.toml @@ -8,3 +8,4 @@ hypr-calendar-interface = { path = "../calendar-interface", package = "calendar- chrono = { workspace = true } google-calendar = "0.7.0" +google-calendar3 = "6.0.0" diff --git a/crates/calendar-outlook/Cargo.toml b/crates/calendar-outlook/Cargo.toml index a401726cc..2a2094601 100644 --- a/crates/calendar-outlook/Cargo.toml +++ b/crates/calendar-outlook/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +graph-rs-sdk = { workspace = true } hypr-calendar-interface = { path = "../calendar-interface", package = "calendar-interface" } -graph-rs-sdk = { workspace = true } +chrono = { workspace = true } +reqwest = { workspace = true, default-features = false } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/calendar-outlook/src/errors.rs b/crates/calendar-outlook/src/errors.rs new file mode 100644 index 000000000..374ded084 --- /dev/null +++ b/crates/calendar-outlook/src/errors.rs @@ -0,0 +1,18 @@ +use serde::{ser::Serializer, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + GraphFailure(#[from] graph_rs_sdk::GraphFailure), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/crates/calendar-outlook/src/lib.rs b/crates/calendar-outlook/src/lib.rs index 28af82660..61d53725f 100644 --- a/crates/calendar-outlook/src/lib.rs +++ b/crates/calendar-outlook/src/lib.rs @@ -1,6 +1,12 @@ +mod errors; +mod models; + +pub use errors::*; +pub use models::*; + use graph_rs_sdk::GraphClient; -use hypr_calendar_interface::{Calendar, CalendarSource, Error, Event, EventFilter}; +use hypr_calendar_interface::Calendar; pub struct Handle { client: GraphClient, @@ -11,14 +17,37 @@ impl Handle { let client = GraphClient::new(token.into()); Self { client } } -} -impl CalendarSource for Handle { - async fn list_calendars(&self) -> Result, Error> { + // https://learn.microsoft.com/en-us/graph/api/user-list-calendars + pub async fn list_calendars(&self) -> Result, Error> { + let _response = self + .client + .user("TODO_USER_ID") + .calendars() + .list_calendars() + .send() + .await? + .json::() + .await?; + Ok(vec![]) } - async fn list_events(&self, _filter: EventFilter) -> Result, Error> { + // https://learn.microsoft.com/en-us/graph/api/event-delta + // https://learn.microsoft.com/en-us/graph/delta-query-events + pub async fn delta_events(&self, calendar_id: &str) -> Result, Error> { + let _delta_2 = self + .client + .user("123") + .calendar_views() + .delta() + .append_query_pair("startdatetime", "2016-12-01T00:00:00Z") + .append_query_pair("enddatetime", "2016-12-30T00:00:00Z") + .send() + .await? + .json::() + .await?; + Ok(vec![]) } } diff --git a/crates/calendar-outlook/src/models.rs b/crates/calendar-outlook/src/models.rs new file mode 100644 index 000000000..aa92b4e43 --- /dev/null +++ b/crates/calendar-outlook/src/models.rs @@ -0,0 +1,64 @@ +// https://learn.microsoft.com/en-us/graph/delta-query-events?tabs=http#sample-initial-response + +// TODO +// use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventDeltaResponse { + #[serde(rename = "@odata.context")] + pub odata_context: Option, + #[serde(rename = "@odata.nextLink")] + pub odata_next_link: Option, + #[serde(rename = "@odata.deltaLink")] + pub odata_delta_link: Option, + pub value: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventDelta { + #[serde(rename = "@odata.type")] + pub odata_type: Option, + #[serde(rename = "@odata.etag")] + pub odata_etag: Option, + pub subject: Option, + pub body: Option, + pub start: Option, + pub end: Option, + pub location: Option, + pub attendees: Option>, + pub organizer: Option, + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Body { + pub content: String, + pub content_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Timestamp { + pub date_time: String, + pub time_zone: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attendee { + pub email_address: EmailAddress, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Organizer { + pub email_address: EmailAddress, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmailAddress { + pub name: String, + pub address: String, +} From 12d7514d7e32badb755d94557159697c08cef5fa Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 15 Mar 2025 20:45:22 +0900 Subject: [PATCH 2/6] wip gcal migration --- Cargo.lock | 4 ++ crates/calendar-google/Cargo.toml | 4 ++ crates/calendar-google/src/beta.rs | 71 ++++++++++++++++++++++++++++ crates/calendar-google/src/errors.rs | 13 +++++ crates/calendar-google/src/lib.rs | 5 ++ crates/calendar-interface/Cargo.toml | 3 +- crates/calendar-outlook/Cargo.toml | 2 +- 7 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 crates/calendar-google/src/beta.rs create mode 100644 crates/calendar-google/src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 52f288837..42f65e0a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1819,6 +1819,9 @@ dependencies = [ "chrono", "google-calendar", "google-calendar3", + "serde", + "serde_json", + "thiserror 2.0.12", ] [[package]] @@ -1828,6 +1831,7 @@ dependencies = [ "anyhow", "chrono", "serde", + "serde_json", "thiserror 2.0.12", ] diff --git a/crates/calendar-google/Cargo.toml b/crates/calendar-google/Cargo.toml index 8c2eb4cb3..ce279bf56 100644 --- a/crates/calendar-google/Cargo.toml +++ b/crates/calendar-google/Cargo.toml @@ -9,3 +9,7 @@ hypr-calendar-interface = { path = "../calendar-interface", package = "calendar- chrono = { workspace = true } google-calendar = "0.7.0" google-calendar3 = "6.0.0" + +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/calendar-google/src/beta.rs b/crates/calendar-google/src/beta.rs new file mode 100644 index 000000000..c74d77425 --- /dev/null +++ b/crates/calendar-google/src/beta.rs @@ -0,0 +1,71 @@ +use google_calendar3::{ + api::Channel, + hyper_rustls::{self, HttpsConnector}, + hyper_util::{self, client::legacy::connect::HttpConnector}, + yup_oauth2, CalendarHub, +}; + +pub struct Handle { + hub: CalendarHub>, +} + +impl Handle { + pub async fn new() -> Result { + let client = + hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) + .build( + hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .unwrap() + .https_or_http() + .enable_http1() + .build(), + ); + + let secret: yup_oauth2::ApplicationSecret = Default::default(); + + let auth = yup_oauth2::InstalledFlowAuthenticator::builder( + secret, + yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, + ) + .build() + .await + .unwrap(); + + let hub = CalendarHub::new(client, auth); + + Ok(Self { hub }) + } +} + +impl Handle { + pub async fn list_calendars(&self) -> Result, crate::Error> { + Ok(vec![]) + } + // https://developers.google.com/calendar/api/guides/sync + pub async fn list_events(&self) -> Result, crate::Error> { + // let mut req = Channel::default(); + + let (_res, _events) = self + .hub + .events() + .list("calendarId") + .updated_min(chrono::Utc::now()) + .time_zone("et") + .time_min(chrono::Utc::now()) + .time_max(chrono::Utc::now()) + .sync_token("At") + .single_events(false) + .show_hidden_invitations(false) + .show_deleted(false) + .order_by("erat") + .max_results(500) + .max_attendees(100) + .add_event_types("Lorem") + .always_include_email(true) + .doit() + .await?; + + Ok(vec![]) + } +} diff --git a/crates/calendar-google/src/errors.rs b/crates/calendar-google/src/errors.rs new file mode 100644 index 000000000..9dbce37bb --- /dev/null +++ b/crates/calendar-google/src/errors.rs @@ -0,0 +1,13 @@ +use serde::{ser::Serializer, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum Error {} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/crates/calendar-google/src/lib.rs b/crates/calendar-google/src/lib.rs index a2d340896..523d919bb 100644 --- a/crates/calendar-google/src/lib.rs +++ b/crates/calendar-google/src/lib.rs @@ -5,6 +5,11 @@ use hypr_calendar_interface::{ Calendar, CalendarSource, Error, Event, EventFilter, Participant, Platform, }; +mod beta; +mod errors; + +pub use errors::*; + pub struct Handle { client: google_calendar::Client, } diff --git a/crates/calendar-interface/Cargo.toml b/crates/calendar-interface/Cargo.toml index c263ad042..b6577c9c9 100644 --- a/crates/calendar-interface/Cargo.toml +++ b/crates/calendar-interface/Cargo.toml @@ -8,4 +8,5 @@ anyhow = { workspace = true } thiserror = { workspace = true } chrono = { workspace = true, features = ["serde"] } -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/calendar-outlook/Cargo.toml b/crates/calendar-outlook/Cargo.toml index 2a2094601..4a05d5c3a 100644 --- a/crates/calendar-outlook/Cargo.toml +++ b/crates/calendar-outlook/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" graph-rs-sdk = { workspace = true } hypr-calendar-interface = { path = "../calendar-interface", package = "calendar-interface" } -chrono = { workspace = true } +chrono = { workspace = true, features = ["serde"] } reqwest = { workspace = true, default-features = false } serde = { workspace = true } serde_json = { workspace = true } From 046e187233b4fcd0cd7d8358555fe02a11e126b2 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 15 Mar 2025 21:01:12 +0900 Subject: [PATCH 3/6] wip --- crates/calendar-google/src/beta.rs | 56 +++++++++++++++++++----------- crates/calendar-outlook/Cargo.toml | 2 +- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/calendar-google/src/beta.rs b/crates/calendar-google/src/beta.rs index c74d77425..0fec434c3 100644 --- a/crates/calendar-google/src/beta.rs +++ b/crates/calendar-google/src/beta.rs @@ -1,14 +1,34 @@ +use std::{future::Future, pin::Pin}; + use google_calendar3::{ - api::Channel, + api::{CalendarListEntry, Channel, Event}, + common::GetToken, hyper_rustls::{self, HttpsConnector}, hyper_util::{self, client::legacy::connect::HttpConnector}, - yup_oauth2, CalendarHub, + CalendarHub, }; pub struct Handle { hub: CalendarHub>, } +#[derive(Debug, Clone)] +struct Storage {} + +type GetTokenOutput<'a> = Pin< + Box< + dyn Future, Box>> + + Send + + 'a, + >, +>; + +impl GetToken for Storage { + fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { + Box::pin(async move { Ok(Some("your_oauth_token_here".to_string())) }) + } +} + impl Handle { pub async fn new() -> Result { let client = @@ -22,16 +42,7 @@ impl Handle { .build(), ); - let secret: yup_oauth2::ApplicationSecret = Default::default(); - - let auth = yup_oauth2::InstalledFlowAuthenticator::builder( - secret, - yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, - ) - .build() - .await - .unwrap(); - + let auth = Storage {}; let hub = CalendarHub::new(client, auth); Ok(Self { hub }) @@ -39,19 +50,24 @@ impl Handle { } impl Handle { - pub async fn list_calendars(&self) -> Result, crate::Error> { - Ok(vec![]) + pub async fn list_calendars(&self) -> Result, crate::Error> { + let (_res, calendar_list) = self.hub.calendar_list().list().doit().await?; + + let calendars = calendar_list.items.unwrap_or_default(); + Ok(calendars) } // https://developers.google.com/calendar/api/guides/sync - pub async fn list_events(&self) -> Result, crate::Error> { + pub async fn list_events( + &self, + calendar_id: impl AsRef, + ) -> Result, crate::Error> { // let mut req = Channel::default(); - let (_res, _events) = self + let (_res, events_wrapper) = self .hub .events() - .list("calendarId") + .list(calendar_id.as_ref()) .updated_min(chrono::Utc::now()) - .time_zone("et") .time_min(chrono::Utc::now()) .time_max(chrono::Utc::now()) .sync_token("At") @@ -61,11 +77,11 @@ impl Handle { .order_by("erat") .max_results(500) .max_attendees(100) - .add_event_types("Lorem") .always_include_email(true) .doit() .await?; - Ok(vec![]) + let events = events_wrapper.items.unwrap_or_default(); + Ok(events) } } diff --git a/crates/calendar-outlook/Cargo.toml b/crates/calendar-outlook/Cargo.toml index 4a05d5c3a..986ff2668 100644 --- a/crates/calendar-outlook/Cargo.toml +++ b/crates/calendar-outlook/Cargo.toml @@ -8,7 +8,7 @@ graph-rs-sdk = { workspace = true } hypr-calendar-interface = { path = "../calendar-interface", package = "calendar-interface" } chrono = { workspace = true, features = ["serde"] } -reqwest = { workspace = true, default-features = false } +reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } From ff398329c4b58339bee803252c41d9b21ff375f0 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 15 Mar 2025 21:29:51 +0900 Subject: [PATCH 4/6] replace existing code with beta --- crates/calendar-google/src/auth.rs | 19 ++++ crates/calendar-google/src/beta.rs | 87 --------------- crates/calendar-google/src/errors.rs | 5 +- crates/calendar-google/src/lib.rs | 157 +++++++++------------------ 4 files changed, 77 insertions(+), 191 deletions(-) create mode 100644 crates/calendar-google/src/auth.rs delete mode 100644 crates/calendar-google/src/beta.rs diff --git a/crates/calendar-google/src/auth.rs b/crates/calendar-google/src/auth.rs new file mode 100644 index 000000000..7a1207514 --- /dev/null +++ b/crates/calendar-google/src/auth.rs @@ -0,0 +1,19 @@ +use google_calendar3::common::GetToken; +use std::{future::Future, pin::Pin}; + +#[derive(Debug, Clone)] +pub struct Storage {} + +type GetTokenOutput<'a> = Pin< + Box< + dyn Future, Box>> + + Send + + 'a, + >, +>; + +impl GetToken for Storage { + fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { + Box::pin(async move { Ok(Some("your_oauth_token_here".to_string())) }) + } +} diff --git a/crates/calendar-google/src/beta.rs b/crates/calendar-google/src/beta.rs deleted file mode 100644 index 0fec434c3..000000000 --- a/crates/calendar-google/src/beta.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{future::Future, pin::Pin}; - -use google_calendar3::{ - api::{CalendarListEntry, Channel, Event}, - common::GetToken, - hyper_rustls::{self, HttpsConnector}, - hyper_util::{self, client::legacy::connect::HttpConnector}, - CalendarHub, -}; - -pub struct Handle { - hub: CalendarHub>, -} - -#[derive(Debug, Clone)] -struct Storage {} - -type GetTokenOutput<'a> = Pin< - Box< - dyn Future, Box>> - + Send - + 'a, - >, ->; - -impl GetToken for Storage { - fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { - Box::pin(async move { Ok(Some("your_oauth_token_here".to_string())) }) - } -} - -impl Handle { - pub async fn new() -> Result { - let client = - hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .build( - hyper_rustls::HttpsConnectorBuilder::new() - .with_native_roots() - .unwrap() - .https_or_http() - .enable_http1() - .build(), - ); - - let auth = Storage {}; - let hub = CalendarHub::new(client, auth); - - Ok(Self { hub }) - } -} - -impl Handle { - pub async fn list_calendars(&self) -> Result, crate::Error> { - let (_res, calendar_list) = self.hub.calendar_list().list().doit().await?; - - let calendars = calendar_list.items.unwrap_or_default(); - Ok(calendars) - } - // https://developers.google.com/calendar/api/guides/sync - pub async fn list_events( - &self, - calendar_id: impl AsRef, - ) -> Result, crate::Error> { - // let mut req = Channel::default(); - - let (_res, events_wrapper) = self - .hub - .events() - .list(calendar_id.as_ref()) - .updated_min(chrono::Utc::now()) - .time_min(chrono::Utc::now()) - .time_max(chrono::Utc::now()) - .sync_token("At") - .single_events(false) - .show_hidden_invitations(false) - .show_deleted(false) - .order_by("erat") - .max_results(500) - .max_attendees(100) - .always_include_email(true) - .doit() - .await?; - - let events = events_wrapper.items.unwrap_or_default(); - Ok(events) - } -} diff --git a/crates/calendar-google/src/errors.rs b/crates/calendar-google/src/errors.rs index 9dbce37bb..9902ffa0b 100644 --- a/crates/calendar-google/src/errors.rs +++ b/crates/calendar-google/src/errors.rs @@ -1,7 +1,10 @@ use serde::{ser::Serializer, Serialize}; #[derive(Debug, thiserror::Error)] -pub enum Error {} +pub enum Error { + #[error(transparent)] + GoogleCalendarError(#[from] google_calendar3::Error), +} impl Serialize for Error { fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/calendar-google/src/lib.rs b/crates/calendar-google/src/lib.rs index 523d919bb..1188a44bc 100644 --- a/crates/calendar-google/src/lib.rs +++ b/crates/calendar-google/src/lib.rs @@ -1,123 +1,74 @@ // https://developers.google.com/calendar/api/v3/reference/calendars // https://developers.google.com/calendar/api/v3/reference/events -use hypr_calendar_interface::{ - Calendar, CalendarSource, Error, Event, EventFilter, Participant, Platform, +use google_calendar3::{ + api::{CalendarListEntry, Event}, + hyper_rustls::{self, HttpsConnector}, + hyper_util::{self, client::legacy::connect::HttpConnector}, + CalendarHub, }; -mod beta; +mod auth; mod errors; pub use errors::*; pub struct Handle { - client: google_calendar::Client, + hub: CalendarHub>, } +// impl CalendarSource for Handle { impl Handle { - pub async fn new(token: impl Into) -> Self { - let client = google_calendar::Client::new_from_env(token.into(), "".to_string()).await; - Self { client } - } -} + pub async fn new() -> Result { + let client = + hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) + .build( + hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .unwrap() + .https_or_http() + .enable_http1() + .build(), + ); -impl CalendarSource for Handle { - async fn list_calendars(&self) -> Result, Error> { - let list = self - .client - .calendar_list() - .list_all(google_calendar::types::MinAccessRole::Noop, false, false) - .await? - .body - .iter() - .map(|calendar| Calendar { - id: calendar.id.clone(), - platform: Platform::Google, - name: calendar.summary.clone(), - }) - .collect(); + let auth = crate::auth::Storage {}; + let hub = CalendarHub::new(client, auth); - Ok(list) + Ok(Self { hub }) } +} - async fn list_events(&self, filter: EventFilter) -> Result, Error> { - let mut all_events = Vec::new(); - - for calendar in filter.calendars { - let events: Vec = self - .client - .events() - .list( - &calendar.id, - "", - 100, - 500, - google_calendar::types::OrderBy::StartTime, - "", - &Vec::new(), - "", - &Vec::new(), - false, - false, - true, - &filter.from.to_rfc3339(), - &filter.to.to_rfc3339(), - "", - "", - ) - .await? - .body - .iter() - .map(|event| { - let start = event.start.clone().unwrap(); - let end = event.end.clone().unwrap(); - - let start = start.date_time.unwrap_or_else(|| { - start - .date - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap() - .and_local_timezone(chrono::Local) - .unwrap() - .into() - }); - let end = end.date_time.unwrap_or_else(|| { - end.date - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap() - .and_local_timezone(chrono::Local) - .unwrap() - .into() - }); - - let participants = event - .attendees - .iter() - .map(|a| Participant { - name: a.display_name.clone(), - email: Some(a.email.clone()), - }) - .collect::>(); - - Event { - id: event.id.clone(), - calendar_id: calendar.id.clone(), - platform: Platform::Google, - name: event.summary.clone(), - note: event.description.clone(), - participants, - start_date: start, - end_date: end, - google_event_url: Some(event.html_link.clone()), - } - }) - .collect(); - - all_events.extend(events); - } +impl Handle { + pub async fn list_calendars(&self) -> Result, crate::Error> { + let (_res, calendar_list) = self.hub.calendar_list().list().doit().await?; + let calendars = calendar_list.items.unwrap_or_default(); + Ok(calendars) + } + // https://developers.google.com/calendar/api/guides/sync + pub async fn list_events( + &self, + calendar_id: impl AsRef, + ) -> Result, crate::Error> { + let (_res, events_wrapper) = self + .hub + .events() + .list(calendar_id.as_ref()) + .updated_min(chrono::Utc::now()) + .time_min(chrono::Utc::now()) + .time_max(chrono::Utc::now()) + .sync_token("At") + .single_events(false) + .show_hidden_invitations(false) + .show_deleted(false) + .order_by("erat") + .max_results(500) + .max_attendees(100) + .always_include_email(true) + .doit() + .await?; - Ok(all_events) + let _next_sync_token = events_wrapper.next_sync_token; + let events = events_wrapper.items.unwrap_or_default(); + Ok(events) } } From 78625a0830295f56308023873e01ca10bd700d12 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 15 Mar 2025 22:07:49 +0900 Subject: [PATCH 5/6] wip --- crates/calendar-google/src/auth.rs | 24 ++++++++++++--- crates/calendar-google/src/lib.rs | 1 + crates/calendar-outlook/src/lib.rs | 48 ++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/crates/calendar-google/src/auth.rs b/crates/calendar-google/src/auth.rs index 7a1207514..3c41d50b2 100644 --- a/crates/calendar-google/src/auth.rs +++ b/crates/calendar-google/src/auth.rs @@ -1,10 +1,17 @@ -use google_calendar3::common::GetToken; +pub use google_calendar3::common::GetToken; use std::{future::Future, pin::Pin}; #[derive(Debug, Clone)] -pub struct Storage {} +pub struct Storage { + // TODO: some user_id -type GetTokenOutput<'a> = Pin< + // we might want to store nango client here, with lifetime specified + + // Actually, we just make GetToken public, and expect server provide it. + // we expect that we receive impl GetToken. +} + +pub type GetTokenOutput<'a> = Pin< Box< dyn Future, Box>> + Send @@ -12,8 +19,17 @@ type GetTokenOutput<'a> = Pin< >, >; +impl Storage { + pub async fn impl_get_token( + &self, + ) -> Result, Box> { + // Fetch token from nango, using user_id + Ok(Some("TODO".to_string())) + } +} + impl GetToken for Storage { fn get_token<'a>(&'a self, _scopes: &'a [&str]) -> GetTokenOutput<'a> { - Box::pin(async move { Ok(Some("your_oauth_token_here".to_string())) }) + Box::pin(self.impl_get_token()) } } diff --git a/crates/calendar-google/src/lib.rs b/crates/calendar-google/src/lib.rs index 1188a44bc..524b815bc 100644 --- a/crates/calendar-google/src/lib.rs +++ b/crates/calendar-google/src/lib.rs @@ -11,6 +11,7 @@ use google_calendar3::{ mod auth; mod errors; +pub use auth::*; pub use errors::*; pub struct Handle { diff --git a/crates/calendar-outlook/src/lib.rs b/crates/calendar-outlook/src/lib.rs index 61d53725f..ac67afbf5 100644 --- a/crates/calendar-outlook/src/lib.rs +++ b/crates/calendar-outlook/src/lib.rs @@ -8,21 +8,51 @@ use graph_rs_sdk::GraphClient; use hypr_calendar_interface::Calendar; -pub struct Handle { - client: GraphClient, +pub struct Client { + inner: GraphClient, } -impl Handle { - pub async fn new(token: impl Into) -> Self { - let client = GraphClient::new(token.into()); - Self { client } +impl std::ops::Deref for Client { + type Target = GraphClient; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Client { + pub fn with_user(&self, user_id: impl Into) -> UserClient { + UserClient { + user_id: user_id.into(), + client: self, + } + } +} + +#[derive(Default)] +pub struct ClientBuilder { + token: Option, +} + +impl ClientBuilder { + pub fn build(self) -> Client { + Client { + inner: GraphClient::new(self.token.unwrap()), + } } +} + +pub struct UserClient<'a> { + user_id: String, + client: &'a Client, +} +impl<'a> UserClient<'a> { // https://learn.microsoft.com/en-us/graph/api/user-list-calendars pub async fn list_calendars(&self) -> Result, Error> { let _response = self .client - .user("TODO_USER_ID") + .user(&self.user_id) .calendars() .list_calendars() .send() @@ -35,10 +65,10 @@ impl Handle { // https://learn.microsoft.com/en-us/graph/api/event-delta // https://learn.microsoft.com/en-us/graph/delta-query-events - pub async fn delta_events(&self, calendar_id: &str) -> Result, Error> { + pub async fn delta_events(&self, _calendar_id: &str) -> Result, Error> { let _delta_2 = self .client - .user("123") + .user(&self.user_id) .calendar_views() .delta() .append_query_pair("startdatetime", "2016-12-01T00:00:00Z") From 1034e83d497b0f5a131d0917d51206e1c7adb9d8 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sun, 16 Mar 2025 13:45:29 +0900 Subject: [PATCH 6/6] it compiles --- Cargo.lock | 15 ++++ Cargo.toml | 5 ++ apps/app/server/src/worker/calendar.rs | 68 ++++++++++++------- crates/calendar-apple/src/lib.rs | 3 + crates/calendar-apple/src/models.rs | 3 + crates/calendar-google/src/lib.rs | 16 ++--- crates/calendar-outlook/src/lib.rs | 4 +- .../calendar-outlook/src/models/calendar.rs | 38 +++++++++++ .../src/{models.rs => models/event.rs} | 2 +- crates/calendar-outlook/src/models/mod.rs | 5 ++ crates/calendar/Cargo.toml | 18 +++++ crates/calendar/src/lib.rs | 21 ++++++ 12 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 crates/calendar-apple/src/models.rs create mode 100644 crates/calendar-outlook/src/models/calendar.rs rename crates/calendar-outlook/src/{models.rs => models/event.rs} (94%) create mode 100644 crates/calendar-outlook/src/models/mod.rs create mode 100644 crates/calendar/Cargo.toml create mode 100644 crates/calendar/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 42f65e0a2..73ea143ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1796,6 +1796,21 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calendar" +version = "0.1.0" +dependencies = [ + "calendar-apple", + "calendar-google", + "calendar-interface", + "calendar-outlook", + "schemars", + "serde", + "serde_json", + "specta", + "thiserror 2.0.12", +] + [[package]] name = "calendar-apple" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 062c2e803..761649a85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ resolver = "2" members = ["apps/app/server", "apps/desktop/src-tauri", "apps/mobile/src-tauri", "crates/*", "plugins/*"] [workspace.dependencies] +hypr-calendar = { path = "crates/calendar", package = "calendar" } +hypr-calendar-apple = { path = "crates/calendar-apple", package = "calendar-apple" } +hypr-calendar-google = { path = "crates/calendar-google", package = "calendar-google" } +hypr-calendar-interface = { path = "crates/calendar-interface", package = "calendar-interface" } +hypr-calendar-outlook = { path = "crates/calendar-outlook", package = "calendar-outlook" } hypr-data = { path = "crates/data", package = "data" } hypr-db-admin = { path = "crates/db-admin", package = "db-admin" } hypr-db-core = { path = "crates/db-core", package = "db-core" } diff --git a/apps/app/server/src/worker/calendar.rs b/apps/app/server/src/worker/calendar.rs index e7ef29bb6..e425ae24a 100644 --- a/apps/app/server/src/worker/calendar.rs +++ b/apps/app/server/src/worker/calendar.rs @@ -3,10 +3,11 @@ use std::collections::HashMap; use apalis::prelude::{Data, Error}; use chrono::{DateTime, Utc}; -use crate::state::WorkerState; -use hypr_calendar_interface::CalendarSource; +use hypr_calendar_google::GetTokenOutput; use hypr_nango::{NangoCredentials, NangoIntegration}; +use crate::state::WorkerState; + #[allow(unused)] #[derive(Default, Debug, Clone)] pub struct Job(DateTime); @@ -17,6 +18,25 @@ impl From> for Job { } } +#[derive(Clone)] +struct AuthProvider { + pub nango: hypr_nango::NangoClient, +} + +impl AuthProvider { + pub async fn impl_get_token( + &self, + ) -> Result, Box> { + Ok(Some("TODO".to_string())) + } +} + +impl hypr_calendar_google::GetToken for AuthProvider { + fn get_token(&self, _scopes: &[&str]) -> GetTokenOutput { + Box::pin(self.impl_get_token()) + } +} + #[tracing::instrument(skip(ctx))] pub async fn perform(job: Job, ctx: Data) -> Result<(), Error> { let now = DateTime::::from_timestamp(job.0.timestamp(), 0).unwrap(); @@ -72,37 +92,37 @@ pub async fn perform(job: Job, ctx: Data) -> Result<(), Error> { for (kind, integrations) in integration_groups { for integration in integrations { - let token = get_oauth_access_token(&ctx.nango, &integration) - .await - .map_err(|e| e.as_worker_error())?; + // let token = get_oauth_access_token(&ctx.nango, &integration) + // .await + // .map_err(|e| e.as_worker_error())?; + + // assert!(kind == NangoIntegration::GoogleCalendar); - assert!(kind == NangoIntegration::GoogleCalendar); - let gcal = hypr_calendar_google::Handle::new(token).await; + let gcal = hypr_calendar_google::Client::new(AuthProvider { + nango: ctx.nango.clone(), + }); let filter = hypr_calendar_interface::EventFilter { calendars: vec![], from: now - chrono::Duration::days(1), to: now + chrono::Duration::days(1), }; - let events = gcal - .list_events(filter) - .await - .map_err(|e| Into::::into(e).as_worker_error())?; + let events = gcal.list_events("TODO_CALENDAR_ID").await.unwrap(); for e in events { - let event = hypr_db_user::Event { - id: uuid::Uuid::new_v4().to_string(), - tracking_id: e.id.clone(), - user_id: user_id.clone(), - calendar_id: "TODO".to_string(), - name: e.name.clone(), - note: e.note.clone(), - start_date: e.start_date, - end_date: e.end_date, - google_event_url: None, - }; - - let _ = user_db.upsert_event(event).await; + // let event = hypr_db_user::Event { + // id: uuid::Uuid::new_v4().to_string(), + // tracking_id: e.id.clone(), + // user_id: user_id.clone(), + // calendar_id: "TODO".to_string(), + // name: e.name.clone(), + // note: e.note.clone(), + // start_date: e.start_date, + // end_date: e.end_date, + // google_event_url: None, + // }; + + // let _ = user_db.upsert_event(event).await; } } } diff --git a/crates/calendar-apple/src/lib.rs b/crates/calendar-apple/src/lib.rs index f9d2769f2..cf5d638e7 100644 --- a/crates/calendar-apple/src/lib.rs +++ b/crates/calendar-apple/src/lib.rs @@ -14,6 +14,9 @@ use hypr_calendar_interface::{ Calendar, CalendarSource, Error, Event, EventFilter, Participant, Platform, }; +mod models; +pub use models::*; + pub struct Handle { event_store: Retained, contacts_store: Retained, diff --git a/crates/calendar-apple/src/models.rs b/crates/calendar-apple/src/models.rs new file mode 100644 index 000000000..cda3fdb82 --- /dev/null +++ b/crates/calendar-apple/src/models.rs @@ -0,0 +1,3 @@ +pub struct Calendar {} + +pub struct Event {} diff --git a/crates/calendar-google/src/lib.rs b/crates/calendar-google/src/lib.rs index 524b815bc..b20b1c95d 100644 --- a/crates/calendar-google/src/lib.rs +++ b/crates/calendar-google/src/lib.rs @@ -1,8 +1,8 @@ // https://developers.google.com/calendar/api/v3/reference/calendars // https://developers.google.com/calendar/api/v3/reference/events +pub use google_calendar3::api::{CalendarListEntry, Event}; use google_calendar3::{ - api::{CalendarListEntry, Event}, hyper_rustls::{self, HttpsConnector}, hyper_util::{self, client::legacy::connect::HttpConnector}, CalendarHub, @@ -14,13 +14,13 @@ mod errors; pub use auth::*; pub use errors::*; -pub struct Handle { +#[derive(Clone)] +pub struct Client { hub: CalendarHub>, } -// impl CalendarSource for Handle { -impl Handle { - pub async fn new() -> Result { +impl Client { + pub fn new(auth: impl auth::GetToken + 'static) -> Self { let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .build( @@ -32,14 +32,12 @@ impl Handle { .build(), ); - let auth = crate::auth::Storage {}; let hub = CalendarHub::new(client, auth); - - Ok(Self { hub }) + Self { hub } } } -impl Handle { +impl Client { pub async fn list_calendars(&self) -> Result, crate::Error> { let (_res, calendar_list) = self.hub.calendar_list().list().doit().await?; let calendars = calendar_list.items.unwrap_or_default(); diff --git a/crates/calendar-outlook/src/lib.rs b/crates/calendar-outlook/src/lib.rs index ac67afbf5..256c635b6 100644 --- a/crates/calendar-outlook/src/lib.rs +++ b/crates/calendar-outlook/src/lib.rs @@ -6,8 +6,6 @@ pub use models::*; use graph_rs_sdk::GraphClient; -use hypr_calendar_interface::Calendar; - pub struct Client { inner: GraphClient, } @@ -49,7 +47,7 @@ pub struct UserClient<'a> { impl<'a> UserClient<'a> { // https://learn.microsoft.com/en-us/graph/api/user-list-calendars - pub async fn list_calendars(&self) -> Result, Error> { + pub async fn list_calendars(&self) -> Result, Error> { let _response = self .client .user(&self.user_id) diff --git a/crates/calendar-outlook/src/models/calendar.rs b/crates/calendar-outlook/src/models/calendar.rs new file mode 100644 index 000000000..8204a3910 --- /dev/null +++ b/crates/calendar-outlook/src/models/calendar.rs @@ -0,0 +1,38 @@ +// https://learn.microsoft.com/en-us/graph/api/user-list-calendars + +// TODO +// use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CalendarListResponse { + #[serde(rename = "@odata.context")] + pub odata_context: Option, + pub value: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CalendarData { + #[serde(rename = "@odata.id")] + pub odata_id: Option, + pub id: String, + pub name: String, + pub color: String, + pub change_key: String, + pub can_share: Option, + pub can_view_private_items: Option, + pub hex_color: Option, + pub can_edit: Option, + pub allowed_online_meeting_providers: Option>, + pub default_online_meeting_provider: Option, + pub is_tallying_responses: Option, + pub is_removable: Option, + pub owner: Owner, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Owner { + pub name: String, + pub address: String, +} diff --git a/crates/calendar-outlook/src/models.rs b/crates/calendar-outlook/src/models/event.rs similarity index 94% rename from crates/calendar-outlook/src/models.rs rename to crates/calendar-outlook/src/models/event.rs index aa92b4e43..6686c7dc3 100644 --- a/crates/calendar-outlook/src/models.rs +++ b/crates/calendar-outlook/src/models/event.rs @@ -1,4 +1,4 @@ -// https://learn.microsoft.com/en-us/graph/delta-query-events?tabs=http#sample-initial-response +// https://learn.microsoft.com/en-us/graph/delta-query-events // TODO // use chrono::{DateTime, Utc}; diff --git a/crates/calendar-outlook/src/models/mod.rs b/crates/calendar-outlook/src/models/mod.rs new file mode 100644 index 000000000..2ebf76be3 --- /dev/null +++ b/crates/calendar-outlook/src/models/mod.rs @@ -0,0 +1,5 @@ +mod calendar; +mod event; + +pub use calendar::*; +pub use event::*; diff --git a/crates/calendar/Cargo.toml b/crates/calendar/Cargo.toml new file mode 100644 index 000000000..b9f4a9eab --- /dev/null +++ b/crates/calendar/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "calendar" +version = "0.1.0" +edition = "2021" + +[dependencies] +hypr-calendar-apple = { workspace = true } +hypr-calendar-google = { workspace = true } +hypr-calendar-interface = { workspace = true } +hypr-calendar-outlook = { workspace = true } + +thiserror = { workspace = true } + +serde = { workspace = true } +serde_json = { workspace = true } + +schemars = { workspace = true } +specta = { workspace = true, features = ["derive"] } diff --git a/crates/calendar/src/lib.rs b/crates/calendar/src/lib.rs new file mode 100644 index 000000000..a96bfdeb1 --- /dev/null +++ b/crates/calendar/src/lib.rs @@ -0,0 +1,21 @@ +#[macro_export] +macro_rules! calendar_common_derives { + ($item:item) => { + #[derive( + Debug, + PartialEq, + Clone, + serde::Serialize, + serde::Deserialize, + specta::Type, + schemars::JsonSchema, + )] + $item + }; +} + +pub enum Calendar { + // Apple(hypr_calendar_apple::Calendar), + Google(hypr_calendar_google::CalendarListEntry), + Outlook(hypr_calendar_outlook::CalendarData), +}