Compare commits

..

9 Commits

Author SHA1 Message Date
Lennart
d639b18005 version 0.4.3 2025-06-23 16:44:21 +02:00
Lennart
6046439fc7 feat(dav): Add show_deleted parameter to get_resource
Fixes #86
2025-06-23 16:43:46 +02:00
Lennart
f9de8a4687 feat: Add show_deleted to get_calendar 2025-06-23 16:35:36 +02:00
Lennart
8dfb47b28f version 0.4.2 2025-06-23 16:13:18 +02:00
Lennart
eb720ded99 ci: Only tag releases as latest container images 2025-06-23 16:12:36 +02:00
Lennart
89ef7b2ced Update vcard date tests 2025-06-23 16:09:22 +02:00
Lennart
6e0129130e Fix birthdays without year in birthday calendar
Fixes #79
2025-06-23 16:03:59 +02:00
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
27 changed files with 207 additions and 118 deletions

View File

@@ -41,12 +41,10 @@ jobs:
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# As long as we don't have releases everything on the main branch shall be tagged as latest
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=semver,pattern={{version}} type=semver,pattern={{version}}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)", "query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?) ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -80,7 +80,7 @@
} }
], ],
"parameters": { "parameters": {
"Right": 2 "Right": 3
}, },
"nullable": [ "nullable": [
false, false,
@@ -100,5 +100,5 @@
false false
] ]
}, },
"hash": "9f930775043a6d4571a8ffd5a981cadf7c51f3f11a189f8461505abec31076e6" "hash": "bb2fa030f2e7c7afdb38c5c54cb31de5293be332d86cf643977d479999542553"
} }

22
Cargo.lock generated
View File

@@ -2736,7 +2736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2779,7 +2779,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2814,7 +2814,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2846,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2897,7 +2897,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2930,7 +2930,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2963,7 +2963,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2997,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3017,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.3.6" version = "0.4.3"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.3.6" version = "0.4.3"
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"

View File

@@ -24,12 +24,16 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
if !user.is_principal(&calendar.principal) { if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
let mut timezones = HashMap::new(); let mut timezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?; let objects = cal_store.get_objects(&principal, &calendar_id).await?;

View File

@@ -25,7 +25,7 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
let calendar = resource_service let calendar = resource_service
.cal_store .cal_store
.get_calendar(&principal, &cal_id) .get_calendar(&principal, &cal_id, false)
.await?; .await?;
let calendar_resource = CalendarResource { let calendar_resource = CalendarResource {
cal: calendar, cal: calendar,

View File

@@ -56,8 +56,12 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
async fn get_resource( async fn get_resource(
&self, &self,
(principal, cal_id): &Self::PathComponents, (principal, cal_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> { ) -> Result<Self::Resource, Error> {
let calendar = self.cal_store.get_calendar(principal, cal_id).await?; let calendar = self
.cal_store
.get_calendar(principal, cal_id, show_deleted)
.await?;
Ok(CalendarResource { Ok(CalendarResource {
cal: calendar, cal: calendar,
read_only: self.cal_store.is_read_only(cal_id), read_only: self.cal_store.is_read_only(cal_id),

View File

@@ -27,7 +27,9 @@ pub async fn get_event<C: CalendarStore>(
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store
.get_calendar(&principal, &calendar_id, false)
.await?;
if !user.is_principal(&calendar.principal) { if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }

View File

@@ -58,10 +58,11 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
calendar_id, calendar_id,
object_id, object_id,
}: &Self::PathComponents, }: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let object = self let object = self
.cal_store .cal_store
.get_object(principal, calendar_id, object_id, false) .get_object(principal, calendar_id, object_id, show_deleted)
.await?; .await?;
Ok(CalendarObjectResource { Ok(CalendarObjectResource {
object, object,

View File

@@ -48,6 +48,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let user = self let user = self
.auth_provider .auth_provider

View File

@@ -49,10 +49,11 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
addressbook_id, addressbook_id,
object_id, object_id,
}: &Self::PathComponents, }: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let object = self let object = self
.addr_store .addr_store
.get_object(principal, addressbook_id, object_id, false) .get_object(principal, addressbook_id, object_id, show_deleted)
.await?; .await?;
Ok(AddressObjectResource { Ok(AddressObjectResource {
object, object,

View File

@@ -59,10 +59,11 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
async fn get_resource( async fn get_resource(
&self, &self,
(principal, addressbook_id): &Self::PathComponents, (principal, addressbook_id): &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Error> { ) -> Result<Self::Resource, Error> {
let addressbook = self let addressbook = self
.addr_store .addr_store
.get_addressbook(principal, addressbook_id, false) .get_addressbook(principal, addressbook_id, show_deleted)
.await .await
.map_err(|_e| Error::NotFound)?; .map_err(|_e| Error::NotFound)?;
Ok(addressbook.into()) Ok(addressbook.into())

View File

@@ -59,6 +59,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let user = self let user = self
.auth_provider .auth_provider

View File

@@ -45,7 +45,7 @@ pub async fn route_delete<R: ResourceService>(
if_match: Option<IfMatch>, if_match: Option<IfMatch>,
if_none_match: Option<IfNoneMatch>, if_none_match: Option<IfNoneMatch>,
) -> Result<(), R::Error> { ) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?; let resource = resource_service.get_resource(path_components, true).await?;
// Kind of a bodge since we don't get unbind from the parent // Kind of a bodge since we don't get unbind from the parent
let privileges = resource.get_user_privileges(principal)?; let privileges = resource.get_user_privileges(principal)?;

View File

@@ -49,7 +49,9 @@ pub(crate) async fn route_propfind<R: ResourceService>(
resource_service: &R, resource_service: &R,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
) -> Result<RSMultistatus<R>, R::Error> { ) -> Result<RSMultistatus<R>, R::Error> {
let resource = resource_service.get_resource(path_components).await?; let resource = resource_service
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Read) { if !privileges.has(&UserPrivilege::Read) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());

View File

@@ -85,7 +85,9 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
operations, operations,
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?; ) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
let mut resource = resource_service.get_resource(path_components).await?; let mut resource = resource_service
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) { if !privileges.has(&UserPrivilege::Write) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());

View File

@@ -34,7 +34,8 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
async fn get_resource( async fn get_resource(
&self, &self,
_path: &Self::PathComponents, path: &Self::PathComponents,
show_deleted: bool,
) -> Result<Self::Resource, Self::Error>; ) -> Result<Self::Resource, Self::Error>;
async fn save_resource( async fn save_resource(

View File

@@ -86,7 +86,11 @@ where
const DAV_HEADER: &str = "1, 3, access-control"; const DAV_HEADER: &str = "1, 3, access-control";
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> { async fn get_resource(
&self,
_: &(),
_show_deleted: bool,
) -> Result<Self::Resource, Self::Error> {
Ok(RootResource::<PRS::Resource, P>::default()) Ok(RootResource::<PRS::Resource, P>::default())
} }

View File

@@ -12,3 +12,12 @@ pub struct FrontendConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub allow_password_login: bool, pub allow_password_login: bool,
} }
impl Default for FrontendConfig {
fn default() -> Self {
Self {
enabled: true,
allow_password_login: true,
}
}
}

View File

@@ -27,7 +27,7 @@ pub async fn route_calendar<C: CalendarStore>(
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
} }
Ok(CalendarPage { Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id).await?, calendar: store.get_calendar(&owner, &cal_id, true).await?,
} }
.into_response()) .into_response())
} }

View File

@@ -62,14 +62,14 @@ impl AddressObject {
&self.vcf &self.vcf
} }
pub fn get_anniversary(&self) -> Option<CalDateTime> { pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?; let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok() CalDateTime::parse_vcard(prop).ok()
} }
pub fn get_birthday(&self) -> Option<CalDateTime> { pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?; let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok() CalDateTime::parse_vcard(prop).ok()
} }
pub fn get_full_name(&self) -> Option<&str> { pub fn get_full_name(&self) -> Option<&str> {
@@ -78,14 +78,15 @@ impl AddressObject {
} }
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(anniversary) = self.get_anniversary() { Ok(
if let Some((anniversary, contains_year)) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() { let fullname = if let Some(name) = self.get_full_name() {
name name
} else { } else {
return Ok(None); return Ok(None);
}; };
let anniversary = anniversary.date(); let anniversary = anniversary.date();
let year = anniversary.year(); let year = contains_year.then_some(anniversary.year());
let anniversary_start = anniversary.format(LOCAL_DATE); let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary let anniversary_end = anniversary
.succ_opt() .succ_opt()
@@ -93,6 +94,7 @@ impl AddressObject {
.format(LOCAL_DATE); .format(LOCAL_DATE);
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();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(
uid.clone(), uid.clone(),
format!( format!(
@@ -105,12 +107,12 @@ DTSTART;VALUE=DATE:{anniversary_start}
DTEND;VALUE=DATE:{anniversary_end} DTEND;VALUE=DATE:{anniversary_end}
UID:{uid} UID:{uid}
RRULE:FREQ=YEARLY RRULE:FREQ=YEARLY
SUMMARY:💍 {fullname} ({year}) SUMMARY:💍 {fullname}{year_suffix}
TRANSP:TRANSPARENT TRANSP:TRANSPARENT
BEGIN:VALARM BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY ACTION:DISPLAY
DESCRIPTION:💍 {fullname} ({year}) DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
@@ -118,22 +120,25 @@ END:VCALENDAR"#,
)?) )?)
} else { } else {
None None
}) },
)
} }
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(birthday) = self.get_birthday() { Ok(
if let Some((birthday, contains_year)) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() { let fullname = if let Some(name) = self.get_full_name() {
name name
} else { } else {
return Ok(None); return Ok(None);
}; };
let birthday = birthday.date(); let birthday = birthday.date();
let year = birthday.year(); let year = contains_year.then_some(birthday.year());
let birthday_start = birthday.format(LOCAL_DATE); let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
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();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(
uid.clone(), uid.clone(),
format!( format!(
@@ -146,12 +151,12 @@ DTSTART;VALUE=DATE:{birthday_start}
DTEND;VALUE=DATE:{birthday_end} DTEND;VALUE=DATE:{birthday_end}
UID:{uid} UID:{uid}
RRULE:FREQ=YEARLY RRULE:FREQ=YEARLY
SUMMARY:🎂 {fullname} ({year}) SUMMARY:🎂 {fullname}{year_suffix}
TRANSP:TRANSPARENT TRANSP:TRANSPARENT
BEGIN:VALARM BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY ACTION:DISPLAY
DESCRIPTION:🎂 {fullname} ({year}) DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
@@ -159,7 +164,8 @@ END:VCALENDAR"#,
)?) )?)
} else { } else {
None None
}) },
)
} }
/// Get significant dates associated with this address object /// Get significant dates associated with this address object

View File

@@ -254,6 +254,16 @@ impl CalDateTime {
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") { if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
return Ok(CalDateTime::Date(date, timezone)); return Ok(CalDateTime::Date(date, timezone));
} }
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
}
// Also returns whether the date contains a year
pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> {
if let Ok(datetime) = Self::parse(value, None) {
return Ok((datetime, true));
}
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) { if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
// Because 1972 is a leap year // Because 1972 is a leap year
let year = 1972; let year = 1972;
@@ -261,13 +271,15 @@ impl CalDateTime {
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap(); let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap(); let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
return Ok(CalDateTime::Date( return Ok((
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()))?,
timezone, CalTimezone::Local,
),
false,
)); ));
} }
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
} }
@@ -407,24 +419,33 @@ mod tests {
#[test] #[test]
fn test_vcard_date() { fn test_vcard_date() {
assert_eq!( assert_eq!(
CalDateTime::parse("19850412", None).unwrap(), CalDateTime::parse_vcard("19850412").unwrap(),
(
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local crate::CalTimezone::Local
),
true
) )
); );
assert_eq!( assert_eq!(
CalDateTime::parse("1985-04-12", None).unwrap(), CalDateTime::parse_vcard("1985-04-12").unwrap(),
(
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local crate::CalTimezone::Local
),
true
) )
); );
assert_eq!( assert_eq!(
CalDateTime::parse("--0412", None).unwrap(), CalDateTime::parse_vcard("--0412").unwrap(),
(
CalDateTime::Date( CalDateTime::Date(
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(), NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
crate::CalTimezone::Local crate::CalTimezone::Local
),
false
) )
); );
} }

View File

@@ -11,7 +11,12 @@ pub struct CalendarQuery {
#[async_trait] #[async_trait]
pub trait CalendarStore: Send + Sync + 'static { pub trait CalendarStore: Send + Sync + 'static {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error>; async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error>;
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>; async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>; async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;

View File

@@ -1,8 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::Constructor; use derive_more::Constructor;
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use std::sync::Arc;
use crate::{ use crate::{
Calendar, CalendarStore, Error, calendar_store::CalendarQuery, Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
@@ -27,11 +26,20 @@ impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, B
#[async_trait] #[async_trait]
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> { impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
#[inline] #[inline]
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> { async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
if id.starts_with(BIRTHDAYS_PREFIX) { if id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.get_calendar(principal, id).await self.birthday_store
.get_calendar(principal, id, show_deleted)
.await
} else { } else {
self.cal_store.get_calendar(principal, id).await self.cal_store
.get_calendar(principal, id, show_deleted)
.await
} }
} }

View File

@@ -38,11 +38,17 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
/// Objects are all prefixed with BIRTHDAYS_PREFIX /// Objects are all prefixed with BIRTHDAYS_PREFIX
#[async_trait] #[async_trait]
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> { impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> { async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?; let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
let addressbook = self.0.get_addressbook(principal, id, false).await?; let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
Ok(birthday_calendar(addressbook)) Ok(birthday_calendar(addressbook))
} }
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> { async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_addressbooks(principal).await?; let addressbooks = self.0.get_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect()) Ok(addressbooks.into_iter().map(birthday_calendar).collect())

View File

@@ -86,14 +86,17 @@ impl SqliteCalendarStore {
executor: E, executor: E,
principal: &str, principal: &str,
id: &str, id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> { ) -> Result<Calendar, Error> {
let cal = sqlx::query_as!( let cal = sqlx::query_as!(
CalendarRow, CalendarRow,
r#"SELECT * r#"SELECT *
FROM calendars FROM calendars
WHERE (principal, id) = (?, ?)"#, WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?) "#,
principal, principal,
id id,
show_deleted
) )
.fetch_one(executor) .fetch_one(executor)
.await .await
@@ -470,8 +473,13 @@ impl SqliteCalendarStore {
#[async_trait] #[async_trait]
impl CalendarStore for SqliteCalendarStore { impl CalendarStore for SqliteCalendarStore {
#[instrument] #[instrument]
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> { async fn get_calendar(
Self::_get_calendar(&self.db, principal, id).await &self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
Self::_get_calendar(&self.db, principal, id, show_deleted).await
} }
#[instrument] #[instrument]
@@ -509,7 +517,7 @@ impl CalendarStore for SqliteCalendarStore {
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?; let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let cal = match Self::_get_calendar(&mut *tx, principal, id).await { let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
Ok(cal) => Some(cal), Ok(cal) => Some(cal),
Err(Error::NotFound) => None, Err(Error::NotFound) => None,
Err(err) => return Err(err), Err(err) => return Err(err),
@@ -599,7 +607,10 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token }, data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(&principal, &cal_id).await?.push_topic, topic: self
.get_calendar(&principal, &cal_id, true)
.await?
.push_topic,
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };
@@ -624,7 +635,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token }, data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic, topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };
@@ -649,7 +660,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token }, data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic, topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };

View File

@@ -79,6 +79,7 @@ pub struct Config {
pub data_store: DataStoreConfig, pub data_store: DataStoreConfig,
#[serde(default)] #[serde(default)]
pub http: HttpConfig, pub http: HttpConfig,
#[serde(default)]
pub frontend: FrontendConfig, pub frontend: FrontendConfig,
#[serde(default)] #[serde(default)]
pub oidc: Option<OidcConfig>, pub oidc: Option<OidcConfig>,