Compare commits

..

30 Commits

Author SHA1 Message Date
Lennart
a42004501b version 0.8.1 2025-07-26 17:37:44 +02:00
Lennart
89ce14ee86 update ical dependency 2025-07-26 17:37:25 +02:00
Lennart
7fc64d219c outsource some more ical logic to ical-rs fork 2025-07-26 13:32:28 +02:00
Lennart
03294ec106 version 0.8.0 2025-07-25 23:26:57 +02:00
Lennart
a22235d976 sqlite_store: Drop timezone column in favour of timezone_id 2025-07-25 23:01:51 +02:00
Lennart
1ba9a97b3f update .sqlx queries 2025-07-25 22:52:26 +02:00
Lennart
51036ec6d5 Update vtimezone-rs to fix missing timezones 2025-07-25 22:51:35 +02:00
Lennart
e1a10338e0 Calendar data model: Switch to only saving timezone id 2025-07-25 22:32:01 +02:00
Lennart
918f27e8c2 frontend: Fix timezone removal 2025-07-25 22:30:52 +02:00
Lennart
dd34dd23d1 ical: Work on calendar object data structure 2025-07-25 21:44:57 +02:00
Lennart
9910e4ee31 Remove duplicate UTC implementation from CalTimezone 2025-07-25 19:06:23 +02:00
Lennart
c22469dea6 update ical dependency 2025-07-25 18:38:21 +02:00
Lennart
f2899aec6b Move to own ical-rs fork and refactor timezone-related stuff 2025-07-25 18:22:06 +02:00
Lennart K
f9380ca7e4 clippy appeasement 2025-07-24 11:46:28 +02:00
Lennart
e7138b5f8c version 0.7.0 2025-07-23 21:32:12 +02:00
Lennart
84af24a2b7 frontend: fill id with uuid for creation forms 2025-07-23 21:31:10 +02:00
Lennart
4bd6271e33 Update vtimezones-rs 2025-07-23 21:15:15 +02:00
Lennart
d817c1384c frontend: Add error handling to collection forms 2025-07-23 20:48:28 +02:00
Lennart
f8abc22e63 clippy appeasement 2025-07-23 20:41:06 +02:00
Lennart
b7b5ca4f91 Update dependencies 2025-07-23 20:31:16 +02:00
Lennart
caca2d28ed update vtimezones-rs 2025-07-23 20:23:21 +02:00
Lennart
3db2f13c1b rename vzic-rs to vtimezones-rs 2025-07-23 18:19:23 +02:00
Lennart
db01024682 add comment 2025-07-23 18:08:04 +02:00
Lennart
b2f15f2d77 fix: Add timezone-id support to mkcalendar 2025-07-23 18:04:19 +02:00
Lennart
89dd94904b frontend: Add timezone fields to calendar forms 2025-07-23 17:59:54 +02:00
Lennart
5d0263abc1 caldav: Add vtimezone repository to date timezone with timezone-id 2025-07-23 17:55:55 +02:00
Lennart
0ef3e19bd3 caldav: Fix principal collection permissions 2025-07-23 11:28:14 +02:00
Lennart
44912057fc subscription store: Correctly return whether subscription already existed 2025-07-23 11:09:48 +02:00
Lennart
c4f613a803 Add example compose.yml 2025-07-23 11:05:05 +02:00
Lennart
eb8f301e45 update dependencies 2025-07-22 17:57:24 +02:00
48 changed files with 1497 additions and 3851 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL", "query": "SELECT principal, id, displayname, \"order\", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -14,68 +14,63 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "synctoken", "name": "displayname",
"ordinal": 2, "ordinal": 2,
"type_info": "Integer" "type_info": "Text"
}, },
{ {
"name": "displayname", "name": "order",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Integer"
}, },
{ {
"name": "description", "name": "description",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{
"name": "order",
"ordinal": 5,
"type_info": "Integer"
},
{ {
"name": "color", "name": "color",
"ordinal": 6, "ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "timezone_id", "name": "timezone_id",
"ordinal": 8, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "deleted_at", "name": "deleted_at",
"ordinal": 9, "ordinal": 7,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{
"name": "synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{ {
"name": "subscription_url", "name": "subscription_url",
"ordinal": 10, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "push_topic", "name": "push_topic",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "comp_event",
"ordinal": 12, "ordinal": 11,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_todo", "name": "comp_todo",
"ordinal": 13, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_journal",
"ordinal": 14, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
} }
], ],
@@ -85,14 +80,13 @@
"nullable": [ "nullable": [
false, false,
false, false,
false,
true,
true, true,
false, false,
true, true,
true, true,
true, true,
true, true,
false,
true, true,
false, false,
false, false,
@@ -100,5 +94,5 @@
false false
] ]
}, },
"hash": "cce62f7829bd688cd8c7928b587bc31f0e50865c214b1df113350bea2c254237" "hash": "27ac68a4eea40c1cac663cad034028cf6c373354b29e3a5290c18f58101913cd"
} }

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "46ae176a06e314492f661c28436d6370883052c854da43475d7ced60cf8326e3"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "5132ee8198f155242aa332a10019c48ec334884bcf7841c8aa03fd5eb11351d9"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "60b940ff493e7c0fcb2ffe8ae97172c6444525ffeec21b194bd7443d11d06113"
}

View File

@@ -39,43 +39,38 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "timezone", "name": "timezone_id",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{
"name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{ {
"name": "deleted_at", "name": "deleted_at",
"ordinal": 9, "ordinal": 8,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "subscription_url", "name": "subscription_url",
"ordinal": 10, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "push_topic", "name": "push_topic",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "comp_event",
"ordinal": 12, "ordinal": 11,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_todo", "name": "comp_todo",
"ordinal": 13, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_journal",
"ordinal": 14, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
} }
], ],
@@ -93,7 +88,6 @@
true, true,
true, true,
true, true,
true,
false, false,
false, false,
false, false,

View File

@@ -39,43 +39,38 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "timezone", "name": "timezone_id",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{
"name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{ {
"name": "deleted_at", "name": "deleted_at",
"ordinal": 9, "ordinal": 8,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "subscription_url", "name": "subscription_url",
"ordinal": 10, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "push_topic", "name": "push_topic",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "comp_event", "name": "comp_event",
"ordinal": 12, "ordinal": 11,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_todo", "name": "comp_todo",
"ordinal": 13, "ordinal": 12,
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"name": "comp_journal", "name": "comp_journal",
"ordinal": 14, "ordinal": 13,
"type_info": "Bool" "type_info": "Bool"
} }
], ],
@@ -93,7 +88,6 @@
true, true,
true, true,
true, true,
true,
false, false,
false, false,
false, false,

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "d65c9c40606e59dd816a51b9b9ac60fd2ff81aaa358fcc038134e9a68ba45ad7"
}

467
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.6.5" version = "0.8.1"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
@@ -95,8 +95,12 @@ strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { version = "0.11", features = ["generator", "serde"] } ical = { git = "https://github.com/lennart-k/ical-rs", features = [
toml = "0.8" "generator",
"serde",
"chrono-tz",
] }
toml = "0.9"
tower = "0.5" tower = "0.5"
tower-http = { version = "0.6", features = [ tower-http = { version = "0.6", features = [
"trace", "trace",
@@ -126,7 +130,7 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
heck = "0.5" heck = "0.5"
darling = "0.20" darling = "0.21"
reqwest = { version = "0.12", features = [ reqwest = { version = "0.12", features = [
"rustls-tls", "rustls-tls",
"charset", "charset",
@@ -135,6 +139,7 @@ reqwest = { version = "0.12", features = [
openidconnect = "4.0" openidconnect = "4.0"
clap = { version = "4.5", features = ["derive", "env"] } clap = { version = "4.5", features = ["derive", "env"] }
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" } matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
vtimezones-rs = "0.2"
ece = { version = "2.3", default-features = false, features = [ ece = { version = "2.3", default-features = false, features = [
"backend-openssl", "backend-openssl",
] } ] }

22
compose.oidc.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
rustical:
image: ghcr.io/lennart-k/rustical:latest
restart: unless-stopped
environment:
RUSTICAL_FRONTEND__ALLOW_PASSWORD_LOGIN: "false"
RUSTICAL_OIDC__NAME: "Authelia"
RUSTICAL_OIDC__ISSUER: "https://auth.example.com"
RUSTICAL_OIDC__CLIENT_ID: "{{ rustical_oidc_client_id }}"
RUSTICAL_OIDC__CLIENT_SECRET: "{{ rustical_oidc_client_secret }}"
RUSTICAL_OIDC__CLAIM_USERID: "preferred_username"
RUSTICAL_OIDC__SCOPES: '["openid", "profile", "groups"]'
RUSTICAL_OIDC__REQUIRE_GROUP: "app:rustical" # optional
RUSTICAL_OIDC__ALLOW_SIGN_UP: "true"
volumes:
- data:/var/lib/rustical
# Here you probably want to you expose instead
ports:
- 4000:4000
volumes:
data:

View File

@@ -42,3 +42,4 @@ headers.workspace = true
tower-http.workspace = true tower-http.workspace = true
strum.workspace = true strum.workspace = true
strum_macros.workspace = true strum_macros.workspace = true
vtimezones-rs.workspace = true

View File

@@ -63,7 +63,6 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
params: None, params: None,
}); });
} }
let mut ical_calendar = ical_calendar_builder.build();
for object in &objects { for object in &objects {
match object.get_data() { match object.get_data() {
@@ -73,17 +72,21 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
.. ..
}) => { }) => {
timezones.extend(object_timezones); timezones.extend(object_timezones);
ical_calendar.events.push(event.clone()); ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
} }
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => { CalendarObjectComponent::Todo(TodoObject(todo)) => {
ical_calendar.todos.push(todo.clone()); ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
} }
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => { CalendarObjectComponent::Journal(JournalObject(journal)) => {
ical_calendar.journals.push(journal.clone()); ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
} }
} }
} }
let ical_calendar = ical_calendar_builder
.build()
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
let mut resp = Response::builder().status(StatusCode::OK); let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap(); let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap()); hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());

View File

@@ -4,6 +4,7 @@ use crate::calendar::prop::SupportedCalendarComponentSet;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use ical::IcalParser;
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
@@ -82,13 +83,38 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
request.displayname = None request.displayname = None
} }
let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
Some(tzid)
} else if let Some(tz) = request.calendar_timezone {
// TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::new(tz.as_bytes())
.next()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
let timezone = calendar
.timezones
.first()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone
.try_into()
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
Some(timezone.name().to_owned())
} else {
None
};
let calendar = Calendar { let calendar = Calendar {
id: cal_id.to_owned(), id: cal_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
order: request.calendar_order.unwrap_or(0), order: request.calendar_order.unwrap_or(0),
displayname: request.displayname, displayname: request.displayname,
timezone: request.calendar_timezone, timezone_id,
timezone_id: request.calendar_timezone_id,
color: request.calendar_color, color: request.calendar_color,
description: request.calendar_description, description: request.calendar_description,
deleted_at: None, deleted_at: None,

View File

@@ -3,6 +3,7 @@ use crate::Error;
use crate::calendar::prop::ReportMethod; use crate::calendar::prop::ReportMethod;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use ical::IcalParser;
use rustical_dav::extensions::{ use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
}; };
@@ -15,7 +16,6 @@ use rustical_store::Calendar;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::str::FromStr;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")] #[xml(unit_variants_ident = "CalendarPropName")]
@@ -133,7 +133,9 @@ impl Resource for CalendarResource {
CalendarProp::CalendarDescription(self.cal.description.clone()) CalendarProp::CalendarDescription(self.cal.description.clone())
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone.clone()) CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| tz.to_string())
}))
} }
// chrono_tz uses the IANA database // chrono_tz uses the IANA database
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet( CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
@@ -192,18 +194,42 @@ impl Resource for CalendarResource {
Ok(()) Ok(())
} }
CalendarProp::CalendarTimezone(timezone) => { CalendarProp::CalendarTimezone(timezone) => {
// TODO: Ensure that timezone-id is also updated if let Some(tz) = timezone {
self.cal.timezone = timezone; // TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::new(tz.as_bytes())
.next()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?
.map_err(|_| {
rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
)
})?;
let timezone =
calendar
.timezones
.first()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
self.cal.timezone_id = Some(timezone.name().to_owned());
}
Ok(()) Ok(())
} }
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::CalendarTimezoneId(timezone_id) => { CalendarProp::CalendarTimezoneId(timezone_id) => {
if let Some(tzid) = &timezone_id { if let Some(tzid) = &timezone_id {
// Validate timezone id if !vtimezones_rs::VTIMEZONES.contains_key(tzid) {
chrono_tz::Tz::from_str(tzid).map_err(|_| { return Err(rustical_dav::Error::BadRequest(format!(
rustical_dav::Error::BadRequest(format!("Invalid timezone-id: {tzid}")) "Invalid timezone-id: {tzid}"
})?; )));
// TODO: Ensure that timezone is also updated (For now hope that clients play nice) }
} }
self.cal.timezone_id = timezone_id; self.cal.timezone_id = timezone_id;
Ok(()) Ok(())
@@ -244,15 +270,11 @@ impl Resource for CalendarResource {
self.cal.description = None; self.cal.description = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
self.cal.timezone = None;
Ok(())
}
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarTimezoneId => {
self.cal.timezone_id = None; self.cal.timezone_id = None;
Ok(()) Ok(())
} }
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
self.cal.order = 0; self.cal.order = 0;
Ok(()) Ok(())
@@ -305,3 +327,15 @@ impl Resource for CalendarResource {
)) ))
} }
} }
#[cfg(test)]
mod tests {
#[test]
fn test_tzdb_version() {
// Ensure that both chrono_tz and vzic_rs use the same tzdb version
assert_eq!(
chrono_tz::IANA_TZDB_VERSION,
vtimezones_rs::IANA_TZDB_VERSION
);
}
}

View File

@@ -78,12 +78,13 @@ pub async fn put_event<C: CalendarStore>(
true true
}; };
let object = match CalendarObject::from_ics(object_id, body) { let object = match CalendarObject::from_ics(body) {
Ok(obj) => obj, Ok(obj) => obj,
Err(_) => { Err(_) => {
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
} }
}; };
assert_eq!(object.get_id(), object_id);
cal_store cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(principal, calendar_id, object, overwrite)
.await?; .await?;

View File

@@ -121,7 +121,7 @@ impl Resource for PrincipalResource {
} }
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_read( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))
} }

View File

@@ -24,7 +24,6 @@ rustical_dav.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
http.workspace = true http.workspace = true
base64.workspace = true base64.workspace = true
rand.workspace = true
ece.workspace = true ece.workspace = true
axum.workspace = true axum.workspace = true
openssl.workspace = true openssl.workspace = true

View File

@@ -1,7 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from "."; import { escapeXml } from ".";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
@@ -15,14 +14,12 @@ export class CreateAddressbookForm extends LitElement {
return this return this
} }
client = createClient("/carddav")
@property() @property()
user: string = '' user: string = ''
@property() @property()
principal: string = '' principal: string = ''
@property() @property()
addr_id: string = '' addr_id: string = self.crypto.randomUUID()
@property() @property()
displayname: string = '' displayname: string = ''
@property() @property()
@@ -49,7 +46,7 @@ export class CreateAddressbookForm extends LitElement {
<br> <br>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.addr_id = e.target.value} /> <input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -80,8 +77,12 @@ export class CreateAddressbookForm extends LitElement {
alert("Empty displayname") alert("Empty displayname")
return return
} }
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, { let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
data: ` method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav"> <mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set> <set>
<prop> <prop>
@@ -91,7 +92,14 @@ export class CreateAddressbookForm extends LitElement {
</set> </set>
</mkcol> </mkcol>
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -1,7 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from "."; import { escapeXml } from ".";
@customElement("create-calendar-form") @customElement("create-calendar-form")
@@ -14,19 +13,19 @@ export class CreateCalendarForm extends LitElement {
return this return this
} }
client = createClient("/caldav")
@property() @property()
user: string = '' user: string = ''
@property() @property()
principal: string = '' principal: string = ''
@property() @property()
cal_id: string = '' cal_id: string = self.crypto.randomUUID()
@property() @property()
displayname: string = '' displayname: string = ''
@property() @property()
description: string = '' description: string = ''
@property() @property()
timezone_id: string = ''
@property()
color: string = '' color: string = ''
@property() @property()
isSubscription: boolean = false isSubscription: boolean = false
@@ -38,7 +37,6 @@ export class CreateCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef() dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef() form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
@@ -57,7 +55,7 @@ export class CreateCalendarForm extends LitElement {
<br> <br>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.cal_id = e.target.value} /> <input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -65,6 +63,11 @@ export class CreateCalendarForm extends LitElement {
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" @change=${e => this.description = e.target.value} /> <input type="text" name="description" @change=${e => this.description = e.target.value} />
@@ -119,12 +122,18 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected") alert("No calendar components selected")
return return
} }
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
data: ` let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>
<prop> <prop>
<displayname>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''} ${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
@@ -136,6 +145,11 @@ export class CreateCalendarForm extends LitElement {
</mkcol> </mkcol>
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -63,7 +63,7 @@ export class EditAddressbookForm extends LitElement {
alert("Empty displayname") alert("Empty displayname")
return return
} }
await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, { let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: 'PROPPATCH', method: 'PROPPATCH',
headers: { headers: {
'Content-Type': 'application/xml' 'Content-Type': 'application/xml'
@@ -85,6 +85,12 @@ export class EditAddressbookForm extends LitElement {
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -23,6 +23,8 @@ export class EditCalendarForm extends LitElement {
@property() @property()
description: string = '' description: string = ''
@property() @property()
timezone_id: string = ''
@property()
color: string = '' color: string = ''
@property({ @property({
converter: { converter: {
@@ -47,6 +49,11 @@ export class EditCalendarForm extends LitElement {
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} /> <input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} /> <input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
@@ -90,7 +97,7 @@ export class EditCalendarForm extends LitElement {
alert("No calendar components selected") alert("No calendar components selected")
return return
} }
await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, { let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: 'PROPPATCH', method: 'PROPPATCH',
headers: { headers: {
'Content-Type': 'application/xml' 'Content-Type': 'application/xml'
@@ -100,6 +107,7 @@ export class EditCalendarForm extends LitElement {
<set> <set>
<prop> <prop>
<displayname>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ''}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
@@ -109,6 +117,7 @@ export class EditCalendarForm extends LitElement {
</set> </set>
<remove> <remove>
<prop> <prop>
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ''}
${!this.description ? '<CAL:calendar-description />' : ''} ${!this.description ? '<CAL:calendar-description />' : ''}
${!this.color ? '<ICAL:calendar-color />' : ''} ${!this.color ? '<ICAL:calendar-color />' : ''}
</prop> </prop>
@@ -116,6 +125,12 @@ export class EditCalendarForm extends LitElement {
</propertyupdate> </propertyupdate>
` `
}) })
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload() window.location.reload()
return null return null
} }

View File

@@ -25,7 +25,7 @@ export default defineConfig({
format: "es", format: "es",
manualChunks: { manualChunks: {
lit: ["lit"], lit: ["lit"],
webdav: ["webdav"], // webdav: ["webdav"],
} }
} }
}, },

View File

@@ -1,7 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -15,10 +14,9 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateAddressbookForm = class extends i { let CreateAddressbookForm = class extends i {
constructor() { constructor() {
super(); super();
this.client = an("/carddav");
this.user = ""; this.user = "";
this.principal = ""; this.principal = "";
this.addr_id = ""; this.addr_id = self.crypto.randomUUID();
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.dialog = e(); this.dialog = e();
@@ -45,7 +43,7 @@ let CreateAddressbookForm = class extends i {
<br> <br>
<label> <label>
id id
<input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} /> <input type="text" name="id" value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -79,8 +77,12 @@ let CreateAddressbookForm = class extends i {
alert("Empty displayname"); alert("Empty displayname");
return; return;
} }
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, { let response = await fetch(`/carddav/principal/${this.principal || this.user}/${this.addr_id}`, {
data: ` method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav"> <mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set> <set>
<prop> <prop>
@@ -91,6 +93,10 @@ let CreateAddressbookForm = class extends i {
</mkcol> </mkcol>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }

View File

@@ -1,7 +1,6 @@
import { i, x } from "./lit-z6_uA4GX.mjs"; import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs"; import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs"; import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -15,12 +14,12 @@ var __decorateClass = (decorators, target, key, kind) => {
let CreateCalendarForm = class extends i { let CreateCalendarForm = class extends i {
constructor() { constructor() {
super(); super();
this.client = an("/caldav");
this.user = ""; this.user = "";
this.principal = ""; this.principal = "";
this.cal_id = ""; this.cal_id = self.crypto.randomUUID();
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.timezone_id = "";
this.color = ""; this.color = "";
this.isSubscription = false; this.isSubscription = false;
this.subscriptionUrl = ""; this.subscriptionUrl = "";
@@ -49,7 +48,7 @@ let CreateCalendarForm = class extends i {
<br> <br>
<label> <label>
id id
<input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} /> <input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
</label> </label>
<br> <br>
<label> <label>
@@ -57,6 +56,11 @@ let CreateCalendarForm = class extends i {
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} /> <input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
@@ -114,12 +118,17 @@ let CreateCalendarForm = class extends i {
alert("No calendar components selected"); alert("No calendar components selected");
return; return;
} }
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, { let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
data: ` method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>
<prop> <prop>
<displayname>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""} ${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
@@ -131,6 +140,10 @@ let CreateCalendarForm = class extends i {
</mkcol> </mkcol>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }
@@ -150,6 +163,9 @@ __decorateClass([
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "description", 2); ], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezone_id", 2);
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "color", 2); ], CreateCalendarForm.prototype, "color", 2);

View File

@@ -64,7 +64,7 @@ let EditAddressbookForm = class extends i {
alert("Empty displayname"); alert("Empty displayname");
return; return;
} }
await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, { let response = await fetch(`/carddav/principal/${this.principal}/${this.addr_id}`, {
method: "PROPPATCH", method: "PROPPATCH",
headers: { headers: {
"Content-Type": "application/xml" "Content-Type": "application/xml"
@@ -85,6 +85,10 @@ let EditAddressbookForm = class extends i {
</propertyupdate> </propertyupdate>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }

View File

@@ -16,6 +16,7 @@ let EditCalendarForm = class extends i {
super(); super();
this.displayname = ""; this.displayname = "";
this.description = ""; this.description = "";
this.timezone_id = "";
this.color = ""; this.color = "";
this.components = /* @__PURE__ */ new Set(); this.components = /* @__PURE__ */ new Set();
this.dialog = e(); this.dialog = e();
@@ -35,6 +36,11 @@ let EditCalendarForm = class extends i {
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} /> <input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label> </label>
<br> <br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
</label>
<br>
<label> <label>
Description Description
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} /> <input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
@@ -81,7 +87,7 @@ let EditCalendarForm = class extends i {
alert("No calendar components selected"); alert("No calendar components selected");
return; return;
} }
await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, { let response = await fetch(`/caldav/principal/${this.principal}/${this.cal_id}`, {
method: "PROPPATCH", method: "PROPPATCH",
headers: { headers: {
"Content-Type": "application/xml" "Content-Type": "application/xml"
@@ -91,6 +97,7 @@ let EditCalendarForm = class extends i {
<set> <set>
<prop> <prop>
<displayname>${escapeXml(this.displayname)}</displayname> <displayname>${escapeXml(this.displayname)}</displayname>
${this.timezone_id ? `<CAL:calendar-timezone-id>${escapeXml(this.timezone_id)}</CAL:calendar-timezone-id>` : ""}
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""} ${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
@@ -100,6 +107,7 @@ let EditCalendarForm = class extends i {
</set> </set>
<remove> <remove>
<prop> <prop>
${!this.timezone_id ? `<CAL:calendar-timezone-id />` : ""}
${!this.description ? "<CAL:calendar-description />" : ""} ${!this.description ? "<CAL:calendar-description />" : ""}
${!this.color ? "<ICAL:calendar-color />" : ""} ${!this.color ? "<ICAL:calendar-color />" : ""}
</prop> </prop>
@@ -107,6 +115,10 @@ let EditCalendarForm = class extends i {
</propertyupdate> </propertyupdate>
` `
}); });
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload(); window.location.reload();
return null; return null;
} }
@@ -123,6 +135,9 @@ __decorateClass([
__decorateClass([ __decorateClass([
n$1() n$1()
], EditCalendarForm.prototype, "description", 2); ], EditCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "timezone_id", 2);
__decorateClass([ __decorateClass([
n$1() n$1()
], EditCalendarForm.prototype, "color", 2); ], EditCalendarForm.prototype, "color", 2);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
<edit-calendar-form <edit-calendar-form
principal="{{ calendar.principal }}" principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}" cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}" displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.description.as_deref().unwrap_or_default() }}" description="{{ calendar.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.color.as_deref().unwrap_or_default() }}" color="{{ calendar.color.as_deref().unwrap_or_default() }}"

View File

@@ -25,7 +25,7 @@
{% if let Some(timezone_id) = calendar.timezone_id %} {% if let Some(timezone_id) = calendar.timezone_id %}
<p>{{ timezone_id }}</p> <p>{{ timezone_id }}</p>
{% endif %} {% endif %}
{% if let Some(timezone) = calendar.timezone %} {% if let Some(timezone) = calendar.get_vtimezone() %}
<textarea rows="16" readonly>{{ timezone }}</textarea> <textarea rows="16" readonly>{{ timezone }}</textarea>
{% endif %} {% endif %}

View File

@@ -95,9 +95,7 @@ impl AddressObject {
let uid = format!("{}-anniversary", self.get_id()); let uid = format!("{}-anniversary", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(format!(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
@@ -116,8 +114,7 @@ DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ))?)
)?)
} else { } else {
None None
}, },
@@ -139,9 +136,7 @@ END:VCALENDAR"#,
let uid = format!("{}-birthday", self.get_id()); let uid = format!("{}-birthday", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(format!(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
@@ -160,8 +155,7 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ))?)
)?)
} else { } else {
None None
}, },

View File

@@ -1,53 +0,0 @@
use crate::CalDateTimeError;
use chrono::Duration;
use lazy_static::lazy_static;
lazy_static! {
static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?<sign>[+-])?P((?P<W>\d+)W)?((?P<D>\d+)D)?(T((?P<H>\d+)H)?((?P<M>\d+)M)?((?P<S>\d+)S)?)?$").unwrap();
}
pub fn parse_duration(string: &str) -> Result<Duration, CalDateTimeError> {
let captures = RE_DURATION
.captures(string)
.ok_or(CalDateTimeError::InvalidDurationFormat(string.to_string()))?;
let mut duration = Duration::zero();
if let Some(weeks) = captures.name("W") {
duration += Duration::weeks(weeks.as_str().parse().unwrap());
}
if let Some(days) = captures.name("D") {
duration += Duration::days(days.as_str().parse().unwrap());
}
if let Some(hours) = captures.name("H") {
duration += Duration::hours(hours.as_str().parse().unwrap());
}
if let Some(minutes) = captures.name("M") {
duration += Duration::minutes(minutes.as_str().parse().unwrap());
}
if let Some(seconds) = captures.name("S") {
duration += Duration::seconds(seconds.as_str().parse().unwrap());
}
if let Some(sign) = captures.name("sign") {
if sign.as_str() == "-" {
duration = -duration;
}
}
Ok(duration)
}
#[cfg(test)]
mod tests {
use chrono::Duration;
use crate::parse_duration;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("P12W").unwrap(), Duration::weeks(12));
assert_eq!(parse_duration("P12D").unwrap(), Duration::days(12));
assert_eq!(parse_duration("PT12H").unwrap(), Duration::hours(12));
assert_eq!(parse_duration("PT12M").unwrap(), Duration::minutes(12));
assert_eq!(parse_duration("PT12S").unwrap(), Duration::seconds(12));
}
}

View File

@@ -1,24 +1,22 @@
use crate::CalDateTime;
use crate::Error; use crate::Error;
use crate::{CalDateTime, ComponentMut, parse_duration};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use ical::{ use ical::parser::ComponentMut;
generator::IcalEvent, use ical::{generator::IcalEvent, parser::Component, property::Property};
parser::{Component, ical::component::IcalTimeZone},
property::Property,
};
use rrule::{RRule, RRuleSet}; use rrule::{RRule, RRuleSet};
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct EventObject { pub struct EventObject {
pub event: IcalEvent, pub event: IcalEvent,
pub timezones: HashMap<String, IcalTimeZone>, // If a timezone is None that means that in the VCALENDAR object there's a timezone defined
pub(crate) ics: String, // with that name but its not from the Olson DB
pub timezones: HashMap<String, Option<chrono_tz::Tz>>,
} }
impl EventObject { impl EventObject {
pub fn get_dtstart(&self) -> Result<Option<CalDateTime>, Error> { pub fn get_dtstart(&self) -> Result<Option<CalDateTime>, Error> {
if let Some(dtstart) = self.event.get_property("DTSTART") { if let Some(dtstart) = self.event.get_dtstart() {
Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?)) Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?))
} else { } else {
Ok(None) Ok(None)
@@ -26,7 +24,7 @@ impl EventObject {
} }
pub fn get_dtend(&self) -> Result<Option<CalDateTime>, Error> { pub fn get_dtend(&self) -> Result<Option<CalDateTime>, Error> {
if let Some(dtend) = self.event.get_property("DTEND") { if let Some(dtend) = self.event.get_dtend() {
Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?)) Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?))
} else { } else {
Ok(None) Ok(None)
@@ -34,33 +32,21 @@ impl EventObject {
} }
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> { pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
if let Some(_rrule) = self.event.get_property("RRULE") { if self.event.get_rrule().is_some() {
// TODO: understand recurrence rules // TODO: understand recurrence rules
return Ok(None); return Ok(None);
} }
if let Some(dtend) = self.event.get_property("DTEND") { if let Some(dtend) = self.get_dtend()? {
return Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?)); return Ok(Some(dtend));
}; };
let duration = self.get_duration()?.unwrap_or(Duration::days(1)); let duration = self.event.get_duration().unwrap_or(Duration::days(1));
let first_occurence = self.get_dtstart()?; let first_occurence = self.get_dtstart()?;
Ok(first_occurence.map(|first_occurence| first_occurence + duration)) Ok(first_occurence.map(|first_occurence| first_occurence + duration))
} }
pub fn get_duration(&self) -> Result<Option<Duration>, Error> {
if let Some(Property {
value: Some(duration),
..
}) = self.event.get_property("DURATION")
{
Ok(Some(parse_duration(duration)?))
} else {
Ok(None)
}
}
pub fn recurrence_ruleset(&self) -> Result<Option<rrule::RRuleSet>, Error> { pub fn recurrence_ruleset(&self) -> Result<Option<rrule::RRuleSet>, Error> {
let dtstart: DateTime<rrule::Tz> = if let Some(dtstart) = self.get_dtstart()? { let dtstart: DateTime<rrule::Tz> = if let Some(dtstart) = self.get_dtstart()? {
if let Some(dtend) = self.get_dtend()? { if let Some(dtend) = self.get_dtend()? {
@@ -128,7 +114,7 @@ impl EventObject {
} else { } else {
date.format() date.format()
}; };
let mut ev = self.event.clone(); let mut ev = self.event.clone().mutable();
ev.remove_property("RRULE"); ev.remove_property("RRULE");
ev.remove_property("RDATE"); ev.remove_property("RDATE");
ev.remove_property("EXDATE"); ev.remove_property("EXDATE");
@@ -163,7 +149,7 @@ impl EventObject {
params: dtstart_prop.params, params: dtstart_prop.params,
}); });
} }
events.push(ev); events.push(ev.verify()?);
} }
Ok(events) Ok(events)
} else { } else {
@@ -242,11 +228,7 @@ END:VEVENT\r\n",
#[test] #[test]
fn test_expand_recurrence() { fn test_expand_recurrence() {
let event = CalendarObject::from_ics( let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
"318ec6503573d9576818daf93dac07317058d95c".to_string(),
ICS.to_string(),
)
.unwrap();
let event = event.event().unwrap(); let event = event.event().unwrap();
let events: Vec<String> = event let events: Vec<String> = event

View File

@@ -1,7 +1,5 @@
use derive_more::From;
use ical::parser::ical::component::IcalJournal; use ical::parser::ical::component::IcalJournal;
#[derive(Debug, Clone)] #[derive(Debug, Clone, From)]
pub struct JournalObject { pub struct JournalObject(pub IcalJournal);
pub journal: IcalJournal,
pub(crate) ics: String,
}

View File

@@ -4,10 +4,8 @@ use crate::Error;
use chrono::DateTime; use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use derive_more::Display; use derive_more::Display;
use ical::{ use ical::generator::{Emitter, IcalCalendar};
generator::{Emitter, IcalCalendar}, use ical::property::Property;
parser::{Component, ical::component::IcalTimeZone},
};
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader}; use std::{collections::HashMap, io::BufReader};
@@ -47,8 +45,7 @@ impl rustical_xml::ValueDeserialize for CalendarObjectType {
"VJOURNAL" => Ok(Self::Journal), "VJOURNAL" => Ok(Self::Journal),
_ => Err(rustical_xml::XmlError::InvalidValue( _ => Err(rustical_xml::XmlError::InvalidValue(
rustical_xml::ParseValueError::Other(format!( rustical_xml::ParseValueError::Other(format!(
"Invalid value '{}', must be VEVENT, VTODO, or VJOURNAL", "Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL"
val
)), )),
)), )),
} }
@@ -62,15 +59,21 @@ pub enum CalendarObjectComponent {
Journal(JournalObject), Journal(JournalObject),
} }
#[derive(Debug, Clone)] impl Default for CalendarObjectComponent {
pub struct CalendarObject { fn default() -> Self {
id: String, Self::Event(EventObject::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct CalendarObject<const VERIFIED: bool = true> {
data: CalendarObjectComponent, data: CalendarObjectComponent,
cal: IcalCalendar, properties: Vec<Property>,
ics: String,
} }
impl CalendarObject { impl CalendarObject {
pub fn from_ics(object_id: String, ics: String) -> Result<Self, Error> { pub fn from_ics(ics: String) -> Result<Self, Error> {
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes())); let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??; let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() { if parser.next().is_some() {
@@ -91,52 +94,30 @@ impl CalendarObject {
)); ));
} }
let timezones: HashMap<String, IcalTimeZone> = cal let timezones: HashMap<String, Option<chrono_tz::Tz>> = cal
.timezones .timezones
.clone() .clone()
.into_iter() .into_iter()
.filter_map(|timezone| { .map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok()))
let timezone_prop = timezone.get_property("TZID")?.to_owned();
let tzid = timezone_prop.value?;
Some((tzid, timezone))
})
.collect(); .collect();
if let Some(event) = cal.events.first() { let data = if let Some(event) = cal.events.into_iter().next() {
return Ok(CalendarObject { CalendarObjectComponent::Event(EventObject { event, timezones })
id: object_id, } else if let Some(todo) = cal.todos.into_iter().next() {
cal: cal.clone(), CalendarObjectComponent::Todo(todo.into())
data: CalendarObjectComponent::Event(EventObject { } else if let Some(journal) = cal.journals.into_iter().next() {
event: event.clone(), CalendarObjectComponent::Journal(journal.into())
timezones, } else {
ics, return Err(Error::InvalidData(
}),
});
}
if let Some(todo) = cal.todos.first() {
return Ok(CalendarObject {
id: object_id,
cal: cal.clone(),
data: CalendarObjectComponent::Todo(TodoObject {
todo: todo.clone(),
ics,
}),
});
}
if let Some(journal) = cal.journals.first() {
return Ok(CalendarObject {
id: object_id,
cal: cal.clone(),
data: CalendarObjectComponent::Journal(JournalObject {
journal: journal.clone(),
ics,
}),
});
}
Err(Error::InvalidData(
"iCalendar component type not supported :(".to_owned(), "iCalendar component type not supported :(".to_owned(),
)) ));
};
Ok(Self {
data,
properties: cal.properties,
ics,
})
} }
pub fn get_data(&self) -> &CalendarObjectComponent { pub fn get_data(&self) -> &CalendarObjectComponent {
@@ -144,29 +125,26 @@ impl CalendarObject {
} }
pub fn get_id(&self) -> &str { pub fn get_id(&self) -> &str {
&self.id match &self.data {
CalendarObjectComponent::Todo(todo) => todo.0.get_uid(),
CalendarObjectComponent::Event(event) => event.event.get_uid(),
CalendarObjectComponent::Journal(journal) => journal.0.get_uid(),
} }
}
pub fn get_etag(&self) -> String { pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&self.id); hasher.update(self.get_id());
hasher.update(self.get_ics()); hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
pub fn get_ics(&self) -> &str { pub fn get_ics(&self) -> &str {
match &self.data { &self.ics
CalendarObjectComponent::Todo(todo) => &todo.ics,
CalendarObjectComponent::Event(event) => &event.ics,
CalendarObjectComponent::Journal(journal) => &journal.ics,
}
} }
pub fn get_component_name(&self) -> &str { pub fn get_component_name(&self) -> &str {
match self.data { self.get_object_type().as_str()
CalendarObjectComponent::Todo(_) => "VTODO",
CalendarObjectComponent::Event(_) => "VEVENT",
CalendarObjectComponent::Journal(_) => "VJOURNAL",
}
} }
pub fn get_object_type(&self) -> CalendarObjectType { pub fn get_object_type(&self) -> CalendarObjectType {
@@ -206,8 +184,11 @@ impl CalendarObject {
// Only events can be expanded // Only events can be expanded
match &self.data { match &self.data {
CalendarObjectComponent::Event(event) => { CalendarObjectComponent::Event(event) => {
let mut cal = self.cal.clone(); let cal = IcalCalendar {
cal.events = event.expand_recurrence(start, end)?; properties: self.properties.clone(),
events: event.expand_recurrence(start, end)?,
..Default::default()
};
Ok(cal.generate()) Ok(cal.generate())
} }
_ => Ok(self.get_ics().to_string()), _ => Ok(self.get_ics().to_string()),

View File

@@ -1,7 +1,5 @@
use derive_more::From;
use ical::parser::ical::component::IcalTodo; use ical::parser::ical::component::IcalTodo;
#[derive(Debug, Clone)] #[derive(Debug, Clone, From)]
pub struct TodoObject { pub struct TodoObject(pub IcalTodo);
pub todo: IcalTodo,
pub(crate) ics: String,
}

View File

@@ -1,14 +1,8 @@
mod property_ext;
pub use property_ext::*;
mod timestamp; mod timestamp;
mod timezone; mod timezone;
pub use timestamp::*; pub use timestamp::*;
pub use timezone::*; pub use timezone::*;
mod duration;
pub use duration::parse_duration;
mod icalendar; mod icalendar;
pub use icalendar::*; pub use icalendar::*;

View File

@@ -1,46 +0,0 @@
use ical::{generator::IcalEvent, property::Property};
pub trait IcalProperty {
fn get_param(&self, name: &str) -> Option<Vec<&str>>;
fn get_value_type(&self) -> Option<&str>;
fn get_tzid(&self) -> Option<&str>;
}
impl IcalProperty for ical::property::Property {
fn get_param(&self, name: &str) -> Option<Vec<&str>> {
self.params
.as_ref()?
.iter()
.find(|(key, _)| name == key)
.map(|(_, value)| value.iter().map(String::as_str).collect())
}
fn get_value_type(&self) -> Option<&str> {
self.get_param("VALUE")
.and_then(|params| params.into_iter().next())
}
fn get_tzid(&self) -> Option<&str> {
self.get_param("TZID")
.and_then(|params| params.into_iter().next())
}
}
pub trait ComponentMut {
fn remove_property(&mut self, name: &str);
fn set_property(&mut self, prop: Property);
fn push_property(&mut self, prop: Property);
}
impl ComponentMut for IcalEvent {
fn remove_property(&mut self, name: &str) {
self.properties.retain(|prop| prop.name != name);
}
fn set_property(&mut self, prop: Property) {
self.remove_property(&prop.name);
self.push_property(prop);
}
fn push_property(&mut self, prop: Property) {
self.properties.push(prop);
}
}

View File

@@ -1,12 +1,8 @@
use super::timezone::CalTimezone; use super::timezone::ICalTimezone;
use crate::IcalProperty;
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use derive_more::derive::Deref; use derive_more::derive::Deref;
use ical::{ use ical::property::Property;
parser::{Component, ical::component::IcalTimeZone},
property::Property,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rustical_xml::{ValueDeserialize, ValueSerialize}; use rustical_xml::{ValueDeserialize, ValueSerialize};
use std::{borrow::Cow, collections::HashMap, ops::Add}; use std::{borrow::Cow, collections::HashMap, ops::Add};
@@ -68,8 +64,8 @@ pub enum CalDateTime {
// Form 2, example: 19980119T070000Z -> UTC // Form 2, example: 19980119T070000Z -> UTC
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson // Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
// https://en.wikipedia.org/wiki/Tz_database // https://en.wikipedia.org/wiki/Tz_database
DateTime(DateTime<CalTimezone>), DateTime(DateTime<ICalTimezone>),
Date(NaiveDate, CalTimezone), Date(NaiveDate, ICalTimezone),
} }
impl From<CalDateTime> for DateTime<rrule::Tz> { impl From<CalDateTime> for DateTime<rrule::Tz> {
@@ -106,13 +102,13 @@ impl Ord for CalDateTime {
impl From<DateTime<Local>> for CalDateTime { impl From<DateTime<Local>> for CalDateTime {
fn from(value: DateTime<Local>) -> Self { fn from(value: DateTime<Local>) -> Self {
CalDateTime::DateTime(value.with_timezone(&CalTimezone::Local)) CalDateTime::DateTime(value.with_timezone(&ICalTimezone::Local))
} }
} }
impl From<DateTime<Utc>> for CalDateTime { impl From<DateTime<Utc>> for CalDateTime {
fn from(value: DateTime<Utc>) -> Self { fn from(value: DateTime<Utc>) -> Self {
CalDateTime::DateTime(value.with_timezone(&CalTimezone::Utc)) CalDateTime::DateTime(value.with_timezone(&ICalTimezone::Olson(chrono_tz::UTC)))
} }
} }
@@ -136,7 +132,7 @@ impl Add<Duration> for CalDateTime {
impl CalDateTime { impl CalDateTime {
pub fn parse_prop( pub fn parse_prop(
prop: &Property, prop: &Property,
timezones: &HashMap<String, IcalTimeZone>, timezones: &HashMap<String, Option<chrono_tz::Tz>>,
) -> Result<Self, CalDateTimeError> { ) -> Result<Self, CalDateTimeError> {
let prop_value = prop let prop_value = prop
.value .value
@@ -145,28 +141,9 @@ impl CalDateTime {
"empty property".to_owned(), "empty property".to_owned(),
))?; ))?;
// Use the TZID parameter from the property let timezone = if let Some(tzid) = prop.get_param("TZID") {
let timezone = if let Some(tzid) = prop.get_tzid() {
if let Some(timezone) = timezones.get(tzid) { if let Some(timezone) = timezones.get(tzid) {
// X-LIC-LOCATION is often used to refer to a standardised timezone from the Olson timezone.to_owned()
// database
if let Some(olson_name) = timezone
.get_property("X-LIC-LOCATION")
.map(|prop| prop.value.to_owned())
.unwrap_or_default()
{
if let Ok(tz) = olson_name.parse::<Tz>() {
Some(tz)
} else {
return Err(CalDateTimeError::InvalidOlson(olson_name));
}
} else {
// If the TZID matches a name from the Olson database (e.g. Europe/Berlin) we
// guess that we can just use it
tzid.parse::<Tz>().ok()
// TODO: If None: Too bad, we need to manually parse it
// For now it's just treated as localtime
}
} else { } else {
// TZID refers to timezone that does not exist // TZID refers to timezone that does not exist
return Err(CalDateTimeError::InvalidTZID(tzid.to_string())); return Err(CalDateTimeError::InvalidTZID(tzid.to_string()));
@@ -183,7 +160,7 @@ impl CalDateTime {
pub fn format(&self) -> String { pub fn format(&self) -> String {
match self { match self {
Self::DateTime(datetime) => match datetime.timezone() { Self::DateTime(datetime) => match datetime.timezone() {
CalTimezone::Utc => datetime.format(UTC_DATE_TIME).to_string(), ICalTimezone::Olson(chrono_tz::UTC) => datetime.format(UTC_DATE_TIME).to_string(),
_ => datetime.format(LOCAL_DATE_TIME).to_string(), _ => datetime.format(LOCAL_DATE_TIME).to_string(),
}, },
Self::Date(date, _) => date.format(LOCAL_DATE).to_string(), Self::Date(date, _) => date.format(LOCAL_DATE).to_string(),
@@ -208,7 +185,7 @@ impl CalDateTime {
matches!(&self, Self::Date(_, _)) matches!(&self, Self::Date(_, _))
} }
pub fn as_datetime(&self) -> Cow<DateTime<CalTimezone>> { pub fn as_datetime(&self) -> Cow<'_, DateTime<ICalTimezone>> {
match self { match self {
Self::DateTime(datetime) => Cow::Borrowed(datetime), Self::DateTime(datetime) => Cow::Borrowed(datetime),
Self::Date(date, tz) => Cow::Owned( Self::Date(date, tz) => Cow::Owned(
@@ -232,7 +209,7 @@ impl CalDateTime {
} }
return Ok(CalDateTime::DateTime( return Ok(CalDateTime::DateTime(
datetime datetime
.and_local_timezone(CalTimezone::Local) .and_local_timezone(ICalTimezone::Local)
.earliest() .earliest()
.ok_or(CalDateTimeError::LocalTimeGap)?, .ok_or(CalDateTimeError::LocalTimeGap)?,
)); ));
@@ -242,8 +219,8 @@ impl CalDateTime {
return Ok(datetime.and_utc().into()); return Ok(datetime.and_utc().into());
} }
let timezone = timezone let timezone = timezone
.map(CalTimezone::Olson) .map(ICalTimezone::Olson)
.unwrap_or(CalTimezone::Local); .unwrap_or(ICalTimezone::Local);
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) { if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
return Ok(CalDateTime::Date(date, timezone)); return Ok(CalDateTime::Date(date, timezone));
} }
@@ -275,7 +252,7 @@ impl CalDateTime {
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(year, month, day) NaiveDate::from_ymd_opt(year, month, day)
.ok_or(CalDateTimeError::ParseError(value.to_string()))?, .ok_or(CalDateTimeError::ParseError(value.to_string()))?,
CalTimezone::Local, ICalTimezone::Local,
), ),
false, false,
)); ));
@@ -287,7 +264,7 @@ impl CalDateTime {
self.as_datetime().to_utc() self.as_datetime().to_utc()
} }
pub fn timezone(&self) -> CalTimezone { pub fn timezone(&self) -> ICalTimezone {
match &self { match &self {
CalDateTime::DateTime(datetime) => datetime.timezone(), CalDateTime::DateTime(datetime) => datetime.timezone(),
CalDateTime::Date(_, tz) => tz.to_owned(), CalDateTime::Date(_, tz) => tz.to_owned(),
@@ -423,7 +400,7 @@ mod tests {
( (
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local crate::ICalTimezone::Local
), ),
true true
) )
@@ -433,7 +410,7 @@ mod tests {
( (
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local crate::ICalTimezone::Local
), ),
true true
) )
@@ -443,7 +420,7 @@ mod tests {
( (
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
crate::CalTimezone::Local crate::ICalTimezone::Local
), ),
false false
) )

View File

@@ -1,29 +1,26 @@
use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone, Utc}; use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone};
use chrono_tz::Tz; use chrono_tz::Tz;
use derive_more::{Display, From}; use derive_more::{Display, From};
#[derive(Debug, Clone, From, PartialEq, Eq)] #[derive(Debug, Clone, From, PartialEq, Eq)]
pub enum CalTimezone { pub enum ICalTimezone {
Local, Local,
Utc,
Olson(Tz), Olson(Tz),
} }
impl From<CalTimezone> for rrule::Tz { impl From<ICalTimezone> for rrule::Tz {
fn from(value: CalTimezone) -> Self { fn from(value: ICalTimezone) -> Self {
match value { match value {
CalTimezone::Local => Self::LOCAL, ICalTimezone::Local => Self::LOCAL,
CalTimezone::Utc => Self::UTC, ICalTimezone::Olson(tz) => Self::Tz(tz),
CalTimezone::Olson(tz) => Self::Tz(tz),
} }
} }
} }
impl From<rrule::Tz> for CalTimezone { impl From<rrule::Tz> for ICalTimezone {
fn from(value: rrule::Tz) -> Self { fn from(value: rrule::Tz) -> Self {
match value { match value {
rrule::Tz::Local(_) => Self::Local, rrule::Tz::Local(_) => Self::Local,
rrule::Tz::Tz(chrono_tz::UTC) => Self::Utc,
rrule::Tz::Tz(tz) => Self::Olson(tz), rrule::Tz::Tz(tz) => Self::Olson(tz),
} }
} }
@@ -32,7 +29,6 @@ impl From<rrule::Tz> for CalTimezone {
#[derive(Debug, Clone, PartialEq, Display)] #[derive(Debug, Clone, PartialEq, Display)]
pub enum CalTimezoneOffset { pub enum CalTimezoneOffset {
Local(chrono::FixedOffset), Local(chrono::FixedOffset),
Utc(chrono::Utc),
Olson(chrono_tz::TzOffset), Olson(chrono_tz::TzOffset),
} }
@@ -40,19 +36,17 @@ impl chrono::Offset for CalTimezoneOffset {
fn fix(&self) -> chrono::FixedOffset { fn fix(&self) -> chrono::FixedOffset {
match self { match self {
Self::Local(local) => local.fix(), Self::Local(local) => local.fix(),
Self::Utc(utc) => utc.fix(),
Self::Olson(olson) => olson.fix(), Self::Olson(olson) => olson.fix(),
} }
} }
} }
impl TimeZone for CalTimezone { impl TimeZone for ICalTimezone {
type Offset = CalTimezoneOffset; type Offset = CalTimezoneOffset;
fn from_offset(offset: &Self::Offset) -> Self { fn from_offset(offset: &Self::Offset) -> Self {
match offset { match offset {
CalTimezoneOffset::Local(_) => Self::Local, CalTimezoneOffset::Local(_) => Self::Local,
CalTimezoneOffset::Utc(_) => Self::Utc,
CalTimezoneOffset::Olson(offset) => Self::Olson(Tz::from_offset(offset)), CalTimezoneOffset::Olson(offset) => Self::Olson(Tz::from_offset(offset)),
} }
} }
@@ -62,9 +56,6 @@ impl TimeZone for CalTimezone {
Self::Local => Local Self::Local => Local
.offset_from_local_date(local) .offset_from_local_date(local)
.map(CalTimezoneOffset::Local), .map(CalTimezoneOffset::Local),
Self::Utc => Utc
.offset_from_local_date(local)
.map(CalTimezoneOffset::Utc),
Self::Olson(tz) => tz Self::Olson(tz) => tz
.offset_from_local_date(local) .offset_from_local_date(local)
.map(CalTimezoneOffset::Olson), .map(CalTimezoneOffset::Olson),
@@ -79,9 +70,6 @@ impl TimeZone for CalTimezone {
Self::Local => Local Self::Local => Local
.offset_from_local_datetime(local) .offset_from_local_datetime(local)
.map(CalTimezoneOffset::Local), .map(CalTimezoneOffset::Local),
Self::Utc => Utc
.offset_from_local_datetime(local)
.map(CalTimezoneOffset::Utc),
Self::Olson(tz) => tz Self::Olson(tz) => tz
.offset_from_local_datetime(local) .offset_from_local_datetime(local)
.map(CalTimezoneOffset::Olson), .map(CalTimezoneOffset::Olson),
@@ -91,7 +79,6 @@ impl TimeZone for CalTimezone {
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset { fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
match self { match self {
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_datetime(utc)), Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_datetime(utc)),
Self::Utc => CalTimezoneOffset::Utc(Utc.offset_from_utc_datetime(utc)),
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_datetime(utc)), Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_datetime(utc)),
} }
} }
@@ -99,7 +86,6 @@ impl TimeZone for CalTimezone {
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset { fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
match self { match self {
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_date(utc)), Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_date(utc)),
Self::Utc => CalTimezoneOffset::Utc(Utc.offset_from_utc_date(utc)),
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_date(utc)), Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_date(utc)),
} }
} }

View File

@@ -22,7 +22,6 @@ chrono-tz = { workspace = true }
derive_more = { workspace = true, features = ["as_ref"] } derive_more = { workspace = true, features = ["as_ref"] }
rustical_xml.workspace = true rustical_xml.workspace = true
tokio.workspace = true tokio.workspace = true
rand.workspace = true
clap.workspace = true clap.workspace = true
rustical_dav.workspace = true rustical_dav.workspace = true
rustical_ical.workspace = true rustical_ical.workspace = true
@@ -33,6 +32,7 @@ headers.workspace = true
tower.workspace = true tower.workspace = true
futures-core.workspace = true futures-core.workspace = true
tower-sessions.workspace = true tower-sessions.workspace = true
vtimezones-rs.workspace = true
[dev-dependencies] [dev-dependencies]
rstest = { workspace = true } rstest = { workspace = true }

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::synctoken::format_synctoken; use crate::synctoken::format_synctoken;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
@@ -11,7 +13,6 @@ pub struct Calendar {
pub order: i64, pub order: i64,
pub description: Option<String>, pub description: Option<String>,
pub color: Option<String>, pub color: Option<String>,
pub timezone: Option<String>,
pub timezone_id: Option<String>, pub timezone_id: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
pub synctoken: i64, pub synctoken: i64,
@@ -24,4 +25,16 @@ impl Calendar {
pub fn format_synctoken(&self) -> String { pub fn format_synctoken(&self) -> String {
format_synctoken(self.synctoken) format_synctoken(self.synctoken)
} }
pub fn get_timezone(&self) -> Option<chrono_tz::Tz> {
self.timezone_id
.as_ref()
.and_then(|tzid| chrono_tz::Tz::from_str(tzid).ok())
}
pub fn get_vtimezone(&self) -> Option<&'static str> {
self.timezone_id
.as_ref()
.and_then(|tzid| vtimezones_rs::VTIMEZONES.get(tzid).cloned())
}
} }

View File

@@ -20,7 +20,6 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
order: 0, order: 0,
description: None, description: None,
color: None, color: None,
timezone: None,
timezone_id: None, timezone_id: None,
deleted_at: addressbook.deleted_at, deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken, synctoken: addressbook.synctoken,

View File

@@ -0,0 +1 @@
ALTER TABLE calendars ADD COLUMN timezone TEXT;

View File

@@ -0,0 +1,3 @@
-- We don't want to save timezones as ics anymore
-- but instead just rely on the TZDB identifier
ALTER TABLE calendars DROP COLUMN timezone;

View File

@@ -22,7 +22,17 @@ impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error; type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> { fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
Ok(CalendarObject::from_ics(value.id, value.ics)?) let object = CalendarObject::from_ics(value.ics)?;
if object.get_id() != value.id {
return Err(rustical_store::Error::IcalError(
rustical_ical::Error::InvalidData(format!(
"object_id={} and UID={} don't match",
object.get_id(),
value.id
)),
));
}
Ok(object)
} }
} }
@@ -34,7 +44,6 @@ struct CalendarRow {
order: i64, order: i64,
description: Option<String>, description: Option<String>,
color: Option<String>, color: Option<String>,
timezone: Option<String>,
timezone_id: Option<String>, timezone_id: Option<String>,
deleted_at: Option<NaiveDateTime>, deleted_at: Option<NaiveDateTime>,
synctoken: i64, synctoken: i64,
@@ -64,7 +73,6 @@ impl From<CalendarRow> for Calendar {
order: value.order, order: value.order,
description: value.description, description: value.description,
color: value.color, color: value.color,
timezone: value.timezone,
timezone_id: value.timezone_id, timezone_id: value.timezone_id,
deleted_at: value.deleted_at, deleted_at: value.deleted_at,
synctoken: value.synctoken, synctoken: value.synctoken,
@@ -127,7 +135,7 @@ impl SqliteCalendarStore {
) -> Result<Vec<Calendar>, Error> { ) -> Result<Vec<Calendar>, Error> {
let cals = sqlx::query_as!( let cals = sqlx::query_as!(
CalendarRow, CalendarRow,
r#"SELECT * r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal
FROM calendars FROM calendars
WHERE principal = ? AND deleted_at IS NOT NULL"#, WHERE principal = ? AND deleted_at IS NOT NULL"#,
principal principal
@@ -147,8 +155,8 @@ impl SqliteCalendarStore {
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal); let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
sqlx::query!( sqlx::query!(
r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal) r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
calendar.displayname, calendar.displayname,
@@ -156,7 +164,6 @@ impl SqliteCalendarStore {
calendar.order, calendar.order,
calendar.color, calendar.color,
calendar.subscription_url, calendar.subscription_url,
calendar.timezone,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
comp_event, comp_todo, comp_journal comp_event, comp_todo, comp_journal
@@ -178,7 +185,7 @@ impl SqliteCalendarStore {
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal); let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
let result = sqlx::query!( let result = sqlx::query!(
r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ? r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?
WHERE (principal, id) = (?, ?)"#, WHERE (principal, id) = (?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
@@ -186,7 +193,6 @@ impl SqliteCalendarStore {
calendar.description, calendar.description,
calendar.order, calendar.order,
calendar.color, calendar.color,
calendar.timezone,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
comp_event, comp_todo, comp_journal, comp_event, comp_todo, comp_journal,

View File

@@ -31,6 +31,11 @@ impl SubscriptionStore for SqliteStore {
} }
async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error> { async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error> {
let already_exists = match self.get_subscription(&sub.id).await {
Ok(_) => true,
Err(Error::NotFound) => false,
Err(err) => return Err(err),
};
sqlx::query!( sqlx::query!(
r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#, r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#,
sub.id, sub.id,
@@ -41,8 +46,7 @@ impl SubscriptionStore for SqliteStore {
sub.public_key_type, sub.public_key_type,
sub.auth_secret sub.auth_secret
).execute(&self.db).await.map_err(crate::Error::from)?; ).execute(&self.db).await.map_err(crate::Error::from)?;
// TODO: Correctly return whether a subscription already existed Ok(already_exists)
Ok(false)
} }
async fn delete_subscription(&self, id: &str) -> Result<(), Error> { async fn delete_subscription(&self, id: &str) -> Result<(), Error> {
sqlx::query!(r#"DELETE FROM davpush_subscriptions WHERE id = ? "#, id) sqlx::query!(r#"DELETE FROM davpush_subscriptions WHERE id = ? "#, id)

View File

@@ -8,6 +8,8 @@ a CalDAV/CardDAV server
you'd still be one of the first testers so expect bugs and rough edges. you'd still be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
[Installation](installation/index.md){ .md-button }
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database