Remove v2 API endpoints and fix warnings
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1060,9 +1060,6 @@ pub enum CalDAVError {
|
||||
|
||||
#[error("Failed to parse calendar data: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
ConfigError(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<DeleteEventRequestV2>,
|
||||
) -> Result<Json<DeleteEventResponseV2>, 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<Arc<AppState>>,
|
||||
@@ -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<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<CreateEventRequestV2>,
|
||||
) -> Result<Json<CreateEventResponseV2>, 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<String> = request.attendees.into_iter()
|
||||
.map(|att| att.email)
|
||||
.collect();
|
||||
|
||||
// Convert alarms to alarms
|
||||
let alarms: Vec<crate::calendar::EventReminder> = 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<Arc<AppState>>,
|
||||
@@ -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<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<UpdateEventRequestV2>,
|
||||
) -> Result<Json<UpdateEventResponseV2>, 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<String> = request.attendees.into_iter()
|
||||
.map(|att| att.email)
|
||||
.collect();
|
||||
|
||||
// Convert alarms to alarms
|
||||
let alarms: Vec<crate::calendar::EventReminder> = 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::<Vec<&str>>()
|
||||
.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<Arc<AppState>>,
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
.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()
|
||||
|
||||
@@ -123,8 +123,6 @@ pub struct UpdateEventRequest {
|
||||
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
||||
pub update_action: Option<String>, // "update_series" for recurring events
|
||||
pub occurrence_date: Option<String>, // ISO date string for specific occurrence
|
||||
pub exception_dates: Option<Vec<String>>, // ISO datetime strings for EXDATE
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>, // Optional in RFC 5545
|
||||
pub dtstart: DateTime<Utc>, // Direct DateTime, no string parsing!
|
||||
pub dtend: Option<DateTime<Utc>>, // Optional, alternative to duration
|
||||
pub location: Option<String>,
|
||||
pub all_day: bool,
|
||||
|
||||
// Status and classification
|
||||
pub status: Option<EventStatusV2>, // Use enum instead of string
|
||||
pub class: Option<EventClassV2>, // Use enum instead of string
|
||||
pub priority: Option<u8>, // 0-9 priority level
|
||||
|
||||
// People
|
||||
pub organizer: Option<String>, // Organizer email
|
||||
pub attendees: Vec<AttendeeV2>, // Rich attendee objects
|
||||
|
||||
// Categorization
|
||||
pub categories: Vec<String>, // Direct Vec instead of comma-separated
|
||||
|
||||
// Recurrence (simplified for now)
|
||||
pub rrule: Option<String>, // Standard RRULE format
|
||||
|
||||
// Reminders (simplified for now)
|
||||
pub alarms: Vec<AlarmV2>, // Structured alarms
|
||||
|
||||
// Calendar context
|
||||
pub calendar_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateEventRequestV2 {
|
||||
pub uid: String, // Event UID to identify which event to update
|
||||
pub summary: String,
|
||||
pub description: Option<String>,
|
||||
pub dtstart: DateTime<Utc>, // Direct DateTime, no string parsing!
|
||||
pub dtend: Option<DateTime<Utc>>,
|
||||
pub location: Option<String>,
|
||||
pub all_day: bool,
|
||||
|
||||
// Status and classification
|
||||
pub status: Option<EventStatusV2>,
|
||||
pub class: Option<EventClassV2>,
|
||||
pub priority: Option<u8>,
|
||||
|
||||
// People
|
||||
pub organizer: Option<String>,
|
||||
pub attendees: Vec<AttendeeV2>,
|
||||
|
||||
// Categorization
|
||||
pub categories: Vec<String>,
|
||||
|
||||
// Recurrence
|
||||
pub rrule: Option<String>,
|
||||
|
||||
// Reminders
|
||||
pub alarms: Vec<AlarmV2>,
|
||||
|
||||
// Context
|
||||
pub calendar_path: Option<String>,
|
||||
pub update_action: Option<String>, // "update_series" for recurring events
|
||||
pub occurrence_date: Option<DateTime<Utc>>, // Specific occurrence
|
||||
pub exception_dates: Option<Vec<DateTime<Utc>>>, // EXDATE
|
||||
pub until_date: Option<DateTime<Utc>>, // 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<DateTime<Utc>>, // 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<String>, // Common name (CN parameter)
|
||||
pub role: Option<AttendeeRoleV2>, // Role (ROLE parameter)
|
||||
pub status: Option<ParticipationStatusV2>, // Participation status (PARTSTAT parameter)
|
||||
pub rsvp: Option<bool>, // 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<String>, // 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<EventSummaryV2>, // Return created event summary
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateEventResponseV2 {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub event: Option<EventSummaryV2>, // 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<String>,
|
||||
pub dtstart: DateTime<Utc>,
|
||||
pub dtend: Option<DateTime<Utc>>,
|
||||
pub location: Option<String>,
|
||||
pub all_day: bool,
|
||||
pub href: Option<String>,
|
||||
pub etag: Option<String>,
|
||||
}
|
||||
|
||||
// ==================== CONVERSION HELPERS ====================
|
||||
|
||||
// Convert from old request format to new for backward compatibility
|
||||
impl From<crate::models::CreateEventRequest> 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<String> = 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<AttendeeV2> = 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
|
||||
Reference in New Issue
Block a user