From a6d72ce37fc309762acec5315270e92ce6675f29 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Sat, 30 Aug 2025 12:07:33 -0400 Subject: [PATCH] Remove v2 API endpoints and fix warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all v2 API routes (/api/v2/calendar/events/*) - Delete models_v2.rs file and associated types - Remove create_event_v2, update_event_v2, delete_event_v2 handlers - Remove unused occurrence_date and exception_dates from UpdateEventRequest - Remove unused ConfigError variant from CalDAVError - Simplify backend to single unified v1 API using VEvent structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/calendar.rs | 3 - backend/src/handlers.rs | 599 +-------------------------------------- backend/src/lib.rs | 5 - backend/src/models.rs | 2 - backend/src/models_v2.rs | 295 ------------------- 5 files changed, 1 insertion(+), 903 deletions(-) delete mode 100644 backend/src/models_v2.rs diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index be20f26..6d6d428 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -1060,9 +1060,6 @@ pub enum CalDAVError { #[error("Failed to parse calendar data: {0}")] ParseError(String), - - #[error("Configuration error: {0}")] - ConfigError(String), } #[cfg(test)] diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index 445c240..b4b8938 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use chrono::{Datelike, TimeZone}; use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, Attendee, VAlarm, AlarmAction, AlarmTrigger}; -use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}, models_v2::{CreateEventRequestV2, CreateEventResponseV2, UpdateEventRequestV2, UpdateEventResponseV2, DeleteEventRequestV2, DeleteEventResponseV2, DeleteActionV2}}; +use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}}; use crate::calendar::{CalDAVClient, CalendarEvent}; #[derive(Deserialize)] @@ -375,161 +375,6 @@ async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_h Ok(None) } -/// Delete event using v2 API with enum-based delete actions -pub async fn delete_event_v2( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result, ApiError> { - println!("🗑️ Delete event v2 request received: calendar_path='{}', event_href='{}', action={:?}", - request.calendar_path, request.event_href, request.delete_action); - - // Extract and verify token - let token = extract_bearer_token(&headers)?; - let password = extract_password_header(&headers)?; - - // Validate request - if request.calendar_path.trim().is_empty() { - return Err(ApiError::BadRequest("Calendar path is required".to_string())); - } - if request.event_href.trim().is_empty() { - return Err(ApiError::BadRequest("Event href is required".to_string())); - } - - // Create CalDAV config from token and password - let config = state.auth_service.caldav_config_from_token(&token, &password)?; - let client = CalDAVClient::new(config); - - // Handle different delete actions - match request.delete_action { - DeleteActionV2::DeleteThis => { - // Add EXDATE to exclude this specific occurrence - if let Some(occurrence_date) = request.occurrence_date { - println!("🔄 Adding EXDATE for occurrence: {}", occurrence_date); - - // First, fetch the current event to get its data - match fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await { - Ok(Some(mut event)) => { - // Check if it has recurrence rule - if event.rrule.is_some() { - // Calculate the exact datetime for this occurrence by using the original event's time - let original_time = event.dtstart.time(); - let occurrence_datetime = occurrence_date.date_naive().and_time(original_time); - let exception_utc = chrono::Utc.from_utc_datetime(&occurrence_datetime); - - println!("🔄 Original event start: {}", event.dtstart); - println!("🔄 Occurrence date: {}", occurrence_date); - println!("🔄 Calculated EXDATE: {}", exception_utc); - - // Add the exception date - event.exdate.push(exception_utc); - - // Update the event with the new EXDATE - client.update_event(&request.calendar_path, &event, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to update event with EXDATE: {}", e)))?; - - Ok(Json(DeleteEventResponseV2 { - success: true, - message: "Individual occurrence excluded from series successfully".to_string(), - })) - } else { - // Not a recurring event, just delete it completely - client.delete_event(&request.calendar_path, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; - - Ok(Json(DeleteEventResponseV2 { - success: true, - message: "Event deleted successfully".to_string(), - })) - } - }, - Ok(None) => Err(ApiError::NotFound("Event not found".to_string())), - Err(e) => Err(ApiError::Internal(format!("Failed to fetch event: {}", e))), - } - } else { - Err(ApiError::BadRequest("Occurrence date is required for 'delete_this' action".to_string())) - } - }, - DeleteActionV2::DeleteFollowing => { - // Modify RRULE to end before the selected occurrence - if let Some(occurrence_date) = request.occurrence_date { - println!("🔄 Modifying RRULE to end before: {}", occurrence_date); - - // First, fetch the current event to get its data - match fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await { - Ok(Some(mut event)) => { - // Check if it has recurrence rule - if let Some(ref rrule) = event.rrule { - // Calculate the datetime for the occurrence we want to stop before - let original_time = event.dtstart.time(); - let occurrence_datetime = occurrence_date.date_naive().and_time(original_time); - let occurrence_utc = chrono::Utc.from_utc_datetime(&occurrence_datetime); - - // UNTIL should be the last occurrence we want to keep (day before the selected occurrence) - let until_date = occurrence_utc - chrono::Duration::days(1); - let until_str = until_date.format("%Y%m%dT%H%M%SZ").to_string(); - - println!("🔄 Original event start: {}", event.dtstart); - println!("🔄 Occurrence to stop before: {}", occurrence_utc); - println!("🔄 UNTIL date (last to keep): {}", until_date); - println!("🔄 UNTIL string: {}", until_str); - println!("🔄 Original RRULE: {}", rrule); - - // Modify the RRULE to add UNTIL clause - let new_rrule = if rrule.contains("UNTIL=") { - // Replace existing UNTIL - regex::Regex::new(r"UNTIL=[^;]+").unwrap().replace(rrule, &format!("UNTIL={}", until_str)).to_string() - } else { - // Add UNTIL clause - format!("{};UNTIL={}", rrule, until_str) - }; - - println!("🔄 New RRULE: {}", new_rrule); - event.rrule = Some(new_rrule); - - // Update the event with the modified RRULE - client.update_event(&request.calendar_path, &event, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?; - - Ok(Json(DeleteEventResponseV2 { - success: true, - message: "Following occurrences removed from series successfully".to_string(), - })) - } else { - // Not a recurring event, just delete it completely - client.delete_event(&request.calendar_path, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; - - Ok(Json(DeleteEventResponseV2 { - success: true, - message: "Event deleted successfully".to_string(), - })) - } - }, - Ok(None) => Err(ApiError::NotFound("Event not found".to_string())), - Err(e) => Err(ApiError::Internal(format!("Failed to fetch event: {}", e))), - } - } else { - Err(ApiError::BadRequest("Occurrence date is required for 'delete_following' action".to_string())) - } - }, - DeleteActionV2::DeleteSeries => { - // Delete the entire event/series (current default behavior) - client.delete_event(&request.calendar_path, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; - - Ok(Json(DeleteEventResponseV2 { - success: true, - message: "Event series deleted successfully".to_string(), - })) - } - } -} pub async fn delete_event( State(state): State>, @@ -697,168 +542,6 @@ pub async fn delete_event( } } -/// Create event using v2 API with direct DateTime support (no string parsing) -pub async fn create_event_v2( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result, ApiError> { - println!("📝 Create event v2 request received: summary='{}', all_day={}, calendar_path={:?}", - request.summary, request.all_day, request.calendar_path); - - // Extract and verify token - let token = extract_bearer_token(&headers)?; - let password = extract_password_header(&headers)?; - - // Validate request - if request.summary.trim().is_empty() { - return Err(ApiError::BadRequest("Event summary is required".to_string())); - } - - if request.summary.len() > 200 { - return Err(ApiError::BadRequest("Event summary too long (max 200 characters)".to_string())); - } - - // Create CalDAV config from token and password - let config = state.auth_service.caldav_config_from_token(&token, &password)?; - let client = CalDAVClient::new(config); - - // Determine which calendar to use - let calendar_path = if let Some(path) = request.calendar_path { - path - } else { - // Use the first available calendar - let calendar_paths = client.discover_calendars() - .await - .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; - - if calendar_paths.is_empty() { - return Err(ApiError::BadRequest("No calendars available for event creation".to_string())); - } - - calendar_paths[0].clone() - }; - - // Validate that end is after start - if let Some(end) = request.dtend { - if end <= request.dtstart { - return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); - } - } - - // Generate a unique UID for the event - let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp()); - - // Convert V2 enums to calendar module enums - let status = match request.status.unwrap_or_default() { - crate::models_v2::EventStatusV2::Tentative => EventStatus::Tentative, - crate::models_v2::EventStatusV2::Cancelled => EventStatus::Cancelled, - crate::models_v2::EventStatusV2::Confirmed => EventStatus::Confirmed, - }; - - let class = match request.class.unwrap_or_default() { - crate::models_v2::EventClassV2::Private => EventClass::Private, - crate::models_v2::EventClassV2::Confidential => EventClass::Confidential, - crate::models_v2::EventClassV2::Public => EventClass::Public, - }; - - // Convert attendees from V2 to simple email list (for now) - let attendees: Vec = request.attendees.into_iter() - .map(|att| att.email) - .collect(); - - // Convert alarms to alarms - let alarms: Vec = request.alarms.into_iter() - .map(|alarm| crate::calendar::EventReminder { - minutes_before: -alarm.trigger_minutes, // Convert to positive minutes before - action: match alarm.action { - crate::models_v2::AlarmActionV2::Display => crate::calendar::ReminderAction::Display, - crate::models_v2::AlarmActionV2::Email => crate::calendar::ReminderAction::Email, - crate::models_v2::AlarmActionV2::Audio => crate::calendar::ReminderAction::Audio, - }, - description: alarm.description, - }) - .collect(); - - // Create the CalendarEvent struct - much simpler now! - // Create VEvent with required fields - let mut event = VEvent::new(uid, request.dtstart); - - // Set optional fields - event.dtend = request.dtend; - event.summary = Some(request.summary.clone()); - event.description = request.description; - event.location = request.location; - event.status = Some(status); - event.class = Some(class); - event.priority = request.priority; - event.organizer = request.organizer.map(|org| CalendarUser { - cal_address: org, - common_name: None, - dir_entry_ref: None, - sent_by: None, - language: None, - }); - event.attendees = attendees.into_iter().map(|email| Attendee { - cal_address: email, - common_name: None, - role: None, - part_stat: None, - rsvp: None, - cu_type: None, - member: Vec::new(), - delegated_to: Vec::new(), - delegated_from: Vec::new(), - sent_by: None, - dir_entry_ref: None, - language: None, - }).collect(); - event.categories = request.categories; - event.rrule = request.rrule; - event.all_day = request.all_day; - event.alarms = alarms.into_iter().map(|alarm| VAlarm { - action: match alarm.action { - crate::calendar::ReminderAction::Display => calendar_models::AlarmAction::Display, - crate::calendar::ReminderAction::Email => calendar_models::AlarmAction::Email, - crate::calendar::ReminderAction::Audio => calendar_models::AlarmAction::Audio, - }, - trigger: calendar_models::AlarmTrigger::Duration(chrono::Duration::minutes(-alarm.minutes_before as i64)), - duration: None, - repeat: None, - description: alarm.description, - summary: None, - attendees: Vec::new(), - attach: Vec::new(), - }).collect(); - event.calendar_path = Some(calendar_path.clone()); - - // Create the event on the CalDAV server - let event_href = client.create_event(&calendar_path, &event) - .await - .map_err(|e| ApiError::Internal(format!("Failed to create event: {}", e)))?; - - // Fetch the created event to get its details - let created_event = fetch_event_by_href(&client, &calendar_path, &event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to fetch created event: {}", e)))?; - - let event_summary = created_event.map(|e| crate::models_v2::EventSummaryV2 { - uid: e.uid, - summary: e.summary, - dtstart: e.dtstart, - dtend: e.dtend, - location: e.location, - all_day: e.all_day, - href: e.href, - etag: e.etag, - }); - - Ok(Json(CreateEventResponseV2 { - success: true, - message: "Event created successfully".to_string(), - event: event_summary, - })) -} pub async fn create_event( State(state): State>, @@ -1072,286 +755,6 @@ pub async fn create_event( } -/// Update event using v2 API with direct DateTime support (no string parsing) -pub async fn update_event_v2( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result, ApiError> { - println!("🔄 Update event v2 request received: uid='{}', summary='{}', update_action={:?}", - request.uid, request.summary, request.update_action); - - // Extract and verify token - let token = extract_bearer_token(&headers)?; - let password = extract_password_header(&headers)?; - - // Validate request - if request.uid.trim().is_empty() { - return Err(ApiError::BadRequest("Event UID is required".to_string())); - } - - if request.summary.trim().is_empty() { - return Err(ApiError::BadRequest("Event summary is required".to_string())); - } - - if request.summary.len() > 200 { - return Err(ApiError::BadRequest("Event summary too long (max 200 characters)".to_string())); - } - - // Create CalDAV config from token and password - let config = state.auth_service.caldav_config_from_token(&token, &password)?; - let client = CalDAVClient::new(config); - - // Find the event across all calendars (or in the specified calendar) - let calendar_paths = if let Some(path) = &request.calendar_path { - vec![path.clone()] - } else { - client.discover_calendars() - .await - .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))? - }; - - if calendar_paths.is_empty() { - return Err(ApiError::BadRequest("No calendars available for event update".to_string())); - } - - // Validate that end is after start - if let Some(end) = request.dtend { - if end <= request.dtstart { - return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); - } - } - - // Determine if this is a series update - let search_uid = request.uid.clone(); - let is_series_update = request.update_action.as_deref() == Some("update_series"); - - // Search for the event by UID across the specified calendars - let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, href) - for calendar_path in &calendar_paths { - // First try exact match - match client.fetch_event_by_uid(calendar_path, &search_uid).await { - Ok(Some(event)) => { - if let Some(href) = event.href.clone() { - found_event = Some((event, calendar_path.clone(), href)); - break; - } - }, - Ok(None) => { - // If exact match fails, try to find by base UID pattern for recurring events - println!("🔍 Exact match failed for '{}', searching by base UID pattern", search_uid); - match client.fetch_events(calendar_path).await { - Ok(events) => { - for event in events { - if let Some(href) = &event.href { - if event.uid.starts_with(&search_uid) && event.uid != search_uid { - println!("🎯 Found recurring event by pattern: '{}' matches '{}'", event.uid, search_uid); - found_event = Some((event.clone(), calendar_path.clone(), href.clone())); - break; - } - } - } - if found_event.is_some() { - break; - } - }, - Err(e) => { - eprintln!("Error fetching events from {}: {:?}", calendar_path, e); - continue; - } - } - }, - Err(e) => { - eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e); - continue; - } - } - } - - let (mut event, calendar_path, event_href) = found_event - .ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", search_uid)))?; - - // Convert V2 enums to calendar module enums - let status = match request.status.unwrap_or_default() { - crate::models_v2::EventStatusV2::Tentative => EventStatus::Tentative, - crate::models_v2::EventStatusV2::Cancelled => EventStatus::Cancelled, - crate::models_v2::EventStatusV2::Confirmed => EventStatus::Confirmed, - }; - - let class = match request.class.unwrap_or_default() { - crate::models_v2::EventClassV2::Private => EventClass::Private, - crate::models_v2::EventClassV2::Confidential => EventClass::Confidential, - crate::models_v2::EventClassV2::Public => EventClass::Public, - }; - - // Convert attendees from V2 to simple email list (for now) - let attendees: Vec = request.attendees.into_iter() - .map(|att| att.email) - .collect(); - - // Convert alarms to alarms - let alarms: Vec = request.alarms.into_iter() - .map(|alarm| crate::calendar::EventReminder { - minutes_before: -alarm.trigger_minutes, - action: match alarm.action { - crate::models_v2::AlarmActionV2::Display => crate::calendar::ReminderAction::Display, - crate::models_v2::AlarmActionV2::Email => crate::calendar::ReminderAction::Email, - crate::models_v2::AlarmActionV2::Audio => crate::calendar::ReminderAction::Audio, - }, - description: alarm.description, - }) - .collect(); - - // Update the event fields with new data - event.summary = Some(request.summary.clone()); - event.description = request.description; - - // Handle date/time updates based on update type - if is_series_update { - // For series updates, only update the TIME, keep the original DATE - let original_start_date = event.dtstart.date_naive(); - let original_end_date = event.dtend.map(|e| e.date_naive()).unwrap_or(original_start_date); - - let new_start_time = request.dtstart.time(); - let new_end_time = request.dtend.map(|dt| dt.time()).unwrap_or(new_start_time); - - // Combine original date with new time - let updated_start = original_start_date.and_time(new_start_time).and_utc(); - let updated_end = original_end_date.and_time(new_end_time).and_utc(); - - event.dtstart = updated_start; - event.dtend = Some(updated_end); - } else { - // For regular updates, update both date and time - event.dtstart = request.dtstart; - event.dtend = request.dtend; - } - - event.location = request.location; - event.status = Some(status); - event.class = Some(class); - event.priority = request.priority; - event.organizer = request.organizer.map(|org| CalendarUser { - cal_address: org, - common_name: None, - dir_entry_ref: None, - sent_by: None, - language: None, - }); - event.attendees = attendees.into_iter().map(|email| Attendee { - cal_address: email, - common_name: None, - role: None, - part_stat: None, - rsvp: None, - cu_type: None, - member: Vec::new(), - delegated_to: Vec::new(), - delegated_from: Vec::new(), - sent_by: None, - dir_entry_ref: None, - language: None, - }).collect(); - event.categories = request.categories; - event.last_modified = Some(chrono::Utc::now()); - event.all_day = request.all_day; - event.alarms = alarms.into_iter().map(|alarm| VAlarm { - action: match alarm.action { - crate::calendar::ReminderAction::Display => calendar_models::AlarmAction::Display, - crate::calendar::ReminderAction::Email => calendar_models::AlarmAction::Email, - crate::calendar::ReminderAction::Audio => calendar_models::AlarmAction::Audio, - }, - trigger: calendar_models::AlarmTrigger::Duration(chrono::Duration::minutes(-alarm.minutes_before as i64)), - duration: None, - repeat: None, - description: alarm.description, - summary: None, - attendees: Vec::new(), - attach: Vec::new(), - }).collect(); - - // Handle recurrence rule and UID for series updates - if is_series_update { - // For series updates, preserve existing recurrence rule and convert UID to base UID - let parts: Vec<&str> = request.uid.split('-').collect(); - if parts.len() > 1 { - let last_part = parts[parts.len() - 1]; - if last_part.chars().all(|c| c.is_numeric()) { - let base_uid = parts[0..parts.len()-1].join("-"); - event.uid = base_uid; - } - } - - // Handle exception dates - if let Some(exdate) = request.exception_dates { - let mut new_exdate = Vec::new(); - for date in exdate { - new_exdate.push(date); - } - - // Merge with existing exception dates (avoid duplicates) - for new_date in new_exdate { - if !event.exdate.contains(&new_date) { - event.exdate.push(new_date); - } - } - - println!("🔄 Updated exception dates: {} total", event.exdate.len()); - } - - // Handle UNTIL date modification for "This and Future Events" - if let Some(until_date) = request.until_date { - println!("🔄 Adding UNTIL clause to RRULE: {}", until_date); - - if let Some(ref rrule) = event.rrule { - // Remove existing UNTIL if present and add new one - let rrule_without_until = rrule.split(';') - .filter(|part| !part.starts_with("UNTIL=")) - .collect::>() - .join(";"); - - let until_formatted = until_date.format("%Y%m%dT%H%M%SZ").to_string(); - - event.rrule = Some(format!("{};UNTIL={}", rrule_without_until, until_formatted)); - println!("🔄 Modified RRULE: {}", event.rrule.as_ref().unwrap()); - - // Clear exception dates since we're using UNTIL instead - event.exdate.clear(); - println!("🔄 Cleared exception dates for UNTIL approach"); - } - } - } else { - // For regular updates, use the new recurrence rule - event.rrule = request.rrule; - } - - // Update the event on the CalDAV server - client.update_event(&calendar_path, &event, &event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?; - - // Fetch the updated event to return its details - let updated_event = fetch_event_by_href(&client, &calendar_path, &event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to fetch updated event: {}", e)))?; - - let event_summary = updated_event.map(|e| crate::models_v2::EventSummaryV2 { - uid: e.uid, - summary: e.summary, - dtstart: e.dtstart, - dtend: e.dtend, - location: e.location, - all_day: e.all_day, - href: e.href, - etag: e.etag, - }); - - Ok(Json(UpdateEventResponseV2 { - success: true, - message: "Event updated successfully".to_string(), - event: event_summary, - })) -} pub async fn update_event( State(state): State>, diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 36cc0e9..f7bc55d 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -8,7 +8,6 @@ use std::sync::Arc; mod auth; mod models; -mod models_v2; mod handlers; mod calendar; mod config; @@ -45,10 +44,6 @@ pub async fn run_server() -> Result<(), Box> { .route("/api/calendar/events/create", post(handlers::create_event)) .route("/api/calendar/events/update", post(handlers::update_event)) .route("/api/calendar/events/delete", post(handlers::delete_event)) - // V2 API routes with better type safety - .route("/api/v2/calendar/events/create", post(handlers::create_event_v2)) - .route("/api/v2/calendar/events/update", post(handlers::update_event_v2)) - .route("/api/v2/calendar/events/delete", post(handlers::delete_event_v2)) .route("/api/calendar/events/:uid", get(handlers::refresh_event)) .layer( CorsLayer::new() diff --git a/backend/src/models.rs b/backend/src/models.rs index 4cc365d..a6602cc 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -123,8 +123,6 @@ pub struct UpdateEventRequest { pub recurrence_days: Vec, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence pub calendar_path: Option, // Optional - search all calendars if not specified pub update_action: Option, // "update_series" for recurring events - pub occurrence_date: Option, // ISO date string for specific occurrence - pub exception_dates: Option>, // ISO datetime strings for EXDATE #[serde(skip_serializing_if = "Option::is_none")] pub until_date: Option, // ISO datetime string for RRULE UNTIL clause } diff --git a/backend/src/models_v2.rs b/backend/src/models_v2.rs deleted file mode 100644 index 768f7cb..0000000 --- a/backend/src/models_v2.rs +++ /dev/null @@ -1,295 +0,0 @@ -// Simplified RFC 5545-based API models -// Axum imports removed - not needed for model definitions -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -// ==================== CALENDAR REQUESTS ==================== - -#[derive(Debug, Deserialize)] -pub struct CreateCalendarRequestV2 { - pub name: String, - pub description: Option, - pub color: Option, -} - -#[derive(Debug, Deserialize)] -pub struct DeleteCalendarRequestV2 { - pub path: String, -} - -// ==================== EVENT REQUESTS ==================== - -// Simplified create event request using proper DateTime instead of string parsing -#[derive(Debug, Deserialize)] -pub struct CreateEventRequestV2 { - pub summary: String, // title -> summary (RFC 5545 term) - pub description: Option, // Optional in RFC 5545 - pub dtstart: DateTime, // Direct DateTime, no string parsing! - pub dtend: Option>, // Optional, alternative to duration - pub location: Option, - pub all_day: bool, - - // Status and classification - pub status: Option, // Use enum instead of string - pub class: Option, // Use enum instead of string - pub priority: Option, // 0-9 priority level - - // People - pub organizer: Option, // Organizer email - pub attendees: Vec, // Rich attendee objects - - // Categorization - pub categories: Vec, // Direct Vec instead of comma-separated - - // Recurrence (simplified for now) - pub rrule: Option, // Standard RRULE format - - // Reminders (simplified for now) - pub alarms: Vec, // Structured alarms - - // Calendar context - pub calendar_path: Option, -} - -#[derive(Debug, Deserialize)] -pub struct UpdateEventRequestV2 { - pub uid: String, // Event UID to identify which event to update - pub summary: String, - pub description: Option, - pub dtstart: DateTime, // Direct DateTime, no string parsing! - pub dtend: Option>, - pub location: Option, - pub all_day: bool, - - // Status and classification - pub status: Option, - pub class: Option, - pub priority: Option, - - // People - pub organizer: Option, - pub attendees: Vec, - - // Categorization - pub categories: Vec, - - // Recurrence - pub rrule: Option, - - // Reminders - pub alarms: Vec, - - // Context - pub calendar_path: Option, - pub update_action: Option, // "update_series" for recurring events - pub occurrence_date: Option>, // Specific occurrence - pub exception_dates: Option>>, // EXDATE - pub until_date: Option>, // RRULE UNTIL clause -} - -#[derive(Debug, Deserialize)] -pub struct DeleteEventRequestV2 { - pub calendar_path: String, - pub event_href: String, - pub delete_action: DeleteActionV2, // Use enum instead of string - pub occurrence_date: Option>, // Direct DateTime -} - -// ==================== SUPPORTING TYPES ==================== - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum EventStatusV2 { - Tentative, - Confirmed, - Cancelled, -} - -impl Default for EventStatusV2 { - fn default() -> Self { - EventStatusV2::Confirmed - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum EventClassV2 { - Public, - Private, - Confidential, -} - -impl Default for EventClassV2 { - fn default() -> Self { - EventClassV2::Public - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum DeleteActionV2 { - DeleteThis, // "delete_this" - DeleteFollowing, // "delete_following" - DeleteSeries, // "delete_series" -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AttendeeV2 { - pub email: String, // Calendar address - pub name: Option, // Common name (CN parameter) - pub role: Option, // Role (ROLE parameter) - pub status: Option, // Participation status (PARTSTAT parameter) - pub rsvp: Option, // RSVP expectation (RSVP parameter) -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum AttendeeRoleV2 { - Chair, - Required, // REQ-PARTICIPANT - Optional, // OPT-PARTICIPANT - NonParticipant, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum ParticipationStatusV2 { - NeedsAction, - Accepted, - Declined, - Tentative, - Delegated, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AlarmV2 { - pub action: AlarmActionV2, // Action (AUDIO, DISPLAY, EMAIL) - pub trigger_minutes: i32, // Minutes before event (negative = before) - pub description: Option, // Description for display/email -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum AlarmActionV2 { - Audio, - Display, - Email, -} - -// ==================== RESPONSES ==================== - -#[derive(Debug, Serialize)] -pub struct CreateEventResponseV2 { - pub success: bool, - pub message: String, - pub event: Option, // Return created event summary -} - -#[derive(Debug, Serialize)] -pub struct UpdateEventResponseV2 { - pub success: bool, - pub message: String, - pub event: Option, // Return updated event summary -} - -#[derive(Debug, Serialize)] -pub struct DeleteEventResponseV2 { - pub success: bool, - pub message: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct EventSummaryV2 { - pub uid: String, - pub summary: Option, - pub dtstart: DateTime, - pub dtend: Option>, - pub location: Option, - pub all_day: bool, - pub href: Option, - pub etag: Option, -} - -// ==================== CONVERSION HELPERS ==================== - -// Convert from old request format to new for backward compatibility -impl From for CreateEventRequestV2 { - fn from(old: crate::models::CreateEventRequest) -> Self { - use chrono::{NaiveDate, NaiveTime, TimeZone, Utc}; - - // Parse the old string-based date/time format - let start_date = NaiveDate::parse_from_str(&old.start_date, "%Y-%m-%d") - .unwrap_or_else(|_| chrono::Utc::now().date_naive()); - let start_time = NaiveTime::parse_from_str(&old.start_time, "%H:%M") - .unwrap_or_else(|_| NaiveTime::from_hms_opt(0, 0, 0).unwrap()); - let dtstart = Utc.from_utc_datetime(&start_date.and_time(start_time)); - - let end_date = NaiveDate::parse_from_str(&old.end_date, "%Y-%m-%d") - .unwrap_or_else(|_| chrono::Utc::now().date_naive()); - let end_time = NaiveTime::parse_from_str(&old.end_time, "%H:%M") - .unwrap_or_else(|_| NaiveTime::from_hms_opt(1, 0, 0).unwrap()); - let dtend = Some(Utc.from_utc_datetime(&end_date.and_time(end_time))); - - // Parse comma-separated categories - let categories: Vec = if old.categories.trim().is_empty() { - Vec::new() - } else { - old.categories.split(',').map(|s| s.trim().to_string()).collect() - }; - - // Parse comma-separated attendees - let attendees: Vec = if old.attendees.trim().is_empty() { - Vec::new() - } else { - old.attendees.split(',').map(|email| AttendeeV2 { - email: email.trim().to_string(), - name: None, - role: Some(AttendeeRoleV2::Required), - status: Some(ParticipationStatusV2::NeedsAction), - rsvp: Some(true), - }).collect() - }; - - // Convert status string to enum - let status = match old.status.as_str() { - "tentative" => Some(EventStatusV2::Tentative), - "confirmed" => Some(EventStatusV2::Confirmed), - "cancelled" => Some(EventStatusV2::Cancelled), - _ => Some(EventStatusV2::Confirmed), - }; - - // Convert class string to enum - let class = match old.class.as_str() { - "public" => Some(EventClassV2::Public), - "private" => Some(EventClassV2::Private), - "confidential" => Some(EventClassV2::Confidential), - _ => Some(EventClassV2::Public), - }; - - // Create basic alarm if reminder specified - let alarms = if old.reminder == "none" { - Vec::new() - } else { - // Default to 15 minutes before for now - vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: 15, - description: Some("Event reminder".to_string()), - }] - }; - - Self { - summary: old.title, - description: if old.description.trim().is_empty() { None } else { Some(old.description) }, - dtstart, - dtend, - location: if old.location.trim().is_empty() { None } else { Some(old.location) }, - all_day: old.all_day, - status, - class, - priority: old.priority, - organizer: if old.organizer.trim().is_empty() { None } else { Some(old.organizer) }, - attendees, - categories, - rrule: None, // TODO: Convert recurrence string to RRULE - alarms, - calendar_path: old.calendar_path, - } - } -} - -// Error handling - ApiError is available through crate::models::ApiError in handlers \ No newline at end of file