From 78014d6a3aef9a4ef4b3720487a928ecb231b488 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 12 Sep 2024 19:35:31 +0200 Subject: [PATCH] [mod] --- conf/example.json | 2 +- data/example.kal.json | 4 +- source/api/actions/events.ts | 157 +++++++++++++++++ source/api/functions.ts | 4 + source/repositories/calendar.ts | 39 +++++ source/repositories/resource.ts | 2 +- source/services/calendar.ts | 290 ++++++++++++++++++-------------- tools/convert | 161 ++++++++++++++++++ tools/makefile | 1 + 9 files changed, 529 insertions(+), 131 deletions(-) create mode 100644 source/api/actions/events.ts create mode 100755 tools/convert diff --git a/conf/example.json b/conf/example.json index c63431d..bfe52ed 100644 --- a/conf/example.json +++ b/conf/example.json @@ -1,6 +1,6 @@ { "version": 1, "log": [ - {"kind": "stdout", "data": {"threshold": "debug"}} + {"kind": "stdout", "data": {"threshold": "info"}} ] } diff --git a/data/example.kal.json b/data/example.kal.json index e0b12aa..c6ab387 100644 --- a/data/example.kal.json +++ b/data/example.kal.json @@ -338,8 +338,8 @@ "data": { "name": "Lixer", "private": true, - "read_only": true, - "source_url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36" + "url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36", + "read_only": true } } } diff --git a/source/api/actions/events.ts b/source/api/actions/events.ts new file mode 100644 index 0000000..f12311d --- /dev/null +++ b/source/api/actions/events.ts @@ -0,0 +1,157 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_events( + rest_subject : lib_plankton.rest.type_rest + ) : void + { + register< + { + from : int; + to : int; + calendar_ids : ( + null + | + Array<_zeitbild.type.calendar_id> + ); + }, + Array< + { + calendar_id : int; + event : _zeitbild.type.event_object; + } + > + >( + rest_subject, + lib_plankton.http.enum_method.post, + "/events", + { + "description": "stellt Veranstaltungen aus verschiedenen Kalendern zusammen", + "query_parameters": [ + ], + "input_schema": () => ({ + "type": "object", + "nullable": false, + "additionalProperties": false, + "properties": { + "from": { + "type": "number", + "nullable": false, + }, + "to": { + "type": "number", + "nullable": false, + }, + "calendar_id": { + "type": "array", + "nullable": false, + "items": { + "type": "number", + "nullable": false + } + } + }, + "required": [ + "from", + "to", + ] + }), + "output_schema": () => ({ + "type": "array", + "items": { + "type": "object", + "nullable": false, + "additionalProperties": false, + "properties": { + "calendar_id": { + "type": "number", + "nullable": false, + }, + "event": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "nullable": false, + }, + "begin": { + "type": "int", + "nullable": false, + }, + "end": { + "type": "int", + "nullable": true, + }, + "location": { + "type": "string", + "nullable": true, + }, + "description": { + "type": "string", + "nullable": true, + }, + }, + "required": [ + "name", + "begin", + "end", + "location", + "description", + ] + } + }, + "required": [ + "calendar_id", + "event", + ], + } + }), + "restriction": restriction_none, // TODO + "execution": (stuff) => { + if (stuff.input === null) { + return Promise.resolve({ + "status_code": 400, + "data": null, + }); + } + else { + return ( + ( + (stuff.input.calendar_ids !== null) + ? + Promise.resolve(stuff.input.calendar_ids) + : + ( + _zeitbild.service.calendar.overview(0) // TODO: user_id + .then( + (x : any) => x.map((y : any) => y.id) + ) + ) + ) + .then( + (calendar_ids : Array<_zeitbild.type.calendar_id>) => _zeitbild.service.calendar.gather_events( + calendar_ids, + // @ts-ignore + stuff.input.from, + // @ts-ignore + stuff.input.to + ) + ) + .then( + (data : any) => Promise.resolve({ + "status_code": 200, + "data": data, + }) + ) + ); + } + } + } + ); + } + +} diff --git a/source/api/functions.ts b/source/api/functions.ts index a11b616..3462811 100644 --- a/source/api/functions.ts +++ b/source/api/functions.ts @@ -28,6 +28,10 @@ namespace _zeitbild.api { _zeitbild.api.register_calendar_list(rest_subject); } + // misc + { + _zeitbild.api.register_events(rest_subject); + } return rest_subject; diff --git a/source/repositories/calendar.ts b/source/repositories/calendar.ts index 9f93e8a..84b94a3 100644 --- a/source/repositories/calendar.ts +++ b/source/repositories/calendar.ts @@ -275,5 +275,44 @@ namespace _zeitbild.repository.calendar ); } + + /** + */ + export function overview( + user_id : _zeitbild.type.user_id + ) : Promise< + Array< + { + id : _zeitbild.type.calendar_id; + name : string; + public : boolean; + role : (null | _zeitbild.type.role); + } + > + > + { + return ( + _zeitbild.database.get_implementation().query_free_get( + { + "template": "SELECT x.id AS id, x.name AS name, x.public AS public, MAX(y.role) AS role FROM calendars AS x LEFT OUTER JOIN calendar_members AS y ON (x.id = y.calendar_id) WHERE (x.public OR (y.user_id = $user_id)) GROUP BY x.id;", + "arguments": { + "user_id": user_id, + } + } + ) + .then( + (rows) => Promise.resolve( + rows.map( + (row) => ({ + "id": row["id"], + "name": row["name"], + "public": row["public"], + "role": row["role"], + }) + ) + ) + ) + ) + } } diff --git a/source/repositories/resource.ts b/source/repositories/resource.ts index d36ec22..5a3fd58 100644 --- a/source/repositories/resource.ts +++ b/source/repositories/resource.ts @@ -284,7 +284,7 @@ namespace _zeitbild.repository.resource { "kind": "local", "data": { - "events": datasets_extra_local_events.map(x => decode_event(x)), + "events": datasets_extra_local_events.map(x => decode_event(x.preview)), } } ); diff --git a/source/services/calendar.ts b/source/services/calendar.ts index 6fbab73..a787c4e 100644 --- a/source/services/calendar.ts +++ b/source/services/calendar.ts @@ -31,6 +31,25 @@ namespace _zeitbild.service.calendar } + /** + */ + export function overview( + user_id : _zeitbild.type.user_id + ) : Promise< + Array< + { + id : _zeitbild.type.calendar_id; + name : string; + public : boolean; + role : (null | _zeitbild.type.role); + } + > + > + { + return _zeitbild.repository.calendar.overview(user_id); + } + + /** */ export async function get( @@ -39,14 +58,133 @@ namespace _zeitbild.service.calendar { return _zeitbild.repository.calendar.read(calendar_id); } - + /** */ - async function gather_events( - calendar_ids : Array<_zeitbild.type.calendar_id>, + async function get_events( + calendar_id : _zeitbild.type.calendar_id, from_pit : _zeitbild.helpers.type_pit, to_pit : _zeitbild.helpers.type_pit + ) : Promise< + Array< + _zeitbild.type.event_object + > + > + { + const calendar_object : _zeitbild.type.calendar_object = await _zeitbild.repository.calendar.read(calendar_id); + const resource_object : _zeitbild.type.resource_object = await _zeitbild.repository.resource.read(calendar_object.resource_id); + switch (resource_object.kind) { + case "local": { + return Promise.resolve( + resource_object.data.events + .filter( + (event : _zeitbild.type.event_object) => _zeitbild.helpers.pit_is_between( + _zeitbild.helpers.pit_from_datetime(event.begin), + from_pit, + to_pit + ) + ) + ) + break; + } + case "caldav": { + // TODO readonly + const url : lib_plankton.url.type_url = lib_plankton.url.decode( + resource_object.data.url + ); + const http_request : lib_plankton.http.type_request = { + "version": "HTTP/2", + "scheme": ((url.scheme === "https") ? "https" : "http"), + "host": url.host, + "path": (url.path ?? "/"), + "query": url.query, + "method": lib_plankton.http.enum_method.get, + "headers": {}, + "body": null, + }; + // TODO: cache? + const http_response : lib_plankton.http.type_response = await lib_plankton.http.call( + http_request, + { + } + ); + const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode( + http_response.body.toString(), + { + } + ); + return Promise.resolve( + vcalendar.vevents + .map( + (vevent : lib_plankton.ical.type_vevent) => ( + (vevent.dtstart !== undefined) + ? + { + "name": ( + (vevent.summary !== undefined) + ? + vevent.summary + : + "???" + ), + "begin": _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtstart), + "end": ( + (vevent.dtend !== undefined) + ? + _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtend) + : + null + ), + "location": ( + (vevent.location !== undefined) + ? + vevent.location + : + null + ), + "description": ( + (vevent.description !== undefined) + ? + vevent.description + : + null + ), + } + : + null + ) + ) + .filter( + (event) => (event !== null) + ) + .filter( + (event) => _zeitbild.helpers.pit_is_between( + _zeitbild.helpers.pit_from_datetime(event.begin), + from_pit, + to_pit + ) + ) + ); + break; + } + default: { + return Promise.reject( + new Error("invalid resource kind: " + resource_object["kind"]) + ); + break; + } + } + } + + + /** + * @todo user id + */ + export async function gather_events( + calendar_ids : Array<_zeitbild.type.calendar_id>, + from_pit : _zeitbild.helpers.type_pit, + to_pit : _zeitbild.helpers.type_pit, ) : Promise< Array< { @@ -56,136 +194,34 @@ namespace _zeitbild.service.calendar > > { - let result : Array< - { - calendar_id : _zeitbild.type.calendar_id; - event : _zeitbild.type.event_object; - } - > = []; - for await (const calendar_id of calendar_ids) { - const calendar_object : _zeitbild.type.calendar_object = await _zeitbild.repository.calendar.read(calendar_id); - const resource_object : _zeitbild.type.resource_object = await _zeitbild.repository.resource.read(calendar_object.resource_id); - switch (resource_object.kind) { - case "local": { - result = ( - result - .concat( - resource_object.data.events - .filter( - (event : _zeitbild.type.event_object) => _zeitbild.helpers.pit_is_between( - _zeitbild.helpers.pit_from_datetime(event.begin), - from_pit, - to_pit - ) - ) - .map( - (event : _zeitbild.type.event_object) => ({ - "calendar_id": calendar_id, - "event": event, - }) - ) - ) - ); - break; - } - case "caldav": { - // TODO readonly - const url : lib_plankton.url.type_url = lib_plankton.url.decode( - calendar_object.data.url - ); - const http_request : lib_plankton.http.type_request = { - "version": "HTTP/2", - "scheme": ((url.scheme === "https") ? "https" : "http"), - "host": url.host, - "path": (url.path ?? "/"), - "query": url.query, - "method": lib_plankton.http.enum_method.get, - "headers": {}, - "body": null, - }; - // TODO: cache? - const http_response : lib_plankton.http.type_response = await lib_plankton.http.call( - http_request, - { - } - ); - const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode( - http_response.body.toString(), - { - } - ); - result = ( - result - .concat( - vcalendar.vevents - .map( - (vevent : lib_plankton.ical.type_vevent) => ( - (vevent.dtstart !== undefined) - ? - { - "name": ( - (vevent.summary !== undefined) - ? - vevent.summary - : - "???" - ), - "begin": _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtstart), - "end": ( - (vevent.dtend !== undefined) - ? - _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtend) - : - null - ), - "location": ( - (vevent.location !== undefined) - ? - vevent.location - : - null - ), - "description": ( - (vevent.description !== undefined) - ? - vevent.description - : - null - ), - } - : - null - ) - ) - .filter( - (event) => (event !== null) - ) - .filter( - (event) => _zeitbild.helpers.pit_is_between( - _zeitbild.helpers.pit_from_datetime(event.begin), - from_pit, - to_pit - ) - ) - .map( + return ( + Promise.all( + calendar_ids + .map( + (calendar_id) => get_events( + calendar_id, + from_pit, + to_pit + ) + .then( + (events) => Promise.resolve( + events.map( (event) => ({ "calendar_id": calendar_id, "event": event, }) ) ) - ); - break; - } - default: { - return Promise.reject( - new Error("invalid resource kind: " + resource_object["kind"]) - ); - break; - } - } - } - return Promise.resolve(result); + ) + ) + ) + .then( + (sub_results) => sub_results.reduce( + (x, y) => x.concat(y), + [] + ) + ) + ); } } diff --git a/tools/convert b/tools/convert new file mode 100755 index 0000000..fdfa9f6 --- /dev/null +++ b/tools/convert @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +import json as _json +import datetime as _datetime + + +def sql_format( + value +): + if (value is None): + return "NULL" + else: + if (type(value) == bool): + return ('TRUE' if value else 'FALSE') + elif (type(value) == int): + return ("%u" % value) + elif (type(value) == str): + return ("'%s'" % value) + else: + raise ValueError("unhandled type: " + str(type(value))) + + +def string_coin( + template, + arguments +): + result = template + for (key, value, ) in arguments.items(): + result = result.replace("{{%s}}" % key, value) + return result + + +def file_read( + path +): + handle = open(path, "r") + content = handle.read() + handle.close() + return content + + +def datetime_convert( + datetime +): + return ( + None + if + (datetime is None) + else + string_coin( + "{{timezone_shift}}|{{year}}-{{month}}-{{day}}{{macro_time}}", + { + "timezone_shift": ("%02u" % datetime["timezone_shift"]), + "year": ("%04u" % datetime["date"]["year"]), + "month": ("%02u" % datetime["date"]["month"]), + "day": ("%02u" % datetime["date"]["day"]), + "macro_time": ( + "" + if + (datetime["time"] is None) + else + string_coin( + "T{{hour}}:{{minute}}:{{second}}", + { + "hour": ("%02u" % datetime["time"]["hour"]), + "minute": ("%02u" % datetime["time"]["minute"]), + "second": ("%02u" % datetime["time"]["second"]), + } + ) + ) + } + ) + ) + + +def main( +): + data = _json.loads(file_read("data/example.kal.json")) + for user in data["users"]: + print( + string_coin( + "INSERT INTO users(id,name) VALUES ({{id}},{{name}});\n", + { + "id": sql_format(user["id"]), + "name": sql_format(user["object"]["name"]), + } + ) + ) + ids = { + "local_resource": 0, + "caldav_resource": 0, + } + for calendar in data["calendars"]: + if (calendar["object"]["kind"] == "concrete"): + ids["local_resource"] += 1 + print( + string_coin( + "INSERT INTO local_resources(id) VALUES ({{id}});\n", + { + "id": sql_format(ids["local_resource"]) + } + ) + ) + for event in calendar["object"]["data"]["events"]: + print( + string_coin( + "INSERT INTO local_resource_events(local_resource_id,name,begin,end,location,description) VALUES ({{local_resource_id}},{{name}},{{begin}},{{end}},{{location}},{{description}});\n", + { + "local_resource_id": sql_format(ids["local_resource"]), + "name": sql_format(event["name"]), + "begin": sql_format(datetime_convert(event["begin"])), + "end": sql_format(datetime_convert(event["end"])), + "location": sql_format(event["location"]), + "description": sql_format(event["description"]), + } + ) + ) + print( + string_coin( + "INSERT INTO resources(kind,sub_id) VALUES ({{kind}},{{sub_id}});\n", + { + "kind": sql_format("local"), + "sub_id": sql_format(ids["local_resource"]) + } + ) + ) + elif (calendar["object"]["kind"] == "caldav"): + ids["caldav_resource"] += 1 + print( + string_coin( + "INSERT INTO caldav_resources(id,url,read_only) VALUES ({{id}},{{url}},{{read_only}});\n", + { + "id": sql_format(ids["caldav_resource"]), + "url": sql_format(calendar["object"]["data"]["url"]), + "read_only": sql_format(calendar["object"]["data"]["read_only"]), + } + ) + ) + print( + string_coin( + "INSERT INTO resources(kind,sub_id) VALUES ({{kind}},{{sub_id}});\n", + { + "kind": sql_format("caldav"), + "sub_id": sql_format(ids["caldav_resource"]) + } + ) + ) + else: + raise ValueError("invalid") + print( + string_coin( + "INSERT INTO calendars(name,public,resource_id) VALUES ({{name}},{{public}},LAST_INSERT_ROWID());\n", + { + "name": sql_format(calendar["object"]["data"]["name"]), + "public": sql_format(not calendar["object"]["data"]["private"]), + } + ) + ) + + +main() diff --git a/tools/makefile b/tools/makefile index b4fbd34..cc3dbbb 100644 --- a/tools/makefile +++ b/tools/makefile @@ -33,6 +33,7 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/api/actions/meta_ping.ts \ ${dir_source}/api/actions/meta_spec.ts \ ${dir_source}/api/actions/calendar_list.ts \ + ${dir_source}/api/actions/events.ts \ ${dir_source}/api/functions.ts \ ${dir_source}/main.ts @ ${cmd_log} "compile …"