diff --git a/Cargo.lock b/Cargo.lock index 9ea3523ddd..854f25b44a 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" @@ -3268,6 +3292,7 @@ dependencies = [ "owhisper-interface", "ractor", "ratatui", + "rig-core", "serde", "serde_json", "storage", @@ -11118,6 +11143,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" @@ -12300,6 +12334,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" @@ -14582,6 +14625,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" @@ -17755,16 +17830,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", @@ -19168,7 +19236,7 @@ dependencies = [ "nix 0.29.0", "num-derive", "num-traits", - "ordered-float", + "ordered-float 4.6.0", "pest", "pest_derive", "phf 0.11.3", @@ -19977,6 +20045,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ + "futures", + "futures-task", + "pin-project", "tracing", ] @@ -21961,7 +22032,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 8240dd799d..158f11c979 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -60,18 +60,23 @@ 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 } url = { workspace = true } uuid = { workspace = true, features = ["v4"] } -strum = { workspace = true, features = ["derive"] } [dev-dependencies] 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] mock-audio = ["dep:hypr-audio-mock"] 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..d7dace84a6 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,14 @@ 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 = 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) } #[tauri::command] @@ -47,26 +65,70 @@ 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 = match provider { + CalendarProviderType::Apple => access_token(&app).unwrap_or_default(), + _ => require_access_token(&app)?, + }; + 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()) +} + +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")] + { + let status = app + .permissions() + .check(tauri_plugin_permissions::Permission::Calendar) + .await + .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; + Ok(false) + } } diff --git a/plugins/calendar/src/error.rs b/plugins/calendar/src/error.rs index 079f362ef4..41a2451c06 100644 --- a/plugins/calendar/src/error.rs +++ b/plugins/calendar/src/error.rs @@ -1,28 +1,9 @@ -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}")] + #[error(transparent)] + Calendar(#[from] hypr_calendar::Error), + #[error("auth error: {0}")] Auth(String), - #[error("api error: {0}")] - Api(String), - #[error("apple calendar error: {0}")] - Apple(String), } 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); }); }