Compare commits

...

72 Commits

Author SHA1 Message Date
Lennart
eae8e7d768 version 0.4.12 2025-07-07 21:18:46 +02:00
Lennart
105718a4ca frontend: Add xml escaping to collection creation forms 2025-07-07 21:18:16 +02:00
Lennart
0e68f1bdce frontend: refactor collection list to allow for dialogs 2025-07-07 11:22:20 +02:00
Lennart
aa744fcea2 version 0.4.11 2025-07-05 10:41:46 +02:00
Lennart
4a51a669cd frontend: stylesheet 2025-07-05 10:41:20 +02:00
Lennart
07fca05e50 Make hash for app tokens less expensive (they are random anyway) 2025-07-05 10:26:06 +02:00
Lennart
509cc8d7a1 docs: Add documentation to setup some clients (more to follow) 2025-07-05 10:22:32 +02:00
Lennart
4134ab0520 frontend: Add user to global scope and make principal inputs dropdowns for collection creation 2025-07-05 10:04:42 +02:00
Lennart
d8803a38a2 frontend: create-calendar-form put subscription url behind checkbox 2025-07-05 09:10:26 +02:00
Lennart
b5bff08b08 version 0.4.10 2025-07-05 08:50:00 +02:00
Lennart
3ca02d9792 dav: Implement HEAD method 2025-07-05 08:47:22 +02:00
Lennart
ee2cc2174c frontend: Slight stylesheet change 2025-07-05 08:47:09 +02:00
Lennart
caf10912e5 Version 0.4.9 2025-07-04 21:53:07 +02:00
Lennart
ec89cd6fa5 fix header bar on mobile 2025-07-04 21:52:23 +02:00
Lennart K
ae20573670 frontend: Add file sizes to collections 2025-07-04 21:20:49 +02:00
Lennart K
71cee2d20c frontend: Add some iconography 2025-07-04 21:12:28 +02:00
Lennart K
83c6bf247e Add sqlx queries 2025-07-04 20:58:32 +02:00
Lennart K
6bcc03d659 frontend: Add basic information about collections 2025-07-04 20:54:37 +02:00
Lennart K
32f5c01716 frontend: checkbox alignment for create calendar form 2025-07-04 19:57:20 +02:00
Lennart K
40938cba02 Some work on the frontend 2025-07-04 19:44:17 +02:00
Lennart
a5663bf006 Remove unnecessary pwhash command 2025-07-02 23:43:18 +02:00
Lennart
26306fd661 xml: Fix writer type 2025-07-02 23:31:04 +02:00
Lennart
d8e4bd1cc4 xml: Remove generics from XmlSerialize 2025-07-02 19:02:25 +02:00
Lennart K
a18ff2b400 propfind: Add todo comment 2025-07-02 16:51:05 +02:00
Lennart K
bf13d95b97 xml: Make XmlSerialize trait more precise 2025-07-02 12:51:29 +02:00
Lennart K
ee1faa4c20 version 0.4.8 2025-07-01 14:09:58 +02:00
Lennart K
1e999ca0cc feat(frontend): Add bodged field to create group collections 2025-07-01 14:09:32 +02:00
Lennart K
f27245f996 fix(store_sqlite): Principal upsert 2025-07-01 13:49:43 +02:00
Lennart
734455b5ab version 0.4.7 2025-06-30 20:05:01 +02:00
Lennart
8c6a616015 fix sync-collection limit element 2025-06-30 20:03:54 +02:00
Lennart K
828e7399c8 xml: Make serialization more ergonomic and clippy appeasement 2025-06-29 17:00:10 +02:00
Lennart K
891ef6a9f3 write test fixtures for sqlite store 2025-06-29 12:23:23 +02:00
Lennart
7b27ac22a4 Add Thunderbird to tested clients 2025-06-28 11:43:20 +02:00
Lennart
15668bf399 version 0.4.6 2025-06-28 01:19:29 +02:00
Lennart
d2de87072f slight frontend changes 2025-06-28 01:14:55 +02:00
Lennart
ff1e38477b slight frontend changes 2025-06-28 00:53:24 +02:00
Lennart
f4fbb7c964 Add docs commands to Justfile 2025-06-28 00:07:40 +02:00
Lennart
e8e60d4aac Weaken installation warning since I'm becoming more confident. 2025-06-28 00:06:27 +02:00
Lennart
283be0a26c version 0.4.5 2025-06-27 17:40:48 +02:00
Lennart
1060625b9d fix(oidc): Fix login not working for missing groups claim
see #87
2025-06-27 17:38:42 +02:00
Lennart
86ae31e94c tiny steps towards unit testing for each resource 2025-06-27 14:33:25 +02:00
Lennart
e2f5773e3c Dockerfile: Target Rust 1.88 2025-06-27 14:32:08 +02:00
Lennart
b54fbebe7c store: test preparations 2025-06-27 13:58:14 +02:00
Lennart
fe78a82806 clippy appeasement 2025-06-27 13:57:57 +02:00
Lennart
22544b8c2f Justfile: Add commands to build frontend components 2025-06-27 13:57:44 +02:00
Lennart
340b99e491 Dockerfile: Fix llvm dependency for arm64 builds 2025-06-26 22:25:04 +02:00
Lennart
787ea90376 Dockerfile, update Rust to 1.87+ 2025-06-26 22:03:16 +02:00
Lennart
973a86f21a remove some disabled and broken tests 2025-06-26 19:42:06 +02:00
Lennart
39fc2fb55d principal_type refactoring 2025-06-26 12:50:37 +02:00
Lennart
ab4d763304 tiny improvements to documentation 2025-06-26 12:39:23 +02:00
Lennart
9cf74f7198 frontend: Explicitly mark collections from other groups 2025-06-25 16:14:55 +02:00
Lennart
2c2a6006c7 version 0.4.4 2025-06-25 16:03:31 +02:00
Lennart
4600f03b45 frontend: slight improvements to collection lists 2025-06-25 16:01:17 +02:00
Lennart
41fc1e6ea5 frontend: Remove default red for calendars without color 2025-06-25 15:56:46 +02:00
Lennart
b56591c482 frontend: LSP appeasement 2025-06-25 15:54:47 +02:00
Lennart
d639b18005 version 0.4.3 2025-06-23 16:44:21 +02:00
Lennart
6046439fc7 feat(dav): Add show_deleted parameter to get_resource
Fixes #86
2025-06-23 16:43:46 +02:00
Lennart
f9de8a4687 feat: Add show_deleted to get_calendar 2025-06-23 16:35:36 +02:00
Lennart
8dfb47b28f version 0.4.2 2025-06-23 16:13:18 +02:00
Lennart
eb720ded99 ci: Only tag releases as latest container images 2025-06-23 16:12:36 +02:00
Lennart
89ef7b2ced Update vcard date tests 2025-06-23 16:09:22 +02:00
Lennart
6e0129130e Fix birthdays without year in birthday calendar
Fixes #79
2025-06-23 16:03:59 +02:00
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
Lennart
79c66a0b46 fix(caldav): Fix permissions to allow for deletion of calendar subscriptions
fixes #84
2025-06-23 14:04:09 +02:00
Lennart
e5687c6e43 fix(frontend): calendar subscription creation 2025-06-23 14:03:10 +02:00
Lennart
79b67a17c3 Implement deletion button to permanently delete collections 2025-06-23 13:48:00 +02:00
Lennart
7d18faff69 version 0.3.6 2025-06-23 11:21:04 +02:00
Lennart
753f8e90d3 fix(frontend): Fix calendar download link 2025-06-23 11:20:44 +02:00
Lennart
701fa9dd9c Version 3.4.5 2025-06-23 08:54:26 +02:00
Lennart
31b17cfe7f Frontend: Fix dumb typo in calendar creation form
Fixes #82
2025-06-23 08:53:50 +02:00
Lennart
d802a0085a Add Home Assistant to tested clients 2025-06-23 00:42:45 +02:00
123 changed files with 6477 additions and 5274 deletions

View File

@@ -2,3 +2,5 @@
indent_style = space
indent_size = 4
[docs/**/*.md]
indent_size = 4

View File

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

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n REPLACE INTO principals\n (id, displayname, principal_type, password_hash)\n VALUES (?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO principals\n (id, displayname, principal_type, password_hash) VALUES (?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n (displayname, principal_type, password_hash)\n = (excluded.displayname, excluded.principal_type, excluded.password_hash)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
"describe": {
"columns": [
{
"name": "length!: u64",
"ordinal": 0,
"type_info": "Null"
},
{
"name": "deleted!: bool",
"ordinal": 1,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 2
},
"nullable": [
null,
true
]
},
"hash": "660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474"
}

View File

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

View File

@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
"describe": {
"columns": [
{
"name": "length!: u64",
"ordinal": 0,
"type_info": "Null"
},
{
"name": "deleted!: bool",
"ordinal": 1,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 2
},
"nullable": [
null,
true
]
},
"hash": "d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48"
}

626
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.3.4"
version = "0.4.12"
edition = "2024"
description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical"
@@ -139,6 +139,7 @@ ece = { version = "2.3", default-features = false, features = [
"backend-openssl",
] }
openssl = { version = "0.10", features = ["vendored"] }
async-std = { version = "1.13", features = ["attributes"] }
[dependencies]
rustical_store = { workspace = true }

View File

@@ -1,11 +1,11 @@
FROM --platform=$BUILDPLATFORM rust:1.86-alpine AS chef
FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# the compiler will otherwise ask for aarch64-linux-musl-gcc
ENV CC_aarch64_unknown_linux_musl="clang"
ENV AR_aarch64_unknown_linux_musl="llvm-ar"
ENV AR_aarch64_unknown_linux_musl="llvm20-ar"
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"
# Stupid workaound with tempfiles since environment variables
@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
esac
RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \
RUN apk add --no-cache musl-dev llvm20 clang perl pkgconf make \
&& rustup target add "$(cat /tmp/rust_target)" \
&& cargo install cargo-chef --locked \
&& rm -rf "$CARGO_HOME/registry"

View File

@@ -1,2 +1,14 @@
licenses:
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
frontend-dev:
cd crates/frontend/js-components && deno task dev
frontend-build:
cd crates/frontend/js-components && deno task build
docs:
mkdocs build
docs-dev:
mkdocs serve

View File

@@ -3,9 +3,9 @@
a CalDAV/CardDAV server
> [!WARNING]
RustiCal is **not production-ready!**
I've been using RustiCal for the last few weeks and I'm slowly becoming more confident,
however you'd be one of the first testers so expect bugs and rough edges.
RustiCal is under **active development**!
While I've been successfully using RustiCal productively for a few weeks now,
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. :)
## Features
@@ -30,3 +30,5 @@ a CalDAV/CardDAV server
- GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution
- Apple Calendar
- Home Assistant integration
- Thunderbird

View File

@@ -7,6 +7,11 @@ repository.workspace = true
license.workspace = true
publish = false
[dev-dependencies]
rustical_store_sqlite = { workspace = true, features = ["test"] }
rstest.workspace = true
async-std.workspace = true
[dependencies]
axum.workspace = true
axum-extra.workspace = true

View File

@@ -4,7 +4,7 @@ use axum::body::Body;
use axum::extract::State;
use axum::{extract::Path, response::Response};
use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header};
use http::{HeaderValue, Method, StatusCode, header};
use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode};
@@ -19,17 +19,22 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
Path((principal, calendar_id)): Path<(String, String)>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
user: Principal,
method: Method,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, true)
.await?;
let mut timezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
@@ -92,5 +97,9 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
))
.unwrap(),
);
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap())
} else {
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
}
}

View File

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

View File

@@ -292,7 +292,12 @@ impl Resource for CalendarResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only {
if self.cal.subscription_url.is_some() {
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),
));

View File

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

View File

@@ -6,7 +6,7 @@ use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use axum_extra::TypedHeader;
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{HeaderMap, StatusCode};
use http::{HeaderMap, Method, StatusCode};
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::Principal;
@@ -22,12 +22,15 @@ pub async fn get_event<C: CalendarStore>(
}): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: Principal,
method: Method,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let calendar = cal_store
.get_calendar(&principal, &calendar_id, false)
.await?;
if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized);
}
@@ -40,7 +43,11 @@ pub async fn get_event<C: CalendarStore>(
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap())
} else {
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
}
}
#[instrument(skip(cal_store))]

View File

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

View File

@@ -11,8 +11,10 @@ mod service;
pub use service::*;
mod prop;
pub use prop::*;
#[cfg(test)]
pub mod tests;
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct PrincipalResource {
principal: Principal,
members: Vec<String>,

View File

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

View File

@@ -0,0 +1,46 @@
use std::sync::Arc;
use crate::principal::PrincipalResourceService;
use rstest::rstest;
use rustical_dav::resource::ResourceService;
use rustical_store_sqlite::{
SqliteStore,
calendar_store::SqliteCalendarStore,
principal_store::SqlitePrincipalStore,
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
};
#[rstest]
#[tokio::test]
async fn test_principal_resource(
#[from(get_test_calendar_store)]
#[future]
cal_store: SqliteCalendarStore,
#[from(get_test_principal_store)]
#[future]
auth_provider: SqlitePrincipalStore,
#[from(get_test_subscription_store)]
#[future]
sub_store: SqliteStore,
) {
let service = PrincipalResourceService {
cal_store: Arc::new(cal_store.await),
sub_store: Arc::new(sub_store.await),
auth_provider: Arc::new(auth_provider.await),
};
assert!(matches!(
service
.get_resource(&("invalid-user".to_owned(),), true)
.await,
Err(crate::Error::NotFound)
));
let _principal_resource = service
.get_resource(&("user".to_owned(),), true)
.await
.unwrap();
}
#[tokio::test]
async fn test_propfind() {}

View File

@@ -7,6 +7,7 @@ use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use axum_extra::TypedHeader;
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::Method;
use http::{HeaderMap, StatusCode};
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
@@ -25,6 +26,7 @@ pub async fn get_object<AS: AddressbookStore>(
}): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: Principal,
method: Method,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
@@ -49,7 +51,11 @@ pub async fn get_object<AS: AddressbookStore>(
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap())
} else {
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
}
}
#[instrument(skip(addr_store, body))]

View File

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

View File

@@ -5,7 +5,7 @@ use axum::body::Body;
use axum::extract::{Path, State};
use axum::response::Response;
use axum_extra::headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header};
use http::{HeaderValue, Method, StatusCode, header};
use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
@@ -20,6 +20,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: Principal,
method: Method,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
@@ -55,5 +56,9 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
))
.unwrap(),
);
Ok(resp.body(Body::new(vcf)).unwrap())
if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap())
} else {
Ok(resp.body(Body::new(vcf)).unwrap())
}
}

View File

@@ -58,6 +58,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
Ok((result, not_found))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
addr_multiget: &AddressbookMultigetRequest,
prop: &PropfindType<AddressObjectPropWrapperName>,

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ use quick_xml::name::Namespace;
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::collections::{HashMap, HashSet};
// https://datatracker.ietf.org/doc/html/rfc3744
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
pub enum UserPrivilege {
Read,
@@ -15,12 +16,12 @@ pub enum UserPrivilege {
}
impl XmlSerialize for UserPrivilegeSet {
fn serialize<W: std::io::Write>(
fn serialize(
&self,
ns: Option<Namespace>,
tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<W>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> {
#[derive(XmlSerialize)]
pub struct FakeUserPrivilegeSet {
@@ -34,7 +35,6 @@ impl XmlSerialize for UserPrivilegeSet {
.serialize(ns, tag, namespaces, writer)
}
#[allow(refining_impl_trait)]
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
None
}
@@ -47,6 +47,12 @@ pub struct UserPrivilegeSet {
impl UserPrivilegeSet {
pub fn has(&self, privilege: &UserPrivilege) -> bool {
if (privilege == &UserPrivilege::WriteProperties
|| privilege == &UserPrivilege::WriteContent)
&& self.privileges.contains(&UserPrivilege::Write)
{
return true;
}
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
}
@@ -72,6 +78,15 @@ impl UserPrivilegeSet {
}
}
pub fn owner_write_properties(is_owner: bool) -> Self {
// Content is read-only but we can write properties
if is_owner {
Self::write_properties()
} else {
Self::default()
}
}
pub fn read_only() -> Self {
Self {
privileges: HashSet::from([
@@ -81,6 +96,17 @@ impl UserPrivilegeSet {
]),
}
}
pub fn write_properties() -> Self {
Self {
privileges: HashSet::from([
UserPrivilege::Read,
UserPrivilege::WriteProperties,
UserPrivilege::ReadAcl,
UserPrivilege::ReadCurrentUserPrivilegeSet,
]),
}
}
}
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {

View File

@@ -18,11 +18,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
None
}
#[inline]
fn head() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn post() -> Option<MethodFunction<Self>> {
None
@@ -58,8 +53,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
}
if Self::get().is_some() {
allow.push(Method::GET);
}
if Self::head().is_some() {
allow.push(Method::HEAD);
}
if Self::post().is_some() {

View File

@@ -72,16 +72,11 @@ where
return svc(self.resource_service.clone(), req);
}
}
"GET" => {
"GET" | "HEAD" => {
if let Some(svc) = RS::get() {
return svc(self.resource_service.clone(), req);
}
}
"HEAD" => {
if let Some(svc) = RS::head() {
return svc(self.resource_service.clone(), req);
}
}
"POST" => {
if let Some(svc) = RS::post() {
return svc(self.resource_service.clone(), req);

View File

@@ -45,10 +45,11 @@ pub async fn route_delete<R: ResourceService>(
if_match: Option<IfMatch>,
if_none_match: Option<IfNoneMatch>,
) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?;
let resource = resource_service.get_resource(path_components, true).await?;
// Kind of a bodge since we don't get unbind from the parent
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) {
if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into());
}

View File

@@ -49,7 +49,9 @@ pub(crate) async fn route_propfind<R: ResourceService>(
resource_service: &R,
puri: &impl PrincipalUri,
) -> Result<RSMultistatus<R>, R::Error> {
let resource = resource_service.get_resource(path_components).await?;
let resource = resource_service
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Read) {
return Err(Error::Unauthorized.into());
@@ -75,6 +77,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
let mut member_responses = Vec::new();
if depth != &Depth::Zero {
// TODO: authorization check for member resources
for member in resource_service.get_members(path_components).await? {
member_responses.push(member.propfind(
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),

View File

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

View File

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

View File

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

View File

@@ -19,12 +19,12 @@ pub struct PropstatElement<PropType: XmlSerialize> {
pub status: StatusCode,
}
fn xml_serialize_status<W: ::std::io::Write>(
fn xml_serialize_status(
status: &StatusCode,
ns: Option<Namespace>,
tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<W>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> {
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
}
@@ -49,12 +49,12 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
pub propstat: Vec<PropstatWrapper<PropstatType>>,
}
fn xml_serialize_optional_status<W: ::std::io::Write>(
fn xml_serialize_optional_status(
val: &Option<StatusCode>,
ns: Option<Namespace>,
tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<W>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> {
XmlSerialize::serialize(
&val.map(|status| format!("HTTP/1.1 {}", status)),
@@ -111,11 +111,10 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
fn into_response(self) -> axum::response::Response {
use axum::body::Body;
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
if let Err(err) = self.serialize_root(&mut writer) {
return crate::Error::from(err).into_response();
}
let output = match self.serialize_to_string() {
Ok(out) => out,
Err(err) => return crate::Error::from(err).into_response(),
};
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
let hdrs = resp.headers_mut().unwrap();

View File

@@ -23,20 +23,17 @@ mod tests {
#[test]
fn test_serialize_resourcetype() {
let mut buf = Vec::new();
let mut writer = quick_xml::Writer::new(&mut buf);
Document {
let out = Document {
resourcetype: Resourcetype(&[
ResourcetypeInner(Some(crate::namespace::NS_DAV), "displayname"),
ResourcetypeInner(Some(crate::namespace::NS_CALENDARSERVER), "calendar-color"),
]),
}
.serialize_root(&mut writer)
.serialize_to_string()
.unwrap();
let out = String::from_utf8(buf).unwrap();
assert_eq!(
out,
"<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
)
}
}

View File

@@ -1,4 +1,4 @@
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize};
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize, XmlRootTag};
use super::PropfindType;
@@ -32,11 +32,35 @@ impl ValueSerialize for SyncLevel {
}
}
// https://datatracker.ietf.org/doc/html/rfc5323#section-5.17
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct LimitElement {
#[xml(ns = "crate::namespace::NS_DAV")]
pub nresults: NresultsElement,
}
impl From<u64> for LimitElement {
fn from(value: u64) -> Self {
Self {
nresults: NresultsElement(value),
}
}
}
impl From<LimitElement> for u64 {
fn from(value: LimitElement) -> Self {
value.nresults.0
}
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct NresultsElement(#[xml(ty = "text")] u64);
#[derive(XmlDeserialize, Clone, Debug, PartialEq, XmlRootTag)]
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
#[xml(ns = "crate::namespace::NS_DAV")]
#[xml(ns = "crate::namespace::NS_DAV", root = b"sync-collection")]
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")]
pub sync_token: String,
@@ -45,5 +69,48 @@ pub struct SyncCollectionRequest<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV", ty = "untagged")]
pub prop: PropfindType<PN>,
#[xml(ns = "crate::namespace::NS_DAV")]
pub limit: Option<u64>,
pub limit: Option<LimitElement>,
}
#[cfg(test)]
mod tests {
use crate::xml::{
PropElement, PropfindType,
sync_collection::{SyncCollectionRequest, SyncLevel},
};
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlDocument};
const SYNC_COLLECTION_REQUEST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<sync-collection xmlns="DAV:">
<sync-token />
<sync-level>1</sync-level>
<limit>
<nresults>100</nresults>
</limit>
<prop>
<getetag />
</prop>
</sync-collection>
"#;
#[derive(XmlDeserialize, PropName, EnumVariants, PartialEq)]
#[xml(unit_variants_ident = "TestPropName")]
enum TestProp {
Getetag(String),
}
#[test]
fn test_parse_sync_collection_request() {
let request =
SyncCollectionRequest::<TestPropName>::parse_str(SYNC_COLLECTION_REQUEST).unwrap();
assert_eq!(
request,
SyncCollectionRequest {
sync_token: "".to_owned(),
sync_level: SyncLevel::One,
prop: PropfindType::Prop(PropElement(vec![TestPropName::Getetag], vec![])),
limit: Some(100.into())
}
)
}
}

View File

@@ -10,12 +10,12 @@ use std::collections::HashMap;
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
impl XmlSerialize for TagList {
fn serialize<W: std::io::Write>(
fn serialize(
&self,
ns: Option<Namespace>,
tag: Option<&[u8]>,
namespaces: &HashMap<Namespace, &[u8]>,
writer: &mut quick_xml::Writer<W>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> {
let prefix = ns
.map(|ns| namespaces.get(&ns))
@@ -57,7 +57,6 @@ impl XmlSerialize for TagList {
Ok(())
}
#[allow(refining_impl_trait)]
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
None
}

View File

@@ -99,13 +99,13 @@ impl<S: SubscriptionStore> DavPushController<S> {
content_update,
};
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
if let Err(err) = push_message.serialize_root(&mut writer) {
error!("Could not serialize push message: {}", err);
return;
}
let payload = String::from_utf8(output).unwrap();
let payload = match push_message.serialize_to_string() {
Ok(payload) => payload,
Err(err) => {
error!("Could not serialize push message: {}", err);
return;
}
};
for subsciption in subscriptions {
if let Some(allowed_push_servers) = &self.allowed_push_servers {

View File

@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-addressbook-form")
export class CreateAddressbookForm extends LitElement {
@@ -17,13 +18,15 @@ export class CreateAddressbookForm extends LitElement {
client = createClient("/carddav")
@property()
user: String = ''
user: string = ''
@property()
id: String = ''
principal: string = ''
@property()
displayname: String = ''
addr_id: string = ''
@property()
description: String = ''
displayname: string = ''
@property()
description: string = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@@ -34,9 +37,19 @@ export class CreateAddressbookForm extends LitElement {
<dialog ${ref(this.dialog)}>
<h3>Create addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group addressbooks)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" @change=${e => this.id = e.target.value} />
<input type="text" name="id" @change=${e => this.addr_id = e.target.value} />
</label>
<br>
<label>
@@ -50,7 +63,7 @@ export class CreateAddressbookForm extends LitElement {
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
@@ -59,7 +72,7 @@ export class CreateAddressbookForm extends LitElement {
async submit(e: SubmitEvent) {
console.log(this.displayname)
e.preventDefault()
if (!this.id) {
if (!this.addr_id) {
alert("Empty id")
return
}
@@ -68,13 +81,13 @@ export class CreateAddressbookForm extends LitElement {
return
}
// TODO: Escape user input: There's not really a security risk here but would be nicer
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>${this.displayname}</displayname>
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ''}
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ''}
</prop>
</set>
</mkcol>

View File

@@ -2,12 +2,12 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav";
import { escapeXml } from ".";
@customElement("create-calendar-form")
export class CreateCalendarForm extends LitElement {
constructor() {
super()
}
protected override createRenderRoot() {
@@ -17,17 +17,21 @@ export class CreateCalendarForm extends LitElement {
client = createClient("/caldav")
@property()
user: String = ''
user: string = ''
@property()
id: String = ''
principal: string = ''
@property()
displayname: String = ''
cal_id: string = ''
@property()
description: String = ''
displayname: string = ''
@property()
color: String = ''
description: string = ''
@property()
subscriptionUrl: String = ''
color: string = ''
@property()
isSubscription: boolean = false
@property()
subscriptionUrl: string = ''
@property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
@@ -41,9 +45,19 @@ export class CreateCalendarForm extends LitElement {
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group calendars)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" @change=${e => this.id = e.target.value} />
<input type="text" name="id" @change=${e => this.cal_id = e.target.value} />
</label>
<br>
<label>
@@ -61,20 +75,30 @@ export class CreateCalendarForm extends LitElement {
<input type="color" name="color" @change=${e => this.color = e.target.value} />
</label>
<br>
<br>
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
Calendar is subscription to external calendar
<input type="checkbox" name="is_subscription" @change=${e => this.isSubscription = e.target.checked} />
</label>
<br>
${this.isSubscription ? html`
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
</label>
<br>
`: html``}
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label>
Support ${comp}
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label>
<br>
`)}
<br>
<button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
@@ -83,7 +107,7 @@ export class CreateCalendarForm extends LitElement {
async submit(e: SubmitEvent) {
console.log(this.displayname)
e.preventDefault()
if (!this.id) {
if (!this.cal_id) {
alert("Empty id")
return
}
@@ -95,17 +119,17 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
data: `
<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>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''}
<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>` : ''}
${(this.isSubscription && this.subscriptionUrl) ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ''}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${escapeXml(comp)}" />`).join('\n')}
</CAL:supported-calendar-component-set>
</prop>
</set>

View File

@@ -0,0 +1,43 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("delete-button")
export class DeleteButton extends LitElement {
constructor() {
super()
}
@property({ type: Boolean })
trash: boolean = false
@property()
href: string
protected createRenderRoot() {
return this
}
protected render() {
let text = this.trash ? 'Move to trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
}
async _onClick(event: Event) {
event.preventDefault()
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
return
}
let response = await fetch(this.href, {
method: 'DELETE',
headers: {
'X-No-Trashbin': this.trash ? '0' : '1'
}
})
if (response.status < 200 || response.status >= 300) {
alert('An error occured, look into the console')
console.error(response)
return
}
window.location.reload()
}
}

View File

@@ -0,0 +1,9 @@
interface Window {
rusticalUser: {
id: String,
displayname: String | null,
memberships: Array<String>,
principal_type: "individual" | "group" | "room" | String
}
}

View File

@@ -0,0 +1,7 @@
export function escapeXml(unsafe: string): string {
return unsafe.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}

View File

@@ -1,7 +1,7 @@
{
"module": "nodenext",
"compilerOptions": {
"target": "es2024",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"lib": [

View File

@@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
// include: ["lit"]
},
build: {
minify: false,
modulePreload: {
polyfill: false
},
copyPublicDir: false,
lib: {
entry: 'lib/index.ts',
@@ -15,6 +16,7 @@ export default defineConfig({
input: [
"lib/create-calendar-form.ts",
"lib/create-addressbook-form.ts",
"lib/delete-button.ts",
],
output: {
dir: "../public/assets/js/",

View File

@@ -1,49 +1,77 @@
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
return r && e && y(s, l, e), e;
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.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 i = class extends c {
let CreateAddressbookForm = class extends i {
constructor() {
super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
super();
this.client = an("/carddav");
this.user = "";
this.principal = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return u`
return x`
<button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<dialog ${m(this.dialog)}>
<dialog ${n(this.dialog)}>
<h3>Create addressbook</h3>
<form @submit=${this.submit} ${m(this.form)}>
<form @submit=${this.submit} ${n(this.form)}>
<label>
principal (for group addressbooks)
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
<input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
<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=${(t) => this.description = t.target.value} />
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
}}> Cancel </button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(t) {
if (console.log(this.displayname), t.preventDefault(), !this.id) {
async submit(e2) {
console.log(this.displayname);
e2.preventDefault();
if (!this.addr_id) {
alert("Empty id");
return;
}
@@ -51,35 +79,40 @@ let i = class extends c {
alert("Empty displayname");
return;
}
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, {
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
data: `
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>${this.displayname}</displayname>
${this.description ? `<CARD:addressbook-description>${this.description}</CARD:addressbook-description>` : ""}
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CARD:addressbook-description>${escapeXml(this.description)}</CARD:addressbook-description>` : ""}
</prop>
</set>
</mkcol>
`
}), window.location.reload(), null;
});
window.location.reload();
return null;
}
};
a([
o()
], i.prototype, "user", 2);
a([
o()
], i.prototype, "id", 2);
a([
o()
], i.prototype, "displayname", 2);
a([
o()
], i.prototype, "description", 2);
i = a([
h("create-addressbook-form")
], i);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "user", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "principal", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateAddressbookForm.prototype, "description", 2);
CreateAddressbookForm = __decorateClass([
t("create-addressbook-form")
], CreateAddressbookForm);
export {
i as CreateAddressbookForm
CreateAddressbookForm
};

View File

@@ -1,66 +1,108 @@
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as b } from "./webdav-Bz4I5vNH.mjs";
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
return s && i && y(e, l, i), i;
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n as n$1, t } from "./property-D0NJdseG.mjs";
import { e, n, a as escapeXml } from "./index-b86iLJlP.mjs";
import { a as an } from "./webdav-D0R7xCzX.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 r = class extends u {
let CreateCalendarForm = class extends i {
constructor() {
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
super();
this.client = an("/caldav");
this.user = "";
this.principal = "";
this.cal_id = "";
this.displayname = "";
this.description = "";
this.color = "";
this.isSubscription = false;
this.subscriptionUrl = "";
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
this.form = e();
}
createRenderRoot() {
return this;
}
render() {
return c`
return x`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${m(this.dialog)}>
<dialog ${n(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${m(this.form)}>
<form @submit=${this.submit} ${n(this.form)}>
<label>
principal (for group calendars)
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map((membership) => x`
<option value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
<input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
<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=${(t) => this.description = t.target.value} />
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${(t) => this.color = t.target.value} />
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
<br>
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${(t) => this.subscriptionUrl = t.target.value} />
Calendar is subscription to external calendar
<input type="checkbox" name="is_subscription" @change=${(e2) => this.isSubscription = e2.target.checked} />
</label>
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
${this.isSubscription ? x`
<label>
Support ${t}
<input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
Subscription URL
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
</label>
<br>
` : x``}
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
<label>
Support ${comp}
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
</label>
<br>
`)}
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
}}> Cancel </button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(t) {
if (console.log(this.displayname), t.preventDefault(), !this.id) {
async submit(e2) {
console.log(this.displayname);
e2.preventDefault();
if (!this.cal_id) {
alert("Empty id");
return;
}
@@ -72,50 +114,57 @@ let r = class extends u {
alert("No calendar components selected");
return;
}
return await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
data: `
<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>${this.displayname}</displayname>
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
<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>` : ""}
${this.isSubscription && this.subscriptionUrl ? `<CS:source><href>${escapeXml(this.subscriptionUrl)}</href></CS:source>` : ""}
<CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
`)}
${Array.from(this.components.keys()).map((comp) => `<CAL:comp name="${escapeXml(comp)}" />`).join("\n")}
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
}), window.location.reload(), null;
});
window.location.reload();
return null;
}
};
a([
o()
], r.prototype, "user", 2);
a([
o()
], r.prototype, "id", 2);
a([
o()
], r.prototype, "displayname", 2);
a([
o()
], r.prototype, "description", 2);
a([
o()
], r.prototype, "color", 2);
a([
o()
], r.prototype, "subscriptionUrl", 2);
a([
o()
], r.prototype, "components", 2);
r = a([
h("create-calendar-form")
], r);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "user", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "cal_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, "isSubscription", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "subscriptionUrl", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "components", 2);
CreateCalendarForm = __decorateClass([
t("create-calendar-form")
], CreateCalendarForm);
export {
r as CreateCalendarForm
CreateCalendarForm
};

View File

@@ -0,0 +1,55 @@
import { i, x } from "./lit-z6_uA4GX.mjs";
import { n, t } from "./property-D0NJdseG.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 DeleteButton = class extends i {
constructor() {
super();
this.trash = false;
}
createRenderRoot() {
return this;
}
render() {
let text = this.trash ? "Move to trash" : "Delete";
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
}
async _onClick(event) {
event.preventDefault();
if (!this.trash && !confirm("Do you want to delete this collection permanently?")) {
return;
}
let response = await fetch(this.href, {
method: "DELETE",
headers: {
"X-No-Trashbin": this.trash ? "0" : "1"
}
});
if (response.status < 200 || response.status >= 300) {
alert("An error occured, look into the console");
console.error(response);
return;
}
window.location.reload();
}
};
__decorateClass([
n({ type: Boolean })
], DeleteButton.prototype, "trash", 2);
__decorateClass([
n()
], DeleteButton.prototype, "href", 2);
DeleteButton = __decorateClass([
t("delete-button")
], DeleteButton);
export {
DeleteButton
};

View File

@@ -0,0 +1,132 @@
import { E } from "./lit-z6_uA4GX.mjs";
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const f$1 = (o2) => void 0 === o2.strings;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = { CHILD: 2 }, e$1 = (t2) => (...e2) => ({ _$litDirective$: t2, values: e2 });
class i {
constructor(t2) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(t2, e2, i2) {
this._$Ct = t2, this._$AM = e2, this._$Ci = i2;
}
_$AS(t2, e2) {
return this.update(t2, e2);
}
update(t2, e2) {
return this.render(...e2);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const s = (i2, t2) => {
var _a;
const e2 = i2._$AN;
if (void 0 === e2) return false;
for (const i3 of e2) (_a = i3._$AO) == null ? void 0 : _a.call(i3, t2, false), s(i3, t2);
return true;
}, o$1 = (i2) => {
let t2, e2;
do {
if (void 0 === (t2 = i2._$AM)) break;
e2 = t2._$AN, e2.delete(i2), i2 = t2;
} while (0 === (e2 == null ? void 0 : e2.size));
}, r = (i2) => {
for (let t2; t2 = i2._$AM; i2 = t2) {
let e2 = t2._$AN;
if (void 0 === e2) t2._$AN = e2 = /* @__PURE__ */ new Set();
else if (e2.has(i2)) break;
e2.add(i2), c(t2);
}
};
function h$1(i2) {
void 0 !== this._$AN ? (o$1(this), this._$AM = i2, r(this)) : this._$AM = i2;
}
function n$1(i2, t2 = false, e2 = 0) {
const r2 = this._$AH, h2 = this._$AN;
if (void 0 !== h2 && 0 !== h2.size) if (t2) if (Array.isArray(r2)) for (let i3 = e2; i3 < r2.length; i3++) s(r2[i3], false), o$1(r2[i3]);
else null != r2 && (s(r2, false), o$1(r2));
else s(this, i2);
}
const c = (i2) => {
i2.type == t.CHILD && (i2._$AP ?? (i2._$AP = n$1), i2._$AQ ?? (i2._$AQ = h$1));
};
class f extends i {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(i2, t2, e2) {
super._$AT(i2, t2, e2), r(this), this.isConnected = i2._$AU;
}
_$AO(i2, t2 = true) {
var _a, _b;
i2 !== this.isConnected && (this.isConnected = i2, i2 ? (_a = this.reconnected) == null ? void 0 : _a.call(this) : (_b = this.disconnected) == null ? void 0 : _b.call(this)), t2 && (s(this, i2), o$1(this));
}
setValue(t2) {
if (f$1(this._$Ct)) this._$Ct._$AI(t2, this);
else {
const i2 = [...this._$Ct._$AH];
i2[this._$Ci] = t2, this._$Ct._$AI(i2, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const e = () => new h();
class h {
}
const o = /* @__PURE__ */ new WeakMap(), n = e$1(class extends f {
render(i2) {
return E;
}
update(i2, [s2]) {
var _a;
const e2 = s2 !== this.G;
return e2 && void 0 !== this.G && this.rt(void 0), (e2 || this.lt !== this.ct) && (this.G = s2, this.ht = (_a = i2.options) == null ? void 0 : _a.host, this.rt(this.ct = i2.element)), E;
}
rt(t2) {
if (this.isConnected || (t2 = void 0), "function" == typeof this.G) {
const i2 = this.ht ?? globalThis;
let s2 = o.get(i2);
void 0 === s2 && (s2 = /* @__PURE__ */ new WeakMap(), o.set(i2, s2)), void 0 !== s2.get(this.G) && this.G.call(this.ht, void 0), s2.set(this.G, t2), void 0 !== t2 && this.G.call(this.ht, t2);
} else this.G.value = t2;
}
get lt() {
var _a, _b;
return "function" == typeof this.G ? (_a = o.get(this.ht ?? globalThis)) == null ? void 0 : _a.get(this.G) : (_b = this.G) == null ? void 0 : _b.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
function escapeXml(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
export {
escapeXml as a,
e,
n
};

View File

@@ -1,551 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const M = globalThis, B = M.ShadowRoot && (M.ShadyCSS === void 0 || M.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, tt = Symbol(), W = /* @__PURE__ */ new WeakMap();
let ot = class {
constructor(t, e, s) {
if (this._$cssResult$ = !0, s !== tt) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = t, this.t = e;
}
get styleSheet() {
let t = this.o;
const e = this.t;
if (B && t === void 0) {
const s = e !== void 0 && e.length === 1;
s && (t = W.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), s && W.set(e, t));
}
return t;
}
toString() {
return this.cssText;
}
};
const ht = (r) => new ot(typeof r == "string" ? r : r + "", void 0, tt), at = (r, t) => {
if (B) r.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
else for (const e of t) {
const s = document.createElement("style"), i = M.litNonce;
i !== void 0 && s.setAttribute("nonce", i), s.textContent = e.cssText, r.appendChild(s);
}
}, V = B ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => {
let e = "";
for (const s of t.cssRules) e += s.cssText;
return ht(e);
})(r) : r;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const { is: lt, defineProperty: ct, getOwnPropertyDescriptor: dt, getOwnPropertyNames: pt, getOwnPropertySymbols: ut, getPrototypeOf: $t } = Object, f = globalThis, q = f.trustedTypes, _t = q ? q.emptyScript : "", k = f.reactiveElementPolyfillSupport, w = (r, t) => r, j = { toAttribute(r, t) {
switch (t) {
case Boolean:
r = r ? _t : null;
break;
case Object:
case Array:
r = r == null ? r : JSON.stringify(r);
}
return r;
}, fromAttribute(r, t) {
let e = r;
switch (t) {
case Boolean:
e = r !== null;
break;
case Number:
e = r === null ? null : Number(r);
break;
case Object:
case Array:
try {
e = JSON.parse(r);
} catch {
e = null;
}
}
return e;
} }, et = (r, t) => !lt(r, t), J = { attribute: !0, type: String, converter: j, reflect: !1, useDefault: !1, hasChanged: et };
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), f.litPropertyMetadata ?? (f.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
let v = class extends HTMLElement {
static addInitializer(t) {
this._$Ei(), (this.l ?? (this.l = [])).push(t);
}
static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
}
static createProperty(t, e = J) {
if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
const s = Symbol(), i = this.getPropertyDescriptor(t, s, e);
i !== void 0 && ct(this.prototype, t, i);
}
}
static getPropertyDescriptor(t, e, s) {
const { get: i, set: n } = dt(this.prototype, t) ?? { get() {
return this[e];
}, set(o) {
this[e] = o;
} };
return { get: i, set(o) {
const a = i == null ? void 0 : i.call(this);
n == null || n.call(this, o), this.requestUpdate(t, a, s);
}, configurable: !0, enumerable: !0 };
}
static getPropertyOptions(t) {
return this.elementProperties.get(t) ?? J;
}
static _$Ei() {
if (this.hasOwnProperty(w("elementProperties"))) return;
const t = $t(this);
t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(w("finalized"))) return;
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(w("properties"))) {
const e = this.properties, s = [...pt(e), ...ut(e)];
for (const i of s) this.createProperty(i, e[i]);
}
const t = this[Symbol.metadata];
if (t !== null) {
const e = litPropertyMetadata.get(t);
if (e !== void 0) for (const [s, i] of e) this.elementProperties.set(s, i);
}
this._$Eh = /* @__PURE__ */ new Map();
for (const [e, s] of this.elementProperties) {
const i = this._$Eu(e, s);
i !== void 0 && this._$Eh.set(i, e);
}
this.elementStyles = this.finalizeStyles(this.styles);
}
static finalizeStyles(t) {
const e = [];
if (Array.isArray(t)) {
const s = new Set(t.flat(1 / 0).reverse());
for (const i of s) e.unshift(V(i));
} else t !== void 0 && e.push(V(t));
return e;
}
static _$Eu(t, e) {
const s = e.attribute;
return s === !1 ? void 0 : typeof s == "string" ? s : typeof t == "string" ? t.toLowerCase() : void 0;
}
constructor() {
super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev();
}
_$Ev() {
var t;
this._$ES = new Promise((e) => this.enableUpdating = e), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (t = this.constructor.l) == null || t.forEach((e) => e(this));
}
addController(t) {
var e;
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t), this.renderRoot !== void 0 && this.isConnected && ((e = t.hostConnected) == null || e.call(t));
}
removeController(t) {
var e;
(e = this._$EO) == null || e.delete(t);
}
_$E_() {
const t = /* @__PURE__ */ new Map(), e = this.constructor.elementProperties;
for (const s of e.keys()) this.hasOwnProperty(s) && (t.set(s, this[s]), delete this[s]);
t.size > 0 && (this._$Ep = t);
}
createRenderRoot() {
const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
return at(t, this.constructor.elementStyles), t;
}
connectedCallback() {
var t;
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (t = this._$EO) == null || t.forEach((e) => {
var s;
return (s = e.hostConnected) == null ? void 0 : s.call(e);
});
}
enableUpdating(t) {
}
disconnectedCallback() {
var t;
(t = this._$EO) == null || t.forEach((e) => {
var s;
return (s = e.hostDisconnected) == null ? void 0 : s.call(e);
});
}
attributeChangedCallback(t, e, s) {
this._$AK(t, s);
}
_$ET(t, e) {
var n;
const s = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, s);
if (i !== void 0 && s.reflect === !0) {
const o = (((n = s.converter) == null ? void 0 : n.toAttribute) !== void 0 ? s.converter : j).toAttribute(e, s.type);
this._$Em = t, o == null ? this.removeAttribute(i) : this.setAttribute(i, o), this._$Em = null;
}
}
_$AK(t, e) {
var n, o;
const s = this.constructor, i = s._$Eh.get(t);
if (i !== void 0 && this._$Em !== i) {
const a = s.getPropertyOptions(i), h = typeof a.converter == "function" ? { fromAttribute: a.converter } : ((n = a.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? a.converter : j;
this._$Em = i, this[i] = h.fromAttribute(e, a.type) ?? ((o = this._$Ej) == null ? void 0 : o.get(i)) ?? null, this._$Em = null;
}
}
requestUpdate(t, e, s) {
var i;
if (t !== void 0) {
const n = this.constructor, o = this[t];
if (s ?? (s = n.getPropertyOptions(t)), !((s.hasChanged ?? et)(o, e) || s.useDefault && s.reflect && o === ((i = this._$Ej) == null ? void 0 : i.get(t)) && !this.hasAttribute(n._$Eu(t, s)))) return;
this.C(t, e, s);
}
this.isUpdatePending === !1 && (this._$ES = this._$EP());
}
C(t, e, { useDefault: s, reflect: i, wrapped: n }, o) {
s && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t) && (this._$Ej.set(t, o ?? e ?? this[t]), n !== !0 || o !== void 0) || (this._$AL.has(t) || (this.hasUpdated || s || (e = void 0), this._$AL.set(t, e)), i === !0 && this._$Em !== t && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t));
}
async _$EP() {
this.isUpdatePending = !0;
try {
await this._$ES;
} catch (e) {
Promise.reject(e);
}
const t = this.scheduleUpdate();
return t != null && await t, !this.isUpdatePending;
}
scheduleUpdate() {
return this.performUpdate();
}
performUpdate() {
var s;
if (!this.isUpdatePending) return;
if (!this.hasUpdated) {
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
for (const [n, o] of this._$Ep) this[n] = o;
this._$Ep = void 0;
}
const i = this.constructor.elementProperties;
if (i.size > 0) for (const [n, o] of i) {
const { wrapped: a } = o, h = this[n];
a !== !0 || this._$AL.has(n) || h === void 0 || this.C(n, void 0, o, h);
}
}
let t = !1;
const e = this._$AL;
try {
t = this.shouldUpdate(e), t ? (this.willUpdate(e), (s = this._$EO) == null || s.forEach((i) => {
var n;
return (n = i.hostUpdate) == null ? void 0 : n.call(i);
}), this.update(e)) : this._$EM();
} catch (i) {
throw t = !1, this._$EM(), i;
}
t && this._$AE(e);
}
willUpdate(t) {
}
_$AE(t) {
var e;
(e = this._$EO) == null || e.forEach((s) => {
var i;
return (i = s.hostUpdated) == null ? void 0 : i.call(s);
}), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t);
}
_$EM() {
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1;
}
get updateComplete() {
return this.getUpdateComplete();
}
getUpdateComplete() {
return this._$ES;
}
shouldUpdate(t) {
return !0;
}
update(t) {
this._$Eq && (this._$Eq = this._$Eq.forEach((e) => this._$ET(e, this[e]))), this._$EM();
}
updated(t) {
}
firstUpdated(t) {
}
};
v.elementStyles = [], v.shadowRootOptions = { mode: "open" }, v[w("elementProperties")] = /* @__PURE__ */ new Map(), v[w("finalized")] = /* @__PURE__ */ new Map(), k == null || k({ ReactiveElement: v }), (f.reactiveElementVersions ?? (f.reactiveElementVersions = [])).push("2.1.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const C = globalThis, N = C.trustedTypes, K = N ? N.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, st = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, it = "?" + _, ft = `<${it}>`, g = document, P = () => g.createComment(""), x = (r) => r === null || typeof r != "object" && typeof r != "function", I = Array.isArray, At = (r) => I(r) || typeof (r == null ? void 0 : r[Symbol.iterator]) == "function", D = `[
\f\r]`, b = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, Z = /-->/g, F = />/g, A = RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), G = /'/g, Q = /"/g, rt = /^(?:script|style|textarea|title)$/i, mt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), xt = mt(1), E = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), X = /* @__PURE__ */ new WeakMap(), m = g.createTreeWalker(g, 129);
function nt(r, t) {
if (!I(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array");
return K !== void 0 ? K.createHTML(t) : t;
}
const yt = (r, t) => {
const e = r.length - 1, s = [];
let i, n = t === 2 ? "<svg>" : t === 3 ? "<math>" : "", o = b;
for (let a = 0; a < e; a++) {
const h = r[a];
let c, p, l = -1, u = 0;
for (; u < h.length && (o.lastIndex = u, p = o.exec(h), p !== null); ) u = o.lastIndex, o === b ? p[1] === "!--" ? o = Z : p[1] !== void 0 ? o = F : p[2] !== void 0 ? (rt.test(p[2]) && (i = RegExp("</" + p[2], "g")), o = A) : p[3] !== void 0 && (o = A) : o === A ? p[0] === ">" ? (o = i ?? b, l = -1) : p[1] === void 0 ? l = -2 : (l = o.lastIndex - p[2].length, c = p[1], o = p[3] === void 0 ? A : p[3] === '"' ? Q : G) : o === Q || o === G ? o = A : o === Z || o === F ? o = b : (o = A, i = void 0);
const $ = o === A && r[a + 1].startsWith("/>") ? " " : "";
n += o === b ? h + ft : l >= 0 ? (s.push(c), h.slice(0, l) + st + h.slice(l) + _ + $) : h + _ + (l === -2 ? a : $);
}
return [nt(r, n + (r[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
};
class U {
constructor({ strings: t, _$litType$: e }, s) {
let i;
this.parts = [];
let n = 0, o = 0;
const a = t.length - 1, h = this.parts, [c, p] = yt(t, e);
if (this.el = U.createElement(c, s), m.currentNode = this.el.content, e === 2 || e === 3) {
const l = this.el.content.firstChild;
l.replaceWith(...l.childNodes);
}
for (; (i = m.nextNode()) !== null && h.length < a; ) {
if (i.nodeType === 1) {
if (i.hasAttributes()) for (const l of i.getAttributeNames()) if (l.endsWith(st)) {
const u = p[o++], $ = i.getAttribute(l).split(_), H = /([.?@])?(.*)/.exec(u);
h.push({ type: 1, index: n, name: H[2], strings: $, ctor: H[1] === "." ? vt : H[1] === "?" ? Et : H[1] === "@" ? St : R }), i.removeAttribute(l);
} else l.startsWith(_) && (h.push({ type: 6, index: n }), i.removeAttribute(l));
if (rt.test(i.tagName)) {
const l = i.textContent.split(_), u = l.length - 1;
if (u > 0) {
i.textContent = N ? N.emptyScript : "";
for (let $ = 0; $ < u; $++) i.append(l[$], P()), m.nextNode(), h.push({ type: 2, index: ++n });
i.append(l[u], P());
}
}
} else if (i.nodeType === 8) if (i.data === it) h.push({ type: 2, index: n });
else {
let l = -1;
for (; (l = i.data.indexOf(_, l + 1)) !== -1; ) h.push({ type: 7, index: n }), l += _.length - 1;
}
n++;
}
}
static createElement(t, e) {
const s = g.createElement("template");
return s.innerHTML = t, s;
}
}
function S(r, t, e = r, s) {
var o, a;
if (t === E) return t;
let i = s !== void 0 ? (o = e._$Co) == null ? void 0 : o[s] : e._$Cl;
const n = x(t) ? void 0 : t._$litDirective$;
return (i == null ? void 0 : i.constructor) !== n && ((a = i == null ? void 0 : i._$AO) == null || a.call(i, !1), n === void 0 ? i = void 0 : (i = new n(r), i._$AT(r, e, s)), s !== void 0 ? (e._$Co ?? (e._$Co = []))[s] = i : e._$Cl = i), i !== void 0 && (t = S(r, i._$AS(r, t.values), i, s)), t;
}
class gt {
constructor(t, e) {
this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e;
}
get parentNode() {
return this._$AM.parentNode;
}
get _$AU() {
return this._$AM._$AU;
}
u(t) {
const { el: { content: e }, parts: s } = this._$AD, i = ((t == null ? void 0 : t.creationScope) ?? g).importNode(e, !0);
m.currentNode = i;
let n = m.nextNode(), o = 0, a = 0, h = s[0];
for (; h !== void 0; ) {
if (o === h.index) {
let c;
h.type === 2 ? c = new O(n, n.nextSibling, this, t) : h.type === 1 ? c = new h.ctor(n, h.name, h.strings, this, t) : h.type === 6 && (c = new bt(n, this, t)), this._$AV.push(c), h = s[++a];
}
o !== (h == null ? void 0 : h.index) && (n = m.nextNode(), o++);
}
return m.currentNode = g, i;
}
p(t) {
let e = 0;
for (const s of this._$AV) s !== void 0 && (s.strings !== void 0 ? (s._$AI(t, s, e), e += s.strings.length - 2) : s._$AI(t[e])), e++;
}
}
class O {
get _$AU() {
var t;
return ((t = this._$AM) == null ? void 0 : t._$AU) ?? this._$Cv;
}
constructor(t, e, s, i) {
this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = s, this.options = i, this._$Cv = (i == null ? void 0 : i.isConnected) ?? !0;
}
get parentNode() {
let t = this._$AA.parentNode;
const e = this._$AM;
return e !== void 0 && (t == null ? void 0 : t.nodeType) === 11 && (t = e.parentNode), t;
}
get startNode() {
return this._$AA;
}
get endNode() {
return this._$AB;
}
_$AI(t, e = this) {
t = S(this, t, e), x(t) ? t === d || t == null || t === "" ? (this._$AH !== d && this._$AR(), this._$AH = d) : t !== this._$AH && t !== E && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : At(t) ? this.k(t) : this._(t);
}
O(t) {
return this._$AA.parentNode.insertBefore(t, this._$AB);
}
T(t) {
this._$AH !== t && (this._$AR(), this._$AH = this.O(t));
}
_(t) {
this._$AH !== d && x(this._$AH) ? this._$AA.nextSibling.data = t : this.T(g.createTextNode(t)), this._$AH = t;
}
$(t) {
var n;
const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = U.createElement(nt(s.h, s.h[0]), this.options)), s);
if (((n = this._$AH) == null ? void 0 : n._$AD) === i) this._$AH.p(e);
else {
const o = new gt(i, this), a = o.u(this.options);
o.p(e), this.T(a), this._$AH = o;
}
}
_$AC(t) {
let e = X.get(t.strings);
return e === void 0 && X.set(t.strings, e = new U(t)), e;
}
k(t) {
I(this._$AH) || (this._$AH = [], this._$AR());
const e = this._$AH;
let s, i = 0;
for (const n of t) i === e.length ? e.push(s = new O(this.O(P()), this.O(P()), this, this.options)) : s = e[i], s._$AI(n), i++;
i < e.length && (this._$AR(s && s._$AB.nextSibling, i), e.length = i);
}
_$AR(t = this._$AA.nextSibling, e) {
var s;
for ((s = this._$AP) == null ? void 0 : s.call(this, !1, !0, e); t && t !== this._$AB; ) {
const i = t.nextSibling;
t.remove(), t = i;
}
}
setConnected(t) {
var e;
this._$AM === void 0 && (this._$Cv = t, (e = this._$AP) == null || e.call(this, t));
}
}
class R {
get tagName() {
return this.element.tagName;
}
get _$AU() {
return this._$AM._$AU;
}
constructor(t, e, s, i, n) {
this.type = 1, this._$AH = d, this._$AN = void 0, this.element = t, this.name = e, this._$AM = i, this.options = n, s.length > 2 || s[0] !== "" || s[1] !== "" ? (this._$AH = Array(s.length - 1).fill(new String()), this.strings = s) : this._$AH = d;
}
_$AI(t, e = this, s, i) {
const n = this.strings;
let o = !1;
if (n === void 0) t = S(this, t, e, 0), o = !x(t) || t !== this._$AH && t !== E, o && (this._$AH = t);
else {
const a = t;
let h, c;
for (t = n[0], h = 0; h < n.length - 1; h++) c = S(this, a[s + h], e, h), c === E && (c = this._$AH[h]), o || (o = !x(c) || c !== this._$AH[h]), c === d ? t = d : t !== d && (t += (c ?? "") + n[h + 1]), this._$AH[h] = c;
}
o && !i && this.j(t);
}
j(t) {
t === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
}
}
class vt extends R {
constructor() {
super(...arguments), this.type = 3;
}
j(t) {
this.element[this.name] = t === d ? void 0 : t;
}
}
class Et extends R {
constructor() {
super(...arguments), this.type = 4;
}
j(t) {
this.element.toggleAttribute(this.name, !!t && t !== d);
}
}
class St extends R {
constructor(t, e, s, i, n) {
super(t, e, s, i, n), this.type = 5;
}
_$AI(t, e = this) {
if ((t = S(this, t, e, 0) ?? d) === E) return;
const s = this._$AH, i = t === d && s !== d || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, n = t !== d && (s === d || i);
i && this.element.removeEventListener(this.name, this, s), n && this.element.addEventListener(this.name, this, t), this._$AH = t;
}
handleEvent(t) {
var e;
typeof this._$AH == "function" ? this._$AH.call(((e = this.options) == null ? void 0 : e.host) ?? this.element, t) : this._$AH.handleEvent(t);
}
}
class bt {
constructor(t, e, s) {
this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = s;
}
get _$AU() {
return this._$AM._$AU;
}
_$AI(t) {
S(this, t);
}
}
const L = C.litHtmlPolyfillSupport;
L == null || L(U, O), (C.litHtmlVersions ?? (C.litHtmlVersions = [])).push("3.3.0");
const wt = (r, t, e) => {
const s = (e == null ? void 0 : e.renderBefore) ?? t;
let i = s._$litPart$;
if (i === void 0) {
const n = (e == null ? void 0 : e.renderBefore) ?? null;
s._$litPart$ = i = new O(t.insertBefore(P(), n), n, void 0, e ?? {});
}
return i._$AI(r), i;
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const y = globalThis;
class T extends v {
constructor() {
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
}
createRenderRoot() {
var e;
const t = super.createRenderRoot();
return (e = this.renderOptions).renderBefore ?? (e.renderBefore = t.firstChild), t;
}
update(t) {
const e = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = wt(e, this.renderRoot, this.renderOptions);
}
connectedCallback() {
var t;
super.connectedCallback(), (t = this._$Do) == null || t.setConnected(!0);
}
disconnectedCallback() {
var t;
super.disconnectedCallback(), (t = this._$Do) == null || t.setConnected(!1);
}
render() {
return E;
}
}
var Y;
T._$litElement$ = !0, T.finalized = !0, (Y = y.litElementHydrateSupport) == null || Y.call(y, { LitElement: T });
const z = y.litElementPolyfillSupport;
z == null || z({ LitElement: T });
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
export {
d as E,
et as f,
T as i,
j as u,
xt as x
};

View File

@@ -0,0 +1,550 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var _a;
const t$1 = globalThis, e$2 = t$1.ShadowRoot && (void 0 === t$1.ShadyCSS || t$1.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$3 = /* @__PURE__ */ new WeakMap();
let n$2 = class n {
constructor(t2, e2, o2) {
if (this._$cssResult$ = true, o2 !== s$2) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = t2, this.t = e2;
}
get styleSheet() {
let t2 = this.o;
const s2 = this.t;
if (e$2 && void 0 === t2) {
const e2 = void 0 !== s2 && 1 === s2.length;
e2 && (t2 = o$3.get(s2)), void 0 === t2 && ((this.o = t2 = new CSSStyleSheet()).replaceSync(this.cssText), e2 && o$3.set(s2, t2));
}
return t2;
}
toString() {
return this.cssText;
}
};
const r$2 = (t2) => new n$2("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => {
if (e$2) s2.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
else for (const e2 of o2) {
const o3 = document.createElement("style"), n3 = t$1.litNonce;
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
}
}, c$2 = e$2 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
let e2 = "";
for (const s2 of t3.cssRules) e2 += s2.cssText;
return r$2(e2);
})(t2) : t2;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPropertyNames: r$1, getOwnPropertySymbols: o$2, getPrototypeOf: n$1 } = Object, a$1 = globalThis, c$1 = a$1.trustedTypes, l$1 = c$1 ? c$1.emptyScript : "", p$1 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s2) => t2, u$1 = { toAttribute(t2, s2) {
switch (s2) {
case Boolean:
t2 = t2 ? l$1 : null;
break;
case Object:
case Array:
t2 = null == t2 ? t2 : JSON.stringify(t2);
}
return t2;
}, fromAttribute(t2, s2) {
let i2 = t2;
switch (s2) {
case Boolean:
i2 = null !== t2;
break;
case Number:
i2 = null === t2 ? null : Number(t2);
break;
case Object:
case Array:
try {
i2 = JSON.parse(t2);
} catch (t3) {
i2 = null;
}
}
return i2;
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), a$1.litPropertyMetadata ?? (a$1.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
let y$1 = class y extends HTMLElement {
static addInitializer(t2) {
this._$Ei(), (this.l ?? (this.l = [])).push(t2);
}
static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
}
static createProperty(t2, s2 = b) {
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
void 0 !== h2 && e$1(this.prototype, t2, h2);
}
}
static getPropertyDescriptor(t2, s2, i2) {
const { get: e2, set: r2 } = h$1(this.prototype, t2) ?? { get() {
return this[s2];
}, set(t3) {
this[s2] = t3;
} };
return { get: e2, set(s3) {
const h2 = e2 == null ? void 0 : e2.call(this);
r2 == null ? void 0 : r2.call(this, s3), this.requestUpdate(t2, h2, i2);
}, configurable: true, enumerable: true };
}
static getPropertyOptions(t2) {
return this.elementProperties.get(t2) ?? b;
}
static _$Ei() {
if (this.hasOwnProperty(d$1("elementProperties"))) return;
const t2 = n$1(this);
t2.finalize(), void 0 !== t2.l && (this.l = [...t2.l]), this.elementProperties = new Map(t2.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(d$1("finalized"))) return;
if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d$1("properties"))) {
const t3 = this.properties, s2 = [...r$1(t3), ...o$2(t3)];
for (const i2 of s2) this.createProperty(i2, t3[i2]);
}
const t2 = this[Symbol.metadata];
if (null !== t2) {
const s2 = litPropertyMetadata.get(t2);
if (void 0 !== s2) for (const [t3, i2] of s2) this.elementProperties.set(t3, i2);
}
this._$Eh = /* @__PURE__ */ new Map();
for (const [t3, s2] of this.elementProperties) {
const i2 = this._$Eu(t3, s2);
void 0 !== i2 && this._$Eh.set(i2, t3);
}
this.elementStyles = this.finalizeStyles(this.styles);
}
static finalizeStyles(s2) {
const i2 = [];
if (Array.isArray(s2)) {
const e2 = new Set(s2.flat(1 / 0).reverse());
for (const s3 of e2) i2.unshift(c$2(s3));
} else void 0 !== s2 && i2.push(c$2(s2));
return i2;
}
static _$Eu(t2, s2) {
const i2 = s2.attribute;
return false === i2 ? void 0 : "string" == typeof i2 ? i2 : "string" == typeof t2 ? t2.toLowerCase() : void 0;
}
constructor() {
super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
}
_$Ev() {
var _a2;
this._$ES = new Promise((t2) => this.enableUpdating = t2), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (_a2 = this.constructor.l) == null ? void 0 : _a2.forEach((t2) => t2(this));
}
addController(t2) {
var _a2;
(this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t2), void 0 !== this.renderRoot && this.isConnected && ((_a2 = t2.hostConnected) == null ? void 0 : _a2.call(t2));
}
removeController(t2) {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.delete(t2);
}
_$E_() {
const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;
for (const i2 of s2.keys()) this.hasOwnProperty(i2) && (t2.set(i2, this[i2]), delete this[i2]);
t2.size > 0 && (this._$Ep = t2);
}
createRenderRoot() {
const t2 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
return S$1(t2, this.constructor.elementStyles), t2;
}
connectedCallback() {
var _a2;
this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostConnected) == null ? void 0 : _a3.call(t2);
});
}
enableUpdating(t2) {
}
disconnectedCallback() {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t2) => {
var _a3;
return (_a3 = t2.hostDisconnected) == null ? void 0 : _a3.call(t2);
});
}
attributeChangedCallback(t2, s2, i2) {
this._$AK(t2, i2);
}
_$ET(t2, s2) {
var _a2;
const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2);
if (void 0 !== e2 && true === i2.reflect) {
const h2 = (void 0 !== ((_a2 = i2.converter) == null ? void 0 : _a2.toAttribute) ? i2.converter : u$1).toAttribute(s2, i2.type);
this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
}
}
_$AK(t2, s2) {
var _a2, _b;
const i2 = this.constructor, e2 = i2._$Eh.get(t2);
if (void 0 !== e2 && this._$Em !== e2) {
const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== ((_a2 = t3.converter) == null ? void 0 : _a2.fromAttribute) ? t3.converter : u$1;
this._$Em = e2, this[e2] = h2.fromAttribute(s2, t3.type) ?? ((_b = this._$Ej) == null ? void 0 : _b.get(e2)) ?? null, this._$Em = null;
}
}
requestUpdate(t2, s2, i2) {
var _a2;
if (void 0 !== t2) {
const e2 = this.constructor, h2 = this[t2];
if (i2 ?? (i2 = e2.getPropertyOptions(t2)), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === ((_a2 = this._$Ej) == null ? void 0 : _a2.get(t2)) && !this.hasAttribute(e2._$Eu(t2, i2)))) return;
this.C(t2, s2, i2);
}
false === this.isUpdatePending && (this._$ES = this._$EP());
}
C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) {
i2 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t2));
}
async _$EP() {
this.isUpdatePending = true;
try {
await this._$ES;
} catch (t3) {
Promise.reject(t3);
}
const t2 = this.scheduleUpdate();
return null != t2 && await t2, !this.isUpdatePending;
}
scheduleUpdate() {
return this.performUpdate();
}
performUpdate() {
var _a2;
if (!this.isUpdatePending) return;
if (!this.hasUpdated) {
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
for (const [t4, s3] of this._$Ep) this[t4] = s3;
this._$Ep = void 0;
}
const t3 = this.constructor.elementProperties;
if (t3.size > 0) for (const [s3, i2] of t3) {
const { wrapped: t4 } = i2, e2 = this[s3];
true !== t4 || this._$AL.has(s3) || void 0 === e2 || this.C(s3, void 0, i2, e2);
}
}
let t2 = false;
const s2 = this._$AL;
try {
t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), (_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdate) == null ? void 0 : _a3.call(t3);
}), this.update(s2)) : this._$EM();
} catch (s3) {
throw t2 = false, this._$EM(), s3;
}
t2 && this._$AE(s2);
}
willUpdate(t2) {
}
_$AE(t2) {
var _a2;
(_a2 = this._$EO) == null ? void 0 : _a2.forEach((t3) => {
var _a3;
return (_a3 = t3.hostUpdated) == null ? void 0 : _a3.call(t3);
}), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
}
_$EM() {
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
}
get updateComplete() {
return this.getUpdateComplete();
}
getUpdateComplete() {
return this._$ES;
}
shouldUpdate(t2) {
return true;
}
update(t2) {
this._$Eq && (this._$Eq = this._$Eq.forEach((t3) => this._$ET(t3, this[t3]))), this._$EM();
}
updated(t2) {
}
firstUpdated(t2) {
}
};
y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$1 == null ? void 0 : p$1({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ?? (a$1.reactiveElementVersions = [])).push("2.1.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof (t2 == null ? void 0 : t2[Symbol.iterator]), d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
function P(t2, i2) {
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
}
const V = (t2, i2) => {
const s2 = t2.length - 1, o2 = [];
let r2, l2 = 2 === i2 ? "<svg>" : 3 === i2 ? "<math>" : "", c2 = f;
for (let i3 = 0; i3 < s2; i3++) {
const s3 = t2[i3];
let a2, u2, d2 = -1, y3 = 0;
for (; y3 < s3.length && (c2.lastIndex = y3, u2 = c2.exec(s3), null !== u2); ) y3 = c2.lastIndex, c2 === f ? "!--" === u2[1] ? c2 = v : void 0 !== u2[1] ? c2 = _ : void 0 !== u2[2] ? ($.test(u2[2]) && (r2 = RegExp("</" + u2[2], "g")), c2 = m) : void 0 !== u2[3] && (c2 = m) : c2 === m ? ">" === u2[0] ? (c2 = r2 ?? f, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c2.lastIndex - u2[2].length, a2 = u2[1], c2 = void 0 === u2[3] ? m : '"' === u2[3] ? g : p) : c2 === g || c2 === p ? c2 = m : c2 === v || c2 === _ ? c2 = f : (c2 = m, r2 = void 0);
const x2 = c2 === m && t2[i3 + 1].startsWith("/>") ? " " : "";
l2 += c2 === f ? s3 + n2 : d2 >= 0 ? (o2.push(a2), s3.slice(0, d2) + e + s3.slice(d2) + h + x2) : s3 + h + (-2 === d2 ? i3 : x2);
}
return [P(t2, l2 + (t2[s2] || "<?>") + (2 === i2 ? "</svg>" : 3 === i2 ? "</math>" : "")), o2];
};
class N {
constructor({ strings: t2, _$litType$: s2 }, n3) {
let r2;
this.parts = [];
let c2 = 0, a2 = 0;
const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = V(t2, s2);
if (this.el = N.createElement(f2, n3), C.currentNode = this.el.content, 2 === s2 || 3 === s2) {
const t3 = this.el.content.firstChild;
t3.replaceWith(...t3.childNodes);
}
for (; null !== (r2 = C.nextNode()) && d2.length < u2; ) {
if (1 === r2.nodeType) {
if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(e)) {
const i2 = v2[a2++], s3 = r2.getAttribute(t3).split(h), e2 = /([.?@])?(.*)/.exec(i2);
d2.push({ type: 1, index: c2, name: e2[2], strings: s3, ctor: "." === e2[1] ? H : "?" === e2[1] ? I : "@" === e2[1] ? L : k }), r2.removeAttribute(t3);
} else t3.startsWith(h) && (d2.push({ type: 6, index: c2 }), r2.removeAttribute(t3));
if ($.test(r2.tagName)) {
const t3 = r2.textContent.split(h), s3 = t3.length - 1;
if (s3 > 0) {
r2.textContent = i$1 ? i$1.emptyScript : "";
for (let i2 = 0; i2 < s3; i2++) r2.append(t3[i2], l()), C.nextNode(), d2.push({ type: 2, index: ++c2 });
r2.append(t3[s3], l());
}
}
} else if (8 === r2.nodeType) if (r2.data === o$1) d2.push({ type: 2, index: c2 });
else {
let t3 = -1;
for (; -1 !== (t3 = r2.data.indexOf(h, t3 + 1)); ) d2.push({ type: 7, index: c2 }), t3 += h.length - 1;
}
c2++;
}
}
static createElement(t2, i2) {
const s2 = r.createElement("template");
return s2.innerHTML = t2, s2;
}
}
function S(t2, i2, s2 = t2, e2) {
var _a2, _b;
if (i2 === T) return i2;
let h2 = void 0 !== e2 ? (_a2 = s2._$Co) == null ? void 0 : _a2[e2] : s2._$Cl;
const o2 = c(i2) ? void 0 : i2._$litDirective$;
return (h2 == null ? void 0 : h2.constructor) !== o2 && ((_b = h2 == null ? void 0 : h2._$AO) == null ? void 0 : _b.call(h2, false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ?? (s2._$Co = []))[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = S(t2, h2._$AS(t2, i2.values), h2, e2)), i2;
}
class M {
constructor(t2, i2) {
this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i2;
}
get parentNode() {
return this._$AM.parentNode;
}
get _$AU() {
return this._$AM._$AU;
}
u(t2) {
const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = ((t2 == null ? void 0 : t2.creationScope) ?? r).importNode(i2, true);
C.currentNode = e2;
let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0];
for (; void 0 !== l2; ) {
if (o2 === l2.index) {
let i3;
2 === l2.type ? i3 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i3 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i3 = new z(h2, this, t2)), this._$AV.push(i3), l2 = s2[++n3];
}
o2 !== (l2 == null ? void 0 : l2.index) && (h2 = C.nextNode(), o2++);
}
return C.currentNode = r, e2;
}
p(t2) {
let i2 = 0;
for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i2), i2 += s2.strings.length - 2) : s2._$AI(t2[i2])), i2++;
}
}
class R {
get _$AU() {
var _a2;
return ((_a2 = this._$AM) == null ? void 0 : _a2._$AU) ?? this._$Cv;
}
constructor(t2, i2, s2, e2) {
this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = (e2 == null ? void 0 : e2.isConnected) ?? true;
}
get parentNode() {
let t2 = this._$AA.parentNode;
const i2 = this._$AM;
return void 0 !== i2 && 11 === (t2 == null ? void 0 : t2.nodeType) && (t2 = i2.parentNode), t2;
}
get startNode() {
return this._$AA;
}
get endNode() {
return this._$AB;
}
_$AI(t2, i2 = this) {
t2 = S(this, t2, i2), c(t2) ? t2 === E || null == t2 || "" === t2 ? (this._$AH !== E && this._$AR(), this._$AH = E) : t2 !== this._$AH && t2 !== T && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : u(t2) ? this.k(t2) : this._(t2);
}
O(t2) {
return this._$AA.parentNode.insertBefore(t2, this._$AB);
}
T(t2) {
this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2));
}
_(t2) {
this._$AH !== E && c(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r.createTextNode(t2)), this._$AH = t2;
}
$(t2) {
var _a2;
const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2);
if (((_a2 = this._$AH) == null ? void 0 : _a2._$AD) === e2) this._$AH.p(i2);
else {
const t3 = new M(e2, this), s3 = t3.u(this.options);
t3.p(i2), this.T(s3), this._$AH = t3;
}
}
_$AC(t2) {
let i2 = A.get(t2.strings);
return void 0 === i2 && A.set(t2.strings, i2 = new N(t2)), i2;
}
k(t2) {
a(this._$AH) || (this._$AH = [], this._$AR());
const i2 = this._$AH;
let s2, e2 = 0;
for (const h2 of t2) e2 === i2.length ? i2.push(s2 = new R(this.O(l()), this.O(l()), this, this.options)) : s2 = i2[e2], s2._$AI(h2), e2++;
e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2);
}
_$AR(t2 = this._$AA.nextSibling, i2) {
var _a2;
for ((_a2 = this._$AP) == null ? void 0 : _a2.call(this, false, true, i2); t2 && t2 !== this._$AB; ) {
const i3 = t2.nextSibling;
t2.remove(), t2 = i3;
}
}
setConnected(t2) {
var _a2;
void 0 === this._$AM && (this._$Cv = t2, (_a2 = this._$AP) == null ? void 0 : _a2.call(this, t2));
}
}
class k {
get tagName() {
return this.element.tagName;
}
get _$AU() {
return this._$AM._$AU;
}
constructor(t2, i2, s2, e2, h2) {
this.type = 1, this._$AH = E, this._$AN = void 0, this.element = t2, this.name = i2, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = E;
}
_$AI(t2, i2 = this, s2, e2) {
const h2 = this.strings;
let o2 = false;
if (void 0 === h2) t2 = S(this, t2, i2, 0), o2 = !c(t2) || t2 !== this._$AH && t2 !== T, o2 && (this._$AH = t2);
else {
const e3 = t2;
let n3, r2;
for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i2, n3), r2 === T && (r2 = this._$AH[n3]), o2 || (o2 = !c(r2) || r2 !== this._$AH[n3]), r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
}
o2 && !e2 && this.j(t2);
}
j(t2) {
t2 === E ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? "");
}
}
class H extends k {
constructor() {
super(...arguments), this.type = 3;
}
j(t2) {
this.element[this.name] = t2 === E ? void 0 : t2;
}
}
class I extends k {
constructor() {
super(...arguments), this.type = 4;
}
j(t2) {
this.element.toggleAttribute(this.name, !!t2 && t2 !== E);
}
}
class L extends k {
constructor(t2, i2, s2, e2, h2) {
super(t2, i2, s2, e2, h2), this.type = 5;
}
_$AI(t2, i2 = this) {
if ((t2 = S(this, t2, i2, 0) ?? E) === T) return;
const s2 = this._$AH, e2 = t2 === E && s2 !== E || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== E && (s2 === E || e2);
e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
}
handleEvent(t2) {
var _a2;
"function" == typeof this._$AH ? this._$AH.call(((_a2 = this.options) == null ? void 0 : _a2.host) ?? this.element, t2) : this._$AH.handleEvent(t2);
}
}
class z {
constructor(t2, i2, s2) {
this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i2, this.options = s2;
}
get _$AU() {
return this._$AM._$AU;
}
_$AI(t2) {
S(this, t2);
}
}
const j = t.litHtmlPolyfillSupport;
j == null ? void 0 : j(N, R), (t.litHtmlVersions ?? (t.litHtmlVersions = [])).push("3.3.0");
const B = (t2, i2, s2) => {
const e2 = (s2 == null ? void 0 : s2.renderBefore) ?? i2;
let h2 = e2._$litPart$;
if (void 0 === h2) {
const t3 = (s2 == null ? void 0 : s2.renderBefore) ?? null;
e2._$litPart$ = h2 = new R(i2.insertBefore(l(), t3), t3, void 0, s2 ?? {});
}
return h2._$AI(t2), h2;
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const s = globalThis;
class i extends y$1 {
constructor() {
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
}
createRenderRoot() {
var _a2;
const t2 = super.createRenderRoot();
return (_a2 = this.renderOptions).renderBefore ?? (_a2.renderBefore = t2.firstChild), t2;
}
update(t2) {
const r2 = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions);
}
connectedCallback() {
var _a2;
super.connectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(true);
}
disconnectedCallback() {
var _a2;
super.disconnectedCallback(), (_a2 = this._$Do) == null ? void 0 : _a2.setConnected(false);
}
render() {
return T;
}
}
i._$litElement$ = true, i["finalized"] = true, (_a = s.litElementHydrateSupport) == null ? void 0 : _a.call(s, { LitElement: i });
const o = s.litElementPolyfillSupport;
o == null ? void 0 : o({ LitElement: i });
(s.litElementVersions ?? (s.litElementVersions = [])).push("4.2.0");
export {
E,
f$1 as f,
i,
u$1 as u,
x
};

View File

@@ -0,0 +1,47 @@
import { f, u } from "./lit-z6_uA4GX.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t = (t2) => (e, o2) => {
void 0 !== o2 ? o2.addInitializer(() => {
customElements.define(t2, e);
}) : customElements.define(t2, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const o = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }, r = (t2 = o, e, r2) => {
const { kind: n2, metadata: i } = r2;
let s = globalThis.litPropertyMetadata.get(i);
if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n2 && ((t2 = Object.create(t2)).wrapped = true), s.set(r2.name, t2), "accessor" === n2) {
const { name: o2 } = r2;
return { set(r3) {
const n3 = e.get.call(this);
e.set.call(this, r3), this.requestUpdate(o2, n3, t2);
}, init(e2) {
return void 0 !== e2 && this.C(o2, void 0, t2, e2), e2;
} };
}
if ("setter" === n2) {
const { name: o2 } = r2;
return function(r3) {
const n3 = this[o2];
e.call(this, r3), this.requestUpdate(o2, n3, t2);
};
}
throw Error("Unsupported decorator location: " + n2);
};
function n(t2) {
return (e, o2) => "object" == typeof o2 ? r(t2, e, o2) : ((t3, e2, o3) => {
const r2 = e2.hasOwnProperty(o3);
return e2.constructor.createProperty(o3, t3), r2 ? Object.getOwnPropertyDescriptor(e2, o3) : void 0;
})(t2, e, o2);
}
export {
n,
t
};

View File

@@ -1,172 +0,0 @@
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const T = (t) => (e, s) => {
s !== void 0 ? s.addInitializer(() => {
customElements.define(t, e);
}) : customElements.define(t, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
const { kind: i, metadata: n } = s;
let r = globalThis.litPropertyMetadata.get(n);
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
const { name: o } = s;
return { set(h) {
const l = e.get.call(this);
e.set.call(this, h), this.requestUpdate(o, l, t);
}, init(h) {
return h !== void 0 && this.C(o, void 0, t, h), h;
} };
}
if (i === "setter") {
const { name: o } = s;
return function(h) {
const l = this[o];
e.call(this, h), this.requestUpdate(o, l, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function O(t) {
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
const o = n.hasOwnProperty(r);
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
})(t, e, s);
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const v = (t) => t.strings === void 0;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
class m {
constructor(e) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(e, s, i) {
this._$Ct = e, this._$AM = s, this._$Ci = i;
}
_$AS(e, s) {
return this.update(e, s);
}
update(e, s) {
return this.render(...s);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const c = (t, e) => {
var i;
const s = t._$AN;
if (s === void 0) return !1;
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
return !0;
}, a = (t) => {
let e, s;
do {
if ((e = t._$AM) === void 0) break;
s = e._$AN, s.delete(t), t = e;
} while ((s == null ? void 0 : s.size) === 0);
}, u = (t) => {
for (let e; e = t._$AM; t = e) {
let s = e._$AN;
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
else if (s.has(t)) break;
s.add(t), M(e);
}
};
function y(t) {
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
}
function G(t, e = !1, s = 0) {
const i = this._$AH, n = this._$AN;
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
else i != null && (c(i, !1), a(i));
else c(this, t);
}
const M = (t) => {
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
};
class b extends m {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(e, s, i) {
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
}
_$AO(e, s = !0) {
var i, n;
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
}
setValue(e) {
if (v(this._$Ct)) this._$Ct._$AI(e, this);
else {
const s = [...this._$Ct._$AH];
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const P = () => new w();
class w {
}
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
render(t) {
return $;
}
update(t, [e]) {
var i;
const s = e !== this.G;
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
}
rt(t) {
if (this.isConnected || (t = void 0), typeof this.G == "function") {
const e = this.ht ?? globalThis;
let s = d.get(e);
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
} else this.G.value = t;
}
get lt() {
var t, e;
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
O as a,
P as e,
U as n,
T as t
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@
<h2>Overview of licenses:</h2>
<ul class="licenses-overview">
<li><a href="#Apache-2.0">Apache License 2.0</a> (278)</li>
<li><a href="#Apache-2.0">Apache License 2.0</a> (280)</li>
<li><a href="#MIT">MIT License</a> (83)</li>
<li><a href="#Unicode-3.0">Unicode License v3</a> (19)</li>
<li><a href="#AGPL-3.0">GNU Affero General Public License v3.0</a> (12)</li>
@@ -61,7 +61,7 @@
<h3 id="AGPL-3.0">GNU Affero General Public License v3.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/lennart-k/rustical ">rustical 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical 0.4.5</a></li>
</ul>
<pre class="license-text"> GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
@@ -730,16 +730,16 @@ For more information on this, and how to apply and follow the GNU AGPL, see
<h3 id="AGPL-3.0">GNU Affero General Public License v3.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/lennart-k/rustical ">rustical_caldav 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_carddav 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_dav 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_dav_push 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_frontend 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_ical 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_oidc 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_store 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_store_sqlite 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_xml 0.2.2</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_caldav 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_carddav 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_dav 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_dav_push 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_frontend 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_ical 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_oidc 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_store 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_store_sqlite 0.4.5</a></li>
<li><a href=" https://github.com/lennart-k/rustical ">rustical_xml 0.4.5</a></li>
<li><a href=" https://crates.io/crates/xml_derive ">xml_derive 0.1.0</a></li>
</ul>
<pre class="license-text">GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1195,7 +1195,7 @@ You should also get your employer (if you work as a programmer) or school, if an
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide ">miniz_oxide 0.8.8</a></li>
<li><a href=" https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide ">miniz_oxide 0.8.9</a></li>
<li><a href=" https://github.com/taiki-e/pin-project-lite ">pin-project-lite 0.2.16</a></li>
<li><a href=" https://github.com/Actyx/sync_wrapper ">sync_wrapper 1.0.2</a></li>
<li><a href=" https://github.com/time-rs/time ">time-core 0.1.4</a></li>
@@ -2228,7 +2228,7 @@ You should also get your employer (if you work as a programmer) or school, if an
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/bytecodealliance/wasi ">wasi 0.11.0+wasi-snapshot-preview1</a></li>
<li><a href=" https://github.com/bytecodealliance/wasi ">wasi 0.11.1+wasi-snapshot-preview1</a></li>
<li><a href=" https://github.com/bytecodealliance/wasi-rs ">wasi 0.14.2+wasi-0.2.4</a></li>
</ul>
<pre class="license-text">
@@ -2663,6 +2663,191 @@ Software.
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
</pre>
</li>
<li class="license">
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/alexcrichton/openssl-src-rs ">openssl-src 300.5.0+3.5.0</a></li>
</ul>
<pre class="license-text">
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
&quot;License&quot; shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
&quot;Licensor&quot; shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
&quot;Legal Entity&quot; shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
&quot;control&quot; means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
&quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
exercising permissions granted by this License.
&quot;Source&quot; form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
&quot;Object&quot; form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
&quot;Work&quot; shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
&quot;Derivative Works&quot; shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
&quot;Contribution&quot; shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, &quot;submitted&quot;
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
&quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a &quot;NOTICE&quot; text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
</pre>
</li>
<li class="license">
@@ -2857,7 +3042,7 @@ Software.
<li><a href=" https://github.com/microsoft/windows-rs ">windows-core 0.61.2</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-implement 0.60.0</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-interface 0.59.1</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-link 0.1.1</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-link 0.1.3</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-result 0.3.4</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-strings 0.4.2</a></li>
<li><a href=" https://github.com/microsoft/windows-rs ">windows-sys 0.52.0</a></li>
@@ -3288,7 +3473,7 @@ Software.
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/google/zerocopy ">zerocopy 0.8.25</a></li>
<li><a href=" https://github.com/google/zerocopy ">zerocopy 0.8.26</a></li>
</ul>
<pre class="license-text"> Apache License
Version 2.0, January 2004
@@ -3492,217 +3677,6 @@ Software.
See the License for the specific language governing permissions and
limitations under the License.
</pre>
</li>
<li class="license">
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/clap-rs/clap ">clap_builder 4.5.39</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap_derive 4.5.32</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap_lex 0.7.4</a></li>
</ul>
<pre class="license-text"> Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
&quot;License&quot; shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
&quot;Licensor&quot; shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
&quot;Legal Entity&quot; shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
&quot;control&quot; means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
&quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
exercising permissions granted by this License.
&quot;Source&quot; form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
&quot;Object&quot; form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
&quot;Work&quot; shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
&quot;Derivative Works&quot; shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
&quot;Contribution&quot; shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, &quot;submitted&quot;
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
&quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a &quot;NOTICE&quot; text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets &quot;[]&quot;
replaced with your own identifying information. (Don&#x27;t include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same &quot;printed page&quot; as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
</pre>
</li>
<li class="license">
@@ -4127,7 +4101,6 @@ Software.
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-parse 0.2.6</a></li>
<li><a href=" https://github.com/retep998/winapi-rs ">winapi 0.3.9</a></li>
</ul>
<pre class="license-text"> Apache License
@@ -4337,12 +4310,16 @@ Software.
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstream 0.6.18</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-query 1.1.2</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-wincon 3.0.8</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle 1.0.10</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap 4.5.39</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">colorchoice 1.0.3</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstream 0.6.19</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-parse 0.2.7</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-query 1.1.3</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle-wincon 3.0.9</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">anstyle 1.0.11</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap 4.5.40</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap_builder 4.5.40</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap_derive 4.5.40</a></li>
<li><a href=" https://github.com/clap-rs/clap ">clap_lex 0.7.5</a></li>
<li><a href=" https://github.com/rust-cli/anstyle.git ">colorchoice 1.0.4</a></li>
<li><a href=" https://github.com/sfackler/foreign-types ">foreign-types-shared 0.1.1</a></li>
<li><a href=" https://github.com/sfackler/foreign-types ">foreign-types 0.3.2</a></li>
<li><a href=" https://github.com/KokaKiwi/rust-hex ">hex 0.4.3</a></li>
@@ -4350,11 +4327,11 @@ Software.
<li><a href=" https://github.com/polyfill-rs/once_cell_polyfill ">once_cell_polyfill 1.70.1</a></li>
<li><a href=" https://crates.io/crates/openssl-macros ">openssl-macros 0.1.1</a></li>
<li><a href=" https://github.com/sfackler/rust-openssl ">openssl 0.10.73</a></li>
<li><a href=" https://github.com/toml-rs/toml ">serde_spanned 0.6.8</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml 0.8.22</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_datetime 0.6.9</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_edit 0.22.26</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_write 0.1.1</a></li>
<li><a href=" https://github.com/toml-rs/toml ">serde_spanned 0.6.9</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml 0.8.23</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_datetime 0.6.11</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_edit 0.22.27</a></li>
<li><a href=" https://github.com/toml-rs/toml ">toml_write 0.1.2</a></li>
</ul>
<pre class="license-text"> Apache License
Version 2.0, January 2004
@@ -5171,7 +5148,7 @@ END OF TERMS AND CONDITIONS</pre>
<li><a href=" https://github.com/dtolnay/dyn-clone ">dyn-clone 1.0.19</a></li>
<li><a href=" https://github.com/SergioBenitez/Figment ">figment 0.10.19</a></li>
<li><a href=" https://github.com/dtolnay/itoa ">itoa 1.0.15</a></li>
<li><a href=" https://github.com/rust-lang/libc ">libc 0.2.172</a></li>
<li><a href=" https://github.com/rust-lang/libc ">libc 0.2.174</a></li>
<li><a href=" https://github.com/SergioBenitez/proc-macro2-diagnostics ">proc-macro2-diagnostics 0.10.1</a></li>
<li><a href=" https://github.com/dtolnay/proc-macro2 ">proc-macro2 1.0.95</a></li>
<li><a href=" https://github.com/dtolnay/quote ">quote 1.0.40</a></li>
@@ -5183,7 +5160,7 @@ END OF TERMS AND CONDITIONS</pre>
<li><a href=" https://github.com/serde-rs/json ">serde_json 1.0.140</a></li>
<li><a href=" https://github.com/dtolnay/path-to-error ">serde_path_to_error 0.1.17</a></li>
<li><a href=" https://github.com/nox/serde_urlencoded ">serde_urlencoded 0.7.1</a></li>
<li><a href=" https://github.com/dtolnay/syn ">syn 2.0.101</a></li>
<li><a href=" https://github.com/dtolnay/syn ">syn 2.0.104</a></li>
<li><a href=" https://github.com/dtolnay/thiserror ">thiserror-impl 1.0.69</a></li>
<li><a href=" https://github.com/dtolnay/thiserror ">thiserror-impl 2.0.12</a></li>
<li><a href=" https://github.com/dtolnay/thiserror ">thiserror 1.0.69</a></li>
@@ -6009,7 +5986,7 @@ limitations under the License.</pre>
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/seanmonstar/reqwest ">reqwest 0.12.19</a></li>
<li><a href=" https://github.com/seanmonstar/reqwest ">reqwest 0.12.20</a></li>
</ul>
<pre class="license-text"> Apache License
Version 2.0, January 2004
@@ -7058,7 +7035,7 @@ limitations under the License.
<li><a href=" https://github.com/askama-rs/askama ">askama 0.14.0</a></li>
<li><a href=" https://github.com/askama-rs/askama ">askama_derive 0.14.0</a></li>
<li><a href=" https://github.com/askama-rs/askama ">askama_parser 0.14.0</a></li>
<li><a href=" https://github.com/rinja-rs/askama_web/ ">askama_web 0.14.3</a></li>
<li><a href=" https://github.com/rinja-rs/askama_web/ ">askama_web 0.14.4</a></li>
<li><a href=" https://github.com/rinja-rs/askama_web/ ">askama_web_derive 0.1.0</a></li>
</ul>
<pre class="license-text"> Apache License
@@ -8527,15 +8504,15 @@ limitations under the License.</pre>
<ul class="license-used-by">
<li><a href=" https://github.com/gimli-rs/addr2line ">addr2line 0.24.2</a></li>
<li><a href=" https://github.com/smol-rs/atomic-waker ">atomic-waker 1.1.2</a></li>
<li><a href=" https://github.com/Amanieu/atomic-rs ">atomic 0.6.0</a></li>
<li><a href=" https://github.com/cuviper/autocfg ">autocfg 1.4.0</a></li>
<li><a href=" https://github.com/Amanieu/atomic-rs ">atomic 0.6.1</a></li>
<li><a href=" https://github.com/cuviper/autocfg ">autocfg 1.5.0</a></li>
<li><a href=" https://github.com/rust-lang/backtrace-rs ">backtrace 0.3.75</a></li>
<li><a href=" https://github.com/marshallpierce/rust-base64 ">base64 0.21.7</a></li>
<li><a href=" https://github.com/marshallpierce/rust-base64 ">base64 0.22.1</a></li>
<li><a href=" https://github.com/bitflags/bitflags ">bitflags 2.9.1</a></li>
<li><a href=" https://github.com/fitzgen/bumpalo ">bumpalo 3.17.0</a></li>
<li><a href=" https://github.com/rust-lang/cc-rs ">cc 1.2.25</a></li>
<li><a href=" https://github.com/alexcrichton/cfg-if ">cfg-if 1.0.0</a></li>
<li><a href=" https://github.com/fitzgen/bumpalo ">bumpalo 3.19.0</a></li>
<li><a href=" https://github.com/rust-lang/cc-rs ">cc 1.2.27</a></li>
<li><a href=" https://github.com/rust-lang/cfg-if ">cfg-if 1.0.1</a></li>
<li><a href=" https://github.com/smol-rs/concurrent-queue ">concurrent-queue 2.5.0</a></li>
<li><a href=" https://github.com/servo/core-foundation-rs ">core-foundation-sys 0.8.7</a></li>
<li><a href=" https://github.com/crossbeam-rs/crossbeam ">crossbeam-queue 0.3.12</a></li>
@@ -8551,10 +8528,10 @@ limitations under the License.</pre>
<li><a href=" https://github.com/gimli-rs/gimli ">gimli 0.31.1</a></li>
<li><a href=" https://github.com/rust-lang/glob ">glob 0.3.2</a></li>
<li><a href=" https://github.com/zkcrypto/group ">group 0.13.0</a></li>
<li><a href=" https://github.com/rust-lang/hashbrown ">hashbrown 0.15.3</a></li>
<li><a href=" https://github.com/rust-lang/hashbrown ">hashbrown 0.15.4</a></li>
<li><a href=" https://github.com/withoutboats/heck ">heck 0.5.0</a></li>
<li><a href=" https://github.com/seanmonstar/httparse ">httparse 1.10.1</a></li>
<li><a href=" https://github.com/rustls/hyper-rustls ">hyper-rustls 0.27.6</a></li>
<li><a href=" https://github.com/rustls/hyper-rustls ">hyper-rustls 0.27.7</a></li>
<li><a href=" https://github.com/servo/rust-url/ ">idna 1.0.3</a></li>
<li><a href=" https://github.com/hsivonen/idna_adapter ">idna_adapter 1.2.1</a></li>
<li><a href=" https://github.com/indexmap-rs/indexmap ">indexmap 2.9.0</a></li>
@@ -8573,6 +8550,7 @@ limitations under the License.</pre>
<li><a href=" https://github.com/ramosbugs/oauth2-rs ">oauth2 5.0.0</a></li>
<li><a href=" https://github.com/gimli-rs/object ">object 0.36.7</a></li>
<li><a href=" https://github.com/matklad/once_cell ">once_cell 1.21.3</a></li>
<li><a href=" https://github.com/alexcrichton/openssl-src-rs ">openssl-src 300.5.0+3.5.0</a></li>
<li><a href=" https://github.com/smol-rs/parking ">parking 2.2.1</a></li>
<li><a href=" https://github.com/Amanieu/parking_lot ">parking_lot 0.12.4</a></li>
<li><a href=" https://github.com/Amanieu/parking_lot ">parking_lot_core 0.9.11</a></li>
@@ -8583,24 +8561,24 @@ limitations under the License.</pre>
<li><a href=" https://github.com/rust-lang/regex/tree/master/regex-syntax ">regex-syntax 0.8.5</a></li>
<li><a href=" https://github.com/rust-lang/regex ">regex 1.11.1</a></li>
<li><a href=" https://github.com/briansmith/ring ">ring 0.17.14</a></li>
<li><a href=" https://github.com/rust-lang/rustc-demangle ">rustc-demangle 0.1.24</a></li>
<li><a href=" https://github.com/rust-lang/rustc-demangle ">rustc-demangle 0.1.25</a></li>
<li><a href=" https://github.com/djc/rustc-version-rs ">rustc_version 0.4.1</a></li>
<li><a href=" https://github.com/rustls/rustls ">rustls 0.23.27</a></li>
<li><a href=" https://github.com/rustls/rustls ">rustls 0.23.28</a></li>
<li><a href=" https://github.com/bluss/scopeguard ">scopeguard 1.2.0</a></li>
<li><a href=" https://github.com/mitsuhiko/serde-plain ">serde_plain 1.0.2</a></li>
<li><a href=" https://github.com/jonasbb/serde_with/ ">serde_with 3.12.0</a></li>
<li><a href=" https://github.com/jonasbb/serde_with/ ">serde_with_macros 3.12.0</a></li>
<li><a href=" https://github.com/jonasbb/serde_with/ ">serde_with 3.13.0</a></li>
<li><a href=" https://github.com/jonasbb/serde_with/ ">serde_with_macros 3.13.0</a></li>
<li><a href=" https://github.com/vorner/signal-hook ">signal-hook-registry 1.4.5</a></li>
<li><a href=" https://github.com/servo/rust-smallvec ">smallvec 1.15.0</a></li>
<li><a href=" https://github.com/servo/rust-smallvec ">smallvec 1.15.1</a></li>
<li><a href=" https://github.com/rust-lang/socket2 ">socket2 0.5.10</a></li>
<li><a href=" https://github.com/storyyeller/stable_deref_trait ">stable_deref_trait 1.2.0</a></li>
<li><a href=" https://github.com/Amanieu/thread_local-rs ">thread_local 1.1.8</a></li>
<li><a href=" https://github.com/Amanieu/thread_local-rs ">thread_local 1.1.9</a></li>
<li><a href=" https://github.com/seanmonstar/unicase ">unicase 2.8.1</a></li>
<li><a href=" https://github.com/unicode-rs/unicode-xid ">unicode-xid 0.2.6</a></li>
<li><a href=" https://github.com/servo/rust-url ">url 2.5.4</a></li>
<li><a href=" https://github.com/uuid-rs/uuid ">uuid 1.17.0</a></li>
<li><a href=" https://github.com/SergioBenitez/version_check ">version_check 0.9.5</a></li>
<li><a href=" https://github.com/bytecodealliance/wasi ">wasi 0.11.0+wasi-snapshot-preview1</a></li>
<li><a href=" https://github.com/bytecodealliance/wasi ">wasi 0.11.1+wasi-snapshot-preview1</a></li>
<li><a href=" https://github.com/bytecodealliance/wasi-rs ">wasi 0.14.2+wasi-0.2.4</a></li>
<li><a href=" https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend ">wasm-bindgen-backend 0.2.100</a></li>
<li><a href=" https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures ">wasm-bindgen-futures 0.4.50</a></li>
@@ -10271,7 +10249,7 @@ limitations under the License.
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/oyvindln/adler2 ">adler2 2.0.0</a></li>
<li><a href=" https://github.com/oyvindln/adler2 ">adler2 2.0.1</a></li>
<li><a href=" https://github.com/bkchr/proc-macro-crate ">proc-macro-crate 3.3.0</a></li>
</ul>
<pre class="license-text"> Apache License
@@ -11096,7 +11074,7 @@ limitations under the License.
<h3 id="Apache-2.0">Apache License 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/Lokathor/bytemuck ">bytemuck 1.23.0</a></li>
<li><a href=" https://github.com/Lokathor/bytemuck ">bytemuck 1.23.1</a></li>
</ul>
<pre class="license-text">Apache License
Version 2.0, January 2004
@@ -11591,7 +11569,7 @@ limitations under the License.
<li><a href=" https://github.com/TedDriggs/ident_case ">ident_case 1.0.1</a></li>
<li><a href=" https://github.com/SergioBenitez/Pear ">pear 0.2.9</a></li>
<li><a href=" https://github.com/SergioBenitez/Pear ">pear_codegen 0.2.9</a></li>
<li><a href=" https://github.com/r-efi/r-efi ">r-efi 5.2.0</a></li>
<li><a href=" https://github.com/r-efi/r-efi ">r-efi 5.3.0</a></li>
<li><a href=" https://github.com/udoprog/relative-path ">relative-path 1.9.3</a></li>
<li><a href=" https://github.com/fmeringdal/rust-rrule ">rrule 0.14.0</a></li>
<li><a href=" https://github.com/jedisct1/rust-siphash ">siphasher 1.0.1</a></li>
@@ -12006,7 +11984,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<h3 id="CDLA-Permissive-2.0">Community Data License Agreement Permissive 2.0</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/rustls/webpki-roots ">webpki-roots 1.0.0</a></li>
<li><a href=" https://github.com/rustls/webpki-roots ">webpki-roots 1.0.1</a></li>
</ul>
<pre class="license-text"># Community Data License Agreement - Permissive - Version 2.0
@@ -12391,7 +12369,7 @@ THE SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://gitlab.redox-os.org/redox-os/syscall ">redox_syscall 0.5.12</a></li>
<li><a href=" https://gitlab.redox-os.org/redox-os/syscall ">redox_syscall 0.5.13</a></li>
</ul>
<pre class="license-text">Copyright (c) 2017 Redox OS Developers
@@ -12544,7 +12522,7 @@ THE SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/tokio-rs/slab ">slab 0.4.9</a></li>
<li><a href=" https://github.com/tokio-rs/slab ">slab 0.4.10</a></li>
</ul>
<pre class="license-text">Copyright (c) 2019 Carl Lerche
@@ -12631,8 +12609,8 @@ SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-attributes 0.1.28</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-core 0.1.33</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-attributes 0.1.30</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-core 0.1.34</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-log 0.2.0</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-subscriber 0.3.19</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing 0.1.41</a></li>
@@ -12835,7 +12813,7 @@ DEALINGS IN THE SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/hyperium/hyper-util ">hyper-util 0.1.13</a></li>
<li><a href=" https://github.com/hyperium/hyper-util ">hyper-util 0.1.14</a></li>
</ul>
<pre class="license-text">Copyright (c) 2023-2025 Sean McArthur
@@ -13217,7 +13195,7 @@ SOFTWARE.</pre>
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/winnow-rs/winnow ">winnow 0.7.10</a></li>
<li><a href=" https://github.com/winnow-rs/winnow ">winnow 0.7.11</a></li>
</ul>
<pre class="license-text">Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -13301,7 +13279,7 @@ SOFTWARE.</pre>
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-core 0.1.33</a></li>
<li><a href=" https://github.com/tokio-rs/tracing ">tracing-core 0.1.34</a></li>
</ul>
<pre class="license-text">The MIT License (MIT)
@@ -13363,7 +13341,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<ul class="license-used-by">
<li><a href=" https://github.com/BurntSushi/aho-corasick ">aho-corasick 1.1.3</a></li>
<li><a href=" https://github.com/BurntSushi/byteorder ">byteorder 1.5.0</a></li>
<li><a href=" https://github.com/BurntSushi/memchr ">memchr 2.7.4</a></li>
<li><a href=" https://github.com/BurntSushi/memchr ">memchr 2.7.5</a></li>
<li><a href=" https://github.com/BurntSushi/regex-automata ">regex-automata 0.1.10</a></li>
<li><a href=" https://github.com/BurntSushi/walkdir ">walkdir 2.5.0</a></li>
</ul>
@@ -13608,7 +13586,7 @@ THE SOFTWARE.
<ul class="license-used-by">
<li><a href=" https://github.com/BurntSushi/aho-corasick ">aho-corasick 1.1.3</a></li>
<li><a href=" https://github.com/BurntSushi/byteorder ">byteorder 1.5.0</a></li>
<li><a href=" https://github.com/BurntSushi/memchr ">memchr 2.7.4</a></li>
<li><a href=" https://github.com/BurntSushi/memchr ">memchr 2.7.5</a></li>
<li><a href=" https://github.com/BurntSushi/regex-automata ">regex-automata 0.1.10</a></li>
<li><a href=" https://github.com/BurntSushi/same-file ">same-file 1.0.6</a></li>
<li><a href=" https://github.com/BurntSushi/walkdir ">walkdir 2.5.0</a></li>

View File

@@ -1,7 +1,44 @@
:root {
--background-color: #FFF;
--background-darker: #EEE;
--text-on-background-color: #111;
--primary-color: #2F2FE1;
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
--text-on-primary-color: #FFF;
/* --color-red: #FE2060; */
/* --color-red: #EE1D59; */
--color-red: #E31B39;
--dilute-color: black;
--border-color: black;
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #222;
--background-darker: #292929;
--text-on-background-color: #CACACA;
--primary-color: color-mix(in srgb, #2F2FE1, white 15%);
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
--text-on-primary-color: #FFF;
/* --color-red: #FE2060; */
--color-red: #EE1D59;
--dilute-color: white;
--border-color: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
}
}
html,
dialog {
background-color: var(--background-color);
color: var(--text-on-background-color);
}
body {
font-family: sans-serif;
/* position: relative; */
font-family: 'Noto Sans', Helvetica, Arial, sans-serif;
margin: 0 auto;
max-width: 1200px;
min-height: 100%;
}
* {
@@ -12,32 +49,65 @@ body {
padding: 12px;
}
a {
color: var(--text-on-background-color);
}
header {
background: var(--background-darker);
height: 60px;
min-height: 60px;
font-weight: bold;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
padding: 12px;
padding: 4px 12px;
border: 2px solid black;
border: 2px solid var(--border-color);
border-radius: 12px;
margin: 12px;
box-shadow: 4px 2px 12px -5px black;
a {
display: flex;
justify-content: space-between;
a.logo {
font-size: 2em;
text-decoration: none;
color: black;
}
nav {
display: flex;
border-radius: 12px;
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
a {
text-decoration: none;
margin: 4px 8px;
padding: 8px 12px;
border-radius: 12px;
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 2%);
&:hover {
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 20%);
}
&.active {
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 15%);
}
svg.icon {
width: 1.3em;
vertical-align: bottom;
margin-right: 6px;
}
}
}
.logout_form {
display: contents;
button {
margin-left: auto;
}
}
}
@@ -48,25 +118,11 @@ header {
align-items: center;
}
:root {
--background-color: #FFF;
--background-darker: #EEE;
--primary-color: #2F2FE1;
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
--text-on-primary-color: #FFF;
/* --color-red: #FE2060; */
--color-red: #EE1D59;
}
html {
background-color: var(--background-color);
}
button,
.button {
border: none;
background: var(--primary-color);
padding: 8px 12px;
padding: 8px 16px;
border-radius: 8px;
color: var(--text-on-primary-color);
font-size: 0.9em;
@@ -75,7 +131,8 @@ button,
filter: brightness(90%);
}
&.delete {
&.delete,
&.cancel {
background: var(--color-red);
}
}
@@ -93,7 +150,7 @@ input[type="password"] {
}
section {
border: 1px solid black;
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 4px 2px 12px -8px black;
border-collapse: collapse;
@@ -104,30 +161,26 @@ section {
}
table {
border: 1px solid black;
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 4px 2px 12px -6px black;
border-collapse: collapse;
overflow-x: scroll;
display: block;
width: fit-content;
width: 100%;
td,
th {
padding: 8px;
border: 1px solid black;
border: 1px solid var(--border-color);
width: max-content;
}
th {
height: 40px;
}
/* tr:nth-of-type(2n+1) { */
/* background: var(--background-darker); */
/* } */
tr:hover {
background: #DDD;
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
}
tr:first-child th {
@@ -147,85 +200,108 @@ table {
}
}
#page-user {
ul {
padding-left: 0;
ul.collection-list {
padding-left: 0;
li.collection-list-item {
list-style: none;
display: contents;
li.collection-list-item {
list-style: none;
display: block;
position: relative;
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
border: 2px solid var(--border-color);
border-radius: 12px;
margin: 12px 0;
box-shadow: 4px 2px 12px -6px black;
overflow: hidden;
a {
background: #EEE;
display: grid;
min-height: 80px;
grid-template-areas:
". . color-chip"
"title comps color-chip"
"description . color-chip"
"subscription-url . color-chip"
"actions . color-chip"
". . color-chip";
grid-template-rows: 12px auto auto auto auto 12px;
grid-template-columns: min-content auto 80px;
color: inherit;
text-decoration: none;
padding-left: 12px;
a {
position: absolute;
inset: 2px;
}
border: 2px solid black;
border-radius: 12px;
margin: 12px;
box-shadow: 4px 2px 12px -6px black;
overflow: hidden;
.inner {
display: grid;
min-height: 80px;
height: fit-content;
grid-template-areas:
". . color-chip"
"title comps color-chip"
"description description color-chip"
"subscription-url subscription-url color-chip"
"metadata metadata color-chip"
"actions actions color-chip"
". . color-chip";
grid-template-rows: 12px auto auto auto auto auto 12px;
grid-template-columns: min-content auto 80px;
row-gap: 4px;
color: inherit;
text-decoration: none;
padding-left: 12px;
.title {
font-weight: bold;
grid-area: title;
margin-right: 12px;
white-space: nowrap;
}
position: relative;
z-index: 1;
pointer-events: none;
a,
button {
pointer-events: all;
cursor: pointer;
}
.title {
font-weight: bold;
grid-area: title;
margin-right: 12px;
white-space: nowrap;
}
span {
margin: 8px initial;
}
.comps {
grid-area: comps;
span {
margin: 8px initial;
}
.comps {
grid-area: comps;
span {
margin: 0 2px;
background: var(--primary-color);
color: var(--text-on-primary-color);
font-size: .8em;
padding: 3px 8px;
border-radius: 12px;
}
}
.description {
grid-area: description;
white-space: nowrap;
}
.subscription-url {
grid-area: subscription-url;
white-space: nowrap;
}
.color-chip {
background: var(--color);
grid-area: color-chip;
}
.actions {
grid-area: actions;
width: fit-content;
}
&:hover {
background: #DDD;
margin: 0 2px;
background: var(--primary-color);
color: var(--text-on-primary-color);
font-size: .8em;
padding: 3px 8px;
border-radius: 12px;
}
}
.description {
grid-area: description;
white-space: nowrap;
}
.metadata {
grid-area: metadata;
white-space: nowrap;
}
.subscription-url {
grid-area: subscription-url;
white-space: nowrap;
}
.color-chip {
background: var(--color);
grid-area: color-chip;
}
.actions {
grid-area: actions;
width: fit-content;
display: flex;
gap: 12px;
}
}
&:hover {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
}
}
}
@@ -233,3 +309,40 @@ table {
textarea {
width: 100%;
}
dialog {
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 32px;
}
footer {
display: flex;
justify-content: center;
margin-top: 32px;
gap: 24px;
bottom: 20px;
}
input[type="text"],
input[type="password"],
input[type="color"],
select {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
border: 2px solid var(--border-color);
padding: 6px 6px;
color: var(--text-on-background-color);
margin: 2px;
border-radius: 8px;
&:hover,
&:focus {
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 20%);
}
}
svg.icon {
stroke-width: 2px;
color: var(--text-on-background-color);
stroke: var(--text-on-background-color);
}

View File

@@ -0,0 +1,62 @@
<h2>{{user.id }}'s Addressbooks</h2>
<ul class="collection-list">
{% for (meta, addressbook) in addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
<span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
</span>
<span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
method="GET">
<button type="submit">Download</button>
</form>
<delete-button trash
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div>
</div>
</li>
{% else %}
You do not have any addressbooks yet
{% endfor %}
</ul>
{%if !deleted_addressbooks.is_empty() %}
<h3>Deleted Addressbooks</h3>
<ul class="collection-list">
{% for (meta, addressbook) in deleted_addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
<span class="title">
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
</span>
<span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
method="POST" class="restore-form">
<button type="submit">Restore</button>
</form>
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>

View File

@@ -0,0 +1,78 @@
<h2>{{ user.id }}'s Calendars</h2>
<ul class="collection-list">
{% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner">
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
{% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span>
{% endif %}
<div class="actions">
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button>
</form>
{% if !calendar.id.starts_with("_birthdays_") %}
<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
</div>
<div class="color-chip"></div>
</div>
</li>
{% else %}
You do not have any calendars yet
{% endfor %}
</ul>
{%if !deleted_calendars.is_empty() %}
<h3>Deleted Calendars</h3>
<ul class="collection-list">
{% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner">
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
class="restore-form">
<button type="submit">Restore</button>
</form>
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
</div>
<div class="color-chip"></div>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
<create-calendar-form user="{{ user.id }}"></create-calendar-form>

View File

@@ -0,0 +1,56 @@
<h2>{{ user.id }}'s Profile</h2>
{% let groups = user.memberships_without_self() %}
{% if groups.len() > 0 %}
<h3>Groups</h3>
<ul>
{% for group in groups %}
<li>{{ group }}</li>
{% endfor %}
</ul>
{% endif %}
<h3>App tokens</h3>
<table id="app-tokens">
<tr>
<th>Name</th>
<th>Created at</th>
<th></th>
</tr>
{% for app_token in app_tokens %}
<tr>
<td>{{ app_token.name }}</td>
<td>
{% if let Some(created_at) = app_token.created_at %}
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
{% endif %}
</td>
<td>
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
<button type="submit" class="delete">Delete</button>
</form>
</td>
</tr>
{% endfor %}
<tr class="generate">
<td>
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
<label class="font_bold" for="generate_app_token_name">App name</label>
<input type="text" name="name" id="generate_app_token_name" />
</form>
</td>
<td></td>
<td>
<button type="submit" form="form_generate_app_token">Generate</button>
{% if is_apple %}
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
(contains token)</button>
{% endif %}
</td>
</tr>
</table>
{% if let Some(hostname) = davx5_hostname %}
<a
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
in DAVx5</a>
{% endif %}

View File

@@ -0,0 +1,8 @@
<!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

View File

@@ -0,0 +1,8 @@
<!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M1 20V19C1 15.134 4.13401 12 8 12V12C11.866 12 15 15.134 15 19V20" stroke-linecap="round"></path>
<path d="M13 14V14C13 11.2386 15.2386 9 18 9V9C20.7614 9 23 11.2386 23 14V14.5" stroke-linecap="round"></path>
<path d="M8 12C10.2091 12 12 10.2091 12 8C12 5.79086 10.2091 4 8 4C5.79086 4 4 5.79086 4 8C4 10.2091 5.79086 12 8 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M18 9C19.6569 9 21 7.65685 21 6C21 4.34315 19.6569 3 18 3C16.3431 3 15 4.34315 15 6C15 7.65685 16.3431 9 18 9Z" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

View File

@@ -0,0 +1,6 @@
<!-- Adapted from https://iconoir.com/ -->
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M5 20V19C5 15.134 8.13401 12 12 12V12C15.866 12 19 15.134 19 19V20" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

View File

@@ -12,7 +12,8 @@
<body>
{% block header %}
<header>
<a href="/frontend/user">RustiCal</a>
<a class="logo" href="/frontend/user">RustiCal</a>
{% block header_center %}{% endblock %}
<form method="POST" action="/frontend/logout" class="logout_form">
<button type="submit">Log out</button>
</form>
@@ -23,6 +24,7 @@
</div>
</body>
<footer>
<a href="{{ env!("CARGO_PKG_REPOSITORY") }}" target="_blank">RustiCal {{ env!("CARGO_PKG_VERSION") }}</a>
<a href="/frontend/assets/licenses.html" target="_blank">Open Source Licenses</a>
</footer>
</html>

View File

@@ -10,12 +10,4 @@
<pre>{{ addressbook|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{% endblock %}

View File

@@ -31,12 +31,4 @@
<pre>{{ calendar|json }}</pre>
<h2>Delete</h2>
<section>
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
<button type="submit">Move to trash</button>
</form>
</section>
{%endblock %}

View File

@@ -1,174 +1,23 @@
{% extends "layouts/default.html" %}
{% block imports %}
<template id="data-rustical-user">{{ user|json }}</template>
<script>
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script>
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
{% endblock %}
{% block header_center %}
<nav class="header-center">
<a href="/frontend/user/{{ user.id }}" {% if S::name() == "profile" %}class="active"{% endif %}>{% include "icons/user.svg" %}Profile</a>
<a href="/frontend/user/{{ user.id }}/calendar" {% if S::name() == "calendars" %}class="active"{% endif %}>{% include "icons/calendar.svg" %}Calendars</a>
<a href="/frontend/user/{{ user.id }}/addressbook" {% if S::name() == "addressbooks" %}class="active"{% endif %}>{% include "icons/group.svg" %}Addressbooks</a>
</nav>
{% endblock %}
{% block content %}
<div id="page-user">
<h1>Welcome {{ user.id }}!</h1>
<section>
<h2>Profile</h2>
<h3>Groups</h3>
<ul>
{% for group in user.memberships() %}
<li>{{ group }}</li>
{% endfor %}
</ul>
<h3>App tokens</h3>
<table id="app-tokens">
<tr>
<th>Name</th>
<th>Created at</th>
<th></th>
</tr>
{% for app_token in app_tokens %}
<tr>
<td>{{ app_token.name }}</td>
<td>
{% if let Some(created_at) = app_token.created_at %}
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
{% endif %}
</td>
<td>
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
<button type="submit" class="delete">Delete</button>
</form>
</td>
</tr>
{% endfor %}
<tr class="generate">
<td>
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
<label class="font_bold" for="generate_app_token_name">App name</label>
<input type="text" name="name" id="generate_app_token_name" />
</form>
</td>
<td></td>
<td>
<button type="submit" form="form_generate_app_token">Generate</button>
{% if is_apple %}
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile (contains token)</button>
{% endif %}
</td>
</tr>
</table>
{% if let Some(hostname) = davx5_hostname %}
<a href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure in DAVx5</a>
{% endif %}
</section>
<section>
<h2>Calendars</h2>
<ul>
{% for calendar in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("red".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
{% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span>
{% endif %}
<div class="actions">
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button>
</form>
</div>
<div class="color-chip"></div>
</a>
</li>
{% else %}
You do not have any calendars yet
{% endfor %}
</ul>
{%if !deleted_calendars.is_empty() %}
<h3>Deleted Calendars</h3>
<ul>
{% for calendar in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("red".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button>
</form>
</div>
<div class="color-chip"></div>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
</section>
<section>
<h2>Addressbooks</h2>
<ul>
{% for addressbook in addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span>
<span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
<button type="submit">Download</button>
</form>
</div>
</a>
</li>
{% else %}
You do not have any addressbooks yet
{% endfor %}
</ul>
{%if !deleted_addressbooks.is_empty() %}
<h3>Deleted Addressbooks</h3>
<ul>
{% for addressbook in deleted_addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
<span class="title">{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}</span>
<span class="description">
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
<button type="submit">Restore</button>
</form>
</div>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
</section>
{{ section|safe }}
{% endblock %}

View File

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

View File

@@ -8,6 +8,7 @@ use axum::{
};
use headers::{ContentType, HeaderMapExt};
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,
@@ -20,15 +21,16 @@ mod assets;
mod config;
pub mod nextcloud_login;
mod oidc_user_store;
pub(crate) mod pages;
mod routes;
pub use config::FrontendConfig;
use oidc_user_store::OidcUserStore;
use crate::routes::{
addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook},
addressbook::{route_addressbook, route_addressbook_restore},
app_token::{route_delete_app_token, route_post_app_token},
calendar::{route_calendar, route_calendar_restore, route_delete_calendar},
calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login, route_post_logout},
user::{route_get_home, route_root, route_user_named},
};
@@ -56,27 +58,21 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
post(route_delete_app_token::<AP>),
)
// Calendar
.route("/user/{user}/calendar", get(route_calendars::<CS>))
.route(
"/user/{user}/calendar/{calendar}",
get(route_calendar::<CS>),
)
.route(
"/user/{user}/calendar/{calendar}/delete",
post(route_delete_calendar::<CS>),
)
.route(
"/user/{user}/calendar/{calendar}/restore",
post(route_calendar_restore::<CS>),
)
// Addressbook
.route("/user/{user}/addressbook", get(route_addressbooks::<AS>))
.route(
"/user/{user}/addressbook/{addressbook}",
get(route_addressbook::<AS>),
)
.route(
"/user/{user}/addressbook/{addressbook}/delete",
post(route_delete_addressbook::<AS>),
)
.route(
"/user/{user}/addressbook/{addressbook}/restore",
post(route_addressbook_restore::<AS>),

View File

@@ -0,0 +1 @@
pub mod user;

View File

@@ -0,0 +1,14 @@
use askama::Template;
use askama_web::WebTemplate;
use rustical_store::auth::Principal;
pub trait Section: Template {
fn name() -> &'static str;
}
#[derive(Template, WebTemplate)]
#[template(path = "pages/user.html")]
pub struct UserPage<S: Section> {
pub user: Principal,
pub section: S,
}

View File

@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
None => (StatusCode::CREATED, "Restored").into_response(),
})
}
pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store
.delete_addressbook(&owner, &addressbook_id, true)
.await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -0,0 +1,75 @@
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 crate::pages::user::{Section, UserPage};
impl Section for AddressbooksSection {
fn name() -> &'static str {
"addressbooks"
}
}
#[derive(Template, WebTemplate)]
#[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 async fn route_addressbooks<AS: AddressbookStore>(
Path(user_id): Path<String>,
Extension(addr_store): Extension<Arc<AS>>,
user: Principal,
) -> impl IntoResponse {
if user_id != user.id {
return StatusCode::UNAUTHORIZED.into_response();
}
let mut addressbooks = vec![];
for group in user.memberships() {
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
}
let mut deleted_addressbooks = vec![];
for group in user.memberships() {
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
}
let mut addressbook_infos = vec![];
for addressbook in addressbooks {
addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
addressbook,
));
}
let mut deleted_addressbook_infos = vec![];
for addressbook in deleted_addressbooks {
deleted_addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
addressbook,
));
}
UserPage {
section: AddressbooksSection {
user: user.clone(),
addressbooks: addressbook_infos,
deleted_addressbooks: deleted_addressbook_infos,
},
user,
}
.into_response()
}

View File

@@ -27,7 +27,7 @@ pub async fn route_calendar<C: CalendarStore>(
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id).await?,
calendar: store.get_calendar(&owner, &cal_id, true).await?,
}
.into_response())
}
@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
None => (StatusCode::CREATED, "Restored").into_response(),
})
}
pub async fn route_delete_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>,
user: Principal,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store.delete_calendar(&owner, &cal_id, true).await?;
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
}

View File

@@ -0,0 +1,74 @@
use std::sync::Arc;
use crate::pages::user::{Section, UserPage};
use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, auth::Principal};
impl Section for CalendarsSection {
fn name() -> &'static str {
"calendars"
}
}
#[derive(Template, WebTemplate)]
#[template(path = "components/sections/calendars_section.html")]
pub struct CalendarsSection {
pub user: Principal,
pub calendars: Vec<(CollectionMetadata, Calendar)>,
pub deleted_calendars: Vec<(CollectionMetadata, Calendar)>,
}
pub async fn route_calendars<CS: CalendarStore>(
Path(user_id): Path<String>,
Extension(cal_store): Extension<Arc<CS>>,
user: Principal,
) -> impl IntoResponse {
if user_id != user.id {
return StatusCode::UNAUTHORIZED.into_response();
}
let mut calendars = vec![];
for group in user.memberships() {
calendars.extend(cal_store.get_calendars(group).await.unwrap());
}
let mut calendar_infos = vec![];
for calendar in calendars {
calendar_infos.push((
cal_store
.calendar_metadata(&calendar.principal, &calendar.id)
.await
.unwrap(),
calendar,
));
}
let mut deleted_calendars = vec![];
for group in user.memberships() {
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
}
let mut deleted_calendar_infos = vec![];
for calendar in deleted_calendars {
deleted_calendar_infos.push((
cal_store
.calendar_metadata(&calendar.principal, &calendar.id)
.await
.unwrap(),
calendar,
));
}
UserPage {
section: CalendarsSection {
user: user.clone(),
calendars: calendar_infos,
deleted_calendars: deleted_calendar_infos,
},
user,
}
.into_response()
}

View File

@@ -1,5 +1,7 @@
pub mod addressbook;
pub mod addressbooks;
pub mod app_token;
pub mod calendar;
pub mod calendars;
pub mod login;
pub mod user;

View File

@@ -11,19 +11,23 @@ use axum_extra::{TypedHeader, extract::Host};
use headers::UserAgent;
use http::StatusCode;
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarStore,
AddressbookStore, CalendarStore,
auth::{AppToken, AuthenticationProvider, Principal},
};
use crate::pages::user::{Section, UserPage};
impl Section for ProfileSection {
fn name() -> &'static str {
"profile"
}
}
#[derive(Template, WebTemplate)]
#[template(path = "pages/user.html")]
pub struct UserPage {
#[template(path = "components/sections/profile_section.html")]
pub struct ProfileSection {
pub user: Principal,
pub app_tokens: Vec<AppToken>,
pub calendars: Vec<Calendar>,
pub deleted_calendars: Vec<Calendar>,
pub addressbooks: Vec<Addressbook>,
pub deleted_addressbooks: Vec<Addressbook>,
pub is_apple: bool,
pub davx5_hostname: Option<String>,
}
@@ -69,14 +73,13 @@ pub async fn route_user_named<
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
UserPage {
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
calendars,
deleted_calendars,
addressbooks,
deleted_addressbooks,
section: ProfileSection {
user: user.clone(),
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
is_apple,
davx5_hostname,
},
user,
is_apple,
davx5_hostname,
}
.into_response()
}

View File

@@ -62,14 +62,14 @@ impl AddressObject {
&self.vcf
}
pub fn get_anniversary(&self) -> Option<CalDateTime> {
let prop = self.vcard.get_property("ANNIVERSARY")?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
pub fn get_birthday(&self) -> Option<CalDateTime> {
let prop = self.vcard.get_property("BDAY")?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
pub fn get_full_name(&self) -> Option<&str> {
@@ -78,25 +78,27 @@ impl AddressObject {
}
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(anniversary) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let anniversary = anniversary.date();
let year = anniversary.year();
let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary
.succ_opt()
.unwrap_or(anniversary)
.format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
Ok(
if let Some((anniversary, contains_year)) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let anniversary = anniversary.date();
let year = contains_year.then_some(anniversary.year());
let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary
.succ_opt()
.unwrap_or(anniversary)
.format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -105,39 +107,42 @@ DTSTART;VALUE=DATE:{anniversary_start}
DTEND;VALUE=DATE:{anniversary_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:💍 {fullname} ({year})
SUMMARY:💍 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:💍 {fullname} ({year})
DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR"#,
),
)?)
} else {
None
})
),
)?)
} else {
None
},
)
}
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(birthday) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let birthday = birthday.date();
let year = birthday.year();
let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
Ok(
if let Some((birthday, contains_year)) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() {
name
} else {
return Ok(None);
};
let birthday = birthday.date();
let year = contains_year.then_some(birthday.year());
let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
uid.clone(),
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -146,20 +151,21 @@ DTSTART;VALUE=DATE:{birthday_start}
DTEND;VALUE=DATE:{birthday_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:🎂 {fullname} ({year})
SUMMARY:🎂 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:🎂 {fullname} ({year})
DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR"#,
),
)?)
} else {
None
})
),
)?)
} else {
None
},
)
}
/// Get significant dates associated with this address object

View File

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

View File

@@ -7,6 +7,9 @@ pub enum OidcError {
#[error("Cannot generate redirect url, something's not configured correctly")]
OidcParseError(#[from] ParseError),
#[error("Error fetching user info: {0}")]
UserInfo(String),
#[error(transparent)]
OidcConfigurationError(#[from] ConfigurationError),

View File

@@ -41,7 +41,7 @@ struct OidcState {
#[derive(Debug, Deserialize, Serialize)]
struct GroupAdditionalClaims {
#[serde(default)]
pub groups: Vec<String>,
groups: Option<Vec<String>>,
}
impl openidconnect::AdditionalClaims for GroupAdditionalClaims {}
@@ -190,12 +190,14 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
)?
.request_async(&http_client)
.await
.map_err(|_| OidcError::Other("Error fetching user info"))?;
.map_err(|e| OidcError::UserInfo(e.to_string()))?;
if let Some(require_group) = &oidc_config.require_group {
if !user_info_claims
.additional_claims()
.groups
.clone()
.unwrap_or_default()
.contains(require_group)
{
return Ok((

View File

@@ -1,4 +1,4 @@
use crate::{Error, addressbook::Addressbook};
use crate::{CollectionMetadata, Error, addressbook::Addressbook};
use async_trait::async_trait;
use rustical_ical::AddressObject;
@@ -35,6 +35,12 @@ pub trait AddressbookStore: Send + Sync + 'static {
synctoken: i64,
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>;
async fn addressbook_metadata(
&self,
principal: &str,
addressbook_id: &str,
) -> Result<CollectionMetadata, Error>;
async fn get_objects(
&self,
principal: &str,

View File

@@ -3,7 +3,10 @@ mod principal;
use crate::error::Error;
use async_trait::async_trait;
pub use principal::{AppToken, Principal, PrincipalType};
mod principal_type;
pub use principal_type::*;
pub use principal::{AppToken, Principal};
#[async_trait]
pub trait AuthenticationProvider: Send + Sync + 'static {

View File

@@ -1,3 +1,4 @@
use crate::{Secret, auth::PrincipalType};
use axum::{
body::Body,
extract::{FromRequestParts, OptionalFromRequestParts},
@@ -6,67 +7,8 @@ use axum::{
use chrono::{DateTime, Utc};
use derive_more::Display;
use http::{HeaderValue, StatusCode, header};
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
use std::{convert::Infallible, fmt::Display};
use crate::Secret;
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}
use std::convert::Infallible;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AppToken {
@@ -83,6 +25,7 @@ pub struct Principal {
pub displayname: Option<String>,
#[serde(default)]
pub principal_type: PrincipalType,
#[serde(skip_serializing)]
pub password: Option<Secret<String>>,
#[serde(default)]
pub memberships: Vec<String>,

View File

@@ -0,0 +1,60 @@
use std::fmt::Display;
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
Individual,
Group,
Resource,
Room,
Unknown,
// TODO: X-Name, IANA-token
}
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
PrincipalType::Resource => "RESOURCE",
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}

View File

@@ -1,4 +1,4 @@
use crate::{Calendar, error::Error};
use crate::{Calendar, CollectionMetadata, error::Error};
use async_trait::async_trait;
use chrono::NaiveDate;
use rustical_ical::CalendarObject;
@@ -11,7 +11,12 @@ pub struct CalendarQuery {
#[async_trait]
pub trait CalendarStore: Send + Sync + 'static {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error>;
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error>;
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
@@ -48,6 +53,12 @@ pub trait CalendarStore: Send + Sync + 'static {
self.get_objects(principal, cal_id).await
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<CollectionMetadata, Error>;
async fn get_objects(
&self,
principal: &str,

View File

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

View File

@@ -16,7 +16,7 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
displayname: addressbook
.displayname
.map(|name| format!("{} birthdays", name)),
.map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
@@ -38,11 +38,17 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
/// 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) -> Result<Calendar, Error> {
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
let addressbook = self.0.get_addressbook(principal, id, false).await?;
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())
@@ -98,6 +104,17 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
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,

View File

@@ -11,6 +11,9 @@ mod secret;
mod subscription_store;
pub mod synctoken;
#[cfg(test)]
pub mod tests;
pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore;
@@ -34,3 +37,11 @@ pub struct CollectionOperation {
pub topic: String,
pub data: CollectionOperationInfo,
}
#[derive(Default, Debug, Clone)]
pub struct CollectionMetadata {
pub len: usize,
pub deleted_len: usize,
pub size: u64,
pub deleted_size: u64,
}

View File

@@ -1,7 +1,7 @@
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
pub fn format_synctoken(synctoken: i64) -> String {
format!("{}{}", SYNC_NAMESPACE, synctoken)
format!("{SYNC_NAMESPACE}{synctoken}")
}
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {

View File

View File

@@ -1,35 +0,0 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;UNTIL=20361026T010000Z;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:67d830c3e681950b6a12f7c287b316269a19fcf7
DTSTAMP:20230831T102923Z
DTSTART;TZID=Europe/Berlin:20230829T043000
DTEND;TZID=Europe/Berlin:20230829T045500
SEQUENCE:2
SUMMARY:asdjlk
TRANSP:OPAQUE
CLASS:PUBLIC
CREATED:20230831T103040Z
LAST-MODIFIED:20230831T103040Z
END:VEVENT
END:VCALENDAR

View File

@@ -1,109 +0,0 @@
BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20230104T023643Z
TZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
DTSTART:18930401T000632
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160430T230000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19440403T020000
RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEMT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
DTSTART:19450524T010000
RDATE:19470511T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
DTSTART:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19460101T000000
RDATE:19800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19800928T030000
RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

View File

@@ -1,46 +0,0 @@
// use rstest::rstest;
// use rstest_reuse::{self, apply, template};
// use rustical_store::{CalendarObject, CalendarStore};
// use rustical_store_sqlite::{calendar_store::SqliteCalendarStore, create_test_db};
//
// const TIMEZONE: &str = include_str!("examples/timezone.ics");
// const EVENT: &str = include_str!("examples/event.ics");
//
// #[template]
// #[rstest]
// #[case::sqlite(async {
// let (send, _recv) = tokio::sync::mpsc::channel(100);
// SqliteCalendarStore::new(create_test_db().await.unwrap(), send)
// })]
// async fn cal_store<CS: CalendarStore>(
// #[future(awt)]
// #[case]
// mut store: CS,
// ) {
// }
// TODO: Reimplement, add test principal
// #[apply(cal_store)]
// #[tokio::test]
// async fn test_create_event<CS: CalendarStore>(store: CS) {
// store
// .insert_calendar(rustical_store::Calendar {
// id: "test".to_owned(),
// displayname: Some("Test Calendar".to_owned()),
// principal: "testuser".to_owned(),
// timezone: Some(TIMEZONE.to_owned()),
// ..Default::default() // timezone: TIMEZONE.to_owned(),
// })
// .await
// .unwrap();
//
// let object = CalendarObject::from_ics("asd".to_owned(), EVENT.to_owned()).unwrap();
// store
// .put_object("testuser".to_owned(), "test".to_owned(), object, true)
// .await
// .unwrap();
//
// let event = store.get_object("testuser", "test", "asd").await.unwrap();
// assert_eq!(event.get_ics(), EVENT);
// assert_eq!(event.get_id(), "asd");
// }

View File

@@ -7,6 +7,12 @@ repository.workspace = true
license.workspace = true
publish = false
[features]
test = ["dep:rstest"]
[dev-dependencies]
rstest.workspace = true
[dependencies]
tokio.workspace = true
rustical_store = { workspace = true }
@@ -22,3 +28,4 @@ password-hash.workspace = true
uuid.workspace = true
pbkdf2.workspace = true
rustical_ical.workspace = true
rstest = { workspace = true, optional = true }

View File

@@ -3,8 +3,8 @@ use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::AddressObject;
use rustical_store::{
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
synctoken::format_synctoken,
Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
CollectionOperationInfo, Error, synctoken::format_synctoken,
};
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
@@ -223,6 +223,28 @@ impl SqliteAddressbookStore {
Ok((objects, deleted_objects, new_synctoken))
}
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
addressbook_id: &str,
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
struct ObjectEntry {
length: u64,
deleted: bool,
}
Ok(sqlx::query_as!(
ObjectEntry,
"SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
principal,
addressbook_id
)
.fetch_all(executor)
.await.map_err(crate::Error::from)?
.into_iter()
.map(|row| (row.length, row.deleted))
.collect())
}
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
@@ -442,6 +464,29 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
}
#[instrument]
async fn addressbook_metadata(
&self,
principal: &str,
addressbook_id: &str,
) -> Result<CollectionMetadata, rustical_store::Error> {
let mut sizes = vec![];
let mut deleted_sizes = vec![];
for (size, deleted) in Self::_list_objects(&self.db, principal, addressbook_id).await? {
if deleted {
deleted_sizes.push(size)
} else {
sizes.push(size)
}
}
Ok(CollectionMetadata {
len: sizes.len(),
deleted_len: deleted_sizes.len(),
size: sizes.iter().sum(),
deleted_size: deleted_sizes.iter().sum(),
})
}
#[instrument]
async fn get_objects(
&self,

View File

@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarStore, Error};
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -86,14 +86,17 @@ impl SqliteCalendarStore {
executor: E,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let cal = sqlx::query_as!(
CalendarRow,
r#"SELECT *
FROM calendars
WHERE (principal, id) = (?, ?)"#,
WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?) "#,
principal,
id
id,
show_deleted
)
.fetch_one(executor)
.await
@@ -239,6 +242,28 @@ impl SqliteCalendarStore {
Ok(())
}
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
cal_id: &str,
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
struct ObjectEntry {
length: u64,
deleted: bool,
}
Ok(sqlx::query_as!(
ObjectEntry,
"SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
principal,
cal_id
)
.fetch_all(executor)
.await.map_err(crate::Error::from)?
.into_iter()
.map(|row| (row.length, row.deleted))
.collect())
}
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
@@ -470,8 +495,13 @@ impl SqliteCalendarStore {
#[async_trait]
impl CalendarStore for SqliteCalendarStore {
#[instrument]
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
Self::_get_calendar(&self.db, principal, id).await
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
Self::_get_calendar(&self.db, principal, id, show_deleted).await
}
#[instrument]
@@ -509,7 +539,7 @@ impl CalendarStore for SqliteCalendarStore {
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let cal = match Self::_get_calendar(&mut *tx, principal, id).await {
let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
Ok(cal) => Some(cal),
Err(Error::NotFound) => None,
Err(err) => return Err(err),
@@ -544,6 +574,28 @@ impl CalendarStore for SqliteCalendarStore {
Self::_calendar_query(&self.db, principal, cal_id, query).await
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<CollectionMetadata, Error> {
let mut sizes = vec![];
let mut deleted_sizes = vec![];
for (size, deleted) in Self::_list_objects(&self.db, principal, cal_id).await? {
if deleted {
deleted_sizes.push(size)
} else {
sizes.push(size)
}
}
Ok(CollectionMetadata {
len: sizes.len(),
deleted_len: deleted_sizes.len(),
size: sizes.iter().sum(),
deleted_size: deleted_sizes.iter().sum(),
})
}
#[instrument]
async fn get_objects(
&self,
@@ -599,7 +651,10 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
topic: self
.get_calendar(&principal, &cal_id, true)
.await?
.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};
@@ -624,7 +679,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic,
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};
@@ -649,7 +704,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id).await?.push_topic,
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};

Some files were not shown because too many files have changed in this diff Show More