From 458c8dd40169e603bfbbbf152391511fa7e53cd1 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 16 Mar 2026 14:40:07 +0900 Subject: [PATCH 1/2] done Signed-off-by: Yujong Lee --- Cargo.lock | 92 +++++- Cargo.toml | 1 + apps/cli/Cargo.toml | 3 + crates/calendar/Cargo.toml | 22 ++ {plugins => crates}/calendar/src/convert.rs | 0 crates/calendar/src/error.rs | 24 ++ {plugins => crates}/calendar/src/fetch.rs | 0 crates/calendar/src/lib.rs | 277 ++++++++++++++++ plugins/calendar/Cargo.toml | 9 +- plugins/calendar/src/commands.rs | 72 +++- plugins/calendar/src/error.rs | 25 +- plugins/calendar/src/ext.rs | 345 -------------------- plugins/calendar/src/lib.rs | 7 +- 13 files changed, 472 insertions(+), 405 deletions(-) create mode 100644 crates/calendar/Cargo.toml rename {plugins => crates}/calendar/src/convert.rs (100%) create mode 100644 crates/calendar/src/error.rs rename {plugins => crates}/calendar/src/fetch.rs (100%) create mode 100644 crates/calendar/src/lib.rs delete mode 100644 plugins/calendar/src/ext.rs diff --git a/Cargo.lock b/Cargo.lock index ac609884b0..c10c9d4451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,6 +868,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-any" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" + [[package]] name = "askama" version = "0.13.1" @@ -2821,6 +2827,24 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calendar" +version = "0.1.0" +dependencies = [ + "api-client", + "apple-calendar", + "calendar-interface", + "chrono", + "chrono-tz 0.10.4", + "google-calendar", + "outlook-calendar", + "reqwest 0.13.2", + "serde", + "serde_json", + "specta", + "thiserror 2.0.18", +] + [[package]] name = "calendar-interface" version = "0.1.0" @@ -3266,10 +3290,12 @@ dependencies = [ "owhisper-interface", "ractor", "ratatui", + "rig-core", "serde", "serde_json", "storage", "strsim", + "strum 0.27.2", "tachyonfx", "tempfile", "textwrap", @@ -11115,6 +11141,15 @@ dependencies = [ "url", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -12297,6 +12332,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -14579,6 +14623,38 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rig-core" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb001344690ad016a095c6384b09b93ea12551490b4ed1a197058aeac990d6" +dependencies = [ + "as-any", + "async-stream", + "base64 0.22.1", + "bytes", + "eventsource-stream", + "fastrand", + "futures", + "futures-timer", + "glob", + "http 1.4.0", + "mime", + "mime_guess", + "nanoid", + "ordered-float 5.1.0", + "pin-project-lite", + "reqwest 0.13.2", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-futures", + "url", +] + [[package]] name = "ring" version = "0.17.14" @@ -17752,16 +17828,9 @@ dependencies = [ name = "tauri-plugin-calendar" version = "0.1.0" dependencies = [ - "api-client", - "apple-calendar", + "calendar", "calendar-interface", - "chrono", - "chrono-tz 0.10.4", - "google-calendar", - "outlook-calendar", - "reqwest 0.13.2", "serde", - "serde_json", "specta", "specta-typescript", "tauri", @@ -19165,7 +19234,7 @@ dependencies = [ "nix 0.29.0", "num-derive", "num-traits", - "ordered-float", + "ordered-float 4.6.0", "pest", "pest_derive", "phf 0.11.3", @@ -19974,6 +20043,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ + "futures", + "futures-task", + "pin-project", "tracing", ] @@ -21958,7 +22030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" dependencies = [ "log", - "ordered-float", + "ordered-float 4.6.0", "strsim", "thiserror 1.0.69", "wezterm-dynamic-derive", diff --git a/Cargo.toml b/Cargo.toml index a9dea063b7..679d3f7e1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ hypr-buffer = { path = "crates/buffer", package = "buffer" } hypr-bundle = { path = "crates/bundle", package = "bundle" } hypr-cactus = { path = "crates/cactus", package = "cactus" } hypr-cactus-model = { path = "crates/cactus-model", package = "cactus-model" } +hypr-calendar = { path = "crates/calendar", package = "calendar" } hypr-calendar-interface = { path = "crates/calendar-interface", package = "calendar-interface" } hypr-chatwoot = { path = "crates/chatwoot", package = "chatwoot" } hypr-cli-tui = { path = "crates/cli-tui", package = "cli-tui" } diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 60b578ca2e..3ceb336d26 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -58,9 +58,11 @@ tui-textarea = { workspace = true } open = { workspace = true } ractor = { workspace = true, features = ["async-trait"] } +rig = { package = "rig-core", version = "0.32" } serde = { workspace = true } serde_json = { workspace = true } strsim = "0.11" +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "sync", "time"] } tokio-stream = { workspace = true } @@ -73,6 +75,7 @@ clap-verbosity-flag = { version = "3", features = ["tracing"] } cli-docs = { path = "../../crates/cli-docs" } clap_complete = { workspace = true } clio = { version = "0.3", features = ["clap-parse"] } +strum = { workspace = true, features = ["derive"] } url = { workspace = true } [features] diff --git a/crates/calendar/Cargo.toml b/crates/calendar/Cargo.toml new file mode 100644 index 0000000000..800b56100d --- /dev/null +++ b/crates/calendar/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "calendar" +version = "0.1.0" +edition = "2024" + +[features] +specta = ["dep:specta"] + +[dependencies] +hypr-api-client = { workspace = true } +hypr-apple-calendar = { workspace = true, features = ["specta"] } +hypr-calendar-interface = { workspace = true } +hypr-google-calendar = { workspace = true, features = ["specta"] } +hypr-outlook-calendar = { workspace = true, features = ["specta"] } + +chrono = { workspace = true, features = ["serde"] } +chrono-tz = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +specta = { workspace = true, features = ["chrono"], optional = true } +thiserror = { workspace = true } diff --git a/plugins/calendar/src/convert.rs b/crates/calendar/src/convert.rs similarity index 100% rename from plugins/calendar/src/convert.rs rename to crates/calendar/src/convert.rs diff --git a/crates/calendar/src/error.rs b/crates/calendar/src/error.rs new file mode 100644 index 0000000000..18de351a48 --- /dev/null +++ b/crates/calendar/src/error.rs @@ -0,0 +1,24 @@ +use hypr_calendar_interface::CalendarProviderType; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("not authenticated")] + NotAuthenticated, + #[error("provider {provider:?} is not available on this platform")] + ProviderUnavailable { provider: CalendarProviderType }, + #[error("operation '{operation}' is not supported for provider {provider:?}")] + UnsupportedOperation { + operation: &'static str, + provider: CalendarProviderType, + }, + #[error("invalid datetime for field '{field}': {value}")] + InvalidDateTime { field: &'static str, value: String }, + #[error("invalid auth header: {0}")] + InvalidAuthHeader(#[from] reqwest::header::InvalidHeaderValue), + #[error("http client error: {0}")] + HttpClient(#[from] reqwest::Error), + #[error("api error: {0}")] + Api(String), + #[error("apple calendar error: {0}")] + Apple(String), +} diff --git a/plugins/calendar/src/fetch.rs b/crates/calendar/src/fetch.rs similarity index 100% rename from plugins/calendar/src/fetch.rs rename to crates/calendar/src/fetch.rs diff --git a/crates/calendar/src/lib.rs b/crates/calendar/src/lib.rs new file mode 100644 index 0000000000..7e8db52b89 --- /dev/null +++ b/crates/calendar/src/lib.rs @@ -0,0 +1,277 @@ +mod convert; +mod error; +mod fetch; + +pub use error::Error; +pub use hypr_calendar_interface::{ + CalendarEvent, CalendarListItem, CalendarProviderType, CreateEventInput, EventFilter, +}; + +#[cfg(target_os = "macos")] +pub use hypr_apple_calendar::setup_change_notification; + +#[cfg(target_os = "macos")] +use chrono::{DateTime, Utc}; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +pub struct ProviderConnectionIds { + pub provider: CalendarProviderType, + pub connection_ids: Vec, +} + +pub fn available_providers() -> Vec { + #[cfg(target_os = "macos")] + let providers = vec![ + CalendarProviderType::Apple, + CalendarProviderType::Google, + CalendarProviderType::Outlook, + ]; + + #[cfg(not(target_os = "macos"))] + let providers = vec![CalendarProviderType::Google, CalendarProviderType::Outlook]; + + providers +} + +pub async fn list_connection_ids( + api_base_url: &str, + access_token: Option<&str>, + apple_authorized: bool, +) -> Result, Error> { + let mut result = Vec::new(); + + #[cfg(target_os = "macos")] + { + if apple_authorized { + result.push(ProviderConnectionIds { + provider: CalendarProviderType::Apple, + connection_ids: vec!["apple".to_string()], + }); + } + } + + #[cfg(not(target_os = "macos"))] + let _ = apple_authorized; + + let token = match access_token { + Some(t) if !t.is_empty() => t, + _ => return Ok(result), + }; + + let all = fetch::list_all_connection_ids(api_base_url, token).await?; + + for (integration_id, connection_ids) in all { + let provider = match integration_id.as_str() { + "google-calendar" => CalendarProviderType::Google, + "outlook-calendar" => CalendarProviderType::Outlook, + _ => continue, + }; + result.push(ProviderConnectionIds { + provider, + connection_ids, + }); + } + + Ok(result) +} + +pub async fn is_provider_enabled( + api_base_url: &str, + access_token: Option<&str>, + apple_authorized: bool, + provider: CalendarProviderType, +) -> Result { + let all = list_connection_ids(api_base_url, access_token, apple_authorized).await?; + Ok(all + .iter() + .any(|p| p.provider == provider && !p.connection_ids.is_empty())) +} + +pub async fn list_calendars( + api_base_url: &str, + access_token: &str, + provider: CalendarProviderType, + connection_id: &str, +) -> Result, Error> { + match provider { + CalendarProviderType::Apple => { + let calendars = list_apple_calendars()?; + Ok(convert::convert_apple_calendars(calendars)) + } + CalendarProviderType::Google => { + let calendars = + fetch::list_google_calendars(api_base_url, access_token, connection_id).await?; + Ok(convert::convert_google_calendars(calendars)) + } + CalendarProviderType::Outlook => { + let calendars = + fetch::list_outlook_calendars(api_base_url, access_token, connection_id).await?; + Ok(convert::convert_outlook_calendars(calendars)) + } + } +} + +pub async fn list_events( + api_base_url: &str, + access_token: &str, + provider: CalendarProviderType, + connection_id: &str, + filter: EventFilter, +) -> Result, Error> { + match provider { + CalendarProviderType::Apple => { + let events = list_apple_events(filter)?; + Ok(convert::convert_apple_events(events)) + } + CalendarProviderType::Google => { + let calendar_id = filter.calendar_tracking_id.clone(); + let events = + fetch::list_google_events(api_base_url, access_token, connection_id, filter) + .await?; + Ok(convert::convert_google_events(events, &calendar_id)) + } + CalendarProviderType::Outlook => { + let calendar_id = filter.calendar_tracking_id.clone(); + let events = + fetch::list_outlook_events(api_base_url, access_token, connection_id, filter) + .await?; + Ok(convert::convert_outlook_events(events, &calendar_id)) + } + } +} + +pub fn open_calendar(provider: CalendarProviderType) -> Result<(), Error> { + match provider { + CalendarProviderType::Apple => open_apple_calendar(), + _ => Err(Error::UnsupportedOperation { + operation: "open_calendar", + provider, + }), + } +} + +pub fn create_event( + provider: CalendarProviderType, + input: CreateEventInput, +) -> Result { + match provider { + CalendarProviderType::Apple => create_apple_event(input), + _ => Err(Error::UnsupportedOperation { + operation: "create_event", + provider, + }), + } +} + +// --- Apple helpers --- + +#[cfg(target_os = "macos")] +fn open_apple_calendar() -> Result<(), Error> { + let script = String::from( + " + tell application \"Calendar\" + activate + switch view to month view + view calendar at current date + end tell + ", + ); + + std::process::Command::new("osascript") + .arg("-e") + .arg(script) + .spawn() + .map_err(|e| Error::Apple(e.to_string()))? + .wait() + .map_err(|e| Error::Apple(e.to_string()))?; + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn list_apple_calendars() -> Result, Error> { + let handle = hypr_apple_calendar::Handle::new(); + handle + .list_calendars() + .map_err(|e| Error::Apple(e.to_string())) +} + +#[cfg(target_os = "macos")] +fn list_apple_events( + filter: EventFilter, +) -> Result, Error> { + let handle = hypr_apple_calendar::Handle::new(); + let filter = hypr_apple_calendar::types::EventFilter { + from: filter.from, + to: filter.to, + calendar_tracking_id: filter.calendar_tracking_id, + }; + + handle + .list_events(filter) + .map_err(|e| Error::Apple(e.to_string())) +} + +#[cfg(target_os = "macos")] +fn create_apple_event(input: CreateEventInput) -> Result { + let handle = hypr_apple_calendar::Handle::new(); + + let start_date = parse_datetime(&input.started_at, "started_at")?; + let end_date = parse_datetime(&input.ended_at, "ended_at")?; + + let input = hypr_apple_calendar::types::CreateEventInput { + title: input.title, + start_date, + end_date, + calendar_id: input.calendar_tracking_id, + is_all_day: input.is_all_day, + location: input.location, + notes: input.notes, + url: input.url, + }; + + handle + .create_event(input) + .map_err(|e| Error::Apple(e.to_string())) +} + +#[cfg(target_os = "macos")] +fn parse_datetime(value: &str, field: &'static str) -> Result, Error> { + DateTime::parse_from_rfc3339(value) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|_| Error::InvalidDateTime { + field, + value: value.to_string(), + }) +} + +#[cfg(not(target_os = "macos"))] +fn open_apple_calendar() -> Result<(), Error> { + Err(Error::ProviderUnavailable { + provider: CalendarProviderType::Apple, + }) +} + +#[cfg(not(target_os = "macos"))] +fn list_apple_calendars() -> Result, Error> { + Err(Error::ProviderUnavailable { + provider: CalendarProviderType::Apple, + }) +} + +#[cfg(not(target_os = "macos"))] +fn list_apple_events( + _filter: EventFilter, +) -> Result, Error> { + Err(Error::ProviderUnavailable { + provider: CalendarProviderType::Apple, + }) +} + +#[cfg(not(target_os = "macos"))] +fn create_apple_event(_input: CreateEventInput) -> Result { + Err(Error::ProviderUnavailable { + provider: CalendarProviderType::Apple, + }) +} diff --git a/plugins/calendar/Cargo.toml b/plugins/calendar/Cargo.toml index 106f5773d1..01f0809737 100644 --- a/plugins/calendar/Cargo.toml +++ b/plugins/calendar/Cargo.toml @@ -8,22 +8,15 @@ links = "tauri-plugin-calendar" description = "" [dependencies] -hypr-api-client = { workspace = true } -hypr-apple-calendar = { workspace = true, features = ["specta"] } +hypr-calendar = { workspace = true, features = ["specta"] } hypr-calendar-interface = { workspace = true } -hypr-google-calendar = { workspace = true, features = ["specta"] } -hypr-outlook-calendar = { workspace = true, features = ["specta"] } tauri = { workspace = true, features = ["test"] } tauri-plugin-auth = { workspace = true } tauri-plugin-permissions = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } -chrono = { workspace = true, features = ["serde"] } -chrono-tz = { workspace = true } -reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } specta = { workspace = true, features = ["chrono"] } thiserror = { workspace = true } diff --git a/plugins/calendar/src/commands.rs b/plugins/calendar/src/commands.rs index f38dc923ec..da1987b7d3 100644 --- a/plugins/calendar/src/commands.rs +++ b/plugins/calendar/src/commands.rs @@ -1,15 +1,16 @@ use hypr_calendar_interface::{ CalendarEvent, CalendarListItem, CalendarProviderType, CreateEventInput, EventFilter, }; +use tauri::Manager; +use tauri_plugin_auth::AuthPluginExt; +use tauri_plugin_permissions::PermissionsPluginExt; -use crate::CalendarPluginExt; use crate::error::Error; -use crate::ext::ProviderConnectionIds; #[tauri::command] #[specta::specta] pub fn available_providers() -> Vec { - crate::ext::available_providers() + hypr_calendar::available_providers() } #[tauri::command] @@ -18,15 +19,25 @@ pub async fn is_provider_enabled( app: tauri::AppHandle, provider: CalendarProviderType, ) -> Result { - app.calendar().is_provider_enabled(provider).await + let config = app.state::(); + let token = access_token(&app); + let apple = is_apple_authorized(&app).await; + hypr_calendar::is_provider_enabled(&config.api_base_url, token.as_deref(), apple, provider) + .await + .map_err(Into::into) } #[tauri::command] #[specta::specta] pub async fn list_connection_ids( app: tauri::AppHandle, -) -> Result, Error> { - app.calendar().list_connection_ids().await +) -> Result, Error> { + let config = app.state::(); + let token = access_token(&app); + let apple = is_apple_authorized(&app).await; + hypr_calendar::list_connection_ids(&config.api_base_url, token.as_deref(), apple) + .await + .map_err(Into::into) } #[tauri::command] @@ -36,7 +47,11 @@ pub async fn list_calendars( provider: CalendarProviderType, connection_id: String, ) -> Result, Error> { - app.calendar().list_calendars(provider, connection_id).await + let config = app.state::(); + let token = access_token(&app).unwrap_or_default(); + hypr_calendar::list_calendars(&config.api_base_url, &token, provider, &connection_id) + .await + .map_err(Into::into) } #[tauri::command] @@ -47,26 +62,55 @@ pub async fn list_events( connection_id: String, filter: EventFilter, ) -> Result, Error> { - app.calendar() - .list_events(provider, connection_id, filter) - .await + let config = app.state::(); + let token = access_token(&app).unwrap_or_default(); + hypr_calendar::list_events( + &config.api_base_url, + &token, + provider, + &connection_id, + filter, + ) + .await + .map_err(Into::into) } #[tauri::command] #[specta::specta] pub fn open_calendar( - app: tauri::AppHandle, + _app: tauri::AppHandle, provider: CalendarProviderType, ) -> Result<(), Error> { - app.calendar().open_calendar(provider) + hypr_calendar::open_calendar(provider).map_err(Into::into) } #[tauri::command] #[specta::specta] pub fn create_event( - app: tauri::AppHandle, + _app: tauri::AppHandle, provider: CalendarProviderType, input: CreateEventInput, ) -> Result { - app.calendar().create_event(provider, input) + hypr_calendar::create_event(provider, input).map_err(Into::into) +} + +fn access_token(app: &tauri::AppHandle) -> Option { + app.access_token().ok().flatten().filter(|t| !t.is_empty()) +} + +async fn is_apple_authorized(app: &tauri::AppHandle) -> bool { + #[cfg(target_os = "macos")] + { + app.permissions() + .check(tauri_plugin_permissions::Permission::Calendar) + .await + .map(|s| matches!(s, tauri_plugin_permissions::PermissionStatus::Authorized)) + .unwrap_or(false) + } + + #[cfg(not(target_os = "macos"))] + { + let _ = app; + false + } } diff --git a/plugins/calendar/src/error.rs b/plugins/calendar/src/error.rs index 079f362ef4..34672510bb 100644 --- a/plugins/calendar/src/error.rs +++ b/plugins/calendar/src/error.rs @@ -1,28 +1,7 @@ -use hypr_calendar_interface::CalendarProviderType; - #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("not authenticated")] - NotAuthenticated, - #[error("provider {provider:?} is not available on this platform")] - ProviderUnavailable { provider: CalendarProviderType }, - #[error("operation '{operation}' is not supported for provider {provider:?}")] - UnsupportedOperation { - operation: &'static str, - provider: CalendarProviderType, - }, - #[error("invalid datetime for field '{field}': {value}")] - InvalidDateTime { field: &'static str, value: String }, - #[error("invalid auth header: {0}")] - InvalidAuthHeader(#[from] reqwest::header::InvalidHeaderValue), - #[error("http client error: {0}")] - HttpClient(#[from] reqwest::Error), - #[error("auth plugin error: {0}")] - Auth(String), - #[error("api error: {0}")] - Api(String), - #[error("apple calendar error: {0}")] - Apple(String), + #[error(transparent)] + Calendar(#[from] hypr_calendar::Error), } impl serde::Serialize for Error { diff --git a/plugins/calendar/src/ext.rs b/plugins/calendar/src/ext.rs deleted file mode 100644 index 4f845c9edb..0000000000 --- a/plugins/calendar/src/ext.rs +++ /dev/null @@ -1,345 +0,0 @@ -#[cfg(target_os = "macos")] -use chrono::{DateTime, Utc}; -use hypr_calendar_interface::{ - CalendarEvent, CalendarListItem, CalendarProviderType, CreateEventInput, EventFilter, -}; -use hypr_google_calendar::{CalendarListEntry as GoogleCalendar, Event as GoogleEvent}; -use hypr_outlook_calendar::{Calendar as OutlookCalendar, Event as OutlookEvent}; -use tauri_plugin_auth::AuthPluginExt; -use tauri_plugin_permissions::PermissionsPluginExt; - -use crate::error::Error; -use crate::fetch; - -#[derive(serde::Serialize, serde::Deserialize, specta::Type, Clone, Debug)] -pub struct ProviderConnectionIds { - pub provider: CalendarProviderType, - pub connection_ids: Vec, -} - -pub struct CalendarExt<'a, R: tauri::Runtime, M: tauri::Manager> { - manager: &'a M, - _runtime: std::marker::PhantomData R>, -} - -pub fn available_providers() -> Vec { - #[cfg(target_os = "macos")] - let providers = vec![ - CalendarProviderType::Apple, - CalendarProviderType::Google, - CalendarProviderType::Outlook, - ]; - - #[cfg(not(target_os = "macos"))] - let providers = vec![CalendarProviderType::Google, CalendarProviderType::Outlook]; - - providers -} - -impl<'a, R: tauri::Runtime, M: tauri::Manager> CalendarExt<'a, R, M> { - pub async fn list_calendars( - &self, - provider: CalendarProviderType, - connection_id: String, - ) -> Result, Error> { - match provider { - CalendarProviderType::Apple => { - let calendars = self.list_apple_calendars()?; - Ok(crate::convert::convert_apple_calendars(calendars)) - } - CalendarProviderType::Google => { - let calendars = self.list_google_calendars(&connection_id).await?; - Ok(crate::convert::convert_google_calendars(calendars)) - } - CalendarProviderType::Outlook => { - let calendars = self.list_outlook_calendars(&connection_id).await?; - Ok(crate::convert::convert_outlook_calendars(calendars)) - } - } - } - - pub async fn list_events( - &self, - provider: CalendarProviderType, - connection_id: String, - filter: EventFilter, - ) -> Result, Error> { - match provider { - CalendarProviderType::Apple => { - let events = self.list_apple_events(filter)?; - Ok(crate::convert::convert_apple_events(events)) - } - CalendarProviderType::Google => { - let calendar_id = filter.calendar_tracking_id.clone(); - let events = self.list_google_events(&connection_id, filter).await?; - Ok(crate::convert::convert_google_events(events, &calendar_id)) - } - CalendarProviderType::Outlook => { - let calendar_id = filter.calendar_tracking_id.clone(); - let events = self.list_outlook_events(&connection_id, filter).await?; - Ok(crate::convert::convert_outlook_events(events, &calendar_id)) - } - } - } - - pub fn open_calendar(&self, provider: CalendarProviderType) -> Result<(), Error> { - match provider { - CalendarProviderType::Apple => self.open_apple_calendar(), - _ => Err(Error::UnsupportedOperation { - operation: "open_calendar", - provider, - }), - } - } - - pub fn create_event( - &self, - provider: CalendarProviderType, - input: CreateEventInput, - ) -> Result { - match provider { - CalendarProviderType::Apple => self.create_apple_event(input), - _ => Err(Error::UnsupportedOperation { - operation: "create_event", - provider, - }), - } - } - - pub async fn list_connection_ids(&self) -> Result, Error> { - let mut result = Vec::new(); - - #[cfg(target_os = "macos")] - { - let status = self - .manager - .permissions() - .check(tauri_plugin_permissions::Permission::Calendar) - .await - .map_err(|e| Error::Api(e.to_string()))?; - - if matches!( - status, - tauri_plugin_permissions::PermissionStatus::Authorized - ) { - result.push(ProviderConnectionIds { - provider: CalendarProviderType::Apple, - connection_ids: vec!["apple".to_string()], - }); - } - } - - let token = match self.get_access_token() { - Ok(token) => token, - Err(_) => return Ok(result), - }; - - let config = self.manager.state::(); - let all = fetch::list_all_connection_ids(&config.api_base_url, &token).await?; - - for (integration_id, connection_ids) in all { - let provider = match integration_id.as_str() { - "google-calendar" => CalendarProviderType::Google, - "outlook-calendar" => CalendarProviderType::Outlook, - _ => continue, - }; - result.push(ProviderConnectionIds { - provider, - connection_ids, - }); - } - - Ok(result) - } - - pub async fn is_provider_enabled(&self, provider: CalendarProviderType) -> Result { - let all = self.list_connection_ids().await?; - Ok(all - .iter() - .any(|p| p.provider == provider && !p.connection_ids.is_empty())) - } - - fn get_access_token(&self) -> Result { - let token = self - .manager - .access_token() - .map_err(|e| Error::Auth(e.to_string()))?; - - match token { - Some(token) if !token.is_empty() => Ok(token), - _ => Err(Error::NotAuthenticated), - } - } - - async fn list_google_calendars( - &self, - connection_id: &str, - ) -> Result, Error> { - let token = self.get_access_token()?; - let config = self.manager.state::(); - fetch::list_google_calendars(&config.api_base_url, &token, connection_id).await - } - - async fn list_google_events( - &self, - connection_id: &str, - filter: EventFilter, - ) -> Result, Error> { - let token = self.get_access_token()?; - let config = self.manager.state::(); - fetch::list_google_events(&config.api_base_url, &token, connection_id, filter).await - } - - async fn list_outlook_calendars( - &self, - connection_id: &str, - ) -> Result, Error> { - let token = self.get_access_token()?; - let config = self.manager.state::(); - fetch::list_outlook_calendars(&config.api_base_url, &token, connection_id).await - } - - async fn list_outlook_events( - &self, - connection_id: &str, - filter: EventFilter, - ) -> Result, Error> { - let token = self.get_access_token()?; - let config = self.manager.state::(); - fetch::list_outlook_events(&config.api_base_url, &token, connection_id, filter).await - } - - #[cfg(target_os = "macos")] - fn open_apple_calendar(&self) -> Result<(), Error> { - let script = String::from( - " - tell application \"Calendar\" - activate - switch view to month view - view calendar at current date - end tell - ", - ); - - std::process::Command::new("osascript") - .arg("-e") - .arg(script) - .spawn() - .map_err(|e| Error::Apple(e.to_string()))? - .wait() - .map_err(|e| Error::Apple(e.to_string()))?; - - Ok(()) - } - - #[cfg(target_os = "macos")] - fn list_apple_calendars( - &self, - ) -> Result, Error> { - let handle = hypr_apple_calendar::Handle::new(); - handle - .list_calendars() - .map_err(|e| Error::Apple(e.to_string())) - } - - #[cfg(target_os = "macos")] - fn list_apple_events( - &self, - filter: EventFilter, - ) -> Result, Error> { - let handle = hypr_apple_calendar::Handle::new(); - let filter = hypr_apple_calendar::types::EventFilter { - from: filter.from, - to: filter.to, - calendar_tracking_id: filter.calendar_tracking_id, - }; - - handle - .list_events(filter) - .map_err(|e| Error::Apple(e.to_string())) - } - - #[cfg(target_os = "macos")] - fn create_apple_event(&self, input: CreateEventInput) -> Result { - let handle = hypr_apple_calendar::Handle::new(); - - let start_date = parse_datetime(&input.started_at, "started_at")?; - let end_date = parse_datetime(&input.ended_at, "ended_at")?; - - let input = hypr_apple_calendar::types::CreateEventInput { - title: input.title, - start_date, - end_date, - calendar_id: input.calendar_tracking_id, - is_all_day: input.is_all_day, - location: input.location, - notes: input.notes, - url: input.url, - }; - - handle - .create_event(input) - .map_err(|e| Error::Apple(e.to_string())) - } - - #[cfg(not(target_os = "macos"))] - fn open_apple_calendar(&self) -> Result<(), Error> { - Err(Error::ProviderUnavailable { - provider: CalendarProviderType::Apple, - }) - } - - #[cfg(not(target_os = "macos"))] - fn list_apple_calendars( - &self, - ) -> Result, Error> { - Err(Error::ProviderUnavailable { - provider: CalendarProviderType::Apple, - }) - } - - #[cfg(not(target_os = "macos"))] - fn list_apple_events( - &self, - _filter: EventFilter, - ) -> Result, Error> { - Err(Error::ProviderUnavailable { - provider: CalendarProviderType::Apple, - }) - } - - #[cfg(not(target_os = "macos"))] - fn create_apple_event(&self, _input: CreateEventInput) -> Result { - Err(Error::ProviderUnavailable { - provider: CalendarProviderType::Apple, - }) - } -} - -#[cfg(target_os = "macos")] -fn parse_datetime(value: &str, field: &'static str) -> Result, Error> { - DateTime::parse_from_rfc3339(value) - .map(|dt| dt.with_timezone(&Utc)) - .map_err(|_| Error::InvalidDateTime { - field, - value: value.to_string(), - }) -} - -pub trait CalendarPluginExt { - fn calendar(&self) -> CalendarExt<'_, R, Self> - where - Self: tauri::Manager + Sized; -} - -impl> CalendarPluginExt for T { - fn calendar(&self) -> CalendarExt<'_, R, Self> - where - Self: Sized, - { - CalendarExt { - manager: self, - _runtime: std::marker::PhantomData, - } - } -} diff --git a/plugins/calendar/src/lib.rs b/plugins/calendar/src/lib.rs index 9e68358129..3f7817c819 100644 --- a/plugins/calendar/src/lib.rs +++ b/plugins/calendar/src/lib.rs @@ -1,13 +1,10 @@ mod commands; -mod convert; mod error; mod events; -mod ext; -mod fetch; pub use error::Error; pub use events::*; -pub use ext::{CalendarExt, CalendarPluginExt, ProviderConnectionIds}; +pub use hypr_calendar::ProviderConnectionIds; pub(crate) struct PluginConfig { pub api_base_url: String, @@ -45,7 +42,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { use tauri_specta::Event; let app_handle = app.app_handle().clone(); - hypr_apple_calendar::setup_change_notification(move || { + hypr_calendar::setup_change_notification(move || { let _ = CalendarChangedEvent.emit(&app_handle); }); } From 91a9297d0bed3c59ebf1a400a54a5e09c78d3046 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 16 Mar 2026 16:13:55 +0900 Subject: [PATCH 2/2] fix error handling Signed-off-by: Yujong Lee --- plugins/calendar/src/commands.rs | 36 ++++++++++++++++++++++++-------- plugins/calendar/src/error.rs | 2 ++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/plugins/calendar/src/commands.rs b/plugins/calendar/src/commands.rs index da1987b7d3..d7dace84a6 100644 --- a/plugins/calendar/src/commands.rs +++ b/plugins/calendar/src/commands.rs @@ -21,7 +21,7 @@ pub async fn is_provider_enabled( ) -> Result { let config = app.state::(); let token = access_token(&app); - let apple = is_apple_authorized(&app).await; + let apple = is_apple_authorized(&app).await?; hypr_calendar::is_provider_enabled(&config.api_base_url, token.as_deref(), apple, provider) .await .map_err(Into::into) @@ -34,7 +34,7 @@ pub async fn list_connection_ids( ) -> Result, Error> { let config = app.state::(); let token = access_token(&app); - let apple = is_apple_authorized(&app).await; + let apple = is_apple_authorized(&app).await?; hypr_calendar::list_connection_ids(&config.api_base_url, token.as_deref(), apple) .await .map_err(Into::into) @@ -48,7 +48,10 @@ pub async fn list_calendars( connection_id: String, ) -> Result, Error> { let config = app.state::(); - let token = access_token(&app).unwrap_or_default(); + let token = match provider { + CalendarProviderType::Apple => access_token(&app).unwrap_or_default(), + _ => require_access_token(&app)?, + }; hypr_calendar::list_calendars(&config.api_base_url, &token, provider, &connection_id) .await .map_err(Into::into) @@ -63,7 +66,10 @@ pub async fn list_events( filter: EventFilter, ) -> Result, Error> { let config = app.state::(); - let token = access_token(&app).unwrap_or_default(); + let token = match provider { + CalendarProviderType::Apple => access_token(&app).unwrap_or_default(), + _ => require_access_token(&app)?, + }; hypr_calendar::list_events( &config.api_base_url, &token, @@ -98,19 +104,31 @@ fn access_token(app: &tauri::AppHandle) -> Option app.access_token().ok().flatten().filter(|t| !t.is_empty()) } -async fn is_apple_authorized(app: &tauri::AppHandle) -> bool { +fn require_access_token(app: &tauri::AppHandle) -> Result { + let token = app.access_token().map_err(|e| Error::Auth(e.to_string()))?; + match token { + Some(t) if !t.is_empty() => Ok(t), + _ => Err(hypr_calendar::Error::NotAuthenticated.into()), + } +} + +async fn is_apple_authorized(app: &tauri::AppHandle) -> Result { #[cfg(target_os = "macos")] { - app.permissions() + let status = app + .permissions() .check(tauri_plugin_permissions::Permission::Calendar) .await - .map(|s| matches!(s, tauri_plugin_permissions::PermissionStatus::Authorized)) - .unwrap_or(false) + .map_err(|e| hypr_calendar::Error::Api(e.to_string()))?; + Ok(matches!( + status, + tauri_plugin_permissions::PermissionStatus::Authorized + )) } #[cfg(not(target_os = "macos"))] { let _ = app; - false + Ok(false) } } diff --git a/plugins/calendar/src/error.rs b/plugins/calendar/src/error.rs index 34672510bb..41a2451c06 100644 --- a/plugins/calendar/src/error.rs +++ b/plugins/calendar/src/error.rs @@ -2,6 +2,8 @@ pub enum Error { #[error(transparent)] Calendar(#[from] hypr_calendar::Error), + #[error("auth error: {0}")] + Auth(String), } impl serde::Serialize for Error {