adding card dav and cald dav
This commit is contained in:
@@ -0,0 +1,813 @@
|
||||
/**
|
||||
* CalDAV Adapter Module
|
||||
*
|
||||
* This module provides conversion between CalDAV protocol XML structures and OxiCloud domain objects.
|
||||
* It handles parsing CalDAV request XML and generating CalDAV response XML according to RFC 4791.
|
||||
*/
|
||||
|
||||
use std::io::{Read, Write, BufReader};
|
||||
use chrono::{DateTime, Utc};
|
||||
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::adapters::webdav_adapter::{WebDavAdapter, QualifiedName, PropFindType, PropFindRequest, Result, WebDavError};
|
||||
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
|
||||
|
||||
/// CalDAV report type
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CalDavReportType {
|
||||
/// Calendar-query report
|
||||
CalendarQuery {
|
||||
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
||||
props: Vec<QualifiedName>,
|
||||
},
|
||||
/// Calendar-multiget report
|
||||
CalendarMultiget {
|
||||
hrefs: Vec<String>,
|
||||
props: Vec<QualifiedName>,
|
||||
},
|
||||
/// Sync-collection report
|
||||
SyncCollection {
|
||||
sync_token: String,
|
||||
props: Vec<QualifiedName>,
|
||||
}
|
||||
}
|
||||
|
||||
/// CalDAV adapter for converting between XML and domain objects
|
||||
pub struct CalDavAdapter;
|
||||
|
||||
impl CalDavAdapter {
|
||||
/// Parse a REPORT XML request for CalDAV
|
||||
pub fn parse_report<R: Read>(reader: R) -> Result<CalDavReportType> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut in_calendar_query = false;
|
||||
let mut in_calendar_multiget = false;
|
||||
let mut in_sync_collection = false;
|
||||
let mut in_prop = false;
|
||||
let mut in_filter = false;
|
||||
let mut in_time_range = false;
|
||||
let mut start_time: Option<DateTime<Utc>> = None;
|
||||
let mut end_time: Option<DateTime<Utc>> = None;
|
||||
let mut props = Vec::new();
|
||||
let mut hrefs = Vec::new();
|
||||
let mut sync_token = String::new();
|
||||
|
||||
loop {
|
||||
match xml_reader.read_event_into(&mut buffer) {
|
||||
Ok(Event::Start(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "calendar-query" || s.ends_with(":calendar-query") => in_calendar_query = true,
|
||||
s if s == "calendar-multiget" || s.ends_with(":calendar-multiget") => in_calendar_multiget = true,
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = true,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = true,
|
||||
s if s == "filter" || s.ends_with(":filter") => in_filter = true,
|
||||
s if s == "time-range" || s.ends_with(":time-range") => {
|
||||
in_time_range = true;
|
||||
|
||||
// Parse time-range attributes
|
||||
for attr in e.attributes() {
|
||||
if let Ok(attr) = attr {
|
||||
let attr_name = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_value = attr.unescape_value().unwrap_or_default();
|
||||
|
||||
if attr_name == "start" {
|
||||
// Parse ISO date format with Z for UTC
|
||||
start_time = DateTime::parse_from_rfc3339(&attr_value)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&Utc));
|
||||
} else if attr_name == "end" {
|
||||
end_time = DateTime::parse_from_rfc3339(&attr_value)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&Utc));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
s if s == "sync-token" || s.ends_with(":sync-token") => {
|
||||
// We'll capture the text in the Text event
|
||||
},
|
||||
s if s == "href" || s.ends_with(":href") => {
|
||||
// We'll capture the text in the Text event
|
||||
},
|
||||
_ if in_prop => {
|
||||
// Add property to request
|
||||
let namespace = WebDavAdapter::extract_namespace(name_str);
|
||||
let prop_name = WebDavAdapter::extract_local_name(name_str);
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
},
|
||||
_ => { /* Ignore other elements */ }
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.unescape().unwrap_or_default();
|
||||
|
||||
// Check if we're in sync-token element
|
||||
if in_sync_collection && !in_prop && !in_filter {
|
||||
sync_token = text.to_string();
|
||||
}
|
||||
|
||||
// Check if we're in href element
|
||||
if (in_calendar_multiget || in_sync_collection) && !in_prop && !in_filter {
|
||||
hrefs.push(text.to_string());
|
||||
}
|
||||
},
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "calendar-query" || s.ends_with(":calendar-query") => in_calendar_query = false,
|
||||
s if s == "calendar-multiget" || s.ends_with(":calendar-multiget") => in_calendar_multiget = false,
|
||||
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = false,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "filter" || s.ends_with(":filter") => in_filter = false,
|
||||
s if s == "time-range" || s.ends_with(":time-range") => in_time_range = false,
|
||||
_ => ()
|
||||
}
|
||||
},
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
if in_prop {
|
||||
// Add empty property element to request
|
||||
let namespace = WebDavAdapter::extract_namespace(name_str);
|
||||
let prop_name = WebDavAdapter::extract_local_name(name_str);
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
} else if name_str == "time-range" || name_str.ends_with(":time-range") {
|
||||
// Parse time-range attributes
|
||||
for attr in e.attributes() {
|
||||
if let Ok(attr) = attr {
|
||||
let attr_name = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let attr_value = attr.unescape_value().unwrap_or_default();
|
||||
|
||||
if attr_name == "start" {
|
||||
// Parse ISO date format with Z for UTC
|
||||
start_time = DateTime::parse_from_rfc3339(&attr_value)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&Utc));
|
||||
} else if attr_name == "end" {
|
||||
end_time = DateTime::parse_from_rfc3339(&attr_value)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&Utc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
// Create the appropriate report type based on what we parsed
|
||||
let report_type = if in_calendar_query {
|
||||
// If both start and end time are present, create a time range
|
||||
let time_range = if let (Some(start), Some(end)) = (start_time, end_time) {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
CalDavReportType::CalendarQuery {
|
||||
time_range,
|
||||
props,
|
||||
}
|
||||
} else if in_calendar_multiget {
|
||||
CalDavReportType::CalendarMultiget {
|
||||
hrefs,
|
||||
props,
|
||||
}
|
||||
} else if in_sync_collection {
|
||||
CalDavReportType::SyncCollection {
|
||||
sync_token,
|
||||
props,
|
||||
}
|
||||
} else {
|
||||
// Default to empty calendar query
|
||||
CalDavReportType::CalendarQuery {
|
||||
time_range: None,
|
||||
props,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(report_type)
|
||||
}
|
||||
|
||||
/// Generate a PROPFIND response for calendars
|
||||
pub fn generate_calendars_propfind_response<W: Write>(
|
||||
writer: W,
|
||||
calendars: &[CalendarDto],
|
||||
request: &PropFindRequest,
|
||||
base_href: &str,
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:C", "urn:ietf:params:xml:ns:caldav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
|
||||
// Add responses for calendars
|
||||
for calendar in calendars {
|
||||
Self::write_calendar_response(&mut xml_writer, calendar, request, &format!("{}{}/", base_href, calendar.id))?;
|
||||
}
|
||||
|
||||
// End multistatus
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write calendar properties as a response
|
||||
fn write_calendar_response<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
calendar: &CalendarDto,
|
||||
request: &PropFindRequest,
|
||||
href: &str,
|
||||
) -> Result<()> {
|
||||
// Start response element
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
|
||||
|
||||
// Write href
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
|
||||
|
||||
// Write propstat
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
|
||||
|
||||
// Start prop
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
|
||||
|
||||
// Write properties based on request type
|
||||
match &request.prop_find_type {
|
||||
PropFindType::AllProp => {
|
||||
// Write all standard properties for a calendar
|
||||
Self::write_calendar_standard_props(xml_writer, calendar)?;
|
||||
},
|
||||
PropFindType::PropName => {
|
||||
// Write only property names (empty elements)
|
||||
Self::write_calendar_prop_names(xml_writer)?;
|
||||
},
|
||||
PropFindType::Prop(props) => {
|
||||
// Write requested properties
|
||||
Self::write_calendar_requested_props(xml_writer, calendar, props)?;
|
||||
}
|
||||
}
|
||||
|
||||
// End prop
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
|
||||
|
||||
// Write status
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
|
||||
|
||||
// End propstat
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
|
||||
|
||||
// End response
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write standard calendar properties
|
||||
fn write_calendar_standard_props<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
calendar: &CalendarDto,
|
||||
) -> Result<()> {
|
||||
// Common WebDAV properties
|
||||
|
||||
// Resource type (collection + calendar)
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:collection")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
|
||||
|
||||
// Display name
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
|
||||
// Last modified
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
|
||||
// ETag
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", calendar.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// Content type for calendar collection
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VCALENDAR")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
|
||||
// CalDAV specific properties
|
||||
|
||||
// Supported calendar component set
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:comp").with_attributes([("name", "VEVENT")])))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:supported-calendar-component-set")))?;
|
||||
|
||||
// Calendar timezone (empty for UTC)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
|
||||
// Calendar color
|
||||
if let Some(color) = &calendar.color {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(color)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CS:calendar-color")))?;
|
||||
}
|
||||
|
||||
// Support calendar-access (RFC4791)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
|
||||
// Current user privilege set
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
|
||||
// Only add write privilege if user owns the calendar or has write access
|
||||
if calendar.owner_id == "current_user_id" { // This should be replaced with actual user check
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:write")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
}
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
|
||||
// Calendar description if present
|
||||
if let Some(desc) = &calendar.description {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-description")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(desc)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-description")))?;
|
||||
}
|
||||
|
||||
// Custom properties
|
||||
for (name, value) in &calendar.custom_properties {
|
||||
// Skip properties that start with _ - they're internal
|
||||
if !name.starts_with('_') {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(&format!("CS:{}", name))))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(value)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new(&format!("CS:{}", name))))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write calendar property names
|
||||
fn write_calendar_prop_names<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
) -> Result<()> {
|
||||
// Common WebDAV property names
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:getcontenttype")))?;
|
||||
|
||||
// CalDAV specific property names
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-description")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write requested calendar properties
|
||||
fn write_calendar_requested_props<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
calendar: &CalendarDto,
|
||||
props: &[QualifiedName],
|
||||
) -> Result<()> {
|
||||
for prop in props {
|
||||
match (prop.namespace.as_str(), prop.name.as_str()) {
|
||||
// DAV namespace properties
|
||||
("DAV:", "resourcetype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:resourcetype")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:collection")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:resourcetype")))?;
|
||||
},
|
||||
("DAV:", "displayname") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:displayname")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.name)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:displayname")))?;
|
||||
},
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&calendar.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", calendar.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VCALENDAR")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
("DAV:", "current-user-privilege-set") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:current-user-privilege-set")))?;
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:read")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
|
||||
// Only add write privilege if user owns the calendar or has write access
|
||||
if calendar.owner_id == "current_user_id" { // This should be replaced with actual user check
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:privilege")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:write")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:privilege")))?;
|
||||
}
|
||||
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:current-user-privilege-set")))?;
|
||||
},
|
||||
|
||||
// CalDAV namespace properties
|
||||
("urn:ietf:params:xml:ns:caldav", "supported-calendar-component-set") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:supported-calendar-component-set")))?;
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:comp").with_attributes([("name", "VEVENT")])))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:supported-calendar-component-set")))?;
|
||||
},
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-timezone") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-timezone")))?;
|
||||
},
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-access") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-access")))?;
|
||||
},
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-description") => {
|
||||
if let Some(desc) = &calendar.description {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-description")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(desc)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-description")))?;
|
||||
} else {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("C:calendar-description")))?;
|
||||
}
|
||||
},
|
||||
|
||||
// CalendarServer namespace properties
|
||||
("http://calendarserver.org/ns/", "calendar-color") => {
|
||||
if let Some(color) = &calendar.color {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("CS:calendar-color")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(color)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("CS:calendar-color")))?;
|
||||
} else {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("CS:calendar-color")))?;
|
||||
}
|
||||
},
|
||||
|
||||
// Custom properties from the calendar
|
||||
_ => {
|
||||
// Check if it's a custom property
|
||||
if let Some(value) = calendar.custom_properties.get(&prop.name) {
|
||||
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
|
||||
format!("CS:{}", prop.name)
|
||||
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
|
||||
format!("C:{}", prop.name)
|
||||
} else if prop.namespace == "DAV:" {
|
||||
format!("D:{}", prop.name)
|
||||
} else {
|
||||
format!("{}:{}", prop.namespace, prop.name)
|
||||
};
|
||||
|
||||
xml_writer.write_event(Event::Start(BytesStart::new(&prop_name)))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(value)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new(&prop_name)))?;
|
||||
} else {
|
||||
// Property not found, write empty element
|
||||
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
|
||||
format!("CS:{}", prop.name)
|
||||
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
|
||||
format!("C:{}", prop.name)
|
||||
} else if prop.namespace == "DAV:" {
|
||||
format!("D:{}", prop.name)
|
||||
} else {
|
||||
format!("{}:{}", prop.namespace, prop.name)
|
||||
};
|
||||
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(&prop_name)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a response for calendar events
|
||||
pub fn generate_calendar_events_response<W: Write>(
|
||||
writer: W,
|
||||
events: &[CalendarEventDto],
|
||||
request: &CalDavReportType,
|
||||
base_href: &str,
|
||||
) -> Result<()> {
|
||||
let mut xml_writer = Writer::new(writer);
|
||||
|
||||
// Start multistatus response
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:C", "urn:ietf:params:xml:ns:caldav"),
|
||||
("xmlns:CS", "http://calendarserver.org/ns/"),
|
||||
])))?;
|
||||
|
||||
// Determine which properties to include based on request type
|
||||
let props = match request {
|
||||
CalDavReportType::CalendarQuery { props, .. } => props.clone(),
|
||||
CalDavReportType::CalendarMultiget { props, .. } => props.clone(),
|
||||
CalDavReportType::SyncCollection { props, .. } => props.clone(),
|
||||
};
|
||||
|
||||
// Add responses for events
|
||||
for event in events {
|
||||
// Create the event href based on its UID
|
||||
let href = format!("{}{}.ics", base_href, event.ical_uid);
|
||||
|
||||
// Write event response
|
||||
Self::write_event_response(&mut xml_writer, event, &props, &href)?;
|
||||
}
|
||||
|
||||
// End multistatus
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write event properties as a response
|
||||
fn write_event_response<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
event: &CalendarEventDto,
|
||||
props: &[QualifiedName],
|
||||
href: &str,
|
||||
) -> Result<()> {
|
||||
// Start response element
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
|
||||
|
||||
// Write href
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
|
||||
|
||||
// Write propstat
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
|
||||
|
||||
// Start prop
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
|
||||
|
||||
// If no specific props requested, return all common ones
|
||||
if props.is_empty() {
|
||||
Self::write_event_standard_props(xml_writer, event)?;
|
||||
} else {
|
||||
// Write specifically requested properties
|
||||
Self::write_event_requested_props(xml_writer, event, props)?;
|
||||
}
|
||||
|
||||
// End prop
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
|
||||
|
||||
// Write status
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
|
||||
|
||||
// End propstat
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
|
||||
|
||||
// End response
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write standard event properties
|
||||
fn write_event_standard_props<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
event: &CalendarEventDto,
|
||||
) -> Result<()> {
|
||||
// Common WebDAV properties
|
||||
|
||||
// Resource type (empty for non-collection)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
|
||||
// ETag based on updated_at timestamp
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// Content type
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VEVENT")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
|
||||
// Last modified
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
|
||||
// CalDAV specific properties
|
||||
|
||||
// Calendar data (iCalendar format)
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-data")))?;
|
||||
// In a full implementation, we would generate a complete iCalendar component here
|
||||
// For now, we'll just provide a basic example
|
||||
let ical_data = format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//OxiCloud//NONSGML Calendar//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:{}\r\n\
|
||||
SUMMARY:{}\r\n\
|
||||
DTSTART:{}\r\n\
|
||||
DTEND:{}\r\n\
|
||||
{}\
|
||||
DTSTAMP:{}\r\n\
|
||||
END:VEVENT\r\n\
|
||||
END:VCALENDAR\r\n",
|
||||
event.ical_uid,
|
||||
event.summary.replace("\n", "\\n"),
|
||||
event.start_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.end_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.rrule.as_ref().map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event.updated_at.format("%Y%m%dT%H%M%SZ"),
|
||||
);
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&ical_data)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-data")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write requested event properties
|
||||
fn write_event_requested_props<W: Write>(
|
||||
xml_writer: &mut Writer<W>,
|
||||
event: &CalendarEventDto,
|
||||
props: &[QualifiedName],
|
||||
) -> Result<()> {
|
||||
for prop in props {
|
||||
match (prop.namespace.as_str(), prop.name.as_str()) {
|
||||
// DAV namespace properties
|
||||
("DAV:", "resourcetype") => {
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
},
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", event.id))))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
},
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new("text/calendar; component=VEVENT")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
|
||||
},
|
||||
("DAV:", "getlastmodified") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getlastmodified")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&event.updated_at.to_rfc2822())))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
},
|
||||
|
||||
// CalDAV namespace properties
|
||||
("urn:ietf:params:xml:ns:caldav", "calendar-data") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("C:calendar-data")))?;
|
||||
// In a full implementation, we would generate a complete iCalendar component here
|
||||
// For now, we'll just provide a basic example
|
||||
let ical_data = format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//OxiCloud//NONSGML Calendar//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:{}\r\n\
|
||||
SUMMARY:{}\r\n\
|
||||
DTSTART:{}\r\n\
|
||||
DTEND:{}\r\n\
|
||||
{}\
|
||||
DTSTAMP:{}\r\n\
|
||||
END:VEVENT\r\n\
|
||||
END:VCALENDAR\r\n",
|
||||
event.ical_uid,
|
||||
event.summary.replace("\n", "\\n"),
|
||||
event.start_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.end_time.format("%Y%m%dT%H%M%SZ"),
|
||||
event.rrule.as_ref().map_or("".to_string(), |r| format!("RRULE:{}\r\n", r)),
|
||||
event.updated_at.format("%Y%m%dT%H%M%SZ"),
|
||||
);
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&ical_data)))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("C:calendar-data")))?;
|
||||
},
|
||||
|
||||
// Property not supported
|
||||
_ => {
|
||||
// Write empty element
|
||||
let prop_name = if prop.namespace == "http://calendarserver.org/ns/" {
|
||||
format!("CS:{}", prop.name)
|
||||
} else if prop.namespace == "urn:ietf:params:xml:ns:caldav" {
|
||||
format!("C:{}", prop.name)
|
||||
} else if prop.namespace == "DAV:" {
|
||||
format!("D:{}", prop.name)
|
||||
} else {
|
||||
format!("{}:{}", prop.namespace, prop.name)
|
||||
};
|
||||
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new(&prop_name)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a MKCALENDAR XML request
|
||||
pub fn parse_mkcalendar<R: Read>(reader: R) -> Result<(String, Option<String>, Option<String>)> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut in_mkcalendar = false;
|
||||
let mut in_set = false;
|
||||
let mut in_prop = false;
|
||||
let mut in_displayname = false;
|
||||
let mut in_description = false;
|
||||
let mut in_calendar_color = false;
|
||||
|
||||
let mut displayname = String::new();
|
||||
let mut description = None;
|
||||
let mut color = None;
|
||||
|
||||
loop {
|
||||
match xml_reader.read_event_into(&mut buffer) {
|
||||
Ok(Event::Start(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => in_mkcalendar = true,
|
||||
s if in_mkcalendar && (s == "set" || s.ends_with(":set")) => in_set = true,
|
||||
s if in_set && (s == "prop" || s.ends_with(":prop")) => in_prop = true,
|
||||
s if in_prop && (s == "displayname" || s.ends_with(":displayname")) => in_displayname = true,
|
||||
s if in_prop && (s == "calendar-description" || s.ends_with(":calendar-description")) => in_description = true,
|
||||
s if in_prop && (s == "calendar-color" || s.ends_with(":calendar-color")) => in_calendar_color = true,
|
||||
_ => ()
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.unescape().unwrap_or_default();
|
||||
|
||||
if in_displayname {
|
||||
displayname = text.to_string();
|
||||
} else if in_description {
|
||||
description = Some(text.to_string());
|
||||
} else if in_calendar_color {
|
||||
color = Some(text.to_string());
|
||||
}
|
||||
},
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
match name_str {
|
||||
s if s == "mkcalendar" || s.ends_with(":mkcalendar") => in_mkcalendar = false,
|
||||
s if s == "set" || s.ends_with(":set") => in_set = false,
|
||||
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
|
||||
s if s == "displayname" || s.ends_with(":displayname") => in_displayname = false,
|
||||
s if s == "calendar-description" || s.ends_with(":calendar-description") => in_description = false,
|
||||
s if s == "calendar-color" || s.ends_with(":calendar-color") => in_calendar_color = false,
|
||||
_ => ()
|
||||
}
|
||||
},
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(WebDavError::XmlError(e)),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
// If no displayname specified, generate a default one based on UUID
|
||||
if displayname.is_empty() {
|
||||
displayname = format!("Calendar {}", Uuid::new_v4());
|
||||
}
|
||||
|
||||
Ok((displayname, description, color))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//! Adapters module for translating between external protocols and internal models
|
||||
|
||||
pub mod webdav_adapter;
|
||||
pub mod caldav_adapter;
|
||||
|
||||
@@ -123,7 +123,7 @@ impl WebDavAdapter {
|
||||
/// Parse a PROPFIND XML request
|
||||
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.trim_text(true);
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut in_propfind = false;
|
||||
@@ -649,7 +649,7 @@ impl WebDavAdapter {
|
||||
/// Parse a PROPPATCH XML request
|
||||
pub fn parse_proppatch<R: Read>(reader: R) -> Result<(Vec<PropValue>, Vec<QualifiedName>)> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.trim_text(true);
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut in_propertyupdate = false;
|
||||
@@ -849,7 +849,7 @@ impl WebDavAdapter {
|
||||
/// Parse a LOCK XML request
|
||||
pub fn parse_lockinfo<R: Read>(reader: R) -> Result<(LockScope, LockType, Option<String>)> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
xml_reader.trim_text(true);
|
||||
xml_reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut in_lockinfo = false;
|
||||
@@ -996,7 +996,7 @@ impl WebDavAdapter {
|
||||
}
|
||||
|
||||
/// Helper method to extract namespace from tag name
|
||||
fn extract_namespace(name: &str) -> String {
|
||||
pub fn extract_namespace(name: &str) -> String {
|
||||
if let Some(idx) = name.rfind(':') {
|
||||
if idx > 0 {
|
||||
return name[..idx].to_string();
|
||||
@@ -1007,7 +1007,7 @@ impl WebDavAdapter {
|
||||
}
|
||||
|
||||
/// Helper method to extract local name from tag name
|
||||
fn extract_local_name(name: &str) -> String {
|
||||
pub fn extract_local_name(name: &str) -> String {
|
||||
if let Some(idx) = name.rfind(':') {
|
||||
if idx > 0 && idx < name.len() - 1 {
|
||||
return name[idx+1..].to_string();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressBookDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub owner_id: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for AddressBookDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: "Default Address Book".to_string(),
|
||||
owner_id: "default".to_string(),
|
||||
description: None,
|
||||
color: None,
|
||||
is_public: false,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressBook> for AddressBookDto {
|
||||
fn from(book: AddressBook) -> Self {
|
||||
Self {
|
||||
id: book.id.to_string(),
|
||||
name: book.name,
|
||||
owner_id: book.owner_id,
|
||||
description: book.description,
|
||||
color: book.color,
|
||||
is_public: book.is_public,
|
||||
created_at: book.created_at,
|
||||
updated_at: book.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateAddressBookDto {
|
||||
pub name: String,
|
||||
pub owner_id: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateAddressBookDto {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: Option<bool>,
|
||||
pub user_id: String, // Current user making the update
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShareAddressBookDto {
|
||||
pub address_book_id: String,
|
||||
pub user_id: String,
|
||||
pub can_write: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnshareAddressBookDto {
|
||||
pub address_book_id: String,
|
||||
pub user_id: String,
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use std::collections::HashMap;
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
|
||||
/// DTO for calendar data transfer
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CalendarDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub owner_id: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub custom_properties: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for CalendarDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
owner_id: String::new(),
|
||||
description: None,
|
||||
color: None,
|
||||
is_public: false,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
custom_properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Calendar> for CalendarDto {
|
||||
fn from(calendar: Calendar) -> Self {
|
||||
Self {
|
||||
id: calendar.id().to_string(),
|
||||
name: calendar.name().to_string(),
|
||||
owner_id: calendar.owner_id().to_string(),
|
||||
description: calendar.description().map(|s| s.to_string()),
|
||||
color: calendar.color().map(|s| s.to_string()),
|
||||
is_public: false, // This needs to be set separately as it's not part of the domain entity
|
||||
created_at: *calendar.created_at(),
|
||||
updated_at: *calendar.updated_at(),
|
||||
custom_properties: calendar.custom_properties().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DTO for calendar creation
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateCalendarDto {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: Option<bool>,
|
||||
}
|
||||
|
||||
/// DTO for calendar update
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateCalendarDto {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_public: Option<bool>,
|
||||
}
|
||||
|
||||
/// DTO for calendar sharing
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CalendarShareDto {
|
||||
pub calendar_id: String,
|
||||
pub user_id: String,
|
||||
pub access_level: String, // 'read', 'write', 'owner'
|
||||
}
|
||||
|
||||
/// DTO for calendar event data transfer
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CalendarEventDto {
|
||||
pub id: String,
|
||||
pub calendar_id: String,
|
||||
pub summary: String,
|
||||
pub description: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub end_time: DateTime<Utc>,
|
||||
pub all_day: bool,
|
||||
pub rrule: Option<String>,
|
||||
pub ical_uid: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for CalendarEventDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
calendar_id: String::new(),
|
||||
summary: String::new(),
|
||||
description: None,
|
||||
location: None,
|
||||
start_time: Utc::now(),
|
||||
end_time: Utc::now(),
|
||||
all_day: false,
|
||||
rrule: None,
|
||||
ical_uid: String::new(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarEvent> for CalendarEventDto {
|
||||
fn from(event: CalendarEvent) -> Self {
|
||||
Self {
|
||||
id: event.id().to_string(),
|
||||
calendar_id: event.calendar_id().to_string(),
|
||||
summary: event.summary().to_string(),
|
||||
description: event.description().map(|s| s.to_string()),
|
||||
location: event.location().map(|s| s.to_string()),
|
||||
start_time: *event.start_time(),
|
||||
end_time: *event.end_time(),
|
||||
all_day: event.all_day(),
|
||||
rrule: event.rrule().map(|s| s.to_string()),
|
||||
ical_uid: event.ical_uid().to_string(),
|
||||
created_at: *event.created_at(),
|
||||
updated_at: *event.updated_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DTO for calendar event creation using iCalendar data
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateEventICalDto {
|
||||
pub calendar_id: String,
|
||||
pub ical_data: String,
|
||||
}
|
||||
|
||||
/// DTO for calendar event creation with structured data
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateEventDto {
|
||||
pub calendar_id: String,
|
||||
pub summary: String,
|
||||
pub description: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub end_time: DateTime<Utc>,
|
||||
pub all_day: Option<bool>,
|
||||
pub rrule: Option<String>,
|
||||
pub user_id: String, // Added for authorization
|
||||
}
|
||||
|
||||
/// DTO for updating a calendar event
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateEventDto {
|
||||
pub summary: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub start_time: Option<DateTime<Utc>>,
|
||||
pub end_time: Option<DateTime<Utc>>,
|
||||
pub all_day: Option<bool>,
|
||||
pub rrule: Option<String>,
|
||||
pub user_id: String, // Added for authorization
|
||||
}
|
||||
|
||||
/// DTO for querying events in a time range
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EventQueryDto {
|
||||
pub calendar_id: String,
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// DTO for pagination
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaginationDto {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::{Contact, Email, Phone, Address, ContactGroup};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailDto {
|
||||
pub email: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<Email> for EmailDto {
|
||||
fn from(email: Email) -> Self {
|
||||
Self {
|
||||
email: email.email,
|
||||
r#type: email.r#type,
|
||||
is_primary: email.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhoneDto {
|
||||
pub number: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<Phone> for PhoneDto {
|
||||
fn from(phone: Phone) -> Self {
|
||||
Self {
|
||||
number: phone.number,
|
||||
r#type: phone.r#type,
|
||||
is_primary: phone.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressDto {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub postal_code: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<Address> for AddressDto {
|
||||
fn from(address: Address) -> Self {
|
||||
Self {
|
||||
street: address.street,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postal_code: address.postal_code,
|
||||
country: address.country,
|
||||
r#type: address.r#type,
|
||||
is_primary: address.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactDto {
|
||||
pub id: String,
|
||||
pub address_book_id: String,
|
||||
pub uid: String,
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub email: Vec<EmailDto>,
|
||||
pub phone: Vec<PhoneDto>,
|
||||
pub address: Vec<AddressDto>,
|
||||
pub organization: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub photo_url: Option<String>,
|
||||
pub birthday: Option<NaiveDate>,
|
||||
pub anniversary: Option<NaiveDate>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
impl Default for ContactDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
address_book_id: uuid::Uuid::new_v4().to_string(),
|
||||
uid: format!("{}@oxicloud", uuid::Uuid::new_v4()),
|
||||
full_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
nickname: None,
|
||||
email: Vec::new(),
|
||||
phone: Vec::new(),
|
||||
address: Vec::new(),
|
||||
organization: None,
|
||||
title: None,
|
||||
notes: None,
|
||||
photo_url: None,
|
||||
birthday: None,
|
||||
anniversary: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
etag: uuid::Uuid::new_v4().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Contact> for ContactDto {
|
||||
fn from(contact: Contact) -> Self {
|
||||
Self {
|
||||
id: contact.id.to_string(),
|
||||
address_book_id: contact.address_book_id.to_string(),
|
||||
uid: contact.uid,
|
||||
full_name: contact.full_name,
|
||||
first_name: contact.first_name,
|
||||
last_name: contact.last_name,
|
||||
nickname: contact.nickname,
|
||||
email: contact.email.into_iter().map(EmailDto::from).collect(),
|
||||
phone: contact.phone.into_iter().map(PhoneDto::from).collect(),
|
||||
address: contact.address.into_iter().map(AddressDto::from).collect(),
|
||||
organization: contact.organization,
|
||||
title: contact.title,
|
||||
notes: contact.notes,
|
||||
photo_url: contact.photo_url,
|
||||
birthday: contact.birthday,
|
||||
anniversary: contact.anniversary,
|
||||
created_at: contact.created_at,
|
||||
updated_at: contact.updated_at,
|
||||
etag: contact.etag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateContactDto {
|
||||
pub address_book_id: String,
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub email: Vec<EmailDto>,
|
||||
pub phone: Vec<PhoneDto>,
|
||||
pub address: Vec<AddressDto>,
|
||||
pub organization: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub photo_url: Option<String>,
|
||||
pub birthday: Option<NaiveDate>,
|
||||
pub anniversary: Option<NaiveDate>,
|
||||
pub user_id: String, // User creating the contact
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateContactDto {
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub email: Option<Vec<EmailDto>>,
|
||||
pub phone: Option<Vec<PhoneDto>>,
|
||||
pub address: Option<Vec<AddressDto>>,
|
||||
pub organization: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub photo_url: Option<String>,
|
||||
pub birthday: Option<NaiveDate>,
|
||||
pub anniversary: Option<NaiveDate>,
|
||||
pub user_id: String, // User updating the contact
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateContactVCardDto {
|
||||
pub address_book_id: String,
|
||||
pub vcard: String,
|
||||
pub user_id: String, // User creating the contact
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactGroupDto {
|
||||
pub id: String,
|
||||
pub address_book_id: String,
|
||||
pub name: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub members_count: Option<i32>,
|
||||
}
|
||||
|
||||
impl From<ContactGroup> for ContactGroupDto {
|
||||
fn from(group: ContactGroup) -> Self {
|
||||
Self {
|
||||
id: group.id.to_string(),
|
||||
address_book_id: group.address_book_id.to_string(),
|
||||
name: group.name,
|
||||
created_at: group.created_at,
|
||||
updated_at: group.updated_at,
|
||||
members_count: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateContactGroupDto {
|
||||
pub address_book_id: String,
|
||||
pub name: String,
|
||||
pub user_id: String, // User creating the group
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateContactGroupDto {
|
||||
pub name: String,
|
||||
pub user_id: String, // User updating the group
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GroupMembershipDto {
|
||||
pub group_id: String,
|
||||
pub contact_id: String,
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
pub mod address_book_dto;
|
||||
pub mod calendar_dto;
|
||||
pub mod contact_dto;
|
||||
pub mod favorites_dto;
|
||||
pub mod file_dto;
|
||||
pub mod folder_dto;
|
||||
pub mod i18n_dto;
|
||||
pub mod pagination;
|
||||
pub mod user_dto;
|
||||
pub mod trash_dto;
|
||||
pub mod recent_dto;
|
||||
pub mod search_dto;
|
||||
pub mod share_dto;
|
||||
pub mod favorites_dto;
|
||||
pub mod recent_dto;
|
||||
pub mod trash_dto;
|
||||
pub mod user_dto;
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Port for external calendar storage mechanisms
|
||||
#[async_trait]
|
||||
pub trait CalendarStoragePort: Send + Sync + 'static {
|
||||
// Calendar operations
|
||||
async fn create_calendar(&self, calendar: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
|
||||
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError>;
|
||||
|
||||
// Calendar sharing
|
||||
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError>;
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError>;
|
||||
|
||||
// Calendar properties
|
||||
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError>;
|
||||
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError>;
|
||||
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<std::collections::HashMap<String, String>, DomainError>;
|
||||
|
||||
// Event operations
|
||||
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn list_events_by_calendar(&self, calendar_id: &str) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
async fn get_events_in_time_range(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
}
|
||||
|
||||
/// Port for calendar use cases
|
||||
#[async_trait]
|
||||
pub trait CalendarUseCase: Send + Sync + 'static {
|
||||
// Calendar operations
|
||||
async fn create_calendar(&self, calendar: CreateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError>;
|
||||
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
|
||||
async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
async fn list_public_calendars(&self, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarDto>, DomainError>;
|
||||
|
||||
// Calendar sharing
|
||||
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError>;
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError>;
|
||||
|
||||
// Event operations
|
||||
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn list_events(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
async fn get_events_in_range(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||
ShareAddressBookDto, UnshareAddressBookDto
|
||||
};
|
||||
use crate::application::dtos::contact_dto::{
|
||||
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
|
||||
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto
|
||||
};
|
||||
|
||||
pub type CardDavRepositoryError = DomainError;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AddressBookUseCase: Send + Sync + 'static {
|
||||
// Address Book operations
|
||||
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<AddressBookDto, DomainError>;
|
||||
async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result<AddressBookDto, DomainError>;
|
||||
async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result<AddressBookDto, DomainError>;
|
||||
async fn list_user_address_books(&self, user_id: &str) -> Result<Vec<AddressBookDto>, DomainError>;
|
||||
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError>;
|
||||
|
||||
// Address Book sharing
|
||||
async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, bool)>, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ContactUseCase: Send + Sync + 'static {
|
||||
// Contact operations
|
||||
async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError>;
|
||||
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError>;
|
||||
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError>;
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_contact(&self, contact_id: &str, user_id: &str) -> Result<ContactDto, DomainError>;
|
||||
async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>;
|
||||
async fn search_contacts(&self, address_book_id: &str, query: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>;
|
||||
|
||||
// Contact Group operations
|
||||
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError>;
|
||||
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError>;
|
||||
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_group(&self, group_id: &str, user_id: &str) -> Result<ContactGroupDto, DomainError>;
|
||||
async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError>;
|
||||
|
||||
// Group membership
|
||||
async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError>;
|
||||
async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError>;
|
||||
async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError>;
|
||||
|
||||
// vCard operations
|
||||
async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result<String, DomainError>;
|
||||
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError>;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod auth_ports;
|
||||
pub mod calendar_ports;
|
||||
pub mod carddav_ports;
|
||||
pub mod favorites_ports;
|
||||
pub mod file_ports;
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
pub mod file_ports;
|
||||
pub mod storage_ports;
|
||||
pub mod auth_ports;
|
||||
pub mod trash_ports;
|
||||
pub mod recent_ports;
|
||||
pub mod share_ports;
|
||||
pub mod favorites_ports;
|
||||
pub mod recent_ports;
|
||||
pub mod storage_ports;
|
||||
pub mod trash_ports;
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
@@ -83,4 +84,11 @@ pub trait StorageUsagePort: Send + Sync + 'static {
|
||||
|
||||
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
|
||||
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Generic storage service interface for calendar and contact services
|
||||
#[async_trait]
|
||||
pub trait StorageUseCase: Send + Sync + 'static {
|
||||
/// Handle a request with the specified action and parameters
|
||||
async fn handle_request(&self, action: &str, params: Value) -> Result<Value, DomainError>;
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
|
||||
CreateEventDto, UpdateEventDto, CreateEventICalDto
|
||||
};
|
||||
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
pub struct CalendarService {
|
||||
calendar_storage: Arc<dyn CalendarStoragePort>,
|
||||
}
|
||||
|
||||
impl CalendarService {
|
||||
pub fn new(calendar_storage: Arc<dyn CalendarStoragePort>) -> Self {
|
||||
Self {
|
||||
calendar_storage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CalendarUseCase for CalendarService {
|
||||
async fn create_calendar(&self, calendar: CreateCalendarDto) -> Result<CalendarDto, DomainError> {
|
||||
// This function requires the current user context which will come from middleware
|
||||
// For now, we'll use a dummy implementation that needs to be completed
|
||||
|
||||
// In a real implementation, get user_id from current user context
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
self.calendar_storage.create_calendar(calendar, user_id).await
|
||||
}
|
||||
|
||||
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> {
|
||||
// In a real implementation, we would:
|
||||
// 1. Get the current user ID from middleware
|
||||
// 2. Verify that the user has access to this calendar
|
||||
// 3. Update the calendar if they have permission
|
||||
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to update this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.update_calendar(calendar_id, update).await
|
||||
}
|
||||
|
||||
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to delete this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.delete_calendar(calendar_id).await
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Get the calendar
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
// Check if user has access or if calendar is public
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
|
||||
if !has_access && !calendar.is_public {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(calendar)
|
||||
}
|
||||
|
||||
async fn list_my_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
self.calendar_storage.list_calendars_by_owner(user_id).await
|
||||
}
|
||||
|
||||
async fn list_shared_calendars(&self) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
self.calendar_storage.list_calendars_shared_with_user(user_id).await
|
||||
}
|
||||
|
||||
async fn list_public_calendars(&self, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
let limit = limit.unwrap_or(100);
|
||||
let offset = offset.unwrap_or(0);
|
||||
|
||||
self.calendar_storage.list_public_calendars(limit, offset).await
|
||||
}
|
||||
|
||||
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> {
|
||||
let current_user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if current user has access
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
// Only the owner can share the calendar
|
||||
if calendar.owner_id != current_user_id {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"Only the calendar owner can change sharing settings"
|
||||
));
|
||||
}
|
||||
|
||||
// Validate access_level
|
||||
match access_level {
|
||||
"read" | "write" | "owner" => {},
|
||||
_ => return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Calendar",
|
||||
format!("Invalid access level: {}. Valid values are: read, write, owner", access_level)
|
||||
)),
|
||||
}
|
||||
|
||||
self.calendar_storage.share_calendar(calendar_id, user_id, access_level).await
|
||||
}
|
||||
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
let current_user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if current user has access
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
// Only the owner can change sharing settings
|
||||
if calendar.owner_id != current_user_id {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"Only the calendar owner can change sharing settings"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.remove_calendar_sharing(calendar_id, user_id).await
|
||||
}
|
||||
|
||||
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let current_user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if current user has access
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
// Only the owner can view sharing settings
|
||||
if calendar.owner_id != current_user_id {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"Only the calendar owner can view sharing settings"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.get_calendar_shares(calendar_id).await
|
||||
}
|
||||
|
||||
async fn create_event(&self, event: CreateEventDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to add events to this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.create_event(event).await
|
||||
}
|
||||
|
||||
async fn create_event_from_ical(&self, event: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to add events to this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.create_event_from_ical(event).await
|
||||
}
|
||||
|
||||
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Get the event to find its calendar
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to update events in this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.update_event(event_id, update).await
|
||||
}
|
||||
|
||||
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Get the event to find its calendar
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
|
||||
if !has_access {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to delete events in this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.delete_event(event_id).await
|
||||
}
|
||||
|
||||
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Get the event
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(&event.calendar_id, user_id).await?;
|
||||
|
||||
// Check if calendar is public
|
||||
let calendar = self.calendar_storage.get_calendar(&event.calendar_id).await?;
|
||||
|
||||
if !has_access && !calendar.is_public {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view events in this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn list_events(&self, calendar_id: &str, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
|
||||
// Check if calendar is public
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
if !has_access && !calendar.is_public {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view events in this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
// Use pagination if provided
|
||||
if limit.is_some() || offset.is_some() {
|
||||
let limit = limit.unwrap_or(100);
|
||||
let offset = offset.unwrap_or(0);
|
||||
|
||||
self.calendar_storage.list_events_by_calendar_paginated(calendar_id, limit, offset).await
|
||||
} else {
|
||||
self.calendar_storage.list_events_by_calendar(calendar_id).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_events_in_range(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let user_id = "current_user_id"; // This should come from middleware
|
||||
|
||||
// Check if user has access to the calendar
|
||||
let has_access = self.calendar_storage.check_calendar_access(calendar_id, user_id).await?;
|
||||
|
||||
// Check if calendar is public
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
|
||||
if !has_access && !calendar.is_public {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view events in this calendar"
|
||||
));
|
||||
}
|
||||
|
||||
self.calendar_storage.get_events_in_time_range(calendar_id, &start, &end).await
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,7 +87,7 @@ impl From<FileServiceError> for DomainError {
|
||||
match err {
|
||||
FileServiceError::NotFound(id) => DomainError::not_found("File", id),
|
||||
FileServiceError::Conflict(path) => DomainError::already_exists("File", path),
|
||||
FileServiceError::InvalidPath(path) => DomainError::validation_error("File", format!("Invalid path: {}", path)),
|
||||
FileServiceError::InvalidPath(path) => DomainError::validation_error(format!("Invalid path: {}", path)),
|
||||
FileServiceError::AccessError(msg) => DomainError::access_denied("File", msg),
|
||||
FileServiceError::InternalError(msg) => DomainError::internal_error("File", msg),
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
pub mod auth_application_service;
|
||||
pub mod batch_operations;
|
||||
pub mod calendar_service;
|
||||
pub mod contact_service;
|
||||
pub mod favorites_service;
|
||||
pub mod file_management_service;
|
||||
pub mod file_retrieval_service;
|
||||
pub mod file_service;
|
||||
pub mod file_upload_service;
|
||||
pub mod file_use_case_factory;
|
||||
pub mod folder_service;
|
||||
pub mod i18n_application_service;
|
||||
pub mod storage_mediator;
|
||||
|
||||
// Nuevos servicios refactorizados
|
||||
pub mod file_upload_service;
|
||||
pub mod file_retrieval_service;
|
||||
pub mod file_management_service;
|
||||
pub mod file_use_case_factory;
|
||||
pub mod auth_application_service;
|
||||
pub mod trash_service;
|
||||
pub mod recent_service;
|
||||
pub mod search_service;
|
||||
pub mod share_service;
|
||||
pub mod favorites_service;
|
||||
pub mod recent_service;
|
||||
pub mod storage_mediator;
|
||||
pub mod storage_usage_service;
|
||||
pub mod trash_service;
|
||||
|
||||
#[cfg(test)]
|
||||
mod trash_service_test;
|
||||
|
||||
@@ -47,8 +47,8 @@ impl From<ShareServiceError> for DomainError {
|
||||
ShareServiceError::InvalidPassword(s) => DomainError::access_denied("Share", s),
|
||||
ShareServiceError::Expired => DomainError::access_denied("Share", "Share has expired".to_string()),
|
||||
ShareServiceError::Repository(s) => DomainError::internal_error("Share", s),
|
||||
ShareServiceError::InvalidItemType(s) => DomainError::validation_error("Share", s),
|
||||
ShareServiceError::Validation(s) => DomainError::validation_error("Share", s),
|
||||
ShareServiceError::InvalidItemType(s) => DomainError::validation_error(s),
|
||||
ShareServiceError::Validation(s) => DomainError::validation_error(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ impl TrashUseCase for TrashService {
|
||||
debug!("Getting trash items for user: {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
|
||||
@@ -119,7 +119,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid item UUID: {} - Error: {}", item_id, e);
|
||||
return Err(DomainError::validation_error("Item", format!("Invalid item ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid item ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,7 +131,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid user UUID: {} - Error: {}", user_id, e);
|
||||
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,7 +244,7 @@ impl TrashUseCase for TrashService {
|
||||
debug!("Folder moved to trash: {}", item_id);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(DomainError::validation_error("Item", format!("Invalid item type: {}", item_type))),
|
||||
_ => Err(DomainError::validation_error(format!("Invalid item type: {}", item_type))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid trash ID format: {} - {}", trash_id, e);
|
||||
return Err(DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -270,7 +270,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid user ID format: {} - {}", user_id, e);
|
||||
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -384,7 +384,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid trash ID format: {} - {}", trash_id, e);
|
||||
return Err(DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid trash ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -395,7 +395,7 @@ impl TrashUseCase for TrashService {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Invalid user ID format: {} - {}", user_id, e);
|
||||
return Err(DomainError::validation_error("User", format!("Invalid user ID: {}", e)));
|
||||
return Err(DomainError::validation_error(format!("Invalid user ID: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -500,7 +500,7 @@ impl TrashUseCase for TrashService {
|
||||
info!("Emptying trash for user {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
// Get all items in the user's trash
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
|
||||
Reference in New Issue
Block a user