[task-192]

## Tasks

- [192](https://vikunja.ramsch.sx/tasks/192)

## Zugehörige MRs

- [datamodel](misc/zeitbild-datamodel#1)
- [frontend-dali](misc/zeitbild-frontend-dali#1)

Reviewed-on: misc/zeitbild-backend#1
Co-authored-by: Fenris Wolf <fenris@folksprak.org>
Co-committed-by: Fenris Wolf <fenris@folksprak.org>
This commit is contained in:
fenris 2025-09-25 17:05:15 +02:00 committed by fenris
parent 39fbe6af80
commit f90567d043
19 changed files with 766 additions and 138 deletions

View file

@ -1,20 +0,0 @@
{
"version": 1,
"log": [
{
"kind": "stdout",
"data": {
"threshold": "info"
}
}
],
"session_management": {
"in_memory": false,
"lifetime": 3600
},
"authentication": {
"kind": "internal",
"data": {
}
}
}

View file

@ -3,19 +3,22 @@
{ {
"id": 1, "id": 1,
"name": "alice", "name": "alice",
"email_address": "alice@example.org", "email_address": "alice@example.org",
"dav_token": null,
"password": "alice" "password": "alice"
}, },
{ {
"id": 2, "id": 2,
"name": "bob", "name": "bob",
"email_address": "bob@example.org", "email_address": "bob@example.org",
"dav_token": "bob_dav",
"password": "bob" "password": "bob"
}, },
{ {
"id": 3, "id": 3,
"name": "charlie", "name": "charlie",
"email_address": "charlie@example.org", "email_address": "charlie@example.org",
"dav_token": null,
"password": "charlie" "password": "charlie"
} }
], ],

58
misc/conf-example.json Normal file
View file

@ -0,0 +1,58 @@
{
"version": 1,
"log": [
{
"kind": "stdout",
"data": {
"threshold": "info",
"format": "jsonl_structured"
}
}
],
"session_management": {
"in_memory": false,
"lifetime": 3600
},
"authentication": {
"kind": "internal",
"data": {
}
},
"database": {
"kind": "sqlite",
"data": {
"path": "../zeitbild.sqlite"
}
},
"caldav": {
"address": "http://localhost:8000/calendars/-/lampukistan",
"username": "lampukistan-{{username}}",
"password": "{{password}}",
"setup_hints": [
{
"label": "Android",
"link": "https://www.android-user.de/caldavcarddav-kalender-und-adressbuecher-ohne-google-synchronisieren/",
"remark": null
},
{
"label": "iOS",
"link": "https://all-inkl.com/wichtig/anleitungen/programme/e-mail/caldav-kalenderfunktion/ios-mail_460.html",
"remark": "eigentlich für Server 'all-inkl.com' — Zugangsdaten müssen entsprechend geändert werden"
},
{
"label": "Thunderbird",
"link": "https://www.uni-bielefeld.de/einrichtungen/bits/services/kuz/e-mail-und-kalender/anleitung/kalender-konfiguration-unter-thunderbird/",
"remark": "eigentlich für Server 'uni-bielefeld.de' — Zugangsdaten müssen entsprechend geändert werden"
},
{
"label": "Evolution",
"link": "https://help.gnome.org/users/evolution/stable/calendar-caldav.html.de"
},
{
"label": "MS Outlook",
"link": "https://www.united-domains.de/help/faq-article/wie-synchronisiere-ich-meinen-kalender-caldav-mit-ms-outlook/",
"remark": null
}
]
}
}

View file

@ -29,10 +29,9 @@ namespace _zeitbild.api
} }
| |
{ {
kind : "caldav"; kind : "ics_feed";
data : { data : {
url : string; url : string;
read_only : boolean;
from_fucked_up_wordpress : boolean; from_fucked_up_wordpress : boolean;
}; };
} }
@ -75,12 +74,11 @@ namespace _zeitbild.api
}; };
break; break;
} }
case "caldav": { case "ics_feed": {
resource_object = { resource_object = {
"kind": "caldav", "kind": "ics_feed",
"data": { "data": {
"url": stuff.input.resource.data.url, "url": stuff.input.resource.data.url,
"read_only": stuff.input.resource.data.read_only,
"from_fucked_up_wordpress": stuff.input.resource.data.from_fucked_up_wordpress, "from_fucked_up_wordpress": stuff.input.resource.data.from_fucked_up_wordpress,
} }
}; };

View file

@ -0,0 +1,127 @@
namespace _zeitbild.api
{
/**
*/
export function register_export_ics(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
null,
(
lib_plankton.ical.type_vcalendar
|
string
)
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/export/ics",
{
"description": "trägt Veranstaltungen aus verschiedenen Kalendern zusammen im ics-Format",
"query_parameters": () => ([
{
"name": "from",
"required": false,
"description": "UNIX timestamp",
},
{
"name": "to",
"required": false,
"description": "UNIX timestamp",
},
{
"name": "calendar_ids",
"required": false,
"description": "comma separated",
},
]),
"output_schema": () => ({
"nullable": false,
"type": "string",
}),
"response_body_mimetype": "text/calendar",
"response_body_encode": (output) => Buffer.from(
(typeof(output) === "string")
?
output
:
lib_plankton.ical.ics_encode(output)
),
"restriction": restriction_web_auth,
"execution": async (stuff) => {
const user : {id : _zeitbild.type_user_id; object : _zeitbild.type_user_object;} = await _zeitbild.api.user_from_web_auth(stuff);
const from : lib_plankton.pit.type_pit = (
("from" in stuff.query_parameters)
?
parseInt(stuff.query_parameters["from"])
:
lib_plankton.pit.shift_week(
lib_plankton.pit.now(),
-2
)
);
const to : lib_plankton.pit.type_pit = (
("to" in stuff.query_parameters)
?
parseInt(stuff.query_parameters["to"])
:
lib_plankton.pit.shift_week(
lib_plankton.pit.now(),
+6
)
);
const calendar_ids_wanted : (null | Array<_zeitbild.type_calendar_id>) = (
(
("calendar_ids" in stuff.query_parameters)
&&
(stuff.query_parameters["calendar_ids"] !== null)
)
?
lib_plankton.call.convey(
stuff.query_parameters["calendar_ids"],
[
(x : string) => x.split(","),
(x : Array<string>) => x.map(parseInt),
(x : Array<int>) => x.filter(y => (! isNaN(y)))
]
)
:
null
);
return (
_zeitbild.service.calendar.gather_events(
calendar_ids_wanted,
from,
to,
user.id
)
.then(
(events_extended) => Promise.resolve(
{
"status_code": 200,
"data": _zeitbild.helpers.icalendar_vcalendar_from_own_event_list(
events_extended
)
}
)
)
.catch(
(reason) => Promise.resolve(
{
"status_code": 403,
"data": String(reason),
}
)
)
);
}
}
);
}
}

View file

@ -74,6 +74,7 @@ namespace _zeitbild.api
{ {
"name": data.userinfo.name, "name": data.userinfo.name,
"email_address": data.userinfo.email, "email_address": data.userinfo.email,
"dav_token": null,
} }
); );
lib_plankton.log.info( lib_plankton.log.info(

View file

@ -0,0 +1,152 @@
namespace _zeitbild.api
{
/**
*/
export function register_user_dav_conf(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
null,
(
null
|
{
address : string;
username : string;
password : (null | string);
setup_hints : Array<
{
label : string;
link : string;
remark : (null | string);
}
>;
}
)
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/user_dav_conf",
{
"description": "gibt die CalDAV-Zugangsdaten eines Nutzers aus",
"output_schema": () => ({
"nullable": true,
"type": "object",
"properties": {
"address": {
"nullable": false,
"type": "string"
},
"username": {
"nullable": false,
"type": "string"
},
"password": {
"nullable": true,
"type": "string"
},
"setup_hints": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "object",
"properties": {
"label": {
"nullable": false,
"type": "string"
},
"link": {
"nullable": false,
"type": "string"
},
"remark": {
"nullable": true,
"type": "string",
"default": null,
},
},
"required": [
"label",
"link",
],
"additionalProperties": false
},
"default": []
},
},
"required": [
"address",
"username",
"password",
"setup_hints",
],
"additionalProperties": false
}),
"restriction": restriction_logged_in,
"execution": async (stuff) => {
let result : (
null
|
{
address : string;
username : string;
password : (null | string);
setup_hints : Array<
{
label : string;
link : string;
remark : (null | string);
}
>;
}
) = null;
const raw : (null | any) = _zeitbild.conf.get()["caldav"];
if (raw === null)
{
result = null;
}
else
{
const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(stuff);
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name);
const user_object : _zeitbild.type_user_object = await _zeitbild.service.user.get(user_id);
const arguments_ : Record<string, string> = Object.fromEntries(
[
{"key": "username", "value": user_object.name},
{"key": "password", "value": user_object.dav_token},
]
.filter(
entry => (entry.value !== null)
)
.map(
entry => ([entry.key, entry.value as string])
)
);
result = {
"address": lib_plankton.string.coin(raw["address"], arguments_),
"username": lib_plankton.string.coin(raw["username"], arguments_),
"password": (
(user_object.dav_token === null)
?
null
:
lib_plankton.string.coin(raw["password"], arguments_)
),
"setup_hints": raw["setup_hints"],
};
}
return Promise.resolve(
{
"status_code": 200,
"data": result,
}
);
}
}
);
}
}

View file

@ -0,0 +1,53 @@
namespace _zeitbild.api
{
/**
*/
export function register_user_dav_token(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
// string,
null,
null
>(
rest_subject,
lib_plankton.http.enum_method.patch,
"/user_dav_token",
{
"description": "setzt/überschreibt den DAV-Token eines Nutzers",
/*
"input_schema": () => ({
"nullable": false,
"type": "string"
}),
*/
"input_schema": () => ({
"nullable": true,
}),
"output_schema": () => ({
"nullable": true
}),
"restriction": restriction_logged_in,
"execution": async (stuff) => {
const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(stuff);
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name);
// TODO: outsource to user service?
const user_object : _zeitbild.type_user_object = await _zeitbild.service.user.get(user_id);
// user_object.dav_token = stuff.input;
user_object.dav_token = lib_plankton.random.generate_string({"length": 12});
await _zeitbild.service.user.change(user_id, user_object);
return Promise.resolve(
{
"status_code": 200,
"data": null,
}
);
}
}
);
}
}

View file

@ -31,6 +31,15 @@ namespace _zeitbild.api
_zeitbild.api.register_session_end(rest_subject); _zeitbild.api.register_session_end(rest_subject);
_zeitbild.api.register_session_oidc(rest_subject); _zeitbild.api.register_session_oidc(rest_subject);
} }
// user
{
_zeitbild.api.register_users(rest_subject);
// caldav
{
_zeitbild.api.register_user_dav_conf(rest_subject);
_zeitbild.api.register_user_dav_token(rest_subject);
}
}
// calendar // calendar
{ {
_zeitbild.api.register_calendar_list(rest_subject); _zeitbild.api.register_calendar_list(rest_subject);
@ -46,13 +55,15 @@ namespace _zeitbild.api
_zeitbild.api.register_calendar_event_remove(rest_subject); _zeitbild.api.register_calendar_event_remove(rest_subject);
} }
} }
// export
{
_zeitbild.api.register_export_ics(rest_subject);
}
// misc // misc
{ {
_zeitbild.api.register_users(rest_subject);
_zeitbild.api.register_events(rest_subject); _zeitbild.api.register_events(rest_subject);
} }
return rest_subject; return rest_subject;
} }

View file

@ -282,7 +282,95 @@ namespace _zeitbild.conf
"data": { "data": {
} }
} }
} },
"external_resources": {
"nullable": false,
"type": "object",
"properties": {
"lifetime": {
"nullable": false,
"type": "integer",
"default": 14400
}
},
"additionalProperties": false,
"required": [
],
"default": {
}
},
"caldav": {
"nullable": true,
"type": "object",
"properties": {
"address": {
"nullable": false,
"type": "string"
},
"username": {
"nullable": false,
"type": "string"
},
"password": {
"nullable": false,
"type": "string"
},
"setup_hints": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "object",
"properties": {
"label": {
"nullable": false,
"type": "string"
},
"link": {
"nullable": false,
"type": "string"
},
"remark": {
"nullable": true,
"type": "string",
"default": null
},
},
"required": [
"label",
"link",
],
"additionalProperties": false
},
"default": []
},
},
"required": [
"address",
"username",
"password"
],
"additionalProperties": false,
"default": null
},
"misc": {
"nullable": false,
"type": "object",
"properties": {
/**
* @todo make mandatory
*/
"auth_salt": {
"nullable": false,
"type": "string",
"default": "unsafe_auth_salt"
}
},
"required": [
],
"additionalProperties": false,
"default": {}
},
}, },
"required": [ "required": [
"version" "version"

View file

@ -7,7 +7,7 @@ namespace _zeitbild.helpers
/** /**
* @todo timezone * @todo timezone
*/ */
function ical_datetime_to_own_datetime( function icalendar_datetime_to_own_datetime(
ical_datetime : lib_plankton.ical.type_datetime ical_datetime : lib_plankton.ical.type_datetime
) : lib_plankton.pit.type_datetime ) : lib_plankton.pit.type_datetime
{ {
@ -31,12 +31,12 @@ namespace _zeitbild.helpers
) )
}; };
} }
/** /**
* @todo timezone * @todo timezone
*/ */
export function ical_dt_to_own_datetime( export function icalendar_dt_to_own_datetime(
ical_dt: lib_plankton.ical.type_dt ical_dt: lib_plankton.ical.type_dt
) : lib_plankton.pit.type_datetime ) : lib_plankton.pit.type_datetime
{ {
@ -58,6 +58,125 @@ namespace _zeitbild.helpers
} }
/**
* @todo timezone
*/
export function icalendar_dt_from_own_datetime(
datetime : lib_plankton.pit.type_datetime
) : lib_plankton.ical.type_dt
{
return {
"tzid": "Europe/Berlin",
"value": {
"date": {
"year": datetime.date.year,
"month": datetime.date.month,
"day": datetime.date.day,
},
"time": (
(datetime.time === null)
?
null
:
{
"utc": true,
"hour": datetime.time.hour,
"minute": datetime.time.minute,
"second": datetime.time.second,
}
)
},
};
}
/**
*/
export function icalendar_vevent_from_own_event(
event_extended : _zeitbild.type_event_extended,
index : int,
{
"stamp": stamp = "adhoc",
}
:
{
stamp ?: string;
}
=
{
}
) : lib_plankton.ical.type_vevent
{
return {
"uid": lib_plankton.string.coin(
"zeitbild_{{stamp}}_{{id}}",
{
"stamp": stamp,
// "id": event_extended.event_id.toFixed(0),
"id": index.toFixed(0),
}
),
"dtstamp": icalendar_dt_from_own_datetime(event_extended.event_object.begin).value,
"dtstart": icalendar_dt_from_own_datetime(event_extended.event_object.begin),
"dtend": (
(event_extended.event_object.end === null)
?
undefined
:
icalendar_dt_from_own_datetime(event_extended.event_object.end)
),
"location": (event_extended.event_object.location ?? undefined),
"summary": event_extended.event_object.name,
"url": (event_extended.event_object.link ?? undefined),
"description": (event_extended.event_object.description ?? undefined),
/**
* @todo transform name
*/
"categories": [event_extended.calendar_name],
};
}
/**
* @todo assign better uids
*/
export function icalendar_vcalendar_from_own_event_list(
events_extended : Array<_zeitbild.type_event_extended>
) : lib_plankton.ical.type_vcalendar
{
const pit_now : lib_plankton.pit.type_pit = lib_plankton.pit.now();
const datetime_now : lib_plankton.pit.type_datetime = lib_plankton.pit.to_datetime(pit_now);
const stamp : string = lib_plankton.string.coin(
"{{year}}{{month}}{{day}}",
{
"year": datetime_now.date.year.toFixed(0).padStart(4, "0"),
"month": datetime_now.date.month.toFixed(0).padStart(2, "0"),
"day": datetime_now.date.day.toFixed(0).padStart(2, "0"),
}
);
return {
"version": "2.0",
"prodid": "",
"vevents": (
events_extended
.map<lib_plankton.ical.type_vevent>(
(event_extended, index) => icalendar_vevent_from_own_event(
event_extended,
index,
{
"stamp": stamp,
}
)
)
),
"method": "PUBLISH",
"vtimezone": {
"tzid": "Europe/Berlin",
},
};
}
/** /**
*/ */
export async function template_coin( export async function template_coin(

View file

@ -8,6 +8,7 @@ type type_data = {
id : int; id : int;
name : string; name : string;
email_address : string; email_address : string;
dav_token : (null | string);
password : string; password : string;
} }
>; >;
@ -36,10 +37,9 @@ type type_data = {
} }
| |
{ {
kind : "caldav"; kind : "ics_feed";
data : { data : {
url : string; url : string;
read_only : boolean;
from_fucked_up_wordpress ?: boolean; from_fucked_up_wordpress ?: boolean;
}; };
} }
@ -73,6 +73,7 @@ async function data_init(
const user_object : _zeitbild.type_user_object = { const user_object : _zeitbild.type_user_object = {
"name": user_raw.name, "name": user_raw.name,
"email_address": user_raw.email_address, "email_address": user_raw.email_address,
"dav_token": user_raw.dav_token,
}; };
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object user_object
@ -108,13 +109,12 @@ async function data_init(
); );
break; break;
} }
case "caldav": case "ics_feed":
{ {
resource_object = { resource_object = {
"kind": "caldav", "kind": "ics_feed",
"data": { "data": {
"url": calendar_raw.resource.data.url, "url": calendar_raw.resource.data.url,
"read_only": calendar_raw.resource.data.read_only,
"from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false), "from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false),
} }
}; };
@ -384,7 +384,6 @@ async function main(
case "api-doc": { case "api-doc": {
lib_plankton.log.set_main_logger([]); lib_plankton.log.set_main_logger([]);
const rest_subject : lib_plankton.rest_http.type_rest = _zeitbild.api.make(); const rest_subject : lib_plankton.rest_http.type_rest = _zeitbild.api.make();
lib_plankton.log.conf_pop();
process.stdout.write( process.stdout.write(
JSON.stringify( JSON.stringify(
lib_plankton.rest_http.to_oas(rest_subject), lib_plankton.rest_http.to_oas(rest_subject),
@ -480,6 +479,7 @@ async function main(
) )
.catch( .catch(
(error) => { (error) => {
// console.error(error);
process.stderr.write(String(error) + "\n"); process.stderr.write(String(error) + "\n");
} }
) )

View file

@ -42,7 +42,7 @@ namespace _zeitbild.repository.resource
/** /**
*/ */
var _caldav_resource_store : ( var _ics_feed_resource_store : (
null null
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_store<
@ -126,7 +126,7 @@ namespace _zeitbild.repository.resource
/** /**
*/ */
function get_caldav_resource_store( function get_ics_feed_resource_store(
) : lib_plankton.storage.type_store< ) : lib_plankton.storage.type_store<
int, int,
Record<string, any>, Record<string, any>,
@ -135,11 +135,11 @@ namespace _zeitbild.repository.resource
Record<string, any> Record<string, any>
> >
{ {
if (_caldav_resource_store === null) { if (_ics_feed_resource_store === null) {
_caldav_resource_store = lib_plankton.storage.sql_table_autokey_store( _ics_feed_resource_store = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "caldav_resources", "table_name": "ics_feed_resources",
"key_name": "id", "key_name": "id",
} }
); );
@ -147,7 +147,7 @@ namespace _zeitbild.repository.resource
else { else {
// do nothing // do nothing
} }
return _caldav_resource_store; return _ics_feed_resource_store;
} }
@ -358,15 +358,14 @@ namespace _zeitbild.repository.resource
} }
); );
} }
case "caldav": { case "ics_feed": {
const dataset_extra_caldav : Record<string, any> = await get_caldav_resource_store().read(dataset_core.sub_id); const dataset_extra_ics_feed : Record<string, any> = await get_ics_feed_resource_store().read(dataset_core.sub_id);
return Promise.resolve<_zeitbild.type_resource_object>( return Promise.resolve<_zeitbild.type_resource_object>(
{ {
"kind": "caldav", "kind": "ics_feed",
"data": { "data": {
"url": dataset_extra_caldav["url"], "url": dataset_extra_ics_feed["url"],
"read_only": dataset_extra_caldav["read_only"], "from_fucked_up_wordpress": dataset_extra_ics_feed["from_fucked_up_wordpress"],
"from_fucked_up_wordpress": dataset_extra_caldav["from_fucked_up_wordpress"],
} }
} }
); );
@ -405,18 +404,17 @@ namespace _zeitbild.repository.resource
return Promise.resolve<_zeitbild.type_resource_id>(resource_id); return Promise.resolve<_zeitbild.type_resource_id>(resource_id);
break; break;
} }
case "caldav": { case "ics_feed": {
const caldav_resource_id : int = await get_caldav_resource_store().create( const ics_feed_resource_id : int = await get_ics_feed_resource_store().create(
{ {
"url": resource_object.data.url, "url": resource_object.data.url,
"read_only": resource_object.data.read_only,
"from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress, "from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress,
} }
); );
const resource_id : _zeitbild.type_resource_id = await get_resource_core_store().create( const resource_id : _zeitbild.type_resource_id = await get_resource_core_store().create(
{ {
"kind": "caldav", "kind": "ics_feed",
"sub_id": caldav_resource_id, "sub_id": ics_feed_resource_id,
} }
); );
await lib_plankton.cache.clear(_zeitbild.cache_regular); await lib_plankton.cache.clear(_zeitbild.cache_regular);
@ -491,12 +489,12 @@ namespace _zeitbild.repository.resource
*/ */
break; break;
} }
case "caldav": { case "ics_feed": {
await get_caldav_resource_store().update( await get_ics_feed_resource_store().update(
dataset_core["sub_id"], dataset_core["sub_id"],
{ {
"url": resource_object.data.url, "url": resource_object.data.url,
"read_only": resource_object.data.read_only, "from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress,
} }
); );
await lib_plankton.cache.clear(_zeitbild.cache_regular); await lib_plankton.cache.clear(_zeitbild.cache_regular);

View file

@ -53,6 +53,7 @@ namespace _zeitbild.repository.user
return { return {
"name": user_object.name, "name": user_object.name,
"email_address": user_object.email_address, "email_address": user_object.email_address,
"dav_token": user_object.dav_token,
}; };
} }
@ -66,6 +67,7 @@ namespace _zeitbild.repository.user
return { return {
"name": row["name"], "name": row["name"],
"email_address": row["email_address"], "email_address": row["email_address"],
"dav_token": row["dav_token"],
}; };
} }
@ -119,6 +121,19 @@ namespace _zeitbild.repository.user
} }
/**
*/
export async function update(
user_id : _zeitbild.type_user_id,
user_object : _zeitbild.type_user_object
) : Promise<void>
{
const dispersal : Record<string, any> = encode(user_object);
await get_store().update(user_id, dispersal);
return Promise.resolve<void>(undefined);
}
/** /**
*/ */
export async function identify( export async function identify(

View file

@ -327,41 +327,54 @@ namespace _zeitbild.service.calendar
); );
break; break;
} }
case "caldav": { case "ics_feed": {
// TODO readonly // TODO readonly
const url : lib_plankton.url.type_url = lib_plankton.url.decode( const vcalendar : lib_plankton.ical.type_vcalendar = await lib_plankton.cache.get<lib_plankton.ical.type_vcalendar>(
resource_object.data.url _zeitbild.cache_external_resources,
); resource_object.data.url,
const http_request : lib_plankton.http.type_request = { _zeitbild.conf.get().external_resources.lifetime,
"version": "HTTP/2", async () => {
"scheme": ((url.scheme === "https") ? "https" : "http"), const url : lib_plankton.url.type_url = lib_plankton.url.decode(
"host": url.host, resource_object.data.url
"path": (url.path ?? "/"), );
"query": url.query, const http_request : lib_plankton.http.type_request = {
"method": lib_plankton.http.enum_method.get, "version": "HTTP/2",
"headers": {}, "scheme": ((url.scheme === "https") ? "https" : "http"),
"body": null, "host": url.host,
}; "path": (url.path ?? "/"),
// TODO: cache? "query": url.query,
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call( "method": lib_plankton.http.enum_method.get,
http_request, "headers": {},
{ "body": null,
};
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(
http_request,
{
}
);
const ics_raw : string = (
(http_response.body === null)
?
""
:
http_response.body.toString()
);
const vcalendar_list : Array<lib_plankton.ical.type_vcalendar> = lib_plankton.ical.ics_decode_multi(
ics_raw,
{
"ignore_unhandled_instruction_keys": resource_object.data.from_fucked_up_wordpress,
"from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress,
}
);
const vcalendar : lib_plankton.ical.type_vcalendar = {
// required
"version": vcalendar_list[0].version,
"prodid": vcalendar_list[0].prodid,
"vevents": vcalendar_list.map(x => x.vevents).reduce((x, y) => x.concat(y), []),
};
return Promise.resolve<lib_plankton.ical.type_vcalendar>(vcalendar);
} }
); );
const ics_raw : string = http_response.body.toString();
const vcalendar_list : Array<lib_plankton.ical.type_vcalendar> = lib_plankton.ical.ics_decode_multi(
ics_raw,
{
"ignore_unhandled_instruction_keys": resource_object.data.from_fucked_up_wordpress,
"from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress,
}
);
const vcalendar : lib_plankton.ical.type_vcalendar = {
// required
"version": vcalendar_list[0].version,
"prodid": vcalendar_list[0].prodid,
"vevents": vcalendar_list.map(x => x.vevents).reduce((x, y) => x.concat(y), []),
};
return Promise.resolve( return Promise.resolve(
vcalendar.vevents vcalendar.vevents
.map( .map(
@ -376,11 +389,11 @@ namespace _zeitbild.service.calendar
: :
"???" "???"
), ),
"begin": _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtstart), "begin": _zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtstart),
"end": ( "end": (
(vevent.dtend !== undefined) (vevent.dtend !== undefined)
? ?
_zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtend) _zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtend)
: :
null null
), ),
@ -441,19 +454,6 @@ namespace _zeitbild.service.calendar
} }
/**
*/
type type_gather_events_result = Array<
{
calendar_id : _zeitbild.type_calendar_id;
calendar_name : string;
access_level : _zeitbild.enum_access_level;
event_id : (null | _zeitbild.type_local_resource_event_id);
event_object : _zeitbild.type_event_object;
}
>;
/** /**
*/ */
export async function gather_events( export async function gather_events(
@ -461,7 +461,7 @@ namespace _zeitbild.service.calendar
from_pit : lib_plankton.pit.type_pit, from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit, to_pit : lib_plankton.pit.type_pit,
user_id : (null | _zeitbild.type_user_id) user_id : (null | _zeitbild.type_user_id)
) : Promise<type_gather_events_result> ) : Promise<Array<_zeitbild.type_event_extended>>
{ {
const calendar_ids_allowed : Array<_zeitbild.type_calendar_id> = ( const calendar_ids_allowed : Array<_zeitbild.type_calendar_id> = (
(await overview(user_id)) (await overview(user_id))
@ -483,8 +483,8 @@ namespace _zeitbild.service.calendar
) )
); );
calendar_ids.sort(); calendar_ids.sort();
return lib_plankton.cache.get_complex<any, type_gather_events_result>( return lib_plankton.cache.get_complex<any, Array<_zeitbild.type_event_extended>>(
_zeitbild.cache, _zeitbild.cache_regular,
"gather_events", "gather_events",
{ {
"user_id": user_id, "user_id": user_id,
@ -492,6 +492,10 @@ namespace _zeitbild.service.calendar
"to_pit": to_pit, "to_pit": to_pit,
"calendar_ids": calendar_ids, "calendar_ids": calendar_ids,
}, },
/**
* @todo expire?
*/
null,
() => ( () => (
Promise.all( Promise.all(
calendar_ids calendar_ids

View file

@ -31,7 +31,7 @@ namespace _zeitbild.service.resource
return Promise.resolve<_zeitbild.type_event_object>(event_object); return Promise.resolve<_zeitbild.type_event_object>(event_object);
break; break;
} }
case "caldav": { case "ics_feed": {
// TODO // TODO
return Promise.reject(new Error("not implemented")); return Promise.reject(new Error("not implemented"));
break; break;
@ -63,14 +63,8 @@ namespace _zeitbild.service.resource
return Promise.resolve<_zeitbild.type_local_resource_event_id>(local_resource_event_id); return Promise.resolve<_zeitbild.type_local_resource_event_id>(local_resource_event_id);
break; break;
} }
case "caldav": { case "ics_feed": {
if (resource_object.data.read_only) { return Promise.reject(new Error("unavailable"));
return Promise.reject(new Error("can not add event to read only caldav resource"));
}
else {
// TODO
return Promise.reject(new Error("not implemented"));
}
break; break;
} }
default: { default: {
@ -102,14 +96,8 @@ namespace _zeitbild.service.resource
return Promise.resolve<void>(undefined); return Promise.resolve<void>(undefined);
break; break;
} }
case "caldav": { case "ics_feed": {
if (resource_object.data.read_only) { return Promise.reject(new Error("unavailable"));
return Promise.reject(new Error("can not change event of read only caldav resource"));
}
else {
// TODO
return Promise.reject(new Error("not implemented"));
}
break; break;
} }
default: { default: {
@ -139,14 +127,8 @@ namespace _zeitbild.service.resource
return Promise.resolve<void>(undefined); return Promise.resolve<void>(undefined);
break; break;
} }
case "caldav": { case "ics_feed": {
if (resource_object.data.read_only) { return Promise.reject(new Error("unavailable"));
return Promise.reject(new Error("can not delete event from read only caldav resource"));
}
else {
// TODO
return Promise.reject(new Error("not implemented"));
}
break; break;
} }
default: { default: {

View file

@ -28,6 +28,16 @@ namespace _zeitbild.service.user
} }
/**
*/
export function get(
user_id : _zeitbild.type_user_id
) : Promise<_zeitbild.type_user_object>
{
return _zeitbild.repository.user.read(user_id);
}
/** /**
*/ */
export function add( export function add(
@ -37,4 +47,15 @@ namespace _zeitbild.service.user
return _zeitbild.repository.user.create(user_object); return _zeitbild.repository.user.create(user_object);
} }
/**
*/
export function change(
user_id : _zeitbild.type_user_id,
user_object : _zeitbild.type_user_object
) : Promise<void>
{
return _zeitbild.repository.user.update(user_id, user_object);
}
} }

View file

@ -28,6 +28,11 @@ namespace _zeitbild
| |
string string
); );
dav_token : (
null
|
string
);
}; };
@ -82,10 +87,9 @@ namespace _zeitbild
} }
| |
{ {
kind : "caldav"; kind : "ics_feed";
data : { data : {
url : string; url : string;
read_only : boolean;
from_fucked_up_wordpress : boolean; from_fucked_up_wordpress : boolean;
}; };
} }
@ -111,5 +115,16 @@ namespace _zeitbild
}; };
resource_id : type_resource_id; resource_id : type_resource_id;
}; };
/**
*/
export type type_event_extended = {
calendar_id : type_calendar_id;
calendar_name : string;
access_level : enum_access_level;
event_id : (null | type_local_resource_event_id);
event_object : type_event_object;
};
} }

View file

@ -65,6 +65,8 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/api/actions/session_oidc.ts \ ${dir_source}/api/actions/session_oidc.ts \
${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/session_end.ts \
${dir_source}/api/actions/users.ts \ ${dir_source}/api/actions/users.ts \
${dir_source}/api/actions/user_dav_conf.ts \
${dir_source}/api/actions/user_dav_token.ts \
${dir_source}/api/actions/calendar_list.ts \ ${dir_source}/api/actions/calendar_list.ts \
${dir_source}/api/actions/calendar_get.ts \ ${dir_source}/api/actions/calendar_get.ts \
${dir_source}/api/actions/calendar_add.ts \ ${dir_source}/api/actions/calendar_add.ts \
@ -75,6 +77,7 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/api/actions/calendar_event_change.ts \ ${dir_source}/api/actions/calendar_event_change.ts \
${dir_source}/api/actions/calendar_event_remove.ts \ ${dir_source}/api/actions/calendar_event_remove.ts \
${dir_source}/api/actions/events.ts \ ${dir_source}/api/actions/events.ts \
${dir_source}/api/actions/export_ics.ts \
${dir_source}/api/functions.ts \ ${dir_source}/api/functions.ts \
${dir_source}/main.ts ${dir_source}/main.ts
@ ${cmd_log} "compile …" @ ${cmd_log} "compile …"