Compare commits

...

37 commits

Author SHA1 Message Date
fenris 4733260de1 [fix] repository:calendar 2025-10-23 21:57:21 +02:00
fenris 2c019723bf Merge pull request 'Gruppen-Steuerung' (#2) from task-416 into main
Reviewed-on: #2
2025-10-23 19:16:29 +02:00
fenris bef69283f6 [task-416] [mod] repository📆sonder-query los werden 2025-10-23 19:08:00 +02:00
fenris 2a672381dd [fix] tools:deploy 2025-10-23 13:19:14 +02:00
fenris 24322588d7 [fix] tools:deploy 2025-10-23 13:19:04 +02:00
fenris 2eed983131 [task-416] [mod] oidc group handling [mod] calendar overview query handling (PostgreSQL specific) 2025-10-23 13:18:51 +02:00
fenris 3c49c744b3 [task-416] [upd] plankton 2025-10-23 13:17:10 +02:00
fenris 78729c6111 [task-416] 2025-10-23 11:34:00 +02:00
fenris 45acbbbcd9 [task-416] [upd] plankton 2025-10-23 11:33:41 +02:00
fenris d8c0e55340 [sty] 2025-10-22 00:49:54 +02:00
fenris 127c676fd1 [mod] example conf:caldav stuff 2025-10-22 00:49:35 +02:00
fenris e6571aeff4 [add] data:house_and_garden 2025-10-22 00:49:10 +02:00
fenris 6c548272fe [task-388] 2025-10-17 13:16:56 +02:00
fenris e552560887 [mod] 2025-10-17 00:42:11 +02:00
fenris 944043d873 [add] exaple-data:minimal 2025-10-14 23:31:18 +02:00
fenris 60f1c5a0de [mod] api:action:events:hash mit ausgeben 2025-10-14 23:31:05 +02:00
fenris e66da38cb1 [upd] plankton 2025-10-14 23:30:34 +02:00
fenris bf4f2fab6e [task-396] 2025-10-13 13:19:33 +02:00
fenris 439f046b1d [mod] Beispiel-Daten mit relativen Datums-Angaben 2025-10-13 10:48:47 +02:00
fenris 8163a12a68 [upd] plankton 2025-10-07 09:33:10 +02:00
fenris f5963c3553 [sty] 2025-10-06 21:42:50 +02:00
fenris 243f1bb155 [add] api:session_status 2025-10-02 16:58:27 +02:00
fenris e33fb8bc53 [sty] main [mod] main:session prolongation 2025-10-02 16:58:11 +02:00
fenris 0e8e9ec68b [mod] conf 2025-10-02 16:57:36 +02:00
fenris d8a74f8e37 [sty] api:action:session_begin 2025-10-02 16:57:17 +02:00
fenris 477eeadc14 [fix] tools:plankton 2025-10-02 16:56:42 +02:00
fenris 8b480ecc92 [upd] plankton 2025-10-02 16:56:28 +02:00
fenris 002e057d45 [sty] 2025-10-02 14:30:52 +02:00
fenris 49b9481701 [sty] 2025-10-02 14:30:19 +02:00
fenris 671cbfea01 [mod] readme 2025-09-26 11:51:47 +02:00
fenris efb7b09785 [fix] readme:meta link 2025-09-26 10:01:23 +02:00
fenris 0e6be12680 [mod] conf-example 2025-09-26 02:25:51 +02:00
fenris 3aac310a65 [fix] ics export 2025-09-26 02:25:41 +02:00
fenris 115a128d62 [del] doc [mod] readme 2025-09-25 17:39:48 +02:00
fenris d38e1da8f9 [add] Lizenz-Infos für Quelldateien 2025-09-25 17:18:16 +02:00
fenris 663b6b85b5 [mov] data-example 2025-09-25 17:12:21 +02:00
fenris f90567d043 [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>
2025-09-25 17:05:15 +02:00
58 changed files with 5356 additions and 1693 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

@ -1,94 +0,0 @@
# Konzept
## Notizen
- Kalender sollen unabhängig von Nutzern bestehen können
- Zugriffs-Stufen
- `none`: kein Zugriff
- `view`: nur lesend
- `edit`: lesend und schreibend
- `admin`: kann alles (auch Kalender löschen)
- einem Kalender können beliebig viele Nutzer zugeordnet werden, die jeweils bestimmte Zugriffs-Stufen zugewiesen haben
- Veranstaltungen bilden keine eigene Domäne
- es gibt verschiedene Arten von Quellen:
- lokal
- enthält Veranstaltungen
- caldav
- enthält keine eigenen Veranstaltungen
- sollte read-only- und read/write-Modus haben
- nach dem Anmelden sieht man eine Kalender-Ansicht mit folgenden Kalendern kombiniert angezeigt:
- öffentliche Kalender
- nicht öffentliche Kalendar, bei welchen man mindestens Lese-Berechtigung hat
- öffentliche Kalendar können ohne Anmeldung betrachtet werden, jedoch nur mit einem schwer bis gar nicht erratbaren Link
## Zugriffssteuerung
Es gibt folgende Zugriffsstufen :
- `none`: kein Zugriff
- `view`: nur Lese-Zugriff
- `edit`: Lese- und Schreib-Zugriff
- `admin`: Lese- und Schreib-Zugriff + Bearbeitung von Kalender-Eigenschaften
Zur Bestimmung welche Zugriffsstufe ein Nutzer auf einen Kalender/Termin hat, werden drei Werte betrachtet:
- Öffentlichkeit des Kalenders (`public`)
- Standard-Zugriffsstufe des Kalenders (`default_level`)
- dem Nutzer zugewiesene Zugriffsstufe für den Kalender (`attributed_level`)
Diese Angaben fließen in folgende Formel ein:
```
access_level = max(
(if public then 'view' else 'none'),
(if (attributed = NULL) then default_level else attributed_level)
)
```
… wobei die Ordnung der Zugriffsstufen der oben ersichtlichen Reihenfolge entspricht.
Einzeln aufgeführt ergibt das:
| public | default level | attributed level | result |
|-- |-- |-- |-- |
| no | none | - | none |
| no | none | none | none |
| no | none | view | view |
| no | none | edit | edit |
| no | none | admin | admin |
| no | view | - | view |
| no | view | none | none |
| no | view | view | view |
| no | view | edit | edit |
| no | view | admin | admin |
| no | edit | - | edit |
| no | edit | none | none |
| no | edit | view | view |
| no | edit | edit | edit |
| no | edit | admin | admin |
| no | admin | - | admin |
| no | admin | none | none |
| no | admin | view | view |
| no | admin | edit | edit |
| no | admin | admin | admin |
| yes | none | - | view |
| yes | none | none | view |
| yes | none | view | view |
| yes | none | edit | edit |
| yes | none | admin | admin |
| yes | view | - | view |
| yes | view | none | view |
| yes | view | view | view |
| yes | view | edit | edit |
| yes | view | admin | admin |
| yes | edit | - | edit |
| yes | edit | none | view |
| yes | edit | view | view |
| yes | edit | edit | edit |
| yes | edit | admin | admin |
| yes | admin | - | admin |
| yes | admin | none | none |
| yes | admin | view | view |
| yes | admin | edit | edit |
| yes | admin | admin | admin |

View file

@ -1,11 +1,11 @@
/** /**
* @author fenris * @author fenris
*/ */
declare type int = number; type int = number;
/** /**
* @author fenris * @author fenris
*/ */
declare type float = number; type float = number;
declare var process: any; declare var process: any;
declare var require: any; declare var require: any;
declare class Buffer { declare class Buffer {
@ -22,7 +22,7 @@ declare namespace lib_plankton.base {
/** /**
* @author fenris * @author fenris
*/ */
declare type type_pseudopointer<type_value> = { type type_pseudopointer<type_value> = {
value: type_value; value: type_value;
}; };
/** /**
@ -229,9 +229,10 @@ declare namespace lib_plankton.base {
function object_merge(core: Record<string, any>, mantle: Record<string, any>): Record<string, any>; function object_merge(core: Record<string, any>, mantle: Record<string, any>): Record<string, any>;
/** /**
*/ */
function buffer_show(buffer: Buffer, { "block_size": option_block_size, "break_char": option_break_char, }?: { function buffer_show(buffer: Buffer, { "block_size": option_block_size, "break_char": option_break_char, "render_readable_characters": render_readable_characters, }?: {
block_size?: int; block_size?: int;
break_char?: string; break_char?: string;
render_readable_characters?: boolean;
}): string; }): string;
} }
declare module lib_plankton.pod { declare module lib_plankton.pod {
@ -586,6 +587,10 @@ declare namespace lib_plankton.call {
* @author fenris * @author fenris
*/ */
export function timeout(procedure: (() => void), delay_in_seconds: float): int; export function timeout(procedure: (() => void), delay_in_seconds: float): int;
/**
* @author fenris
*/
export function loop(procedure: (() => void), delay_in_seconds: float): int;
/** /**
* Promise version of "setTimeout" * Promise version of "setTimeout"
* *
@ -653,6 +658,9 @@ declare namespace lib_plankton.call {
/** /**
*/ */
export function sleep(seconds: float): Promise<void>; export function sleep(seconds: float): Promise<void>;
/**
*/
export function null_prop<type_value_from, type_value_to>(value_from: (null | type_value_from), function_: ((value: type_value_from) => type_value_to)): (null | type_value_to);
export {}; export {};
} }
declare namespace lib_plankton.email { declare namespace lib_plankton.email {
@ -2315,7 +2323,7 @@ declare namespace lib_plankton.storage.memory {
clear(): Promise<void>; clear(): Promise<void>;
write(key: any, value: any): Promise<boolean>; write(key: any, value: any): Promise<boolean>;
delete(key: any): Promise<void>; delete(key: any): Promise<void>;
read(key: any): Promise<type_item>; read(key: any): Promise<Awaited<type_item>>;
search(term: any): Promise<{ search(term: any): Promise<{
key: string; key: string;
preview: string; preview: string;
@ -3073,26 +3081,73 @@ declare namespace lib_plankton.session {
}; };
/** /**
*/ */
function begin(name: string, options?: { function begin(name: string, { "lifetime": lifetime, "data": data, }?: {
lifetime?: int; lifetime?: int;
data?: any; data?: any;
}): Promise<string>; }): Promise<string>;
/** /**
*/ */
function get(key: string): Promise<type_session>; function get(key: string, { "prolongation": prolongation, }?: {
prolongation?: (null | int);
}): Promise<type_session>;
/** /**
*/ */
function end(key: string): Promise<void>; function end(key: string): Promise<void>;
/** /**
*/ */
function setup(options?: { function setup({ "key_length": key_length, "key_max_attempts": key_max_attempts, "default_lifetime": default_lifetime, "default_prolongation": default_prolongation, "data_chest": data_chest, "clear": clear, }?: {
key_length?: int; key_length?: int;
key_max_attempts?: int; key_max_attempts?: int;
default_lifetime?: int; default_lifetime?: int;
default_prolongation?: (null | int);
data_chest?: lib_plankton.storage.type_chest<string, any, void, string, string>; data_chest?: lib_plankton.storage.type_chest<string, any, void, string, string>;
clear?: boolean; clear?: boolean;
}): Promise<void>; }): Promise<void>;
} }
declare namespace lib_plankton.base64 {
/**
* @author fenris
*/
type type_source = string;
/**
* @author fenris
*/
type type_target = string;
/**
* @author fenris
*/
export function encode(source: type_source): type_target;
/**
* @author fenris
*/
export function decode(target: type_target): type_source;
/**
* @author fenris
*/
export function implementation_code(): lib_plankton.code.type_code<type_source, type_target>;
export {};
}
declare namespace lib_plankton.base64 {
/**
* @author fenris
*/
class class_base64 implements lib_plankton.code.interface_code<string, string> {
/**
* @author fenris
*/
constructor();
/**
* @implementation
* @author fenris
*/
encode(x: string): string;
/**
* @implementation
* @author fenris
*/
decode(x: string): string;
}
}
declare namespace lib_plankton { declare namespace lib_plankton {
namespace order { namespace order {
/** /**
@ -4077,7 +4132,7 @@ declare namespace lib_plankton.server {
}; };
/** /**
*/ */
function make(handle: ((input: string, metadata?: type_metadata) => Promise<string>), options?: { function make(handle: ((input: string, metadata?: type_metadata) => Promise<string>), { "host": host, "port": port, "threshold": threshold, }?: {
host?: string; host?: string;
port?: int; port?: int;
threshold?: (null | float); threshold?: (null | float);
@ -4501,50 +4556,6 @@ declare namespace lib_plankton.map.collatemap {
export function implementation_map<type_key, type_value>(subject: type_subject<type_key, type_value>): type_map<type_key, type_value>; export function implementation_map<type_key, type_value>(subject: type_subject<type_key, type_value>): type_map<type_key, type_value>;
export {}; export {};
} }
declare namespace lib_plankton.base64 {
/**
* @author fenris
*/
type type_source = string;
/**
* @author fenris
*/
type type_target = string;
/**
* @author fenris
*/
export function encode(source: type_source): type_target;
/**
* @author fenris
*/
export function decode(target: type_target): type_source;
/**
* @author fenris
*/
export function implementation_code(): lib_plankton.code.type_code<type_source, type_target>;
export {};
}
declare namespace lib_plankton.base64 {
/**
* @author fenris
*/
class class_base64 implements lib_plankton.code.interface_code<string, string> {
/**
* @author fenris
*/
constructor();
/**
* @implementation
* @author fenris
*/
encode(x: string): string;
/**
* @implementation
* @author fenris
*/
decode(x: string): string;
}
}
declare namespace lib_plankton.auth { declare namespace lib_plankton.auth {
/** /**
*/ */
@ -4586,9 +4597,11 @@ declare namespace lib_plankton.auth.oidc {
type type_token = string; type type_token = string;
/** /**
*/ */
type type_userinfo = { export type type_userinfo = {
name: (null | string); name: (null | string);
label: (null | string);
email: (null | string); email: (null | string);
groups: (null | Array<string>);
}; };
/** /**
*/ */

File diff suppressed because it is too large Load diff

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

@ -0,0 +1,70 @@
{
"version": 1,
"log": [
{
"kind": "file",
"data": {
"path": "/tmp/zeitbild/log.jsonl",
"threshold": "info",
"format": "jsonl_structured"
}
},
{
"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/-/demo",
"username": "demo-{{username}}",
"password": "{{password}}",
"setup_hints": [
{
"label": "Liste von Clients",
"link": "https://devguide.calconnect.org/CalDAV/Client-Implementations/"
},
{
"label": "Android",
"link": "https://goneuland.de/davx5-android-kalender-per-caldav-integrieren-z-b-nextcloud/",
"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://all-inkl.com/wichtig/anleitungen/programme/e-mail/caldav-kalenderfunktion/outlook-2019_468.html",
"remark": "eigentlich für Server 'all-inkl.com' — Zugangsdaten müssen entsprechend geändert werden"
}
]
}
}

View file

@ -0,0 +1,328 @@
{
"groups": [
{
"id": 1,
"name": "gaertner",
"label": "Gärtner"
},
{
"id": 2,
"name": "bewohner",
"label": "Bewohner"
}
],
"users": [
{
"id": 1,
"name": "alice",
"groups": [1],
"email_address": "alice@example.org",
"dav_token": null,
"password": "alice"
},
{
"id": 2,
"name": "bob",
"groups": [2],
"email_address": "bob@example.org",
"dav_token": null,
"password": "bob"
},
{
"id": 3,
"name": "charlie",
"groups": [1, 2],
"email_address": "charlie@example.org",
"dav_token": "charlie_dav",
"password": "charlie"
}
],
"calendars": [
{
"id": 1,
"name": "Feiertage",
"hue": 0.0000,
"access": {
"public": true,
"default_level": "view",
"attributed_group": [
],
"attributed_user": [
{
"user_id": 3,
"level": "admin"
}
]
},
"resource": {
"kind": "local",
"data": {
"events": [
{
"name": "Tag des Wassers",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": [0]},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [1]}
],
"time": null
},
"end": null,
"location": null,
"description": null
},
{
"name": "Tag der Erde",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": [0]},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [6]}
],
"time": null
},
"end": null,
"location": null,
"description": null
},
{
"name": "Tag der Luft",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": [0]},
{"action": "shift_week", "args": [2]},
{"action": "shift_day", "args": [4]}
],
"time": null
},
"end": null,
"location": null,
"description": null
}
]
}
}
},
{
"id": 2,
"name": "Garten",
"hue": 0.3333,
"access": {
"public": false,
"default_level": "none",
"attributed_group": [
{
"group_id": 1,
"level": "view"
}
],
"attributed_user": [
{
"user_id": 1,
"level": "admin"
}
]
},
"resource": {
"kind": "local",
"data": {
"events": [
{
"name": "Unkraut jähten",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [3]}
],
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [3]}
],
"time": {"hour": 11, "minute": 0, "second": 0}
},
"location": "Drosselweg 7",
"description": "Giersch und Rainkohl haben wider gewuchert"
},
{
"name": "Holz hacken",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [0]}
],
"time": {"hour": 13, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [0]}
],
"time": {"hour": 14, "minute": 0, "second": 0}
},
"location": "Drosselweg 7",
"description": "die alte Birke ist bereits in Segmente geschnitten und muss nun noch in handliche Scheite zerlegt werden"
},
{
"name": "Blumen gießen",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 18, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 19, "minute": 0, "second": 0}
},
"location": "Drosselweg 7",
"description": "erst in der Abendzeit, damit die Sonne tief steht und das Gießen sich lohnt"
},
{
"name": "Grill-Fete",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [5]}
],
"time": {"hour": 18, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [5]}
],
"time": {"hour": 23, "minute": 0, "second": 0}
},
"location": "Drosselweg 7",
"description": "Grillgut ist selbst mitzubringen; Getränke sind bereits vor Ort"
}
]
}
}
},
{
"id": 3,
"name": "Haus",
"hue": 0.6667,
"access": {
"public": false,
"default_level": "none",
"attributed_group": [
{
"group_id": 2,
"level": "view"
}
],
"attributed_user": [
{
"user_id": 2,
"level": "admin"
}
]
},
"resource": {
"kind": "local",
"data": {
"events": [
{
"name": "Bad putzen",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [4]}
],
"time": {"hour": 8, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [4]}
],
"time": {"hour": 9, "minute": 0, "second": 0}
},
"location": "Eschenhain 11",
"description": "das Waschbecken sollte unbedingt von Keimen befreit werden"
},
{
"name": "Abstellkammer aufräumen",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [0]}
],
"time": {"hour": 12, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [0]}
],
"time": {"hour": 13, "minute": 0, "second": 0}
},
"location": "Eschenhain 11",
"description": "einiges davon kann vermutlich gleich entsorgt werden"
},
{
"name": "Treppe wischen",
"begin": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [2]},
{"action": "shift_day", "args": [1]}
],
"time": {"hour": 14, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 0,
"date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [2]},
{"action": "shift_day", "args": [1]}
],
"time": {"hour": 15, "minute": 0, "second": 0}
},
"location": "Eschenhain 11",
"description": "die dreckigen Schuhe haben ihre Spuren hinterlassen"
}
]
}
}
}
]
}

View file

@ -4,18 +4,21 @@
"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"
} }
], ],
@ -23,7 +26,9 @@
{ {
"id": 1, "id": 1,
"name": "LV Lampukistan", "name": "LV Lampukistan",
"hue": 0.0000,
"access": { "access": {
"public": true,
"default_level": "view", "default_level": "view",
"attributed": [ "attributed": [
{ {
@ -44,12 +49,20 @@
"name": "Aufstand: Mieten", "name": "Aufstand: Mieten",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 14}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 12, "minute": 0, "second": 0} "time": {"hour": 12, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 14}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [-1]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 15, "minute": 0, "second": 0} "time": {"hour": 15, "minute": 0, "second": 0}
}, },
"location": "Porada Ninfu, Haupt-Markt", "location": "Porada Ninfu, Haupt-Markt",
@ -59,12 +72,20 @@
"name": "Aufstand: Waffen", "name": "Aufstand: Waffen",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 21}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 12, "minute": 0, "second": 0} "time": {"hour": 12, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 21}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 15, "minute": 0, "second": 0} "time": {"hour": 15, "minute": 0, "second": 0}
}, },
"location": "Tandreell, Stoiber-Platz", "location": "Tandreell, Stoiber-Platz",
@ -74,12 +95,20 @@
"name": "Aufstand: Essen", "name": "Aufstand: Essen",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 10, "day": 28}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 12, "minute": 0, "second": 0} "time": {"hour": 12, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 10, "day": 28}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [6]}
],
"time": {"hour": 15, "minute": 0, "second": 0} "time": {"hour": 15, "minute": 0, "second": 0}
}, },
"location": "Kawanda, Nord-Bahnhof", "location": "Kawanda, Nord-Bahnhof",
@ -92,7 +121,9 @@
{ {
"id": 2, "id": 2,
"name": "KV Zepettel-Region", "name": "KV Zepettel-Region",
"hue": 0.3333,
"access": { "access": {
"public": false,
"default_level": "view", "default_level": "view",
"attributed": [ "attributed": [
{ {
@ -113,12 +144,20 @@
"name": "Feier: Bier", "name": "Feier: Bier",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 18}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [3]}
],
"time": {"hour": 18, "minute": 0, "second": 0} "time": {"hour": 18, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 18}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [3]}
],
"time": {"hour": 23, "minute": 0, "second": 0} "time": {"hour": 23, "minute": 0, "second": 0}
}, },
"location": "Rudschadinedschad, Schlamm-Park", "location": "Rudschadinedschad, Schlamm-Park",
@ -128,12 +167,20 @@
"name": "Feier: Schnapps", "name": "Feier: Schnapps",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 10, "day": 1}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [2]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 18, "minute": 0, "second": 0} "time": {"hour": 18, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 10, "day": 1}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [2]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 23, "minute": 0, "second": 0} "time": {"hour": 23, "minute": 0, "second": 0}
}, },
"location": "Kawanda, Ratten-Platz", "location": "Kawanda, Ratten-Platz",
@ -146,7 +193,9 @@
{ {
"id": 3, "id": 3,
"name": "OV Kawanda", "name": "OV Kawanda",
"hue": 0.6667,
"access": { "access": {
"public": false,
"default_level": "view", "default_level": "view",
"attributed": [ "attributed": [
{ {
@ -163,12 +212,20 @@
"name": "Aufräumen: Flaschen", "name": "Aufräumen: Flaschen",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 24}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 15, "minute": 0, "second": 0} "time": {"hour": 15, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 24}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [1]},
{"action": "shift_day", "args": [2]}
],
"time": {"hour": 17, "minute": 0, "second": 0} "time": {"hour": 17, "minute": 0, "second": 0}
}, },
"location": "Kawanda, Penner-Allee", "location": "Kawanda, Penner-Allee",
@ -181,7 +238,9 @@
{ {
"id": 4, "id": 4,
"name": "KV Zepettel-Region | intern", "name": "KV Zepettel-Region | intern",
"hue": 0.8333,
"access": { "access": {
"public": false,
"default_level": "none", "default_level": "none",
"attributed": [ "attributed": [
{ {
@ -202,12 +261,20 @@
"name": "Infostand", "name": "Infostand",
"begin": { "begin": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 16}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [1]}
],
"time": {"hour": 10, "minute": 0, "second": 0} "time": {"hour": 10, "minute": 0, "second": 0}
}, },
"end": { "end": {
"timezone_shift": 2, "timezone_shift": 2,
"date": {"year": 2025, "month": 9, "day": 16}, "date_relative": [
{"action": "trunc_week", "args": []},
{"action": "shift_week", "args": [0]},
{"action": "shift_day", "args": [1]}
],
"time": {"hour": 14, "minute": 0, "second": 0} "time": {"hour": 14, "minute": 0, "second": 0}
}, },
"location": "Rudschadinedschad, Schabracken-Markt", "location": "Rudschadinedschad, Schabracken-Markt",

13
misc/data-minimal.json Normal file
View file

@ -0,0 +1,13 @@
{
"users": [
{
"id": 1,
"name": "user",
"email_address": "user@example.org",
"dav_token": null,
"password": "user"
}
],
"calendars": [
]
}

View file

@ -2,7 +2,7 @@
## Beschreibung ## Beschreibung
- Hintergrund-Dienst für Kalendar-Erstellung und -Darstellung - Hintergrund-Dienst für [zeitbild](/zeitbild/meta)
## Erstellung ## Erstellung

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -14,7 +33,13 @@ namespace _zeitbild.api
access : { access : {
public : boolean; public : boolean;
default_level : string; default_level : string;
attributed : Array< attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : string; level : string;
@ -29,14 +54,14 @@ 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;
}; };
} }
); );
hue : float;
}, },
int int
>( >(
@ -75,12 +100,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,
} }
}; };
@ -94,17 +118,33 @@ namespace _zeitbild.api
"name": stuff.input.name, "name": stuff.input.name,
"access": { "access": {
"public": stuff.input.access.public, "public": stuff.input.access.public,
"default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level), "default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level),
"attributed": lib_plankton.map.hashmap.implementation_map( "attributed_group": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make( lib_plankton.map.hashmap.make(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
stuff.input.access.attributed stuff.input.access.attributed_group
.map(
(entry) => ({
"key": entry.group_id,
"value": _zeitbild.access_level_from_string(entry.level),
})
)
),
}
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
stuff.input.access.attributed_user
.map( .map(
(entry) => ({ (entry) => ({
"key": entry.user_id, "key": entry.user_id,
"value": _zeitbild.value_object.access_level.from_string(entry.level), "value": _zeitbild.access_level_from_string(entry.level),
}) })
) )
.concat( .concat(
@ -120,7 +160,8 @@ namespace _zeitbild.api
) )
), ),
}, },
"resource_id": resource_id "resource_id": resource_id,
"hue": stuff.input.hue,
}; };
return ( return (
_zeitbild.service.calendar.add(calendar_object) _zeitbild.service.calendar.add(calendar_object)

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -11,13 +30,20 @@ namespace _zeitbild.api
register< register<
{ {
name : string; name : string;
hue : float;
access : { access : {
public : boolean; public : boolean;
default_level : ("none" | "view" | "edit" | "admin"); default_level : string;
attributed : Array< attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : ("none" | "view" | "edit" | "admin"); level : string;
} }
>; >;
}; };
@ -54,19 +80,36 @@ namespace _zeitbild.api
); );
const calendar_object_new : _zeitbild.type_calendar_object = { const calendar_object_new : _zeitbild.type_calendar_object = {
"name": stuff.input.name, "name": stuff.input.name,
"hue": stuff.input.hue,
"access": { "access": {
"public": stuff.input.access.public, "public": stuff.input.access.public,
"default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level), "default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level),
"attributed": lib_plankton.map.hashmap.implementation_map( "attributed_group": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make( lib_plankton.map.hashmap.make(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
stuff.input.access.attributed stuff.input.access.attributed_group
.map(
(entry) => ({
"key": entry.group_id,
"value": _zeitbild.access_level_from_string(entry.level),
})
)
),
}
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
stuff.input.access.attributed_user
.map( .map(
(entry) => ({ (entry) => ({
"key": entry.user_id, "key": entry.user_id,
"value": _zeitbild.value_object.access_level.from_string(entry.level), "value": _zeitbild.access_level_from_string(entry.level),
}) })
) )
), ),
@ -74,7 +117,7 @@ namespace _zeitbild.api
) )
), ),
}, },
"resource_id": calendar_object_old.resource_id "resource_id": calendar_object_old.resource_id,
}; };
await _zeitbild.service.calendar.change( await _zeitbild.service.calendar.change(
calendar_id, calendar_id,

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -11,7 +30,10 @@ namespace _zeitbild.api
register< register<
_zeitbild.type_event_object, // TODO aufdröseln _zeitbild.type_event_object, // TODO aufdröseln
( (
null {
local_resource_event_id : (null | int);
hash : string;
}
| |
string string
) )
@ -69,10 +91,12 @@ namespace _zeitbild.api
const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(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); const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name);
if (stuff.input === null) { if (stuff.input === null)
{
return Promise.reject(new Error("impossible")); return Promise.reject(new Error("impossible"));
} }
else { else
{
return ( return (
_zeitbild.service.calendar.event_add( _zeitbild.service.calendar.event_add(
parseInt(stuff.path_parameters["calendar_id"]), parseInt(stuff.path_parameters["calendar_id"]),
@ -80,9 +104,9 @@ namespace _zeitbild.api
user_id user_id
) )
.then( .then(
() => Promise.resolve({ (data) => Promise.resolve({
"status_code": 200, "status_code": 200,
"data": null, "data": data,
}) })
) )
// TODO distinguish // TODO distinguish

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -69,10 +88,12 @@ namespace _zeitbild.api
const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(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); const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name);
if (stuff.input === null) { if (stuff.input === null)
{
return Promise.reject(new Error("impossible")); return Promise.reject(new Error("impossible"));
} }
else { else
{
return ( return (
_zeitbild.service.calendar.event_change( _zeitbild.service.calendar.event_change(
parseInt(stuff.path_parameters["calendar_id"]), parseInt(stuff.path_parameters["calendar_id"]),
@ -81,17 +102,21 @@ namespace _zeitbild.api
user_id user_id
) )
.then( .then(
() => Promise.resolve({ () => Promise.resolve(
"status_code": 200, {
"data": null, "status_code": 200,
}) "data": null,
}
)
) )
// TODO distinguish // TODO distinguish
.catch( .catch(
(reason) => Promise.resolve({ (reason) => Promise.resolve(
"status_code": 403, {
"data": String(reason), "status_code": 403,
}) "data": String(reason),
}
)
) )
); );
} }

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -12,14 +31,22 @@ namespace _zeitbild.api
null, null,
{ {
name : string; name : string;
hue : float;
access : { access : {
public : boolean;
default_level : string; default_level : string;
attributed : Array< attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : string; level : string;
} }
> >;
}; };
resource_id : int; resource_id : int;
} }
@ -44,11 +71,27 @@ namespace _zeitbild.api
); );
const result = { const result = {
"name": calendar_object.name, "name": calendar_object.name,
"hue": calendar_object.hue,
"access": { "access": {
"public": calendar_object.access.public, "public": calendar_object.access.public,
"default_level": _zeitbild.api.access_level_encode(calendar_object.access.default_level), "default_level": _zeitbild.api.access_level_encode(calendar_object.access.default_level),
"attributed": lib_plankton.call.convey( "attributed_group": lib_plankton.call.convey(
calendar_object.access.attributed, calendar_object.access.attributed_group,
[
lib_plankton.map.dump,
(pairs : Array<{key : _zeitbild.type_group_id; value : _zeitbild.enum_access_level;}>) => (
pairs
.map(
(pair : {key : _zeitbild.type_group_id; value : _zeitbild.enum_access_level;}) => ({
"group_id": pair.key,
"level": _zeitbild.api.access_level_encode(pair.value)
})
)
)
]
),
"attributed_user": lib_plankton.call.convey(
calendar_object.access.attributed_user,
[ [
lib_plankton.map.dump, lib_plankton.map.dump,
(pairs : Array<{key : _zeitbild.type_user_id; value : _zeitbild.enum_access_level;}>) => ( (pairs : Array<{key : _zeitbild.type_user_id; value : _zeitbild.enum_access_level;}>) => (

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -14,6 +33,7 @@ namespace _zeitbild.api
{ {
id : int; id : int;
name : string; name : string;
hue : float;
access_level : string; access_level : string;
} }
> >
@ -40,6 +60,10 @@ namespace _zeitbild.api
"type": "string", "type": "string",
"nullable": false, "nullable": false,
}, },
"hue": {
"nullable": false,
"type": "number"
},
"access_level": { "access_level": {
"type": "string", "type": "string",
"nullable": true, "nullable": true,
@ -49,8 +73,8 @@ namespace _zeitbild.api
"required": [ "required": [
"id", "id",
"name", "name",
"public", "hue",
"role", "access_level",
], ],
} }
}), }),
@ -76,7 +100,8 @@ namespace _zeitbild.api
(entry) => ({ (entry) => ({
"id": entry.id, "id": entry.id,
"name": entry.name, "name": entry.name,
"access_level": _zeitbild.value_object.access_level.to_string(entry.access_level), "hue": entry.hue,
"access_level": _zeitbild.access_level_to_string(entry.access_level),
}) })
) )
) )

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -13,8 +32,10 @@ namespace _zeitbild.api
( (
Array< Array<
{ {
hash : _zeitbild.type_event_hash;
calendar_id : int; calendar_id : int;
calendar_name : string; calendar_name : string;
hue : float;
access_level : string; access_level : string;
event_id : (null | int); event_id : (null | int);
event_object : _zeitbild.type_event_object; event_object : _zeitbild.type_event_object;
@ -53,6 +74,10 @@ namespace _zeitbild.api
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"hash": {
"nullable": false,
"type": "string",
},
"calendar_id": { "calendar_id": {
"nullable": false, "nullable": false,
"type": "number", "type": "number",
@ -61,6 +86,10 @@ namespace _zeitbild.api
"nullable": false, "nullable": false,
"type": "string", "type": "string",
}, },
"hue": {
"nullable": false,
"type": "number"
},
"access_level": { "access_level": {
"nullable": false, "nullable": false,
"type": "string", "type": "string",
@ -167,8 +196,11 @@ namespace _zeitbild.api
data data
.map( .map(
(entry) => ({ (entry) => ({
// todo
"hash": entry.hash,
"calendar_id": entry.calendar_id, "calendar_id": entry.calendar_id,
"calendar_name": entry.calendar_name, "calendar_name": entry.calendar_name,
"hue": entry.hue,
"access_level": _zeitbild.api.access_level_encode(entry.access_level), "access_level": _zeitbild.api.access_level_encode(entry.access_level),
"event_id": entry.event_id, "event_id": entry.event_id,
"event_object": entry.event_object, "event_object": entry.event_object,

View file

@ -0,0 +1,146 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
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

@ -0,0 +1,110 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api
{
/**
*/
export function register_group_list(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
null,
Array<
{
id : int;
name : string;
label : string;
}
>
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/groups",
{
"description": "listet alle Gruppen auf",
"query_parameters": () => ([
{
"name": "term",
"required": false,
"description": "search term",
},
]),
"output_schema": () => ({
"type": "array",
"items": {
"nullable": false,
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"nullable": false,
"type": "number",
},
"name": {
"nullable": false,
"type": "string",
},
"label": {
"nullable": false,
"type": "string",
},
},
"required": [
"id",
"name",
"label",
],
}
}),
"restriction": restriction_logged_in,
"execution": async (stuff) => {
const result : Array<
{
id : _zeitbild.type_group_id;
name : string;
label : string;
}
> = (
(await _zeitbild.service.group.list())
.map(
entry => (
{
"id": entry.id,
"name": entry.object.name,
"label": entry.object.label,
}
)
)
);
return Promise.resolve(
{
"status_code": 200,
"data": result,
}
);
}
}
);
}
}

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -46,23 +65,30 @@ namespace _zeitbild.api
}), }),
"restriction": () => restriction_none, "restriction": () => restriction_none,
"execution": () => async ({"input": input}) => { "execution": () => async ({"input": input}) => {
if (input === null) { if (input === null)
{
return Promise.reject(new Error("impossible")); return Promise.reject(new Error("impossible"));
} }
else { else
{
const passed : boolean = await _zeitbild.service.auth_internal.check(input.name, input.password); const passed : boolean = await _zeitbild.service.auth_internal.check(input.name, input.password);
if (! passed) { if (! passed)
return Promise.resolve({ {
"status_code": 403, return Promise.resolve(
"data": null, {
}); "status_code": 403,
"data": null,
}
);
} }
else { else {
const session_key : string = await lib_plankton.session.begin(input.name); const session_key : string = await lib_plankton.session.begin(input.name);
return Promise.resolve({ return Promise.resolve(
"status_code": 201, {
"data": session_key, "status_code": 201,
}); "data": session_key,
}
);
} }
} }
}, },

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -24,10 +43,12 @@ namespace _zeitbild.api
"execution": async (stuff) => { "execution": async (stuff) => {
const session : {key : string; value : lib_plankton.session.type_session} = await session_from_stuff(stuff); const session : {key : string; value : lib_plankton.session.type_session} = await session_from_stuff(stuff);
await lib_plankton.session.end(session.key); await lib_plankton.session.end(session.key);
return Promise.resolve({ return Promise.resolve(
"status_code": 200, {
"data": null, "status_code": 200,
}); "data": null,
}
);
}, },
} }
); );

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -52,65 +71,39 @@ namespace _zeitbild.api
"execution": async (stuff) => { "execution": async (stuff) => {
const data : { const data : {
token : string; token : string;
userinfo : { userinfo : lib_plankton.auth.oidc.type_userinfo;
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} = await _zeitbild.auth.oidc_handle_authorization_callback( } = await _zeitbild.auth.oidc_handle_authorization_callback(
(stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null), (stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null),
stuff.query_parameters stuff.query_parameters
); );
if (data.userinfo.name === null) {
return Promise.reject( const user = await _zeitbild.auth.oidc_adapt_user(data.userinfo);
new Error(
"IDP did not return user name" const session_key : string = await lib_plankton.session.begin(
) user.object.name,
); {
} "data": {
else { "oidc_token": data.token,
try {
await _zeitbild.service.user.add(
{
"name": data.userinfo.name,
"email_address": data.userinfo.email,
}
);
lib_plankton.log.info(
"user_provisioned",
{
"name": data.userinfo.name,
}
);
}
catch (error) {
// do nothing
}
const session_key : string = await lib_plankton.session.begin(
data.userinfo.name,
{
"data": {
"oidc_token": data.token,
}
} }
); }
return Promise.resolve( );
{ return Promise.resolve(
"status_code": 200, {
"data": lib_plankton.string.coin( "status_code": 200,
"<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>", "data": lib_plankton.string.coin(
{ "<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>",
"url": lib_plankton.string.coin( {
data.redirect_uri_template, "url": lib_plankton.string.coin(
{ data.redirect_uri_template,
"session_key": session_key, {
} "session_key": session_key,
), }
} ),
), }
} ),
); }
} );
}, },
} }
); );

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -0,0 +1,94 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api
{
/**
*/
export function register_session_status(
rest_subject : lib_plankton.rest_http.type_rest
) : void
{
register<
null,
{
logged_in : boolean;
name : (null | string);
}
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/session/status",
{
"description": "gibt Information über den Nutzer aus",
"output_schema": () => ({
"nullable": false,
"type": "object",
"additionalProperties": false,
"properties": {
"logged_in": {
"nullable": false,
"type": "boolean",
},
"name": {
"nullable": true,
"type": "string"
},
},
"required": [
"logged_in",
"name",
],
}),
"restriction": restriction_none,
"execution": async (stuff) => {
const user_id : (null | _zeitbild.type_user_id) = await (
session_from_stuff(stuff)
.then(
(session : {key : string; value : lib_plankton.session.type_session;}) => (
_zeitbild.service.user.identify(session.value.name)
.catch(x => Promise.resolve(null))
)
)
.catch(x => Promise.resolve(null))
);
const user_object : (null | _zeitbild.type_user_object) = (
(user_id === null)
?
null
:
(await _zeitbild.service.user.get(user_id))
);
return Promise.resolve(
{
"status_code": 200,
"data": {
"logged_in": (user_id !== null),
"name": ((user_object === null) ? null : user_object.name),
}
}
);
}
}
);
}
}

View file

@ -0,0 +1,172 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
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,73 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {
@ -30,6 +49,20 @@ namespace _zeitbild.api
_zeitbild.api.register_session_begin(rest_subject); _zeitbild.api.register_session_begin(rest_subject);
_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);
_zeitbild.api.register_session_status(rest_subject);
}
// groups
{
_zeitbild.api.register_group_list(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
{ {
@ -46,13 +79,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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.api namespace _zeitbild.api
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.auth namespace _zeitbild.auth
{ {
@ -33,10 +52,12 @@ namespace _zeitbild.auth
key : string key : string
) : string ) : string
{ {
if (_oidc_redict_uri_template_map === null) { if (_oidc_redict_uri_template_map === null)
{
throw (new Error("apparently not initialized yet")); throw (new Error("apparently not initialized yet"));
} }
else { else
{
return _oidc_redict_uri_template_map.get(key); return _oidc_redict_uri_template_map.get(key);
} }
} }
@ -48,7 +69,8 @@ namespace _zeitbild.auth
) : Promise<void> ) : Promise<void>
{ {
switch (_zeitbild.conf.get().authentication.kind) { switch (_zeitbild.conf.get().authentication.kind) {
case "internal": { case "internal":
{
_subject = lib_plankton.auth.internal.implementation_auth( _subject = lib_plankton.auth.internal.implementation_auth(
{ {
"password_image_chest": { "password_image_chest": {
@ -64,7 +86,8 @@ namespace _zeitbild.auth
); );
break; break;
} }
case "oidc": { case "oidc":
{
_subject_oidc = lib_plankton.auth.oidc.make( _subject_oidc = lib_plankton.auth.oidc.make(
{ {
"url_authorization": _zeitbild.conf.get().authentication.data.url_authorization, "url_authorization": _zeitbild.conf.get().authentication.data.url_authorization,
@ -77,6 +100,7 @@ namespace _zeitbild.auth
"openid", "openid",
"profile", "profile",
"email", "email",
"groups",
], ],
"label": _zeitbild.conf.get().authentication.data.label, "label": _zeitbild.conf.get().authentication.data.label,
} }
@ -89,7 +113,8 @@ namespace _zeitbild.auth
return Promise.resolve(undefined); return Promise.resolve(undefined);
break; break;
} }
default: { default:
{
// do nothing // do nothing
break; break;
} }
@ -104,12 +129,16 @@ namespace _zeitbild.auth
input : any input : any
) : Promise<{kind : string; data : any;}> ) : Promise<{kind : string; data : any;}>
{ {
switch (_zeitbild.conf.get().authentication.kind) { switch (_zeitbild.conf.get().authentication.kind)
case "oidc": { {
if ((_subject_oidc === null) || (_oidc_redict_uri_template_map === null)) { case "oidc":
{
if ((_subject_oidc === null) || (_oidc_redict_uri_template_map === null))
{
throw (new Error("not initialized yet")); throw (new Error("not initialized yet"));
} }
else { else
{
const stuff : {state : string; authorization_url : string;} = lib_plankton.auth.oidc.prepare_login(_subject_oidc); const stuff : {state : string; authorization_url : string;} = lib_plankton.auth.oidc.prepare_login(_subject_oidc);
_oidc_redict_uri_template_map.set( _oidc_redict_uri_template_map.set(
stuff.state, stuff.state,
@ -127,11 +156,14 @@ namespace _zeitbild.auth
} }
break; break;
} }
default: { default:
if (_subject === null) { {
if (_subject === null)
{
return Promise.reject(new Error("not initialized yet")); return Promise.reject(new Error("not initialized yet"));
} }
else { else
{
return ( return (
_subject.login_prepare() _subject.login_prepare()
.then( .then(
@ -156,25 +188,21 @@ namespace _zeitbild.auth
) : Promise< ) : Promise<
{ {
token : string; token : string;
userinfo : { userinfo : lib_plankton.auth.oidc.type_userinfo;
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} }
> >
{ {
if ((_subject_oidc === null) || (_oidc_redict_uri_template_map === null)) { if ((_subject_oidc === null) || (_oidc_redict_uri_template_map === null))
{
throw (new Error("not initialized yet")); throw (new Error("not initialized yet"));
} }
else { else
{
const state : string = data["state"]; const state : string = data["state"];
const result : { const result : {
token : string; token : string;
userinfo : { userinfo : lib_plankton.auth.oidc.type_userinfo;
name : (null | string);
email : (null | string);
};
} = await lib_plankton.auth.oidc.handle_authorization_callback( } = await lib_plankton.auth.oidc.handle_authorization_callback(
_subject_oidc, _subject_oidc,
cookie, cookie,
@ -183,10 +211,7 @@ namespace _zeitbild.auth
return Promise.resolve< return Promise.resolve<
{ {
token : string; token : string;
userinfo : { userinfo : lib_plankton.auth.oidc.type_userinfo;
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} }
>( >(
@ -199,4 +224,144 @@ namespace _zeitbild.auth
} }
} }
/**
* @todo switch for enabling/disabling auto provisioning
*/
export async function oidc_adapt_user(
userinfo : lib_plankton.auth.oidc.type_userinfo
)
: Promise<
{
id : _zeitbild.type_user_id;
object : _zeitbild.type_user_object;
}
>
{
if (userinfo.name === null)
{
return Promise.reject(new Error("IDP did not return user name"));
}
else
{
// groups
const group_ids : Array<_zeitbild.type_group_id> = await (async () => {
const derive_name : ((group_name_raw : string) => string) = (
(group_name_raw) => lib_plankton.string.coin(
"auto-{{name_raw}}",
{
"name_raw": group_name_raw,
}
)
);
const derive_label : ((group_name_raw : string) => string) = (
(group_name_raw) => lib_plankton.string.coin(
"{{name_raw}}",
{
"name_raw": group_name_raw,
}
)
);
return Promise.all<_zeitbild.type_group_id>(
(userinfo.groups ?? [])
.map(
async (group_name_raw) => {
const group_name : string = derive_name(group_name_raw);
const group_id_raw : (null | _zeitbild.type_group_id) = await (
_zeitbild.repository.group.identify(group_name)
.catch(() => Promise.resolve(null))
);
if (group_id_raw === null)
{
// create
const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add(
{
"name": group_name,
"label": derive_label(group_name_raw),
}
);
lib_plankton.log.info(
"zeitbild.oidc_adapt_user.auto_provisioned_group",
{
"id": group_id,
"name": group_name,
}
);
return group_id;
}
else
{
// update
const group_id : _zeitbild.type_group_id = group_id_raw;
await _zeitbild.service.group.change(
group_id,
{
"name": group_name,
"label": derive_label(group_name_raw),
}
);
return group_id;
}
}
)
);
}) ();
// user
const user : {
id : _zeitbild.type_user_id;
object : _zeitbild.type_user_object;
} = await (async () => {
const user_id_raw : (null | _zeitbild.type_user_id) = await (
_zeitbild.service.user.identify(userinfo.name as string)
.catch(() => Promise.resolve(null))
);
if (user_id_raw === null)
{
// provision
const user_object : _zeitbild.type_user_object = {
"name": (userinfo.name as string),
"groups": group_ids,
"email_address": userinfo.email,
"dav_token": null,
};
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object
);
lib_plankton.log.info(
"user_provisioned",
{
"id": user_id,
"name": user_object.name,
}
);
return {"id": user_id, "object": user_object};
}
else
{
// update
const user_id : _zeitbild.type_user_id = user_id_raw;
const user_object : _zeitbild.type_user_object = await _zeitbild.service.user.get(user_id);
user_object.name = (userinfo.name as string);
user_object.groups = group_ids;
user_object.email_address = userinfo.email;
await _zeitbild.service.user.change(
user_id,
user_object
);
lib_plankton.log.info(
"user_updated",
{
"id": user_id,
"name": user_object.name,
}
);
return {"id": user_id, "object": user_object};
}
}) ();
return user;
}
}
} }

View file

@ -1,3 +1,23 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild namespace _zeitbild
{ {

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.conf namespace _zeitbild.conf
{ {
@ -57,7 +76,51 @@ namespace _zeitbild.conf
] ]
} }
} }
} },
{
"type": "object",
"properties": {
"kind": {
"nullable": false,
"type": "string",
"enum": ["file"]
},
"data": {
"nullable": false,
"type": "object",
"properties": {
"path": {
"nullable": false,
"type": "string"
},
"threshold": {
"nullable": false,
"type": "string",
"enum": [
"debug",
"info",
"notice",
"warning",
"error"
],
"default": "info"
},
"format": {
"nullable": false,
"type": "string",
"enum": [
"human_readable",
"jsonl",
"jsonl_structured",
],
"default": "human_readable",
},
},
"required": [
]
}
}
},
] ]
}, },
"default": [ "default": [
@ -188,8 +251,13 @@ namespace _zeitbild.conf
"lifetime": { "lifetime": {
"nullable": false, "nullable": false,
"type": "integer", "type": "integer",
"default": 900 "default": 3600,
} },
"prolongation": {
"nullable": true,
"type": "integer",
"default": 300,
},
}, },
"required": [ "required": [
], ],
@ -267,7 +335,8 @@ namespace _zeitbild.conf
"url_token", "url_token",
"url_userinfo", "url_userinfo",
"client_id", "client_id",
"client_secret" "client_secret",
"backend_url_base"
] ]
} }
}, },
@ -282,7 +351,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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.database namespace _zeitbild.database
{ {
@ -5,7 +24,7 @@ namespace _zeitbild.database
/** /**
*/ */
const _compatible_revisions : Array<string> = [ const _compatible_revisions : Array<string> = [
"r4", "r6",
]; ];

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
/** /**
*/ */
@ -7,7 +26,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
{ {
@ -36,7 +55,7 @@ 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 +77,130 @@ namespace _zeitbild.helpers
} }
/**
*/
export function icalendar_dt_from_own_datetime(
datetime : lib_plankton.pit.type_datetime
) : lib_plankton.ical.type_dt
{
const datetime_normalized : lib_plankton.pit.type_datetime = lib_plankton.pit.to_datetime(
lib_plankton.pit.from_datetime(datetime),
{
"timezone_shift": 0,
}
);
return {
"tzid": "UTC",
"value": {
"date": {
"year": datetime_normalized.date.year,
"month": datetime_normalized.date.month,
"day": datetime_normalized.date.day,
},
"time": (
(datetime_normalized.time === null)
?
null
:
{
"utc": true,
"hour": datetime_normalized.time.hour,
"minute": datetime_normalized.time.minute,
"second": datetime_normalized.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(

199
source/logic.ts Normal file
View file

@ -0,0 +1,199 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*/
namespace _zeitbild
{
/**
*/
export function access_level_to_string(
access_level : _zeitbild.enum_access_level
)
: string
{
switch (access_level)
{
case _zeitbild.enum_access_level.none: {return "none";}
case _zeitbild.enum_access_level.view: {return "view";}
case _zeitbild.enum_access_level.edit: {return "edit";}
case _zeitbild.enum_access_level.admin: {return "admin";}
default: {throw (new Error("invalid access level: " + String(access_level)));}
}
}
/**
*/
export function access_level_from_string(
representation : string
)
: _zeitbild.enum_access_level
{
switch (representation)
{
case "none": {return _zeitbild.enum_access_level.none;}
case "view": {return _zeitbild.enum_access_level.view;}
case "edit": {return _zeitbild.enum_access_level.edit;}
case "admin": {return _zeitbild.enum_access_level.admin;}
default: {throw (new Error("invalid encoded access level: " + String(representation)));}
}
}
/**
*/
export function access_level_order(
x : _zeitbild.enum_access_level,
y : _zeitbild.enum_access_level
)
: boolean
{
const list : Array<_zeitbild.enum_access_level> = [
_zeitbild.enum_access_level.none,
_zeitbild.enum_access_level.view,
_zeitbild.enum_access_level.edit,
_zeitbild.enum_access_level.admin,
];
return (list.indexOf(x) <= list.indexOf(y));
}
/**
*/
export function access_level_determine_raw(
public_ : boolean,
access_level_attributed : (
null
|
{
default : _zeitbild.enum_access_level,
group : Array<_zeitbild.enum_access_level>;
user : (null | _zeitbild.enum_access_level);
}
)
)
: _zeitbild.enum_access_level
{
return lib_plankton.call.convey(
_zeitbild.enum_access_level.none,
[
// if public
(x : _zeitbild.enum_access_level) => (
public_
?
_zeitbild.enum_access_level.view
:
x
),
// if logged in
(x : _zeitbild.enum_access_level) => (
(access_level_attributed === null)
?
x
:
lib_plankton.call.convey(
x,
[
// default
(y : _zeitbild.enum_access_level) => access_level_attributed.default,
// group
(y : _zeitbild.enum_access_level) => (
lib_plankton.call.null_prop(
lib_plankton.list.max<_zeitbild.enum_access_level, _zeitbild.enum_access_level>(
access_level_attributed.group,
z => z,
{
"compare_value": _zeitbild.access_level_order,
}
),
z => z.value
)
??
y
),
// user
(y : _zeitbild.enum_access_level) => (
(access_level_attributed.user === null)
?
y
:
access_level_attributed.user
),
]
)
),
]
);
}
/**
*/
export function access_level_determine(
calendar_object : _zeitbild.type_calendar_object,
user : (
null
|
{
id : _zeitbild.type_user_id;
object : _zeitbild.type_user_object;
}
)
)
: _zeitbild.enum_access_level
{
return access_level_determine_raw(
calendar_object.access.public,
(
(user === null)
?
null
:
{
"default": calendar_object.access.default_level,
"group": (
user.object.groups
.map<(null | _zeitbild.enum_access_level)>(
group_id => (
calendar_object.access.attributed_group.has(group_id)
?
calendar_object.access.attributed_group.get(group_id)
:
null
)
)
.filter(
x => (x !== null)
)
),
"user": (
lib_plankton.call.try_catch_wrap<_zeitbild.enum_access_level>(
() => calendar_object.access.attributed_user.get(user.id)
).value
)
}
)
);
}
}

View file

@ -1,162 +1,23 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
/** »zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/ */
type type_data = {
users : Array<
{
id : int;
name : string;
email_address : string;
password : string;
}
>;
calendars : Array<
{
id : int;
name : string;
access : {
public ?: boolean;
default_level : ("none" | "view" | "edit" | "admin");
attributed : Array<
{
user_id : int;
level : ("none" | "view" | "edit" | "admin");
}
>;
};
resource : (
{
kind : "local";
data : {
events : Array<
_zeitbild.type_event_object
>
};
}
|
{
kind : "caldav";
data : {
url : string;
read_only : boolean;
from_fucked_up_wordpress ?: boolean;
};
}
);
}
>;
};
/**
*/
async function data_init(
data : type_data
) : Promise<void>
{
let track : {
user : Record<
int,
_zeitbild.type_user_id
>;
calendar : Record<
int,
_zeitbild.type_user_id
>;
} = {
"user": {},
"calendar": {},
};
for await (const user_raw of data.users)
{
const user_object : _zeitbild.type_user_object = {
"name": user_raw.name,
"email_address": user_raw.email_address,
};
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object
);
await _zeitbild.service.auth_internal.set(
user_raw.name,
user_raw.password
);
track.user[user_raw.id] = user_id;
}
for await (const calendar_raw of data.calendars)
{
let resource_object : _zeitbild.type_resource_object;
let resource_id : _zeitbild.type_resource_id;
switch (calendar_raw.resource.kind)
{
case "local":
{
resource_object = {
"kind": "local",
"data": {
"event_ids": [],
}
};
resource_id = await _zeitbild.service.resource.add(
resource_object
);
/*const event_ids : Array<_zeitbild.type_local_resource_event_id> = */await Promise.all(
calendar_raw.resource.data.events
.map(
(event_raw : _zeitbild.type_event_object) => _zeitbild.service.resource.event_add(resource_id, event_raw)
)
);
break;
}
case "caldav":
{
resource_object = {
"kind": "caldav",
"data": {
"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),
}
};
resource_id = await _zeitbild.service.resource.add(
resource_object
);
break;
}
}
const calendar_object : _zeitbild.type_calendar_object =
{
"name": calendar_raw.name,
"access": {
"public": (calendar_raw.access.public ?? false),
"default_level": _zeitbild.value_object.access_level.from_string(calendar_raw.access.default_level),
"attributed": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
calendar_raw.access.attributed
.map(
(entry) => ({
"key": track.user[entry.user_id],
"value": _zeitbild.value_object.access_level.from_string(entry.level),
})
)
),
}
)
),
},
"resource_id": resource_id,
};
const calendar_id : _zeitbild.type_calendar_id = await _zeitbild.service.calendar.add(
calendar_object
);
track.calendar[calendar_raw.id] = calendar_id;
}
return Promise.resolve<void>(undefined);
}
/** /**
*/ */
@ -208,8 +69,8 @@ async function main(
"description": "conf-expose" "description": "conf-expose"
}, },
{ {
"name": "fill", "name": "sample",
"description": "fill" "description": "sample"
}, },
{ {
"name": "help", "name": "help",
@ -311,6 +172,46 @@ async function main(
}; };
break; break;
} }
case "file": {
return {
"kind": "minlevel",
"data": {
"core": {
"kind": "file",
"data": {
"path": log_output.data.path,
"format": lib_plankton.call.distinguish(
{
"kind": log_output.data.format,
"data": null,
},
{
"jsonl": () => ({
"kind": "jsonl",
"data": {
"structured": false,
}
}),
"jsonl_structured": () => ({
"kind": "jsonl",
"data": {
"structured": true,
}
}),
"human_readable": () => ({
"kind": "human_readable",
"data": {
}
}),
}
),
}
},
"threshold": log_output.data.threshold,
}
};
break;
}
default: { default: {
throw (new Error("unhandled")); throw (new Error("unhandled"));
break; break;
@ -347,8 +248,10 @@ async function main(
); );
} }
else { else {
switch (args["action"]) { switch (args["action"])
default: { {
default:
{
lib_plankton.log.error( lib_plankton.log.error(
"main_invalid_action", "main_invalid_action",
{ {
@ -357,7 +260,8 @@ async function main(
); );
break; break;
} }
case "conf-schema": { case "conf-schema":
{
process.stdout.write( process.stdout.write(
JSON.stringify( JSON.stringify(
_zeitbild.conf.schema(), _zeitbild.conf.schema(),
@ -369,7 +273,8 @@ async function main(
); );
break; break;
} }
case "conf-expose": { case "conf-expose":
{
process.stdout.write( process.stdout.write(
JSON.stringify( JSON.stringify(
_zeitbild.conf.get(), _zeitbild.conf.get(),
@ -381,10 +286,10 @@ async function main(
); );
break; break;
} }
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),
@ -394,8 +299,9 @@ async function main(
); );
break; break;
} }
case "fill": { case "sample":
await data_init( {
await _zeitbild.sample.init(
lib_plankton.json.decode( lib_plankton.json.decode(
await lib_plankton.file.read(args.data_path) await lib_plankton.file.read(args.data_path)
) )
@ -403,16 +309,23 @@ async function main(
process.stdout.write("-- done\n"); process.stdout.write("-- done\n");
break; break;
} }
case "serve": { case "serve":
{
// prepare database // prepare database
await _zeitbild.database.check(); await _zeitbild.database.check();
/**
* @todo clear old sessions
*/
await lib_plankton.session.setup( await lib_plankton.session.setup(
{ {
"data_chest": ( "data_chest": (
_zeitbild.conf.get().session_management.in_memory _zeitbild.conf.get().session_management.in_memory
? lib_plankton.storage.memory.implementation_chest<lib_plankton.session.type_session>({}) ?
: lib_plankton.call.convey( lib_plankton.storage.memory.implementation_chest<lib_plankton.session.type_session>({})
:
lib_plankton.call.convey(
lib_plankton.storage.sql_table_common.chest( lib_plankton.storage.sql_table_common.chest(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
@ -434,6 +347,7 @@ async function main(
) )
), ),
"default_lifetime": _zeitbild.conf.get().session_management.lifetime, "default_lifetime": _zeitbild.conf.get().session_management.lifetime,
"default_prolongation": _zeitbild.conf.get().session_management.prolongation,
} }
); );
@ -480,6 +394,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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.repository.auth_internal namespace _zeitbild.repository.auth_internal
{ {

View file

@ -1,23 +1,85 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.repository.calendar namespace _zeitbild.repository.calendar
{ {
/** /**
*/ */
type type_dispersal = { type type_core_row = {
core_row : Record< name : string;
string, hue : int;
any access_public : boolean;
>; access_level_default : int;
access_attributed_rows : Array< resource_id : int;
Record<
string,
any
>
>;
}; };
/**
*/
type type_access_attributed_group_row = {
// calendar_id : int;
group_id : int;
level : int;
};
/**
*/
type type_access_attributed_group_preview = {
group_id : int;
level : int;
};
/**
*/
type type_access_attributed_user_row = {
// calendar_id : int;
user_id : int;
level : int;
};
/**
*/
type type_access_attributed_user_preview = {
user_id : int;
level : int;
};
/**
*/
type type_dispersal = {
core_row : type_core_row;
access_attributed_group_rows : Array<type_access_attributed_group_row>;
access_attributed_user_rows : Array<type_access_attributed_user_row>;
};
/**
*/
const hue_scaling : int = 0xFFFF;
/** /**
*/ */
var _core_store : ( var _core_store : (
@ -35,7 +97,22 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
var _access_attributed_chest : ( var _access_attributed_group_chest : (
null
|
lib_plankton.storage.type_chest<
Array<any>,
Record<string, any>,
lib_plankton.database.type_description_create_table,
lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any>
>
) = null;
/**
*/
var _access_attributed_user_chest : (
null null
| |
lib_plankton.storage.type_chest< lib_plankton.storage.type_chest<
@ -51,7 +128,8 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_core_store( function get_core_store(
) : lib_plankton.storage.type_store< )
: lib_plankton.storage.type_store<
_zeitbild.type_calendar_id, _zeitbild.type_calendar_id,
Record<string, any>, Record<string, any>,
{}, {},
@ -59,7 +137,8 @@ namespace _zeitbild.repository.calendar
Record<string, any> Record<string, any>
> >
{ {
if (_core_store === null) { if (_core_store === null)
{
_core_store = lib_plankton.storage.sql_table_autokey_store( _core_store = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
@ -68,7 +147,8 @@ namespace _zeitbild.repository.calendar
} }
); );
} }
else { else
{
// do nothing // do nothing
} }
return _core_store; return _core_store;
@ -77,7 +157,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_access_attributed_chest( function get_access_attributed_group_chest(
) : lib_plankton.storage.type_chest< ) : lib_plankton.storage.type_chest<
Array<any>, Array<any>,
Record<string, any>, Record<string, any>,
@ -86,19 +166,51 @@ namespace _zeitbild.repository.calendar
Record<string, any> Record<string, any>
> >
{ {
if (_access_attributed_chest === null) { if (_access_attributed_group_chest === null)
_access_attributed_chest = lib_plankton.storage.sql_table_common.chest( {
_access_attributed_group_chest = lib_plankton.storage.sql_table_common.chest(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "calendar_access_attributed", "table_name": "calendar_access_attributed_group",
"key_names": ["calendar_id","group_id"],
}
);
}
else
{
// do nothing
}
return _access_attributed_group_chest;
}
/**
*/
function get_access_attributed_user_chest(
)
: lib_plankton.storage.type_chest<
Array<any>,
Record<string, any>,
lib_plankton.database.type_description_create_table,
lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any>
>
{
if (_access_attributed_user_chest === null)
{
_access_attributed_user_chest = lib_plankton.storage.sql_table_common.chest(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "calendar_access_attributed_user",
"key_names": ["calendar_id","user_id"], "key_names": ["calendar_id","user_id"],
} }
); );
} }
else { else
{
// do nothing // do nothing
} }
return _access_attributed_chest; return _access_attributed_user_chest;
} }
@ -140,17 +252,29 @@ namespace _zeitbild.repository.calendar
*/ */
function encode( function encode(
object : _zeitbild.type_calendar_object object : _zeitbild.type_calendar_object
) : type_dispersal )
: type_dispersal
{ {
return { return {
"core_row": { "core_row": {
"name": object.name, "name": object.name,
"hue": Math.floor(object.hue * hue_scaling),
"access_public": object.access.public, "access_public": object.access.public,
"access_level_default": encode_access_level(object.access.default_level), "access_level_default": encode_access_level(object.access.default_level),
"resource_id": object.resource_id, "resource_id": object.resource_id,
}, },
"access_attributed_rows": ( "access_attributed_group_rows": (
lib_plankton.map.dump(object.access.attributed) lib_plankton.map.dump(object.access.attributed_group)
.map(
({"key": group_id, "value": level}) => ({
// "calendar_id": calendar_id,
"group_id": group_id,
"level": encode_access_level(level),
})
)
),
"access_attributed_user_rows": (
lib_plankton.map.dump(object.access.attributed_user)
.map( .map(
({"key": user_id, "value": level}) => ({ ({"key": user_id, "value": level}) => ({
// "calendar_id": calendar_id, // "calendar_id": calendar_id,
@ -167,24 +291,47 @@ namespace _zeitbild.repository.calendar
*/ */
function decode( function decode(
dispersal : type_dispersal dispersal : type_dispersal
) : _zeitbild.type_calendar_object )
: _zeitbild.type_calendar_object
{ {
return { return {
"name": dispersal.core_row["name"], "name": dispersal.core_row.name,
"hue": (dispersal.core_row.hue / hue_scaling),
"access": { "access": {
"public": dispersal.core_row["access_public"], "public": dispersal.core_row.access_public,
"default_level": decode_access_level(dispersal.core_row["access_level_default"]), "default_level": decode_access_level(dispersal.core_row.access_level_default),
"attributed": lib_plankton.map.hashmap.implementation_map( "attributed_group": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make<_zeitbild.type_group_id, _zeitbild.enum_access_level>(
x => x.toFixed(0),
{
"pairs": (
dispersal.access_attributed_group_rows
.map(
(access_attributed_group_row) => ({
// "calendar_id": access_attributed_group_row["calendar_id"],
// "key": access_attributed_group_row["preview"]["user_id"],
"key": access_attributed_group_row.group_id,
// "value": decode_access_level(access_attributed_group_row["preview"]["level"]),
"value": decode_access_level(access_attributed_group_row.level),
})
)
),
}
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make<_zeitbild.type_user_id, _zeitbild.enum_access_level>( lib_plankton.map.hashmap.make<_zeitbild.type_user_id, _zeitbild.enum_access_level>(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
dispersal.access_attributed_rows dispersal.access_attributed_user_rows
.map( .map(
(access_attributed_row) => ({ (access_attributed_user_row) => ({
// "calendar_id": access_attributed_row["calendar_id"], // "calendar_id": access_attributed_user_row["calendar_id"],
"key": access_attributed_row["preview"]["user_id"], // "key": access_attributed_user_row["preview"]["user_id"],
"value": decode_access_level(access_attributed_row["preview"]["level"]), "key": access_attributed_user_row.user_id,
// "value": decode_access_level(access_attributed_user_row["preview"]["level"]),
"value": decode_access_level(access_attributed_user_row.level),
}) })
) )
), ),
@ -192,45 +339,74 @@ namespace _zeitbild.repository.calendar
) )
), ),
}, },
"resource_id": dispersal.core_row["resource_id"], "resource_id": dispersal.core_row.resource_id,
}; };
} }
/** /**
*/ */
export function read( export async function read(
id : _zeitbild.type_calendar_id id : _zeitbild.type_calendar_id
) : Promise<_zeitbild.type_calendar_object> )
: Promise<_zeitbild.type_calendar_object>
{ {
return ( const core_row : type_core_row = ((await get_core_store().read(id)) as type_core_row);
get_core_store().read(id) const access_attributed_group_rows : Array<type_access_attributed_group_row> = await (
get_access_attributed_group_chest().search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": id,
}
}
)
.then( .then(
(core_row) => ( (hits) => Promise.resolve<Array<type_access_attributed_group_row>>(
get_access_attributed_chest().search( hits
{ .map(
"expression": "(calendar_id = $calendar_id)", hit => (
"arguments": {
"calendar_id": id,
}
}
)
.then(
(access_attributed_rows) => Promise.resolve<type_dispersal>(
{ {
"core_row": core_row, // "calendar_id": null,
"access_attributed_rows": access_attributed_rows, "group_id": hit.preview.group_id,
"level": hit.preview.level,
} }
) )
) )
.then(
(dispersal) => Promise.resolve<_zeitbild.type_calendar_object>(
decode(dispersal)
)
)
) )
) )
); );
const access_attributed_user_rows : Array<type_access_attributed_user_row> = await (
get_access_attributed_user_chest().search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": id,
}
}
)
.then(
(hits) => Promise.resolve<Array<type_access_attributed_user_row>>(
hits
.map(
hit => (
{
// "calendar_id": null,
"user_id": hit.preview.user_id,
"level": hit.preview.level,
}
)
)
)
)
);
const dispersal : type_dispersal = {
"core_row": core_row,
"access_attributed_group_rows": access_attributed_group_rows,
"access_attributed_user_rows": access_attributed_user_rows,
};
const calendar_object : _zeitbild.type_calendar_object = decode(dispersal);
return calendar_object;
} }
@ -245,10 +421,18 @@ namespace _zeitbild.repository.calendar
const calendar_id : _zeitbild.type_calendar_id = await core_store.create( const calendar_id : _zeitbild.type_calendar_id = await core_store.create(
dispersal.core_row dispersal.core_row
); );
for await (const access_attributed_row of dispersal.access_attributed_rows) { for await (const access_attributed_group_row of dispersal.access_attributed_group_rows)
get_access_attributed_chest().write( {
[calendar_id, access_attributed_row["user_id"]], get_access_attributed_group_chest().write(
{"level": access_attributed_row["level"]} [calendar_id, access_attributed_group_row.group_id],
{"level": access_attributed_group_row.level}
);
}
for await (const access_attributed_user_row of dispersal.access_attributed_user_rows)
{
get_access_attributed_user_chest().write(
[calendar_id, access_attributed_user_row.user_id],
{"level": access_attributed_user_row.level}
); );
} }
await lib_plankton.cache.clear(_zeitbild.cache_regular); await lib_plankton.cache.clear(_zeitbild.cache_regular);
@ -272,44 +456,117 @@ namespace _zeitbild.repository.calendar
dispersal.core_row dispersal.core_row
); );
} }
// attributed access // attributed:group
{ {
const access_attributed_chest = get_access_attributed_chest(); const access_attributed_group_chest = get_access_attributed_group_chest();
const hits : Array<Record<string, any>> = await access_attributed_chest.search( const hits : Array<{key : Array<any>; preview : type_access_attributed_group_row;}> = (
{ (await access_attributed_group_chest.search(
"expression": "(calendar_id = $calendar_id)", {
"arguments": { "expression": "(calendar_id = $calendar_id)",
"calendar_id": calendar_id, "arguments": {
"calendar_id": calendar_id,
}
} }
} ))
.map(
hit => (
{
"key": hit.key,
"preview": {
"group_id": hit.preview["group_id"],
"level": hit.preview["level"],
}
}
)
)
); );
const contrast = lib_plankton.list.contrast< const contrast = lib_plankton.list.contrast<
Record<string, any>, {key : Array<any>; preview : type_access_attributed_group_row;},
Record<string, any> type_access_attributed_group_row
>( >(
hits, hits,
hit => hit["user_id"], hit => hit.preview.group_id.toFixed(0),
dispersal.access_attributed_rows, dispersal.access_attributed_group_rows,
row => row["user_id"] row => row.group_id.toFixed(0)
); );
// delete // delete
for await (const entry of contrast.only_left) { for await (const entry of contrast.only_left)
await access_attributed_chest.delete( {
[calendar_id, entry.left["user_id"]] await access_attributed_group_chest.delete(
[calendar_id, entry.left.preview.group_id]
); );
} }
// update // update
for await (const entry of contrast.both) { for await (const entry of contrast.both)
await access_attributed_chest.write( {
[calendar_id, entry.right["user_id"]], await access_attributed_group_chest.write(
{"level": entry.right["level"]} [calendar_id, entry.right.group_id],
{"level": entry.right.level}
); );
} }
// create // create
for await (const entry of contrast.only_right) { for await (const entry of contrast.only_right)
await access_attributed_chest.write( {
[calendar_id, entry.right["user_id"]], await access_attributed_group_chest.write(
{"level": entry.right["level"]} [calendar_id, entry.right.group_id],
{"level": entry.right.level}
);
}
}
// attributed:user
{
const access_attributed_user_chest = get_access_attributed_user_chest();
const hits : Array<{key : Array<any>; preview : type_access_attributed_user_row;}> = (
(await access_attributed_user_chest.search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": calendar_id,
}
}
))
.map(
hit => (
{
"key": hit.key,
"preview": {
"user_id": hit.preview["user_id"],
"level": hit.preview["level"],
}
}
)
)
);
const contrast = lib_plankton.list.contrast<
{key : Array<any>; preview : type_access_attributed_user_row;},
type_access_attributed_user_row
>(
hits,
hit => hit.preview.user_id.toFixed(0),
dispersal.access_attributed_user_rows,
row => row.user_id.toFixed(0)
);
// delete
for await (const entry of contrast.only_left)
{
await access_attributed_user_chest.delete(
[calendar_id, entry.left.preview.user_id]
);
}
// update
for await (const entry of contrast.both)
{
await access_attributed_user_chest.write(
[calendar_id, entry.right.user_id],
{"level": entry.right.level}
);
}
// create
for await (const entry of contrast.only_right)
{
await access_attributed_user_chest.write(
[calendar_id, entry.right.user_id],
{"level": entry.right.level}
); );
} }
} }
@ -320,17 +577,25 @@ namespace _zeitbild.repository.calendar
/** /**
* @todo remove events from resource? * @todo remove events from resource?
* @todo remove resource
*/ */
export async function delete_( export async function delete_(
calendar_id : _zeitbild.type_calendar_id calendar_id : _zeitbild.type_calendar_id
) : Promise<void> )
: Promise<void>
{ {
await lib_plankton.cache.clear(_zeitbild.cache_regular); await lib_plankton.cache.clear(_zeitbild.cache_regular);
const core_store = get_core_store(); const core_store = get_core_store();
const access_attributed_chest = get_access_attributed_chest(); const access_attributed_user_chest = get_access_attributed_user_chest();
// attributed access // attributed:user
{ {
const hits : Array<Record<string, any>> = await access_attributed_chest.search( const chest = get_access_attributed_user_chest();
const hits : Array<
{
key : Array<any>;
preview : Record<string, any>;
}
> = await chest.search(
{ {
"expression": "(calendar_id = $calendar_id)", "expression": "(calendar_id = $calendar_id)",
"arguments": { "arguments": {
@ -338,10 +603,30 @@ namespace _zeitbild.repository.calendar
} }
} }
); );
for await (const hit of hits) { for (const hit of hits)
await access_attributed_chest.delete( {
[calendar_id, hit["user_id"]] await chest.delete(hit.key);
); }
}
// attributed:group
{
const chest = get_access_attributed_group_chest();
const hits : Array<
{
key : Array<any>;
preview : Record<string, any>;
}
> = await chest.search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": calendar_id,
}
}
);
for (const hit of hits)
{
await chest.delete(hit.key);
} }
} }
// core // core
@ -359,93 +644,168 @@ namespace _zeitbild.repository.calendar
type type_overview_entry = { type type_overview_entry = {
id : _zeitbild.type_calendar_id; id : _zeitbild.type_calendar_id;
name : string; name : string;
hue : float;
access_level : _zeitbild.enum_access_level; access_level : _zeitbild.enum_access_level;
} }
/** /**
* @todo caching
*/ */
export async function overview( export async function overview(
user_id : (null | _zeitbild.type_user_id) user_id : (null | _zeitbild.type_user_id)
) : Promise< )
: Promise<
Array< Array<
type_overview_entry type_overview_entry
> >
> >
{ {
type type_data = {
hits_core : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_group : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_user : Array<
{
key : int;
preview : Record<string, any>;
}
>;
};
return lib_plankton.cache.get_complex<any, Array<type_overview_entry>>( return lib_plankton.cache.get_complex<any, Array<type_overview_entry>>(
_zeitbild.cache_regular, _zeitbild.cache_regular,
"calendar_overview", "calendar_overview",
{ {
"user_id": user_id, "user_id": user_id,
}, },
null, 60,
() => ( async () => lib_plankton.call.convey(
lib_plankton.file.read("sql/calendar_overview.sql") {
.then( "hits_core": await get_core_store().search(
(template) => _zeitbild.database.get_implementation().query_free_get(
{ {
"template": template, "expression": "TRUE",
"arguments": { "arguments": {}
"user_id": user_id,
}
} }
) ),
) "hits_access_attributed_group": await get_access_attributed_group_chest().search(
.then( (user_id === null)
(rows) => Promise.resolve( ?
lib_plankton.call.convey( {
rows, "expression": "TRUE",
[ "arguments": {}
(x : Array<Record<string, any>>) => x.map( }
(row : Record<string, any>) => ({ :
"id": row["id"], {
"name": row["name"], "expression": "(group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))",
/** "arguments": {"user_id": user_id}
* @todo unite with _zeitbild.service.calendar.get_access_level }
*/ ),
"access_level": decode_access_level( "hits_access_attributed_user": await get_access_attributed_user_chest().search(
Math.max( (user_id === null)
(row["access_public"] ? 1 : 0), ?
( {
(user_id === null) "expression": "TRUE",
? "arguments": {}
0 }
: :
(row["access_level_attributed"] ?? row["access_level_default"]) {
) "expression": "(user_id = $user_id)",
) "arguments": {"user_id": user_id}
), }
}) ),
), },
(x : Array<type_overview_entry>) => x.filter( [
(row) => ( // transform
! _zeitbild.value_object.access_level.order( (data : type_data) => data.hits_core.map(
row.access_level, (hit_core) => {
_zeitbild.enum_access_level.none const calendar_id : _zeitbild.type_calendar_id = hit_core.key;
) return {
"id": calendar_id,
"name": hit_core.preview["name"],
"hue": (hit_core.preview["hue"] / hue_scaling),
"access_level": _zeitbild.access_level_determine_raw(
hit_core.preview["access_public"],
(
(user_id === null)
?
null
:
{
"default": decode_access_level(hit_core.preview["access_level_default"]),
"group": lib_plankton.call.convey(
data.hits_access_attributed_group,
[
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hit_access_attributed_group => (
(hit_access_attributed_group.preview.calendar_id === calendar_id)
)
),
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hit_access_attributed_group => hit_access_attributed_group.preview.level
),
(x : Array<int>) => x.map(
decode_access_level
),
]
),
"user": lib_plankton.call.convey(
data.hits_access_attributed_user,
[
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hits_access_attributed_user => (
(hits_access_attributed_user.preview.calendar_id === calendar_id)
)
),
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hits_access_attributed_user => hits_access_attributed_user.preview.level
),
(x : Array<int>) => x.map(
decode_access_level
),
(x : Array<_zeitbild.enum_access_level>) => (x[0] ?? null),
]
),
}
) )
), ),
(x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>( };
x, }
{ ),
"compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>( // only keep visible calendars
row => row.access_level, (x : Array<type_overview_entry>) => x.filter(
row => row.id, (row) => (
{ ! _zeitbild.access_level_order(
"order_first": (a, b) => _zeitbild.value_object.access_level.order(b, a), row.access_level,
"order_second": (a, b) => (a <= b) _zeitbild.enum_access_level.none
} )
),
}
),
]
) )
) ),
) // sort by access level and name
(x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>(
x,
{
"compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>(
row => row.access_level,
row => row.id,
{
"order_first": (a, b) => _zeitbild.access_level_order(b, a),
"order_second": (a, b) => (a <= b)
}
),
}
),
]
) )
); );
} }
} }

View file

@ -0,0 +1,207 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.repository.group
{
/**
*/
type type_row = {
name : string;
label : string;
};
/**
*/
type type_preview = {
name : string;
label : string;
};
/**
*/
var _store : (
null
|
lib_plankton.storage.type_store<
_zeitbild.type_user_id,
/*type_row*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any>
>
) = null;
/**
*/
function get_store(
)
: lib_plankton.storage.type_store<
_zeitbild.type_user_id,
/*type_row*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any>
>
{
if (_store === null)
{
_store = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "groups",
"key_name": "id",
}
);
}
else
{
// do nothing
}
return _store;
}
/**
*/
function encode(
group_object : _zeitbild.type_group_object
)
: type_row
{
return {
"name": group_object.name,
"label": group_object.label,
};
}
/**
*/
function decode(
row : type_row
)
: _zeitbild.type_group_object
{
return {
"name": row.name,
"label": row.label,
};
}
/**
*/
export async function list(
)
: Promise<
Array<
{
id : _zeitbild.type_group_id;
object : _zeitbild.type_group_object;
}
>
>
{
const hits : Array<{key : int; preview : /*type_preview*/Record<string, any>;}> = await get_store().search({"expression": "TRUE", "arguments": {}});
return Promise.resolve(
hits
.map(
(hit) => ({
"id": hit.key,
"object": {
"name": hit.preview.name,
"label": hit.preview.label,
}
})
)
);
}
/**
*/
export async function read(
group_id : _zeitbild.type_group_id
)
: Promise<_zeitbild.type_group_object>
{
const row : type_row = ((await get_store().read(group_id)) as type_row);
const group_object : _zeitbild.type_group_object = decode(row);
return Promise.resolve<_zeitbild.type_group_object>(group_object);
}
/**
*/
export async function create(
group_object : _zeitbild.type_group_object
)
: Promise<_zeitbild.type_group_id>
{
const row : type_row = encode(group_object);
const group_id : _zeitbild.type_group_id = await get_store().create(row);
return Promise.resolve<_zeitbild.type_group_id>(group_id);
}
/**
*/
export async function update(
group_id : _zeitbild.type_group_id,
group_object : _zeitbild.type_group_object
)
: Promise<void>
{
const row : type_row = encode(group_object);
await get_store().update(group_id, row);
return Promise.resolve<void>(undefined);
}
/**
*/
export async function identify(
name : string
)
: Promise<_zeitbild.type_group_id>
{
const hits : Array<{key : _zeitbild.type_group_id; preview : /*type_preview*/Record<string, any>;}> = await get_store().search(
{
"expression": "(name = $name)",
"arguments": {
"name": name,
}
}
);
if (hits.length <= 0)
{
return Promise.reject<_zeitbild.type_group_id>(new Error("not found"));
}
else
{
return Promise.resolve<_zeitbild.type_group_id>(hits[0].key);
}
}
}

View file

@ -1,7 +1,42 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.repository.resource namespace _zeitbild.repository.resource
{ {
/**
*/
type type_row = {
kind : string;
sub_id : int;
};
/**
*/
type type_preview = {
kind : string;
sub_id : int;
};
/** /**
*/ */
type type_local_resource_event_stuff = { type type_local_resource_event_stuff = {
@ -42,7 +77,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<
@ -62,10 +97,10 @@ namespace _zeitbild.repository.resource
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_store<
_zeitbild.type_resource_id, _zeitbild.type_resource_id,
Record<string, any>, /*type_row*/Record<string, any>,
{}, {},
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any> /*type_preview*/Record<string, any>
> >
) = null; ) = null;
@ -126,7 +161,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 +170,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 +182,7 @@ namespace _zeitbild.repository.resource
else { else {
// do nothing // do nothing
} }
return _caldav_resource_store; return _ics_feed_resource_store;
} }
@ -358,15 +393,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 +439,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 +524,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

@ -1,14 +0,0 @@
-- Für gewöhnlich würde man hier gruppieren. Aufgrund des UNIQUE-constraints in "calendar_access_attributed" ist das
-- jedoch nicht nötig, da dadurch für jeden Eintrag in "calendar" mit gegebener "user_id" höchstens ein Eintrag in
-- "calendar_access_attributed" passt und da es ein LEFT OUTER JOIN ist, wird es _genau_ ein Eintrag sein
SELECT
x.id AS id,
x.name AS name,
x.access_public AS access_public,
x.access_level_default AS access_level_default,
y.level AS access_level_attributed
FROM
calendars AS x
LEFT OUTER JOIN calendar_access_attributed AS y ON ((x.id = y.calendar_id) AND (y.user_id = $user_id))
;

View file

@ -1,15 +1,88 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.repository.user namespace _zeitbild.repository.user
{ {
/** /**
*/ */
var _store : ( type type_core_row = {
name : string;
email_address : (null | string);
dav_token : (null | string);
};
/**
*/
type type_group_row_slim = {
group_id : int;
};
/**
*/
type type_group_row_fat = {
user_id : int;
group_id : int;
};
/**
*/
type type_preview = {
name : string;
};
/**
*/
type type_dispersal = {
core : type_core_row;
groups : Array<type_group_row_slim>;
};
/**
*/
var _store_core : (
null null
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_store<
_zeitbild.type_user_id, _zeitbild.type_user_id,
Record<string, any>, /*type_core_row*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any>
>
) = null;
/**
*/
var _store_groups : (
null
|
lib_plankton.storage.type_store<
int,
/*type_group_row_fat*/Record<string, any>,
{}, {},
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any> Record<string, any>
@ -19,17 +92,19 @@ namespace _zeitbild.repository.user
/** /**
*/ */
function get_store( function get_store_core(
) : lib_plankton.storage.type_store< )
: lib_plankton.storage.type_store<
_zeitbild.type_user_id, _zeitbild.type_user_id,
Record<string, any>, /*type_core_row*/Record<string, any>,
{}, {},
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any> /*type_preview*/Record<string, any>
> >
{ {
if (_store === null) { if (_store_core === null)
_store = lib_plankton.storage.sql_table_autokey_store( {
_store_core = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "users", "table_name": "users",
@ -37,10 +112,41 @@ namespace _zeitbild.repository.user
} }
); );
} }
else { else
{
// do nothing // do nothing
} }
return _store; return _store_core;
}
/**
*/
function get_store_groups(
)
: lib_plankton.storage.type_store<
int,
/*type_group_row_fat*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_store_groups === null)
{
_store_groups = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "user_groups",
"key_name": "id",
}
);
}
else
{
// do nothing
}
return _store_groups;
} }
@ -48,11 +154,24 @@ namespace _zeitbild.repository.user
*/ */
function encode( function encode(
user_object : _zeitbild.type_user_object user_object : _zeitbild.type_user_object
) : Record<string, any> )
: type_dispersal
{ {
return { return {
"name": user_object.name, "core": {
"email_address": user_object.email_address, "name": user_object.name,
"email_address": user_object.email_address,
"dav_token": user_object.dav_token,
},
"groups": (
user_object.groups.map(
group_id => (
{
"group_id": group_id,
}
)
)
),
}; };
} }
@ -60,12 +179,19 @@ namespace _zeitbild.repository.user
/** /**
*/ */
function decode( function decode(
row : Record<string, any> dispersal : type_dispersal
) : _zeitbild.type_user_object )
: _zeitbild.type_user_object
{ {
return { return {
"name": row["name"], "name": dispersal.core.name,
"email_address": row["email_address"], "groups": (
dispersal.groups.map(
group_row => group_row.group_id,
)
),
"email_address": dispersal.core.email_address,
"dav_token": dispersal.core.dav_token,
}; };
} }
@ -73,7 +199,8 @@ namespace _zeitbild.repository.user
/** /**
*/ */
export async function list( export async function list(
) : Promise< )
: Promise<
Array< Array<
{ {
id : _zeitbild.type_user_id; id : _zeitbild.type_user_id;
@ -82,7 +209,12 @@ namespace _zeitbild.repository.user
> >
> >
{ {
const hits : Array<{key : int; preview : Record<string, any>;}> = await get_store().search({"expression": "TRUE", "arguments": {}}); const hits : Array<{key : int; preview : /*type_preview*/Record<string, any>;}> = await get_store_core().search(
{
"expression": "TRUE",
"arguments": {}
}
);
return Promise.resolve( return Promise.resolve(
hits hits
.map( .map(
@ -99,10 +231,43 @@ namespace _zeitbild.repository.user
*/ */
export async function read( export async function read(
user_id : _zeitbild.type_user_id user_id : _zeitbild.type_user_id
) : Promise<_zeitbild.type_user_object> )
: Promise<_zeitbild.type_user_object>
{ {
const row : Record<string, any> = await get_store().read(user_id); const core_row : type_core_row = ((await get_store_core().read(user_id)) as type_core_row);
const user_object : _zeitbild.type_user_object = decode(row); const group_rows : Array<type_group_row_fat> = (
(
await get_store_groups().search(
{
"expression": "(user_id = $user_id)",
"arguments": {
"user_id": user_id,
}
}
)
)
.map(
hit => (
{
"user_id": hit.preview.user_id,
"group_id": hit.preview.group_id,
}
)
)
);
const dispersal : type_dispersal = {
"core": core_row,
"groups": (
group_rows.map(
group_row_fat => (
{
"group_id": group_row_fat.group_id,
}
)
)
)
};
const user_object : _zeitbild.type_user_object = decode(dispersal);
return Promise.resolve<_zeitbild.type_user_object>(user_object); return Promise.resolve<_zeitbild.type_user_object>(user_object);
} }
@ -111,21 +276,100 @@ namespace _zeitbild.repository.user
*/ */
export async function create( export async function create(
user_object : _zeitbild.type_user_object user_object : _zeitbild.type_user_object
) : Promise<_zeitbild.type_user_id> )
: Promise<_zeitbild.type_user_id>
{ {
const row : Record<string, any> = encode(user_object); const dispersal : type_dispersal = encode(user_object);
const user_id : _zeitbild.type_user_id = await get_store().create(row); // core
const user_id : _zeitbild.type_user_id = await (() => {
return get_store_core().create(dispersal.core);
}) ();
// groups
{
for (const group_row_slim of dispersal.groups)
{
const group_row_fat : type_group_row_fat = {
"user_id": user_id,
"group_id": group_row_slim.group_id,
};
await get_store_groups().create(group_row_fat);
}
}
return Promise.resolve<_zeitbild.type_user_id>(user_id); return Promise.resolve<_zeitbild.type_user_id>(user_id);
} }
/**
*/
export async function update(
user_id : _zeitbild.type_user_id,
user_object : _zeitbild.type_user_object
)
: Promise<void>
{
const dispersal : type_dispersal = encode(user_object);
// core
{
await get_store_core().update(user_id, dispersal.core);
}
// groups
{
const hits : Array<{key : int; preview : Record<string, any>;}> = await get_store_groups().search(
{
"expression": "(user_id = $user_id)",
"arguments": {
"user_id": user_id,
}
}
);
const contrast = lib_plankton.list.contrast(
hits,
hit => hit.preview.group_id.toFixed(0),
dispersal.groups,
group_row_slim => group_row_slim.group_id.toFixed(0)
);
// delete
{
for (const entry of contrast.only_left)
{
await get_store_groups().delete(entry.left.key);
}
}
// update
{
for (const entry of contrast.both)
{
const row_group_fat : type_group_row_fat = {
"user_id": user_id,
"group_id": entry.right.group_id,
};
await get_store_groups().update(entry.left.key, row_group_fat);
}
}
// create
{
for (const entry of contrast.only_right)
{
const row_group_fat : type_group_row_fat = {
"user_id": user_id,
"group_id": entry.right.group_id,
};
await get_store_groups().create(row_group_fat);
}
}
}
return Promise.resolve<void>(undefined);
}
/** /**
*/ */
export async function identify( export async function identify(
name : string name : string
) : Promise<_zeitbild.type_user_id> )
: Promise<_zeitbild.type_user_id>
{ {
const hits : Array<{key : _zeitbild.type_user_id; preview : any;}> = await get_store().search( const hits : Array<{key : _zeitbild.type_user_id; preview : /*type_preview*/Record<string, any>;}> = await get_store_core().search(
{ {
"expression": "(name = $name)", "expression": "(name = $name)",
"arguments": { "arguments": {
@ -133,10 +377,12 @@ namespace _zeitbild.repository.user
} }
} }
); );
if (hits.length <= 0) { if (hits.length <= 0)
{
return Promise.reject<_zeitbild.type_user_id>(new Error("not found")); return Promise.reject<_zeitbild.type_user_id>(new Error("not found"));
} }
else { else
{
return Promise.resolve<_zeitbild.type_user_id>(hits[0].key); return Promise.resolve<_zeitbild.type_user_id>(hits[0].key);
} }
} }

403
source/sample.ts Normal file
View file

@ -0,0 +1,403 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.sample
{
/**
*/
type type_date_absolute = {
year : int;
month : int;
day : int;
};
/**
*/
type type_date_relative = Array<
{
action : "trunc_week";
args : [int];
}
|
{
action : "shift_week";
args : [int];
}
|
{
action : "shift_day";
args : [int];
}
>;
/**
*/
type type_ywd = {
year : int;
week : int;
day : int;
};
/**
*/
type type_time = {
hour : int;
minute : int;
second : int;
};
/**
*/
type type_datetime = (
{
timezone_shift : int;
time : (null | type_time);
}
&
(
{
date_absolute : type_date_absolute;
}
|
{
date_relative : type_date_relative;
}
)
);
/**
*/
type type_data = {
groups : Array<
{
id : int;
name : string;
label ?: string;
}
>;
users : Array<
{
id : int;
name : string;
groups ?: Array<int>;
email_address : string;
dav_token ?: (null | string);
password : string;
}
>;
calendars : Array<
{
id : int;
name : string;
access : {
public ?: boolean;
default_level : ("none" | "view" | "edit" | "admin");
attributed_group ?: Array<
{
group_id : int;
level : ("none" | "view" | "edit" | "admin");
}
>;
attributed_user ?: Array<
{
user_id : int;
level : ("none" | "view" | "edit" | "admin");
}
>;
};
resource : (
{
kind : "local";
data : {
events : Array<
{
/**
* @todo rename to "title"
*/
name : string;
begin : type_datetime;
end : (
null
|
type_datetime
);
location : (
null
|
string
);
link : (
null
|
string
);
description : (
null
|
string
);
}
>
};
}
|
{
kind : "ics_feed";
data : {
url : string;
from_fucked_up_wordpress ?: boolean;
};
}
);
hue ?: (null | float);
}
>;
};
/**
*/
const phi : float = ((Math.sqrt(5) - 1) / 2);
/**
*/
function decode_datetime(
datetime : type_datetime
) : lib_plankton.pit.type_datetime
{
if ("date_relative" in datetime)
{
return {
"timezone_shift": datetime.timezone_shift,
"date": lib_plankton.call.convey(
lib_plankton.pit.now(),
(
datetime.date_relative.map<(x : any) => any>(
entry => {
switch (entry.action)
{
// default: {throw (new Error("unhandled action: " + entry.action));}
case "trunc_week": {return (x => lib_plankton.pit.trunc_week(x));}
case "shift_week": {return (x => lib_plankton.pit.shift_week(x, entry.args[0]));}
case "shift_day": {return (x => lib_plankton.pit.shift_day(x, entry.args[0]));}
}
}
)
.concat(
[
lib_plankton.pit.to_datetime,
x => x.date,
]
)
)
),
"time": datetime.time,
};
}
else
{
return {
"timezone_shift": datetime.timezone_shift,
"date": datetime.date_absolute,
"time": datetime.time,
};
}
}
/**
*/
export async function init(
data : type_data
) : Promise<void>
{
let track : {
group : Record<
int,
_zeitbild.type_group_id
>;
user : Record<
int,
_zeitbild.type_user_id
>;
calendar : Record<
int,
_zeitbild.type_user_id
>;
} = {
"group": {},
"user": {},
"calendar": {},
};
// groups
{
for await (const group_raw of data.groups)
{
const group_object : _zeitbild.type_group_object = {
"name": group_raw.name,
"label": (group_raw.label ?? group_raw.name),
};
const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add(
group_object
);
track.group[group_raw.id] = group_id;
}
}
// users
{
for await (const user_raw of data.users)
{
const user_object : _zeitbild.type_user_object = {
"name": user_raw.name,
"groups": (user_raw.groups ?? []),
"email_address": user_raw.email_address,
"dav_token": (user_raw.dav_token ?? null),
};
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object
);
await _zeitbild.service.auth_internal.set(
user_raw.name,
user_raw.password
);
track.user[user_raw.id] = user_id;
}
}
// calendars
{
for await (const calendar_raw of data.calendars)
{
let resource_object : _zeitbild.type_resource_object;
let resource_id : _zeitbild.type_resource_id;
switch (calendar_raw.resource.kind)
{
case "local":
{
resource_object = {
"kind": "local",
"data": {
"event_ids": [],
}
};
resource_id = await _zeitbild.service.resource.add(
resource_object
);
/*const event_ids : Array<_zeitbild.type_local_resource_event_id> = */await Promise.all(
calendar_raw.resource.data.events
.map(
(event_raw) => {
const event : _zeitbild.type_event_object = {
"name": event_raw.name,
"begin": decode_datetime(event_raw.begin),
"end": (
(event_raw.end === null)
?
null
:
decode_datetime(event_raw.end)
),
"location": event_raw.location,
"link": event_raw.link,
"description": event_raw.description,
};
return _zeitbild.service.resource.event_add(resource_id, event);
}
)
);
break;
}
case "ics_feed":
{
resource_object = {
"kind": "ics_feed",
"data": {
"url": calendar_raw.resource.data.url,
"from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false),
}
};
resource_id = await _zeitbild.service.resource.add(
resource_object
);
break;
}
}
const calendar_object : _zeitbild.type_calendar_object =
{
"name": calendar_raw.name,
"hue": (
calendar_raw.hue
??
((calendar_raw.id * phi) % 1)
),
"access": {
"public": (calendar_raw.access.public ?? false),
"default_level": _zeitbild.access_level_from_string(calendar_raw.access.default_level),
"attributed_group": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
(calendar_raw.access.attributed_group ?? [])
.map(
(entry) => ({
"key": track.user[entry.group_id],
"value": _zeitbild.access_level_from_string(entry.level),
})
)
),
}
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
(calendar_raw.access.attributed_user ?? [])
.map(
(entry) => ({
"key": track.user[entry.user_id],
"value": _zeitbild.access_level_from_string(entry.level),
})
)
),
}
)
),
},
"resource_id": resource_id,
};
const calendar_id : _zeitbild.type_calendar_id = await _zeitbild.service.calendar.add(
calendar_object
);
track.calendar[calendar_raw.id] = calendar_id;
}
}
return Promise.resolve<void>(undefined);
}
}

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.service.auth_internal namespace _zeitbild.service.auth_internal
{ {

View file

@ -1,44 +1,46 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.service.calendar namespace _zeitbild.service.calendar
{ {
/** /**
*/ */
function get_access_level( async function get_access_level(
calendar_object : _zeitbild.type_calendar_object, calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id) user_id : (null | _zeitbild.type_user_id)
) : _zeitbild.enum_access_level )
: Promise<_zeitbild.enum_access_level>
{ {
return ( return _zeitbild.access_level_determine(
lib_plankton.list.max<_zeitbild.enum_access_level, _zeitbild.enum_access_level>( calendar_object,
[ (
( (user_id === null)
calendar_object.access.public ?
? null
_zeitbild.enum_access_level.view :
:
_zeitbild.enum_access_level.none
),
(
(user_id === null)
?
_zeitbild.enum_access_level.none
:
calendar_object.access.attributed.get(
user_id,
lib_plankton.pod.make_filled<_zeitbild.enum_access_level>(
calendar_object.access.default_level
)
)
),
],
x => x,
{ {
"compare_value": _zeitbild.value_object.access_level.order, "id": user_id,
"object": (await _zeitbild.service.user.get(user_id)),
} }
)?.value )
??
_zeitbild.enum_access_level.none
); );
} }
@ -46,7 +48,7 @@ namespace _zeitbild.service.calendar
/** /**
* checks if a user has a sufficient access level * checks if a user has a sufficient access level
*/ */
function wrap_check_access_level<type_result>( async function wrap_check_access_level<type_result>(
calendar_object : _zeitbild.type_calendar_object, calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id), user_id : (null | _zeitbild.type_user_id),
threshold : _zeitbild.enum_access_level, threshold : _zeitbild.enum_access_level,
@ -55,26 +57,29 @@ namespace _zeitbild.service.calendar
=> =>
Promise<type_result> Promise<type_result>
) )
) : Promise<type_result> )
: Promise<type_result>
{ {
const access_level : _zeitbild.enum_access_level = get_access_level( const access_level : _zeitbild.enum_access_level = await get_access_level(
calendar_object, calendar_object,
user_id user_id
); );
if (! _zeitbild.value_object.access_level.order(threshold, access_level)) { if (! _zeitbild.access_level_order(threshold, access_level))
{
return Promise.reject<type_result>( return Promise.reject<type_result>(
new Error( new Error(
lib_plankton.string.coin( lib_plankton.string.coin(
"insufficient access level; at least required: {{threshold}}, actual: {{actual}}", "insufficient access level; at least required: {{threshold}}, actual: {{actual}}",
{ {
"threshold": _zeitbild.value_object.access_level.to_string(threshold), "threshold": _zeitbild.access_level_to_string(threshold),
"actual": _zeitbild.value_object.access_level.to_string(access_level), "actual": _zeitbild.access_level_to_string(access_level),
} }
) )
) )
); );
} }
else { else
{
return success_handler(access_level); return success_handler(access_level);
} }
} }
@ -90,6 +95,7 @@ namespace _zeitbild.service.calendar
id : _zeitbild.type_calendar_id; id : _zeitbild.type_calendar_id;
name : string; name : string;
access_level : _zeitbild.enum_access_level; access_level : _zeitbild.enum_access_level;
hue : float;
} }
> >
> >
@ -192,21 +198,31 @@ namespace _zeitbild.service.calendar
calendar_id : _zeitbild.type_calendar_id, calendar_id : _zeitbild.type_calendar_id,
event_object : _zeitbild.type_event_object, event_object : _zeitbild.type_event_object,
user_id : _zeitbild.type_user_id user_id : _zeitbild.type_user_id
) : Promise<void> ) : Promise<
{
local_resource_event_id : (null | type_local_resource_event_id);
hash : type_event_hash;
}
>
{ {
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read( const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id calendar_id
); );
return wrap_check_access_level<void>( return wrap_check_access_level(
calendar_object, calendar_object,
user_id, user_id,
_zeitbild.enum_access_level.edit, _zeitbild.enum_access_level.edit,
async () => { async () => {
/*const event_id : _zeitbild.type_local_resource_event_id = */await _zeitbild.service.resource.event_add( const local_resource_event_id : _zeitbild.type_local_resource_event_id = await _zeitbild.service.resource.event_add(
calendar_object.resource_id, calendar_object.resource_id,
event_object event_object
); );
return Promise.resolve<void>(undefined); return Promise.resolve(
{
"local_resource_event_id": local_resource_event_id,
"hash": get_event_hash_local(calendar_id, local_resource_event_id),
}
);
} }
); );
} }
@ -266,6 +282,45 @@ namespace _zeitbild.service.calendar
} }
/**
*/
function get_event_hash_local(
calendar_id : _zeitbild.type_calendar_id,
local_resource_event_id : _zeitbild.type_local_resource_event_id
) : string
{
return lib_plankton.string.coin(
"{{calendar_id}}:{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": local_resource_event_id.toFixed(0),
}
)
}
/**
*/
function get_event_hash_ics_feed(
calendar_id : _zeitbild.type_calendar_id,
event_object : _zeitbild.type_event_object
) : string
{
return lib_plankton.string.coin(
"{{calendar_id}}~{{hash}}",
{
"calendar_id": calendar_id.toFixed(0),
"hash": lib_plankton.call.convey(
event_object,
[
(x : any) => lib_plankton.json.encode(x),
(x : string) => lib_plankton.base64.encode(x),
]
)
}
);
}
/** /**
* @todo optimize by reducing the number of database queries * @todo optimize by reducing the number of database queries
*/ */
@ -278,13 +333,14 @@ namespace _zeitbild.service.calendar
Array< Array<
{ {
id : (null | _zeitbild.type_local_resource_event_id); id : (null | _zeitbild.type_local_resource_event_id);
hash : _zeitbild.type_event_hash;
object : _zeitbild.type_event_object; object : _zeitbild.type_event_object;
} }
> >
> >
{ {
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id); const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
return wrap_check_access_level<Array<{id : (null | _zeitbild.type_local_resource_event_id); object : _zeitbild.type_event_object;}>>( return wrap_check_access_level<Array<{id : (null | _zeitbild.type_local_resource_event_id); hash : _zeitbild.type_event_hash; object : _zeitbild.type_event_object;}>>(
calendar_object, calendar_object,
user_id, user_id,
_zeitbild.enum_access_level.view, _zeitbild.enum_access_level.view,
@ -305,6 +361,10 @@ namespace _zeitbild.service.calendar
(event_object) => Promise.resolve( (event_object) => Promise.resolve(
{ {
"id": event_id, "id": event_id,
"hash": get_event_hash_local(
calendar_id,
event_id,
),
"object": event_object, "object": event_object,
} }
) )
@ -327,41 +387,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 +449,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
), ),
@ -416,6 +489,7 @@ namespace _zeitbild.service.calendar
.map( .map(
(event) => ({ (event) => ({
"id": null, "id": null,
"hash": get_event_hash_ics_feed(calendar_id, event),
"object": event, "object": event,
}) })
) )
@ -441,19 +515,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 +522,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 +544,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 +553,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
@ -500,13 +565,14 @@ namespace _zeitbild.service.calendar
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read( const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id calendar_id
); );
const access_level : _zeitbild.enum_access_level = get_access_level( const access_level : _zeitbild.enum_access_level = await get_access_level(
calendar_object, calendar_object,
user_id user_id
); );
const events : Array< const events : Array<
{ {
id : (null | _zeitbild.type_local_resource_event_id); id : (null | _zeitbild.type_local_resource_event_id);
hash : _zeitbild.type_event_hash;
object : _zeitbild.type_event_object; object : _zeitbild.type_event_object;
} }
> = await get_events( > = await get_events(
@ -519,8 +585,10 @@ namespace _zeitbild.service.calendar
events events
.map( .map(
(event_entry) => ({ (event_entry) => ({
"hash": event_entry.hash,
"calendar_id": calendar_id, "calendar_id": calendar_id,
"calendar_name": calendar_object.name, "calendar_name": calendar_object.name,
"hue": calendar_object.hue,
"access_level": access_level, "access_level": access_level,
"event_id": event_entry.id, "event_id": event_entry.id,
"event_object": event_entry.object, "event_object": event_entry.object,

63
source/services/group.ts Normal file
View file

@ -0,0 +1,63 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.service.group
{
/**
*/
export function list(
)
: Promise<
Array<
{
id : _zeitbild.type_group_id;
object : _zeitbild.type_group_object;
}
>
>
{
return _zeitbild.repository.group.list();
}
/**
*/
export function add(
group_object : _zeitbild.type_group_object
)
: Promise<_zeitbild.type_group_id>
{
return _zeitbild.repository.group.create(group_object);
}
/**
*/
export function change(
group_id : _zeitbild.type_group_id,
group_object : _zeitbild.type_group_object
)
: Promise<void>
{
return _zeitbild.repository.group.update(group_id, group_object);
}
}

View file

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.service.resource namespace _zeitbild.service.resource
{ {
@ -31,7 +50,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 +82,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 +115,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 +146,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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _zeitbild.service.user namespace _zeitbild.service.user
{ {
@ -28,6 +47,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 +66,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

@ -1,3 +1,22 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
*/
/** /**
*/ */
@ -14,6 +33,19 @@ namespace _zeitbild
} }
/**
*/
export type type_group_id = int;
/**
*/
export type type_group_object = {
name : string;
label : string;
};
/** /**
*/ */
export type type_user_id = int; export type type_user_id = int;
@ -23,17 +55,28 @@ namespace _zeitbild
*/ */
export type type_user_object = { export type type_user_object = {
name : string; name : string;
groups : Array<
type_group_id
>;
email_address : ( email_address : (
null null
| |
string string
); );
dav_token : (
null
|
string
);
}; };
/** /**
*/ */
export type type_event_object = { export type type_event_object = {
/**
* @todo rename to "title"
*/
name : string; name : string;
begin : lib_plankton.pit.type_datetime; begin : lib_plankton.pit.type_datetime;
end : ( end : (
@ -82,10 +125,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;
}; };
} }
@ -101,10 +143,15 @@ namespace _zeitbild
*/ */
export type type_calendar_object = { export type type_calendar_object = {
name : string; name : string;
hue : float;
access : { access : {
public : boolean; public : boolean;
default_level : enum_access_level; default_level : enum_access_level;
attributed : lib_plankton.map.type_map< attributed_group : lib_plankton.map.type_map<
type_group_id,
enum_access_level
>;
attributed_user : lib_plankton.map.type_map<
type_user_id, type_user_id,
enum_access_level enum_access_level
>; >;
@ -112,4 +159,22 @@ namespace _zeitbild
resource_id : type_resource_id; resource_id : type_resource_id;
}; };
/**
*/
export type type_event_hash = string;
/**
*/
export type type_event_extended = {
hash : type_event_hash;
calendar_id : type_calendar_id;
calendar_name : string;
hue : float;
access_level : enum_access_level;
event_id : (null | type_local_resource_event_id);
event_object : type_event_object;
};
} }

View file

@ -1,55 +0,0 @@
/**
*/
namespace _zeitbild.value_object.access_level
{
/**
*/
export function to_string(
access_level : _zeitbild.enum_access_level
) : string
{
switch (access_level) {
case _zeitbild.enum_access_level.none: {return "none";}
case _zeitbild.enum_access_level.view: {return "view";}
case _zeitbild.enum_access_level.edit: {return "edit";}
case _zeitbild.enum_access_level.admin: {return "admin";}
default: {throw (new Error("invalid access level: " + String(access_level)));}
}
}
/**
*/
export function from_string(
access_level_ : string
) : _zeitbild.enum_access_level
{
switch (access_level_) {
case "none": {return _zeitbild.enum_access_level.none;}
case "view": {return _zeitbild.enum_access_level.view;}
case "edit": {return _zeitbild.enum_access_level.edit;}
case "admin": {return _zeitbild.enum_access_level.admin;}
default: {throw (new Error("invalid encoded access level: " + String(access_level_)));}
}
}
/**
*/
export function order(
x : _zeitbild.enum_access_level,
y : _zeitbild.enum_access_level
) : boolean
{
const list : Array<_zeitbild.enum_access_level> = [
_zeitbild.enum_access_level.none,
_zeitbild.enum_access_level.view,
_zeitbild.enum_access_level.edit,
_zeitbild.enum_access_level.admin,
];
return (list.indexOf(x) <= list.indexOf(y));
}
}

View file

@ -47,6 +47,7 @@ def main():
"--verbose", "--verbose",
"--exclude='conf.json'", "--exclude='conf.json'",
"--exclude='data.sqlite'", "--exclude='data.sqlite'",
"--exclude='log.jsonl'",
("%s/" % args.build_directory), ("%s/" % args.build_directory),
( (
("%s" % args.target_directory) ("%s" % args.target_directory)

View file

@ -18,7 +18,7 @@ cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc
## rules ## rules
.PHONY: default .PHONY: default
default: node_modules sql ${dir_build}/zeitbild node_modules default: node_modules ${dir_build}/zeitbild node_modules
.PHONY: node_modules .PHONY: node_modules
node_modules: node_modules:
@ -26,13 +26,6 @@ node_modules:
@ ${cmd_log} "node modules …" @ ${cmd_log} "node modules …"
@ ${cmd_cp} -r -u ${dir_lib}/node/node_modules/* ${dir_build}/node_modules/ @ ${cmd_cp} -r -u ${dir_lib}/node/node_modules/* ${dir_build}/node_modules/
.PHONY: sql
sql: \
$(wildcard ${dir_source}/repositories/sql/*)
@ ${cmd_log} "sql …"
@ ${cmd_mkdir} ${dir_build}/sql
@ ${cmd_cp} -r -u $^ ${dir_build}/sql/
${dir_temp}/conf.ts: \ ${dir_temp}/conf.ts: \
${dir_source}/conf.ts.tpl \ ${dir_source}/conf.ts.tpl \
${dir_source}/conf.schema.json ${dir_source}/conf.schema.json
@ -47,12 +40,14 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/database.ts \ ${dir_source}/database.ts \
${dir_source}/auth.ts \ ${dir_source}/auth.ts \
${dir_source}/types.ts \ ${dir_source}/types.ts \
${dir_source}/value_objects/access_level.ts \ ${dir_source}/logic.ts \
${dir_source}/repositories/auth_internal.ts \ ${dir_source}/repositories/auth_internal.ts \
${dir_source}/repositories/group.ts \
${dir_source}/repositories/user.ts \ ${dir_source}/repositories/user.ts \
${dir_source}/repositories/resource.ts \ ${dir_source}/repositories/resource.ts \
${dir_source}/repositories/calendar.ts \ ${dir_source}/repositories/calendar.ts \
${dir_source}/services/auth_internal.ts \ ${dir_source}/services/auth_internal.ts \
${dir_source}/services/group.ts \
${dir_source}/services/user.ts \ ${dir_source}/services/user.ts \
${dir_source}/services/resource.ts \ ${dir_source}/services/resource.ts \
${dir_source}/services/calendar.ts \ ${dir_source}/services/calendar.ts \
@ -64,7 +59,11 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/api/actions/session_begin.ts \ ${dir_source}/api/actions/session_begin.ts \
${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/session_status.ts \
${dir_source}/api/actions/group_list.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,7 +74,9 @@ ${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}/sample.ts \
${dir_source}/main.ts ${dir_source}/main.ts
@ ${cmd_log} "compile …" @ ${cmd_log} "compile …"
@ ${cmd_mkdir} $(dir $@) @ ${cmd_mkdir} $(dir $@)

View file

@ -16,6 +16,7 @@ modules="${modules} session"
modules="${modules} file" modules="${modules} file"
modules="${modules} string" modules="${modules} string"
modules="${modules} json" modules="${modules} json"
modules="${modules} base64"
modules="${modules} list" modules="${modules} list"
modules="${modules} order" modules="${modules} order"
modules="${modules} ical" modules="${modules} ical"
@ -43,8 +44,8 @@ mkdir -p ${dir}
mkdir /tmp/sandbox -p mkdir /tmp/sandbox -p
cd /tmp/sandbox cd /tmp/sandbox
ptk fetch node ${modules} ptk fetch node ${modules}
schwamm --include=/tmp/sandboxplankton.swm.json --output=dump:logic-decl > ${dir}/plankton.d.ts schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-decl > ${dir}/plankton.d.ts
schwamm --include=/tmp/sandboxplankton.swm.json --output=dump:logic-impl > ${dir}/plankton.js schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-impl > ${dir}/plankton.js
exit exit
mkdir -p ${dir} mkdir -p ${dir}