mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d639b18005 | ||
|
|
6046439fc7 | ||
|
|
f9de8a4687 | ||
|
|
8dfb47b28f | ||
|
|
eb720ded99 | ||
|
|
89ef7b2ced | ||
|
|
6e0129130e | ||
|
|
c646986c56 | ||
|
|
503cbe3699 |
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -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}}
|
||||||
|
|||||||
@@ -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
22
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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}");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
Reference in New Issue
Block a user