mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 13:32:16 +00:00
Compare commits
32 Commits
v0.10.3
...
96a16951f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a16951f4 | ||
|
|
a32b766c0c | ||
|
|
7a101b7364 | ||
|
|
57275a10b4 | ||
|
|
af239e34bf | ||
|
|
e99b1d9123 | ||
|
|
e39657eb29 | ||
|
|
607db62859 | ||
|
|
eba377b980 | ||
|
|
d5c1ddc590 | ||
|
|
a79e1901b8 | ||
|
|
f29c8fa925 | ||
|
|
54f1ee0788 | ||
|
|
96f221f721 | ||
|
|
ba3b64a9c4 | ||
|
|
22a0337375 | ||
|
|
21902e108a | ||
|
|
08f526fa5b | ||
|
|
ac73f3aaff | ||
|
|
9fdc8434db | ||
|
|
85f3d89235 | ||
|
|
092604694a | ||
|
|
873b40ad10 | ||
|
|
5588137f73 | ||
|
|
7bf00da0e5 | ||
|
|
be08275cd3 | ||
|
|
381af1b877 | ||
|
|
425d10cb99 | ||
|
|
5cdbb3b9d3 | ||
|
|
547e477eca | ||
|
|
c19c3492c3 | ||
|
|
5878b93d62 |
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM calendarobjects\n WHERE (principal, cal_id, id) NOT IN (\n SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog\n )\n ;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "principal",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cal_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "deleted: bool",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a"
|
||||
}
|
||||
12
.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
generated
Normal file
12
.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
|
||||
}
|
||||
74
.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
generated
Normal file
74
.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "principal",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "displayname",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "timezone_id",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "deleted_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "addr_synctoken",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "push_topic",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
|
||||
}
|
||||
74
.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
generated
Normal file
74
.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "principal",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "displayname",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "timezone_id",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "deleted_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "addr_synctoken",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "push_topic",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
|
||||
}
|
||||
12
.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
generated
Normal file
12
.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
|
||||
}
|
||||
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
|
||||
}
|
||||
12
.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
generated
Normal file
12
.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
|
||||
}
|
||||
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM addressobjects\n WHERE (principal, addressbook_id, id) NOT IN (\n SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog\n )\n ;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "principal",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "addressbook_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "deleted: bool",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d"
|
||||
}
|
||||
12
.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
generated
Normal file
12
.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
|
||||
}
|
||||
423
Cargo.lock
generated
423
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
rust-version = "1.91"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
@@ -133,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
heck = "0.5"
|
||||
darling = "0.21"
|
||||
darling = "0.23"
|
||||
reqwest = { version = "0.12", features = [
|
||||
"rustls-tls",
|
||||
"charset",
|
||||
@@ -148,6 +148,7 @@ ece = { version = "2.3", default-features = false, features = [
|
||||
] }
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
async-std = { version = "1.13", features = ["attributes"] }
|
||||
similar-asserts = "1.7"
|
||||
|
||||
[dependencies]
|
||||
rustical_store.workspace = true
|
||||
|
||||
@@ -45,3 +45,4 @@ tower-http.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
vtimezones-rs.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
|
||||
@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
||||
let mut not_found = vec![];
|
||||
|
||||
for href in &cal_query.href {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||
&& let Some(filename) = href.strip_prefix(path)
|
||||
{
|
||||
let filename = filename.trim_start_matches('/');
|
||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||
match store.get_object(principal, cal_id, object_id, false).await {
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
not_found.push(href.to_string());
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
|
||||
@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
|
||||
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
|
||||
if self.read_only {
|
||||
return Err(rustical_dav::Error::PropReadOnly);
|
||||
}
|
||||
match prop {
|
||||
CalendarPropWrapper::Calendar(prop) => match prop {
|
||||
CalendarProp::CalendarColor(color) => {
|
||||
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
|
||||
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
|
||||
if self.read_only {
|
||||
return Err(rustical_dav::Error::PropReadOnly);
|
||||
}
|
||||
match prop {
|
||||
CalendarPropWrapperName::Calendar(prop) => match prop {
|
||||
CalendarPropName::CalendarColor => {
|
||||
@@ -317,16 +311,11 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
|
||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||
if self.cal.subscription_url.is_some() {
|
||||
if self.cal.subscription_url.is_some() || self.read_only {
|
||||
return Ok(UserPrivilegeSet::owner_write_properties(
|
||||
user.is_principal(&self.cal.principal),
|
||||
));
|
||||
}
|
||||
if self.read_only {
|
||||
return Ok(UserPrivilegeSet::owner_read(
|
||||
user.is_principal(&self.cal.principal),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(UserPrivilegeSet::owner_only(
|
||||
user.is_principal(&self.cal.principal),
|
||||
|
||||
@@ -211,6 +211,9 @@ END:VCALENDAR
|
||||
<privilege>
|
||||
<read/>
|
||||
</privilege>
|
||||
<privilege>
|
||||
<write-properties/>
|
||||
</privilege>
|
||||
<privilege>
|
||||
<read-acl/>
|
||||
</privilege>
|
||||
|
||||
@@ -39,9 +39,7 @@ async fn test_propfind() {
|
||||
.unwrap()
|
||||
.trim()
|
||||
.replace("\r\n", "\n");
|
||||
println!("{output}");
|
||||
println!("{}, {} \n\n\n", output.len(), expected_output.len());
|
||||
assert_eq!(output, expected_output);
|
||||
similar_asserts::assert_eq!(expected_output, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
||||
let mut not_found = vec![];
|
||||
|
||||
for href in &addressbook_multiget.href {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||
&& let Some(filename) = href.strip_prefix(path)
|
||||
{
|
||||
let filename = filename.trim_start_matches('/');
|
||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||
match store
|
||||
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
||||
.await
|
||||
{
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
not_found.push(href.to_string());
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
|
||||
@@ -88,7 +88,7 @@ pub async fn route_proppatch<R: ResourceService>(
|
||||
.get_resource(path_components, false)
|
||||
.await?;
|
||||
let privileges = resource.get_user_privileges(principal)?;
|
||||
if !privileges.has(&UserPrivilege::Write) {
|
||||
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { escapeXml } from ".";
|
||||
import { getTimezones } from "./timezones.ts";
|
||||
|
||||
@customElement("create-birthday-calendar-form")
|
||||
export class CreateCalendarForm extends LitElement {
|
||||
protected override createRenderRoot() {
|
||||
return this
|
||||
}
|
||||
|
||||
@property()
|
||||
principal: string = ''
|
||||
@property()
|
||||
addr_id: string = ''
|
||||
@property()
|
||||
displayname: string = ''
|
||||
@property()
|
||||
description: string = ''
|
||||
@property()
|
||||
color: string = ''
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
@property()
|
||||
timezones: Array<String> = []
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||
<dialog ${ref(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit} ${ref(this.form)}>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" @change=${e => this.color = e.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||
</form>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
|
||||
async submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
if (!this.addr_id) {
|
||||
alert("Empty id")
|
||||
return
|
||||
}
|
||||
if (!this.displayname) {
|
||||
alert("Empty displayname")
|
||||
return
|
||||
}
|
||||
|
||||
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||
method: 'MKCOL',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml'
|
||||
},
|
||||
body: `
|
||||
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||
<set>
|
||||
<prop>
|
||||
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
|
||||
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
|
||||
<CAL:supported-calendar-component-set>
|
||||
<CAL:comp name="VEVENT" />
|
||||
</CAL:supported-calendar-component-set>
|
||||
</prop>
|
||||
</set>
|
||||
</mkcol>
|
||||
`
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
alert(`Error ${response.status}: ${await response.text()}`)
|
||||
return null
|
||||
}
|
||||
window.location.reload()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'create-calendar-form': CreateCalendarForm
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
||||
|
||||
rollupOptions: {
|
||||
input: [
|
||||
"lib/create-birthday-calendar-form.ts",
|
||||
"lib/create-calendar-form.ts",
|
||||
"lib/edit-calendar-form.ts",
|
||||
"lib/import-calendar-form.ts",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||
if (decorator = decorators[i2])
|
||||
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||
if (kind && result) __defProp(target, key, result);
|
||||
return result;
|
||||
};
|
||||
let CreateCalendarForm = class extends i {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.principal = "";
|
||||
this.addr_id = "";
|
||||
this.displayname = "";
|
||||
this.description = "";
|
||||
this.color = "";
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
return x`
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||
<dialog ${n(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
<button type="submit" @click=${(event) => {
|
||||
event.preventDefault();
|
||||
this.dialog.value.close();
|
||||
this.form.value.reset();
|
||||
}} class="cancel">Cancel</button>
|
||||
</form>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
async submit(e2) {
|
||||
e2.preventDefault();
|
||||
if (!this.addr_id) {
|
||||
alert("Empty id");
|
||||
return;
|
||||
}
|
||||
if (!this.displayname) {
|
||||
alert("Empty displayname");
|
||||
return;
|
||||
}
|
||||
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||
method: "MKCOL",
|
||||
headers: {
|
||||
"Content-Type": "application/xml"
|
||||
},
|
||||
body: `
|
||||
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||
<set>
|
||||
<prop>
|
||||
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
|
||||
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
|
||||
<CAL:supported-calendar-component-set>
|
||||
<CAL:comp name="VEVENT" />
|
||||
</CAL:supported-calendar-component-set>
|
||||
</prop>
|
||||
</set>
|
||||
</mkcol>
|
||||
`
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
alert(`Error ${response.status}: ${await response.text()}`);
|
||||
return null;
|
||||
}
|
||||
window.location.reload();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "principal", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "addr_id", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "displayname", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "description", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "color", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "timezones", 2);
|
||||
CreateCalendarForm = __decorateClass([
|
||||
t("create-birthday-calendar-form")
|
||||
], CreateCalendarForm);
|
||||
export {
|
||||
CreateCalendarForm
|
||||
};
|
||||
@@ -53,6 +53,11 @@ a {
|
||||
color: var(--text-on-background-color);
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--background-darker);
|
||||
min-height: 60px;
|
||||
@@ -239,10 +244,8 @@ ul.collection-list {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
a,
|
||||
button {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -282,6 +285,7 @@ ul.collection-list {
|
||||
grid-area: actions;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2>{{user.id }}'s Addressbooks</h2>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, addressbook) in addressbooks %}
|
||||
{% for (meta, birthday_cal, addressbook) in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||
<div class="inner">
|
||||
@@ -24,9 +24,17 @@
|
||||
></edit-addressbook-form>
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
{% if !birthday_cal.is_some() %}
|
||||
<create-birthday-calendar-form
|
||||
principal="{{ addressbook.principal }}"
|
||||
addr_id="{{ addressbook.id }}"
|
||||
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
|
||||
></create-birthday-calendar-form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
{{ birthday_cal.is_some() }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -37,7 +45,7 @@
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, addressbook) in deleted_addressbooks %}
|
||||
{% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||
<div class="inner">
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||
<edit-calendar-form
|
||||
principal="{{ calendar.principal }}"
|
||||
cal_id="{{ calendar.id }}"
|
||||
@@ -35,7 +34,6 @@
|
||||
components="{{ calendar.components | json }}"
|
||||
></edit-calendar-form>
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script>
|
||||
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
|
||||
</script>
|
||||
<script type="module" src="/frontend/assets/js/create-birthday-calendar-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
|
||||
|
||||
@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
|
||||
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore,
|
||||
AddressbookStore, CalendarStore, PrefixedCalendarStore,
|
||||
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -39,7 +39,11 @@ use crate::routes::{
|
||||
#[cfg(not(feature = "dev"))]
|
||||
use assets::{Assets, EmbedService};
|
||||
|
||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
pub fn frontend_router<
|
||||
AP: AuthenticationProvider,
|
||||
CS: CalendarStore,
|
||||
AS: AddressbookStore + PrefixedCalendarStore,
|
||||
>(
|
||||
prefix: &'static str,
|
||||
auth_provider: Arc<AP>,
|
||||
cal_store: Arc<CS>,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
|
||||
auth::Principal,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
|
||||
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
|
||||
#[template(path = "components/sections/addressbooks_section.html")]
|
||||
pub struct AddressbooksSection {
|
||||
pub user: Principal,
|
||||
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||
}
|
||||
|
||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(addr_store): Extension<Arc<AS>>,
|
||||
user: Principal,
|
||||
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
|
||||
let mut addressbook_infos = vec![];
|
||||
for addressbook in addressbooks {
|
||||
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||
let birthday_cal = match addr_store
|
||||
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||
.await
|
||||
{
|
||||
Ok(cal) => Some(cal),
|
||||
Err(rustical_store::Error::NotFound) => None,
|
||||
err => Some(err.unwrap()),
|
||||
};
|
||||
addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
birthday_cal,
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
let mut deleted_addressbook_infos = vec![];
|
||||
for addressbook in deleted_addressbooks {
|
||||
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||
let birthday_cal = match addr_store
|
||||
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||
.await
|
||||
{
|
||||
Ok(cal) => Some(cal),
|
||||
Err(rustical_store::Error::NotFound) => None,
|
||||
err => Some(err.unwrap()),
|
||||
};
|
||||
deleted_addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
birthday_cal,
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
|
||||
object_id: &str,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
// read_only refers to objects, metadata may still be updated
|
||||
fn is_read_only(&self, cal_id: &str) -> bool;
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
use crate::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
|
||||
combined_calendar_store::PrefixedCalendarStore,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::Constructor;
|
||||
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
||||
|
||||
#[derive(Constructor, Clone)]
|
||||
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 {
|
||||
Calendar {
|
||||
principal: addressbook.principal,
|
||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
||||
meta: CalendarMetadata {
|
||||
displayname: addressbook
|
||||
.displayname
|
||||
.map(|name| format!("{name} birthdays")),
|
||||
order: 0,
|
||||
description: None,
|
||||
color: None,
|
||||
},
|
||||
timezone_id: None,
|
||||
deleted_at: addressbook.deleted_at,
|
||||
synctoken: addressbook.synctoken,
|
||||
subscription_url: None,
|
||||
push_topic: {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update("birthdays");
|
||||
hasher.update(addressbook.push_topic);
|
||||
format!("{:x}", hasher.finalize())
|
||||
},
|
||||
components: vec![CalendarObjectType::Event],
|
||||
}
|
||||
}
|
||||
|
||||
/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
|
||||
#[async_trait]
|
||||
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
||||
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 addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
|
||||
Ok(birthday_calendar(addressbook))
|
||||
}
|
||||
|
||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||
let addressbooks = self.0.get_addressbooks(principal).await?;
|
||||
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
||||
}
|
||||
|
||||
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||
let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
|
||||
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
||||
}
|
||||
|
||||
async fn update_calendar(
|
||||
&self,
|
||||
_principal: String,
|
||||
_id: String,
|
||||
_calendar: Calendar,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
async fn delete_calendar(
|
||||
&self,
|
||||
_principal: &str,
|
||||
_name: &str,
|
||||
_use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn import_calendar(
|
||||
&self,
|
||||
_calendar: Calendar,
|
||||
_objects: Vec<CalendarObject>,
|
||||
_merge_existing: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn sync_changes(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
synctoken: i64,
|
||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let (objects, deleted_objects, new_synctoken) =
|
||||
self.0.sync_changes(principal, cal_id, synctoken).await?;
|
||||
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
|
||||
.iter()
|
||||
.map(AddressObject::get_birthday_object)
|
||||
.collect();
|
||||
let objects = objects?.into_iter().flatten().collect();
|
||||
|
||||
Ok((objects, deleted_objects, new_synctoken))
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<crate::CollectionMetadata, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
self.0.addressbook_metadata(principal, cal_id).await
|
||||
}
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
|
||||
self.0
|
||||
.get_objects(principal, cal_id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(AddressObject::get_significant_dates)
|
||||
.collect();
|
||||
let objects = objects?
|
||||
.into_iter()
|
||||
.flat_map(HashMap::into_values)
|
||||
.collect();
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
async fn get_object(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
||||
self.0
|
||||
.get_object(principal, cal_id, addressobject_id, show_deleted)
|
||||
.await?
|
||||
.get_significant_dates()?
|
||||
.remove(date_type)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
async fn put_object(
|
||||
&self,
|
||||
_principal: String,
|
||||
_cal_id: String,
|
||||
_object: CalendarObject,
|
||||
_overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn delete_object(
|
||||
&self,
|
||||
_principal: &str,
|
||||
_cal_id: &str,
|
||||
_object_id: &str,
|
||||
_use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
async fn restore_object(
|
||||
&self,
|
||||
_principal: &str,
|
||||
_cal_id: &str,
|
||||
_object_id: &str,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
fn is_read_only(&self, _cal_id: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ pub use error::Error;
|
||||
pub mod auth;
|
||||
mod calendar;
|
||||
mod combined_calendar_store;
|
||||
mod contact_birthday_store;
|
||||
mod secret;
|
||||
mod subscription_store;
|
||||
pub mod synctoken;
|
||||
@@ -17,8 +16,7 @@ pub mod tests;
|
||||
|
||||
pub use addressbook_store::AddressbookStore;
|
||||
pub use calendar_store::CalendarStore;
|
||||
pub use combined_calendar_store::CombinedCalendarStore;
|
||||
pub use contact_birthday_store::ContactBirthdayStore;
|
||||
pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
|
||||
pub use secret::Secret;
|
||||
pub use subscription_store::*;
|
||||
|
||||
|
||||
@@ -30,3 +30,4 @@ uuid.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
rstest = { workspace = true, optional = true }
|
||||
sha2.workspace = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE birthday_calendars;
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE birthday_calendars (
|
||||
principal TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
displayname TEXT,
|
||||
description TEXT,
|
||||
"order" INT DEFAULT 0 NOT NULL,
|
||||
color TEXT,
|
||||
timezone_id TEXT,
|
||||
deleted_at DATETIME,
|
||||
push_topic TEXT NOT NULL,
|
||||
PRIMARY KEY (principal, id),
|
||||
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
|
||||
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
|
||||
-- birthday calendar stores no meaningful data so we can cascade
|
||||
);
|
||||
|
||||
INSERT INTO birthday_calendars
|
||||
(principal, id, displayname, deleted_at, push_topic)
|
||||
SELECT
|
||||
principal,
|
||||
id,
|
||||
displayname || ' birthdays' AS displayname,
|
||||
deleted_at,
|
||||
push_topic || substr(printf('%d', random()), -4) AS push_topic
|
||||
-- jank suffix to ensure that new push_topic is different :D
|
||||
FROM addressbooks;
|
||||
431
crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
Normal file
431
crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
use crate::addressbook_store::SqliteAddressbookStore;
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDateTime;
|
||||
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
|
||||
Error, PrefixedCalendarStore,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use std::collections::HashMap;
|
||||
use tracing::instrument;
|
||||
|
||||
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
||||
|
||||
struct BirthdayCalendarJoinRow {
|
||||
principal: String,
|
||||
id: String,
|
||||
displayname: Option<String>,
|
||||
description: Option<String>,
|
||||
order: i64,
|
||||
color: Option<String>,
|
||||
timezone_id: Option<String>,
|
||||
deleted_at: Option<NaiveDateTime>,
|
||||
push_topic: String,
|
||||
|
||||
addr_synctoken: i64,
|
||||
}
|
||||
|
||||
impl From<BirthdayCalendarJoinRow> for Calendar {
|
||||
fn from(value: BirthdayCalendarJoinRow) -> Self {
|
||||
Self {
|
||||
principal: value.principal,
|
||||
id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
|
||||
meta: CalendarMetadata {
|
||||
displayname: value.displayname,
|
||||
order: value.order,
|
||||
description: value.description,
|
||||
color: value.color,
|
||||
},
|
||||
deleted_at: value.deleted_at,
|
||||
components: vec![CalendarObjectType::Event],
|
||||
timezone_id: value.timezone_id,
|
||||
synctoken: value.addr_synctoken,
|
||||
subscription_url: None,
|
||||
push_topic: value.push_topic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrefixedCalendarStore for SqliteAddressbookStore {
|
||||
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
|
||||
}
|
||||
|
||||
impl SqliteAddressbookStore {
|
||||
#[instrument]
|
||||
pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<Calendar, Error> {
|
||||
let cal = sqlx::query_as!(
|
||||
BirthdayCalendarJoinRow,
|
||||
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
|
||||
FROM birthday_calendars
|
||||
INNER JOIN (
|
||||
SELECT principal AS addr_principal,
|
||||
id AS addr_id,
|
||||
synctoken AS addr_synctoken
|
||||
FROM addressbooks
|
||||
) ON (principal, id) = (addr_principal, addr_id)
|
||||
WHERE (principal, id) = (?, ?)
|
||||
AND ((deleted_at IS NULL) OR ?)
|
||||
"#,
|
||||
principal,
|
||||
id,
|
||||
show_deleted
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(cal.into())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
deleted: bool,
|
||||
) -> Result<Vec<Calendar>, Error> {
|
||||
Ok(
|
||||
sqlx::query_as!(
|
||||
BirthdayCalendarJoinRow,
|
||||
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
|
||||
FROM birthday_calendars
|
||||
INNER JOIN (
|
||||
SELECT principal AS addr_principal,
|
||||
id AS addr_id,
|
||||
synctoken AS addr_synctoken
|
||||
FROM addressbooks
|
||||
) ON (principal, id) = (addr_principal, addr_id)
|
||||
WHERE principal = ?
|
||||
AND (
|
||||
(deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
|
||||
OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
|
||||
)
|
||||
"#,
|
||||
principal,
|
||||
deleted,
|
||||
deleted
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||
let birthday_name = addressbook
|
||||
.displayname
|
||||
.as_ref()
|
||||
.map(|name| format!("{name} birthdays"));
|
||||
let birthday_push_topic = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update("birthdays");
|
||||
hasher.update(&addressbook.push_topic);
|
||||
format!("{:x}", hasher.finalize())
|
||||
};
|
||||
Calendar {
|
||||
principal: addressbook.principal,
|
||||
meta: CalendarMetadata {
|
||||
displayname: birthday_name,
|
||||
order: 0,
|
||||
description: None,
|
||||
color: None,
|
||||
},
|
||||
id: format!("{}{}", Self::PREFIX, addressbook.id),
|
||||
components: vec![CalendarObjectType::Event],
|
||||
timezone_id: None,
|
||||
deleted_at: None,
|
||||
synctoken: Default::default(),
|
||||
subscription_url: None,
|
||||
push_topic: birthday_push_topic,
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
calendar: &Calendar,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let id = calendar
|
||||
.id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
||||
calendar.principal,
|
||||
id,
|
||||
calendar.meta.displayname,
|
||||
calendar.meta.description,
|
||||
calendar.meta.order,
|
||||
calendar.meta.color,
|
||||
calendar.push_topic,
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
if use_trashbin {
|
||||
sqlx::query!(
|
||||
r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
|
||||
principal,
|
||||
id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
|
||||
principal,
|
||||
id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
id: &str,
|
||||
) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
|
||||
principal,
|
||||
id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
calendar: &Calendar,
|
||||
) -> Result<(), Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
|
||||
WHERE (principal, id) = (?, ?)"#,
|
||||
calendar.principal,
|
||||
calendar.id,
|
||||
calendar.meta.displayname,
|
||||
calendar.meta.description,
|
||||
calendar.meta.order,
|
||||
calendar.meta.color,
|
||||
calendar.timezone_id,
|
||||
calendar.push_topic,
|
||||
principal,
|
||||
calendar.id,
|
||||
).execute(executor).await.map_err(crate::Error::from)?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(rustical_store::Error::NotFound);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CalendarStore for SqliteAddressbookStore {
|
||||
#[instrument]
|
||||
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)?;
|
||||
Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||
Self::_get_birthday_calendars(&self.db, principal, false).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||
Self::_get_birthday_calendars(&self.db, principal, true).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn update_calendar(
|
||||
&self,
|
||||
principal: String,
|
||||
id: String,
|
||||
mut calendar: Calendar,
|
||||
) -> Result<(), Error> {
|
||||
assert_eq!(id, calendar.id);
|
||||
calendar.id = calendar
|
||||
.id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?
|
||||
.to_string();
|
||||
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
|
||||
Self::_insert_birthday_calendar(&self.db, &calendar).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn delete_calendar(
|
||||
&self,
|
||||
principal: &str,
|
||||
id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
|
||||
return Ok(());
|
||||
};
|
||||
Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
|
||||
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
|
||||
return Err(Error::NotFound);
|
||||
};
|
||||
Self::_restore_birthday_calendar(&self.db, principal, id).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn import_calendar(
|
||||
&self,
|
||||
_calendar: Calendar,
|
||||
_objects: Vec<CalendarObject>,
|
||||
_merge_existing: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn sync_changes(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
synctoken: i64,
|
||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let (objects, deleted_objects, new_synctoken) =
|
||||
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
|
||||
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
|
||||
.iter()
|
||||
.map(AddressObject::get_birthday_object)
|
||||
.collect();
|
||||
let objects = objects?.into_iter().flatten().collect();
|
||||
|
||||
Ok((objects, deleted_objects, new_synctoken))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<CollectionMetadata, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
self.addressbook_metadata(principal, cal_id).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
|
||||
AddressbookStore::get_objects(self, principal, cal_id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(AddressObject::get_significant_dates)
|
||||
.collect();
|
||||
let objects = objects?
|
||||
.into_iter()
|
||||
.flat_map(HashMap::into_values)
|
||||
.collect();
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_object(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
show_deleted: bool,
|
||||
) -> Result<CalendarObject, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
||||
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
|
||||
.await?
|
||||
.get_significant_dates()?
|
||||
.remove(date_type)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn put_object(
|
||||
&self,
|
||||
_principal: String,
|
||||
_cal_id: String,
|
||||
_object: CalendarObject,
|
||||
_overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn delete_object(
|
||||
&self,
|
||||
_principal: &str,
|
||||
_cal_id: &str,
|
||||
_object_id: &str,
|
||||
_use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn restore_object(
|
||||
&self,
|
||||
_principal: &str,
|
||||
_cal_id: &str,
|
||||
_object_id: &str,
|
||||
) -> Result<(), Error> {
|
||||
Err(Error::ReadOnly)
|
||||
}
|
||||
|
||||
fn is_read_only(&self, _cal_id: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use rustical_store::{
|
||||
};
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::{error, instrument};
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
pub mod birthday_calendar;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AddressObjectRow {
|
||||
@@ -32,6 +34,60 @@ pub struct SqliteAddressbookStore {
|
||||
}
|
||||
|
||||
impl SqliteAddressbookStore {
|
||||
// Commit "orphaned" objects to the changelog table
|
||||
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||
struct Row {
|
||||
principal: String,
|
||||
addressbook_id: String,
|
||||
id: String,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
Row,
|
||||
r#"
|
||||
SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||
FROM addressobjects
|
||||
WHERE (principal, addressbook_id, id) NOT IN (
|
||||
SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog
|
||||
)
|
||||
;
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
for row in rows {
|
||||
let operation = if row.deleted {
|
||||
ChangeOperation::Delete
|
||||
} else {
|
||||
ChangeOperation::Add
|
||||
};
|
||||
warn!(
|
||||
"Commiting orphaned addressbook object ({},{},{}), deleted={}",
|
||||
&row.principal, &row.addressbook_id, &row.id, &row.deleted
|
||||
);
|
||||
log_object_operation(
|
||||
&mut tx,
|
||||
&row.principal,
|
||||
&row.addressbook_id,
|
||||
&row.id,
|
||||
operation,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -90,9 +146,9 @@ impl SqliteAddressbookStore {
|
||||
|
||||
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: String,
|
||||
id: String,
|
||||
addressbook: Addressbook,
|
||||
principal: &str,
|
||||
id: &str,
|
||||
addressbook: &Addressbook,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
||||
@@ -116,7 +172,7 @@ impl SqliteAddressbookStore {
|
||||
|
||||
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
addressbook: Addressbook,
|
||||
addressbook: &Addressbook,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
|
||||
@@ -283,9 +339,9 @@ impl SqliteAddressbookStore {
|
||||
|
||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: String,
|
||||
addressbook_id: String,
|
||||
object: AddressObject,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
object: &AddressObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let (object_id, vcf) = (object.get_id(), object.get_vcf());
|
||||
@@ -397,7 +453,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
id: String,
|
||||
addressbook: Addressbook,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
Self::_update_addressbook(&self.db, principal, id, addressbook).await
|
||||
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@@ -405,7 +461,16 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
&self,
|
||||
addressbook: Addressbook,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
Self::_insert_addressbook(&self.db, addressbook).await
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||
let birthday_cal = Self::default_birthday_calendar(addressbook);
|
||||
Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@@ -521,14 +586,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
|
||||
let object_id = object.get_id().to_owned();
|
||||
|
||||
Self::_put_object(
|
||||
&mut *tx,
|
||||
principal.clone(),
|
||||
addressbook_id.clone(),
|
||||
object,
|
||||
overwrite,
|
||||
)
|
||||
.await?;
|
||||
Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
|
||||
|
||||
let sync_token = log_object_operation(
|
||||
&mut tx,
|
||||
@@ -628,7 +686,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
.await?
|
||||
.push_topic,
|
||||
}) {
|
||||
error!("Push notification about deleted addressbook failed: {err}");
|
||||
error!("Push notification about restored addressbook object failed: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -659,21 +717,44 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
return Err(Error::AlreadyExists);
|
||||
}
|
||||
if existing.is_none() {
|
||||
Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?;
|
||||
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||
}
|
||||
|
||||
let mut sync_token = None;
|
||||
for object in objects {
|
||||
Self::_put_object(
|
||||
&mut *tx,
|
||||
addressbook.principal.clone(),
|
||||
addressbook.id.clone(),
|
||||
object,
|
||||
&addressbook.principal,
|
||||
&addressbook.id,
|
||||
&object,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sync_token = Some(
|
||||
log_object_operation(
|
||||
&mut tx,
|
||||
&addressbook.principal,
|
||||
&addressbook.id,
|
||||
object.get_id(),
|
||||
ChangeOperation::Add,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
if let Some(sync_token) = sync_token
|
||||
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||
data: CollectionOperationInfo::Content { sync_token },
|
||||
topic: self
|
||||
.get_addressbook(&addressbook.principal, &addressbook.id, true)
|
||||
.await?
|
||||
.push_topic,
|
||||
})
|
||||
{
|
||||
error!("Push notification about imported addressbook failed: {err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -685,7 +766,7 @@ async fn log_object_operation(
|
||||
addressbook_id: &str,
|
||||
object_id: &str,
|
||||
operation: ChangeOperation,
|
||||
) -> Result<String, sqlx::Error> {
|
||||
) -> Result<String, Error> {
|
||||
struct Synctoken {
|
||||
synctoken: i64,
|
||||
}
|
||||
@@ -700,7 +781,8 @@ async fn log_object_operation(
|
||||
addressbook_id
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -714,6 +796,7 @@ async fn log_object_operation(
|
||||
operation
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(format_synctoken(synctoken))
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::{error, instrument};
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CalendarObjectRow {
|
||||
@@ -94,6 +94,53 @@ pub struct SqliteCalendarStore {
|
||||
}
|
||||
|
||||
impl SqliteCalendarStore {
|
||||
// Commit "orphaned" objects to the changelog table
|
||||
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||
struct Row {
|
||||
principal: String,
|
||||
cal_id: String,
|
||||
id: String,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
Row,
|
||||
r#"
|
||||
SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||
FROM calendarobjects
|
||||
WHERE (principal, cal_id, id) NOT IN (
|
||||
SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog
|
||||
)
|
||||
;
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
for row in rows {
|
||||
let operation = if row.deleted {
|
||||
ChangeOperation::Delete
|
||||
} else {
|
||||
ChangeOperation::Add
|
||||
};
|
||||
warn!(
|
||||
"Commiting orphaned calendar object ({},{},{}), deleted={}",
|
||||
&row.principal, &row.cal_id, &row.id, &row.deleted
|
||||
);
|
||||
log_object_operation(&mut tx, &row.principal, &row.cal_id, &row.id, operation).await?;
|
||||
}
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -351,9 +398,9 @@ impl SqliteCalendarStore {
|
||||
#[instrument]
|
||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: String,
|
||||
cal_id: String,
|
||||
object: CalendarObject,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
object: &CalendarObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
||||
@@ -600,18 +647,35 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
||||
}
|
||||
|
||||
let mut sync_token = None;
|
||||
for object in objects {
|
||||
Self::_put_object(
|
||||
&mut *tx,
|
||||
calendar.principal.clone(),
|
||||
calendar.id.clone(),
|
||||
object,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
|
||||
|
||||
sync_token = Some(
|
||||
log_object_operation(
|
||||
&mut tx,
|
||||
&calendar.principal,
|
||||
&calendar.id,
|
||||
object.get_id(),
|
||||
ChangeOperation::Add,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
|
||||
if let Some(sync_token) = sync_token
|
||||
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
||||
data: CollectionOperationInfo::Content { sync_token },
|
||||
topic: self
|
||||
.get_calendar(&calendar.principal, &calendar.id, true)
|
||||
.await?
|
||||
.push_topic,
|
||||
})
|
||||
{
|
||||
error!("Push notification about imported calendar failed: {err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -689,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
return Err(Error::ReadOnly);
|
||||
}
|
||||
|
||||
Self::_put_object(
|
||||
&mut *tx,
|
||||
principal.clone(),
|
||||
cal_id.clone(),
|
||||
object,
|
||||
overwrite,
|
||||
)
|
||||
.await?;
|
||||
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
|
||||
|
||||
let sync_token = log_object_operation(
|
||||
&mut tx,
|
||||
@@ -774,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
data: CollectionOperationInfo::Content { sync_token },
|
||||
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||
}) {
|
||||
error!("Push notification about deleted calendar failed: {err}");
|
||||
error!("Push notification about restored calendar object failed: {err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -795,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
}
|
||||
|
||||
// Logs an operation to the events
|
||||
// TODO: Log multiple updates
|
||||
async fn log_object_operation(
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
principal: &str,
|
||||
|
||||
11
src/app.rs
11
src/app.rs
@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
|
||||
use rustical_oidc::OidcConfig;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore,
|
||||
AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
|
||||
SubscriptionStore,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -33,7 +34,11 @@ use tracing::field::display;
|
||||
clippy::too_many_lines,
|
||||
clippy::cognitive_complexity
|
||||
)]
|
||||
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
||||
pub fn make_app<
|
||||
AS: AddressbookStore + PrefixedCalendarStore,
|
||||
CS: CalendarStore,
|
||||
S: SubscriptionStore,
|
||||
>(
|
||||
addr_store: Arc<AS>,
|
||||
cal_store: Arc<CS>,
|
||||
subscription_store: Arc<S>,
|
||||
@@ -45,7 +50,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
||||
session_cookie_samesite_strict: bool,
|
||||
payload_limit_mb: usize,
|
||||
) -> Router<()> {
|
||||
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
|
||||
let birthday_store = addr_store.clone();
|
||||
let combined_cal_store =
|
||||
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ use figment::Figment;
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
use rustical_dav_push::DavPushController;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
|
||||
};
|
||||
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
||||
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
||||
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
|
||||
@@ -56,7 +58,7 @@ async fn get_data_stores(
|
||||
migrate: bool,
|
||||
config: &DataStoreConfig,
|
||||
) -> Result<(
|
||||
Arc<impl AddressbookStore>,
|
||||
Arc<impl AddressbookStore + PrefixedCalendarStore>,
|
||||
Arc<impl CalendarStore>,
|
||||
Arc<impl SubscriptionStore>,
|
||||
Arc<impl AuthenticationProvider>,
|
||||
@@ -69,7 +71,9 @@ async fn get_data_stores(
|
||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||
|
||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||
addressbook_store.repair_orphans().await?;
|
||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||
cal_store.repair_orphans().await?;
|
||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user