mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 01:12:24 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f850f9b3a3 | ||
|
|
0eb8359e26 | ||
|
|
7d961ea93b | ||
|
|
375caedec6 | ||
|
|
2d8d2eb194 | ||
|
|
69e788b363 | ||
|
|
8ea5321503 | ||
|
|
76c03fa4d4 | ||
|
|
a4285fb2ac |
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -3017,7 +3017,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -3060,7 +3060,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3100,7 +3100,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3132,7 +3132,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3157,7 +3157,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3182,7 +3182,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -3215,7 +3215,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3233,7 +3233,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3248,7 +3248,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3282,7 +3282,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3303,7 +3303,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.16",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.9.2"
|
version = "0.9.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"
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ a CalDAV/CardDAV server
|
|||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
RustiCal is under **active development**!
|
RustiCal is under **active development**!
|
||||||
While I've been successfully using RustiCal productively for a few weeks now,
|
While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
|
||||||
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 use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- easy to backup, everything saved in one SQLite database
|
- easy to backup, everything saved in one SQLite database
|
||||||
- also export feature in the frontend
|
- also export feature in the frontend
|
||||||
|
- Import your existing calendars in the frontend
|
||||||
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
|
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
|
||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
|
|||||||
@@ -43,24 +43,24 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
||||||
.gregorian()
|
.gregorian()
|
||||||
.prodid("RustiCal");
|
.prodid("RustiCal");
|
||||||
if calendar.displayname.is_some() {
|
if let Some(displayname) = calendar.meta.displayname {
|
||||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
name: "X-WR-CALNAME".to_owned(),
|
name: "X-WR-CALNAME".to_owned(),
|
||||||
value: calendar.displayname,
|
value: Some(displayname),
|
||||||
params: None,
|
params: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if calendar.description.is_some() {
|
if let Some(description) = calendar.meta.description {
|
||||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
name: "X-WR-CALDESC".to_owned(),
|
name: "X-WR-CALDESC".to_owned(),
|
||||||
value: calendar.description,
|
value: Some(description),
|
||||||
params: None,
|
params: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if calendar.timezone_id.is_some() {
|
if let Some(timezone_id) = calendar.timezone_id {
|
||||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
name: "X-WR-TIMEZONE".to_owned(),
|
name: "X-WR-TIMEZONE".to_owned(),
|
||||||
value: calendar.timezone_id,
|
value: Some(timezone_id),
|
||||||
params: None,
|
params: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ use ical::{
|
|||||||
parser::{Component, ComponentMut},
|
parser::{Component, ComponentMut},
|
||||||
};
|
};
|
||||||
use rustical_ical::{CalendarObject, CalendarObjectType};
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::Principal};
|
use rustical_store::{
|
||||||
|
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
|
||||||
|
};
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@@ -83,10 +85,12 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
let new_cal = Calendar {
|
let new_cal = Calendar {
|
||||||
principal,
|
principal,
|
||||||
id: cal_id,
|
id: cal_id,
|
||||||
displayname,
|
meta: CalendarMetadata {
|
||||||
order: 0,
|
displayname,
|
||||||
description,
|
order: 0,
|
||||||
color: None,
|
description,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
timezone_id,
|
timezone_id,
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
synctoken: 0,
|
synctoken: 0,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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;
|
||||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
|
||||||
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@@ -112,11 +112,13 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
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),
|
meta: CalendarMetadata {
|
||||||
displayname: request.displayname,
|
order: request.calendar_order.unwrap_or(0),
|
||||||
|
displayname: request.displayname,
|
||||||
|
color: request.calendar_color,
|
||||||
|
description: request.calendar_description,
|
||||||
|
},
|
||||||
timezone_id,
|
timezone_id,
|
||||||
color: request.calendar_color,
|
|
||||||
description: request.calendar_description,
|
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
synctoken: 0,
|
synctoken: 0,
|
||||||
subscription_url: request.source.map(|href| href.href),
|
subscription_url: request.source.map(|href| href.href),
|
||||||
|
|||||||
@@ -116,19 +116,17 @@ impl CompFilterElement {
|
|||||||
// TODO: Implement prop-filter (and comp-filter?) at some point
|
// TODO: Implement prop-filter (and comp-filter?) at some point
|
||||||
|
|
||||||
if let Some(time_range) = &self.time_range {
|
if let Some(time_range) = &self.time_range {
|
||||||
if let Some(start) = &time_range.start {
|
if let Some(start) = &time_range.start
|
||||||
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
|
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
|
||||||
if start.deref() > &last_occurence.utc() {
|
&& start.deref() > &last_occurence.utc()
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if let Some(end) = &time_range.end {
|
if let Some(end) = &time_range.end
|
||||||
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) {
|
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
|
||||||
if end.deref() < &first_occurence.utc() {
|
&& end.deref() < &first_occurence.utc()
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -156,15 +154,15 @@ impl From<&FilterElement> for CalendarQuery {
|
|||||||
for comp_filter in comp_filter_vcalendar.comp_filter.iter() {
|
for comp_filter in comp_filter_vcalendar.comp_filter.iter() {
|
||||||
// A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
|
// A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
|
||||||
// whatever we get first
|
// whatever we get first
|
||||||
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") {
|
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO")
|
||||||
if let Some(time_range) = &comp_filter.time_range {
|
&& let Some(time_range) = &comp_filter.time_range
|
||||||
let start = time_range.start.as_ref().map(|start| start.date_naive());
|
{
|
||||||
let end = time_range.end.as_ref().map(|end| end.date_naive());
|
let start = time_range.start.as_ref().map(|start| start.date_naive());
|
||||||
return CalendarQuery {
|
let end = time_range.end.as_ref().map(|end| end.date_naive());
|
||||||
time_start: start,
|
return CalendarQuery {
|
||||||
time_end: end,
|
time_start: start,
|
||||||
};
|
time_end: end,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Default::default()
|
Default::default()
|
||||||
|
|||||||
@@ -128,10 +128,10 @@ impl Resource for CalendarResource {
|
|||||||
Ok(match prop {
|
Ok(match prop {
|
||||||
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
|
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
|
||||||
CalendarPropName::CalendarColor => {
|
CalendarPropName::CalendarColor => {
|
||||||
CalendarProp::CalendarColor(self.cal.color.clone())
|
CalendarProp::CalendarColor(self.cal.meta.color.clone())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarDescription => {
|
CalendarPropName::CalendarDescription => {
|
||||||
CalendarProp::CalendarDescription(self.cal.description.clone())
|
CalendarProp::CalendarDescription(self.cal.meta.description.clone())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone => {
|
CalendarPropName::CalendarTimezone => {
|
||||||
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
|
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
|
||||||
@@ -146,7 +146,7 @@ impl Resource for CalendarResource {
|
|||||||
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
|
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarOrder => {
|
CalendarPropName::CalendarOrder => {
|
||||||
CalendarProp::CalendarOrder(Some(self.cal.order))
|
CalendarProp::CalendarOrder(Some(self.cal.meta.order))
|
||||||
}
|
}
|
||||||
CalendarPropName::SupportedCalendarComponentSet => {
|
CalendarPropName::SupportedCalendarComponentSet => {
|
||||||
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
|
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
|
||||||
@@ -187,11 +187,11 @@ impl Resource for CalendarResource {
|
|||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapper::Calendar(prop) => match prop {
|
CalendarPropWrapper::Calendar(prop) => match prop {
|
||||||
CalendarProp::CalendarColor(color) => {
|
CalendarProp::CalendarColor(color) => {
|
||||||
self.cal.color = color;
|
self.cal.meta.color = color;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::CalendarDescription(description) => {
|
CalendarProp::CalendarDescription(description) => {
|
||||||
self.cal.description = description;
|
self.cal.meta.description = description;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::CalendarTimezone(timezone) => {
|
CalendarProp::CalendarTimezone(timezone) => {
|
||||||
@@ -236,7 +236,7 @@ impl Resource for CalendarResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::CalendarOrder(order) => {
|
CalendarProp::CalendarOrder(order) => {
|
||||||
self.cal.order = order.unwrap_or_default();
|
self.cal.meta.order = order.unwrap_or_default();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::SupportedCalendarComponentSet(comp_set) => {
|
CalendarProp::SupportedCalendarComponentSet(comp_set) => {
|
||||||
@@ -264,11 +264,11 @@ impl Resource for CalendarResource {
|
|||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapperName::Calendar(prop) => match prop {
|
CalendarPropWrapperName::Calendar(prop) => match prop {
|
||||||
CalendarPropName::CalendarColor => {
|
CalendarPropName::CalendarColor => {
|
||||||
self.cal.color = None;
|
self.cal.meta.color = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarDescription => {
|
CalendarPropName::CalendarDescription => {
|
||||||
self.cal.description = None;
|
self.cal.meta.description = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
|
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
|
||||||
@@ -277,7 +277,7 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarPropName::CalendarOrder => {
|
CalendarPropName::CalendarOrder => {
|
||||||
self.cal.order = 0;
|
self.cal.meta.order = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarPropName::SupportedCalendarComponentSet => {
|
CalendarPropName::SupportedCalendarComponentSet => {
|
||||||
@@ -300,10 +300,10 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_displayname(&self) -> Option<&str> {
|
fn get_displayname(&self) -> Option<&str> {
|
||||||
self.cal.displayname.as_deref()
|
self.cal.meta.displayname.as_deref()
|
||||||
}
|
}
|
||||||
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
|
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
|
||||||
self.cal.displayname = name;
|
self.cal.meta.displayname = name;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,5 +79,5 @@ async fn test_propfind() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let output = response.serialize_to_string().unwrap();
|
let _output = response.serialize_to_string().unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ impl XmlSerialize for TagList {
|
|||||||
|
|
||||||
if let Some(qname) = &qname {
|
if let Some(qname) = &qname {
|
||||||
let mut bytes_start = BytesStart::from(qname.to_owned());
|
let mut bytes_start = BytesStart::from(qname.to_owned());
|
||||||
if !has_prefix {
|
if !has_prefix && let Some(ns) = &ns {
|
||||||
if let Some(ns) = &ns {
|
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
|
||||||
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
writer.write_event(Event::Start(bytes_start))?;
|
writer.write_event(Event::Start(bytes_start))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class DeleteButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
let text = this.trash ? 'Move to trash' : 'Delete'
|
let text = this.trash ? 'Trash' : 'Delete'
|
||||||
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
|
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class EditAddressbookForm extends LitElement {
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
|
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
|
||||||
<dialog ${ref(this.dialog)}>
|
<dialog ${ref(this.dialog)}>
|
||||||
<h3>Edit addressbook</h3>
|
<h3>Edit addressbook</h3>
|
||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class EditCalendarForm extends LitElement {
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
|
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
|
||||||
<dialog ${ref(this.dialog)}>
|
<dialog ${ref(this.dialog)}>
|
||||||
<h3>Edit calendar</h3>
|
<h3>Edit calendar</h3>
|
||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ let DeleteButton = class extends i {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
let text = this.trash ? "Move to trash" : "Delete";
|
let text = this.trash ? "Trash" : "Delete";
|
||||||
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
|
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
|
||||||
}
|
}
|
||||||
async _onClick(event) {
|
async _onClick(event) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ let EditAddressbookForm = class extends i {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return x`
|
return x`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
|
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
|
||||||
<dialog ${n(this.dialog)}>
|
<dialog ${n(this.dialog)}>
|
||||||
<h3>Edit addressbook</h3>
|
<h3>Edit addressbook</h3>
|
||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ let EditCalendarForm = class extends i {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return x`
|
return x`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
|
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
|
||||||
<dialog ${n(this.dialog)}>
|
<dialog ${n(this.dialog)}>
|
||||||
<h3>Edit calendar</h3>
|
<h3>Edit calendar</h3>
|
||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<h2>{{ user.id }}'s Calendars</h2>
|
<h2>{{ user.id }}'s Calendars</h2>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, calendar) in calendars %}
|
{% for (meta, calendar) in calendars %}
|
||||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
{% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||||
<li class="collection-list-item" style="--color: {{ color }}">
|
<li class="collection-list-item" style="--color: {{ color }}">
|
||||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
|
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||||
<div class="comps">
|
<div class="comps">
|
||||||
{% for comp in calendar.components %}
|
{% for comp in calendar.components %}
|
||||||
<span>{{ comp }}</span>
|
<span>{{ comp }}</span>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="description">
|
<span class="description">
|
||||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||||
<span class="subscription-url">{{ subscription_url }}</span>
|
<span class="subscription-url">{{ subscription_url }}</span>
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
principal="{{ calendar.principal }}"
|
principal="{{ calendar.principal }}"
|
||||||
cal_id="{{ calendar.id }}"
|
cal_id="{{ calendar.id }}"
|
||||||
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
|
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
|
||||||
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}"
|
displayname="{{ calendar.meta.displayname.as_deref().unwrap_or_default() }}"
|
||||||
description="{{ calendar.description.as_deref().unwrap_or_default() }}"
|
description="{{ calendar.meta.description.as_deref().unwrap_or_default() }}"
|
||||||
color="{{ calendar.color.as_deref().unwrap_or_default() }}"
|
color="{{ calendar.meta.color.as_deref().unwrap_or_default() }}"
|
||||||
components="{{ calendar.components | json }}"
|
components="{{ calendar.components | json }}"
|
||||||
></edit-calendar-form>
|
></edit-calendar-form>
|
||||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||||
@@ -51,13 +51,13 @@
|
|||||||
<h3>Deleted Calendars</h3>
|
<h3>Deleted Calendars</h3>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, calendar) in deleted_calendars %}
|
{% for (meta, calendar) in deleted_calendars %}
|
||||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
{% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||||
<li class="collection-list-item" style="--color: {{ color }}">
|
<li class="collection-list-item" style="--color: {{ color }}">
|
||||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
|
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||||
<div class="comps">
|
<div class="comps">
|
||||||
{% for comp in calendar.components %}
|
{% for comp in calendar.components %}
|
||||||
<span>{{ comp }}</span>
|
<span>{{ comp }}</span>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="description">
|
<span class="description">
|
||||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
|
{% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
|
||||||
<h1>{{ calendar.principal }}/{{ name }}</h1>
|
<h1>{{ calendar.principal }}/{{ name }}</h1>
|
||||||
{% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%}
|
{% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
|
||||||
|
|
||||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||||
<h2>Subscription URL</h2>
|
<h2>Subscription URL</h2>
|
||||||
@@ -25,9 +25,6 @@
|
|||||||
{% 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.get_vtimezone() %}
|
|
||||||
<textarea rows="16" readonly>{{ timezone }}</textarea>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<pre>{{ calendar|json }}</pre>
|
<pre>{{ calendar|json }}</pre>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tower::Service;
|
|||||||
|
|
||||||
#[derive(Clone, RustEmbed, Default)]
|
#[derive(Clone, RustEmbed, Default)]
|
||||||
#[folder = "public/assets"]
|
#[folder = "public/assets"]
|
||||||
|
#[allow(dead_code)] // Since this is not used with the frontend-dev feature
|
||||||
pub struct Assets;
|
pub struct Assets;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|||||||
@@ -192,20 +192,19 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| OidcError::UserInfo(e.to_string()))?;
|
.map_err(|e| OidcError::UserInfo(e.to_string()))?;
|
||||||
|
|
||||||
if let Some(require_group) = &oidc_config.require_group {
|
if let Some(require_group) = &oidc_config.require_group
|
||||||
if !user_info_claims
|
&& !user_info_claims
|
||||||
.additional_claims()
|
.additional_claims()
|
||||||
.groups
|
.groups
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.contains(require_group)
|
.contains(require_group)
|
||||||
{
|
{
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"User is not in an authorized group to use RustiCal",
|
"User is not in an authorized group to use RustiCal",
|
||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = match oidc_config.claim_userid {
|
let user_id = match oidc_config.claim_userid {
|
||||||
|
|||||||
@@ -72,12 +72,11 @@ where
|
|||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if let Some(session) = request.extensions().get::<Session>() {
|
if let Some(session) = request.extensions().get::<Session>()
|
||||||
if let Ok(Some(user_id)) = session.get::<String>("user").await {
|
&& let Ok(Some(user_id)) = session.get::<String>("user").await
|
||||||
if let Ok(Some(user)) = ap.get_principal(&user_id).await {
|
&& let Ok(Some(user)) = ap.get_principal(&user_id).await
|
||||||
request.extensions_mut().insert(user);
|
{
|
||||||
}
|
request.extensions_mut().insert(user);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(auth) = auth_header {
|
if let Some(auth) = auth_header {
|
||||||
|
|||||||
@@ -6,13 +6,23 @@ use rustical_ical::CalendarObjectType;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct Calendar {
|
pub struct CalendarMetadata {
|
||||||
pub principal: String,
|
// Attributes that may be outsourced
|
||||||
pub id: String,
|
|
||||||
pub displayname: Option<String>,
|
pub displayname: Option<String>,
|
||||||
pub order: i64,
|
pub order: i64,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Calendar {
|
||||||
|
// Attributes that may be outsourced
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub meta: CalendarMetadata,
|
||||||
|
|
||||||
|
// Common calendar attributes
|
||||||
|
pub principal: String,
|
||||||
|
pub id: 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,
|
||||||
|
|||||||
@@ -1,282 +1,208 @@
|
|||||||
|
use crate::CalendarStore;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::Constructor;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use rustical_ical::CalendarObject;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::{
|
pub trait PrefixedCalendarStore: CalendarStore {
|
||||||
Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
|
const PREFIX: &'static str;
|
||||||
contact_birthday_store::BIRTHDAYS_PREFIX,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Constructor)]
|
|
||||||
pub struct CombinedCalendarStore<CS: CalendarStore, BS: CalendarStore> {
|
|
||||||
cal_store: Arc<CS>,
|
|
||||||
birthday_store: Arc<BS>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, BS> {
|
#[derive(Clone)]
|
||||||
fn clone(&self) -> Self {
|
pub struct CombinedCalendarStore {
|
||||||
|
stores: HashMap<&'static str, Arc<dyn CalendarStore>>,
|
||||||
|
default: Arc<dyn CalendarStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CombinedCalendarStore {
|
||||||
|
pub fn new(default: Arc<dyn CalendarStore>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cal_store: self.cal_store.clone(),
|
stores: HashMap::new(),
|
||||||
birthday_store: self.birthday_store.clone(),
|
default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_store<CS: PrefixedCalendarStore>(mut self, store: Arc<CS>) -> Self {
|
||||||
|
let store: Arc<dyn CalendarStore> = store;
|
||||||
|
self.stores.insert(CS::PREFIX, store);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_for_id(&self, id: &str) -> Arc<dyn CalendarStore> {
|
||||||
|
self.stores
|
||||||
|
.iter()
|
||||||
|
.find(|&(prefix, _store)| id.starts_with(prefix))
|
||||||
|
.map(|(_prefix, store)| store.clone())
|
||||||
|
.unwrap_or(self.default.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
|
impl CalendarStore for CombinedCalendarStore {
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn get_calendar(
|
async fn get_calendar(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
id: &str,
|
id: &str,
|
||||||
show_deleted: bool,
|
show_deleted: bool,
|
||||||
) -> Result<Calendar, Error> {
|
) -> Result<crate::Calendar, crate::Error> {
|
||||||
if id.starts_with(BIRTHDAYS_PREFIX) {
|
self.store_for_id(id)
|
||||||
self.birthday_store
|
.get_calendar(principal, id, show_deleted)
|
||||||
.get_calendar(principal, id, show_deleted)
|
.await
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.get_calendar(principal, id, show_deleted)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn update_calendar(
|
async fn update_calendar(
|
||||||
&self,
|
&self,
|
||||||
principal: String,
|
principal: String,
|
||||||
id: String,
|
id: String,
|
||||||
calendar: Calendar,
|
calendar: crate::Calendar,
|
||||||
) -> Result<(), crate::Error> {
|
) -> Result<(), crate::Error> {
|
||||||
if id.starts_with(BIRTHDAYS_PREFIX) {
|
self.store_for_id(&id)
|
||||||
self.birthday_store
|
.update_calendar(principal, id, calendar)
|
||||||
.update_calendar(principal, id, calendar)
|
.await
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.update_calendar(principal, id, calendar)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
async fn insert_calendar(&self, calendar: crate::Calendar) -> Result<(), crate::Error> {
|
||||||
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
|
self.store_for_id(&calendar.id)
|
||||||
if calendar.id.starts_with(BIRTHDAYS_PREFIX) {
|
.insert_calendar(calendar)
|
||||||
Err(Error::ReadOnly)
|
.await
|
||||||
} else {
|
|
||||||
self.cal_store.insert_calendar(calendar).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
async fn delete_calendar(
|
||||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
&self,
|
||||||
Ok([
|
principal: &str,
|
||||||
self.cal_store.get_calendars(principal).await?,
|
name: &str,
|
||||||
self.birthday_store.get_calendars(principal).await?,
|
use_trashbin: bool,
|
||||||
]
|
) -> Result<(), crate::Error> {
|
||||||
.concat())
|
self.store_for_id(name)
|
||||||
|
.delete_calendar(principal, name, use_trashbin)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), crate::Error> {
|
||||||
|
self.store_for_id(name)
|
||||||
|
.restore_calendar(principal, name)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_changes(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
synctoken: i64,
|
||||||
|
) -> Result<(Vec<rustical_ical::CalendarObject>, Vec<String>, i64), crate::Error> {
|
||||||
|
self.store_for_id(cal_id)
|
||||||
|
.sync_changes(principal, cal_id, synctoken)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_calendar(
|
||||||
|
&self,
|
||||||
|
calendar: crate::Calendar,
|
||||||
|
objects: Vec<rustical_ical::CalendarObject>,
|
||||||
|
merge_existing: bool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
self.store_for_id(&calendar.id)
|
||||||
|
.import_calendar(calendar, objects, merge_existing)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn calendar_query(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
query: crate::calendar_store::CalendarQuery,
|
||||||
|
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
|
||||||
|
self.store_for_id(cal_id)
|
||||||
|
.calendar_query(principal, cal_id, query)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_object(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
object_id: &str,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
self.store_for_id(cal_id)
|
||||||
|
.restore_object(principal, cal_id, object_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn calendar_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<crate::CollectionMetadata, crate::Error> {
|
||||||
|
self.store_for_id(cal_id)
|
||||||
|
.calendar_metadata(principal, cal_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_objects(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
|
||||||
|
self.store_for_id(cal_id)
|
||||||
|
.get_objects(principal, cal_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_object(
|
||||||
|
&self,
|
||||||
|
principal: String,
|
||||||
|
cal_id: String,
|
||||||
|
object: rustical_ical::CalendarObject,
|
||||||
|
overwrite: bool,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
self.store_for_id(&cal_id)
|
||||||
|
.put_object(principal, cal_id, object, overwrite)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn delete_object(
|
async fn delete_object(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
use_trashbin: bool,
|
use_trashbin: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), crate::Error> {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
self.store_for_id(cal_id)
|
||||||
self.birthday_store
|
.delete_object(principal, cal_id, object_id, use_trashbin)
|
||||||
.delete_object(principal, cal_id, object_id, use_trashbin)
|
.await
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.delete_object(principal, cal_id, object_id, use_trashbin)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn get_object(
|
async fn get_object(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
show_deleted: bool,
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error> {
|
) -> Result<rustical_ical::CalendarObject, crate::Error> {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
self.store_for_id(cal_id)
|
||||||
self.birthday_store
|
.get_object(principal, cal_id, object_id, show_deleted)
|
||||||
.get_object(principal, cal_id, object_id, show_deleted)
|
.await
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.get_object(principal, cal_id, object_id, show_deleted)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
async fn get_calendars(&self, principal: &str) -> Result<Vec<crate::Calendar>, crate::Error> {
|
||||||
async fn sync_changes(
|
let mut calendars = self.default.get_calendars(principal).await?;
|
||||||
|
for store in self.stores.values() {
|
||||||
|
calendars.extend(store.get_calendars(principal).await?);
|
||||||
|
}
|
||||||
|
Ok(calendars)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_deleted_calendars(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
) -> Result<Vec<crate::Calendar>, crate::Error> {
|
||||||
synctoken: i64,
|
let mut calendars = self.default.get_deleted_calendars(principal).await?;
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
for store in self.stores.values() {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
calendars.extend(store.get_deleted_calendars(principal).await?);
|
||||||
self.birthday_store
|
|
||||||
.sync_changes(principal, cal_id, synctoken)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.sync_changes(principal, cal_id, synctoken)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
Ok(calendars)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn calendar_metadata(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
) -> Result<crate::CollectionMetadata, Error> {
|
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.calendar_metadata(principal, cal_id)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store.calendar_metadata(principal, cal_id).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
async fn get_objects(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store.get_objects(principal, cal_id).await
|
|
||||||
} else {
|
|
||||||
self.cal_store.get_objects(principal, cal_id).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn calendar_query(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
query: CalendarQuery,
|
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.calendar_query(principal, cal_id, query)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.calendar_query(principal, cal_id, query)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), Error> {
|
|
||||||
if name.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store.restore_calendar(principal, name).await
|
|
||||||
} else {
|
|
||||||
self.cal_store.restore_calendar(principal, name).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn import_calendar(
|
|
||||||
&self,
|
|
||||||
calendar: Calendar,
|
|
||||||
objects: Vec<CalendarObject>,
|
|
||||||
merge_existing: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if calendar.id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.import_calendar(calendar, objects, merge_existing)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.import_calendar(calendar, objects, merge_existing)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn delete_calendar(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
name: &str,
|
|
||||||
use_trashbin: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if name.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.delete_calendar(principal, name, use_trashbin)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.delete_calendar(principal, name, use_trashbin)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
|
||||||
Ok([
|
|
||||||
self.birthday_store.get_deleted_calendars(principal).await?,
|
|
||||||
self.cal_store.get_deleted_calendars(principal).await?,
|
|
||||||
]
|
|
||||||
.concat())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn restore_object(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
object_id: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.restore_object(principal, cal_id, object_id)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.restore_object(principal, cal_id, object_id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn put_object(
|
|
||||||
&self,
|
|
||||||
principal: String,
|
|
||||||
cal_id: String,
|
|
||||||
object: CalendarObject,
|
|
||||||
overwrite: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
|
||||||
self.birthday_store
|
|
||||||
.put_object(principal, cal_id, object, overwrite)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.cal_store
|
|
||||||
.put_object(principal, cal_id, object, overwrite)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn is_read_only(&self, cal_id: &str) -> bool {
|
fn is_read_only(&self, cal_id: &str) -> bool {
|
||||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
self.store_for_id(cal_id).is_read_only(cal_id)
|
||||||
self.birthday_store.is_read_only(cal_id)
|
|
||||||
} else {
|
|
||||||
self.cal_store.is_read_only(cal_id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use crate::{Addressbook, AddressbookStore, Calendar, CalendarStore, Error};
|
use crate::{
|
||||||
|
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
|
||||||
|
combined_calendar_store::PrefixedCalendarStore,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
||||||
@@ -10,16 +13,22 @@ pub(crate) const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
|||||||
#[derive(Constructor, Clone)]
|
#[derive(Constructor, Clone)]
|
||||||
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
|
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
|
||||||
|
|
||||||
|
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
|
||||||
|
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||||
Calendar {
|
Calendar {
|
||||||
principal: addressbook.principal,
|
principal: addressbook.principal,
|
||||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
||||||
displayname: addressbook
|
meta: CalendarMetadata {
|
||||||
.displayname
|
displayname: addressbook
|
||||||
.map(|name| format!("{name} birthdays")),
|
.displayname
|
||||||
order: 0,
|
.map(|name| format!("{name} birthdays")),
|
||||||
description: None,
|
order: 0,
|
||||||
color: None,
|
description: None,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
timezone_id: None,
|
timezone_id: None,
|
||||||
deleted_at: addressbook.deleted_at,
|
deleted_at: addressbook.deleted_at,
|
||||||
synctoken: addressbook.synctoken,
|
synctoken: addressbook.synctoken,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub use secret::Secret;
|
|||||||
pub use subscription_store::*;
|
pub use subscription_store::*;
|
||||||
|
|
||||||
pub use addressbook::Addressbook;
|
pub use addressbook::Addressbook;
|
||||||
pub use calendar::Calendar;
|
pub use calendar::{Calendar, CalendarMetadata};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CollectionOperationInfo {
|
pub enum CollectionOperationInfo {
|
||||||
|
|||||||
@@ -433,14 +433,14 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
|
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Some(addressbook) = addressbook {
|
if let Some(addressbook) = addressbook
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
data: CollectionOperationInfo::Delete,
|
data: CollectionOperationInfo::Delete,
|
||||||
topic: addressbook.push_topic,
|
topic: addressbook.push_topic,
|
||||||
}) {
|
})
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
{
|
||||||
};
|
error!("Push notification about deleted addressbook failed: {err}");
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
|
|||||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_store::synctoken::format_synctoken;
|
use rustical_store::synctoken::format_synctoken;
|
||||||
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
|
||||||
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
@@ -69,10 +69,12 @@ impl From<CalendarRow> for Calendar {
|
|||||||
Self {
|
Self {
|
||||||
principal: value.principal,
|
principal: value.principal,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
displayname: value.displayname,
|
meta: CalendarMetadata {
|
||||||
order: value.order,
|
displayname: value.displayname,
|
||||||
description: value.description,
|
order: value.order,
|
||||||
color: value.color,
|
description: value.description,
|
||||||
|
color: value.color,
|
||||||
|
},
|
||||||
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,
|
||||||
@@ -159,10 +161,10 @@ impl SqliteCalendarStore {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
calendar.principal,
|
calendar.principal,
|
||||||
calendar.id,
|
calendar.id,
|
||||||
calendar.displayname,
|
calendar.meta.displayname,
|
||||||
calendar.description,
|
calendar.meta.description,
|
||||||
calendar.order,
|
calendar.meta.order,
|
||||||
calendar.color,
|
calendar.meta.color,
|
||||||
calendar.subscription_url,
|
calendar.subscription_url,
|
||||||
calendar.timezone_id,
|
calendar.timezone_id,
|
||||||
calendar.push_topic,
|
calendar.push_topic,
|
||||||
@@ -189,10 +191,10 @@ impl SqliteCalendarStore {
|
|||||||
WHERE (principal, id) = (?, ?)"#,
|
WHERE (principal, id) = (?, ?)"#,
|
||||||
calendar.principal,
|
calendar.principal,
|
||||||
calendar.id,
|
calendar.id,
|
||||||
calendar.displayname,
|
calendar.meta.displayname,
|
||||||
calendar.description,
|
calendar.meta.description,
|
||||||
calendar.order,
|
calendar.meta.order,
|
||||||
calendar.color,
|
calendar.meta.color,
|
||||||
calendar.timezone_id,
|
calendar.timezone_id,
|
||||||
calendar.push_topic,
|
calendar.push_topic,
|
||||||
comp_event, comp_todo, comp_journal,
|
comp_event, comp_todo, comp_journal,
|
||||||
@@ -351,7 +353,6 @@ impl SqliteCalendarStore {
|
|||||||
object: CalendarObject,
|
object: CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// TODO: Prevent objects from being commited to a subscription calendar
|
|
||||||
let (object_id, ics) = (object.get_id(), object.get_ics());
|
let (object_id, ics) = (object.get_id(), object.get_ics());
|
||||||
|
|
||||||
let first_occurence = object
|
let first_occurence = object
|
||||||
@@ -554,14 +555,14 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
|
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Some(cal) = cal {
|
if let Some(cal) = cal
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
data: CollectionOperationInfo::Delete,
|
data: CollectionOperationInfo::Delete,
|
||||||
topic: cal.push_topic,
|
topic: cal.push_topic,
|
||||||
}) {
|
})
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
{
|
||||||
};
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
}
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,11 +668,16 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
object: CalendarObject,
|
object: CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// TODO: Prevent objects from being commited to a subscription calendar
|
|
||||||
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 object_id = object.get_id().to_owned();
|
let object_id = object.get_id().to_owned();
|
||||||
|
|
||||||
|
let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?;
|
||||||
|
if calendar.subscription_url.is_some() {
|
||||||
|
// We cannot commit an object to a subscription calendar
|
||||||
|
return Err(Error::ReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(
|
||||||
&mut *tx,
|
&mut *tx,
|
||||||
principal.to_owned(),
|
principal.to_owned(),
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
a CalDAV/CardDAV server
|
a CalDAV/CardDAV server
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
RustiCal is under **active development**!
|
RustiCal is under **active development**!
|
||||||
While I've been successfully using RustiCal productively for a few weeks now,
|
While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
|
||||||
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 use 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 }
|
[Installation](installation/index.md){ .md-button }
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ a CalDAV/CardDAV server
|
|||||||
|
|
||||||
- easy to backup, everything saved in one SQLite database
|
- easy to backup, everything saved in one SQLite database
|
||||||
- also export feature in the frontend
|
- also export feature in the frontend
|
||||||
|
- Import your existing calendars in the frontend
|
||||||
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
|
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
|
||||||
- lightweight (the container image contains only one binary)
|
- lightweight (the container image contains only one binary)
|
||||||
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
|
|||||||
@@ -40,10 +40,9 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
dav_push_enabled: bool,
|
dav_push_enabled: bool,
|
||||||
session_cookie_samesite_strict: bool,
|
session_cookie_samesite_strict: bool,
|
||||||
) -> Router<()> {
|
) -> Router<()> {
|
||||||
let combined_cal_store = Arc::new(CombinedCalendarStore::new(
|
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
|
||||||
cal_store.clone(),
|
let combined_cal_store =
|
||||||
ContactBirthdayStore::new(addr_store.clone()).into(),
|
Arc::new(CombinedCalendarStore::new(cal_store.clone()).with_store(birthday_store));
|
||||||
));
|
|
||||||
|
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.merge(caldav_router(
|
.merge(caldav_router(
|
||||||
|
|||||||
Reference in New Issue
Block a user