This commit is contained in:
Fenris Wolf 2024-09-12 00:03:29 +02:00
parent 279bedf74e
commit 175e57b1f3
33 changed files with 13494 additions and 7271 deletions

27
.editorconfig Normal file
View file

@ -0,0 +1,27 @@
# see https://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = tab
tab_width = 4
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
curly_bracket_next_line = false
indent_brace_style = K&R
spaces_around_operators = true
spaces_around_brackets = false
quote_type = double
[*.y{,a}ml{,lint}]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 2

View file

@ -1,4 +1,6 @@
{
"view_mode": "table",
"timezone_shift": 0
"version": 1,
"log": [
{"kind": "stdout", "data": {"threshold": "debug"}}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
namespace _zeitbild.api
{
/**
*/
export function register_calendar_list(
rest_subject : lib_plankton.rest.type_rest
) : void
{
register<
null,
Array<
{
id : _zeitbild.type.calendar_id;
preview : {
name : string;
};
}
>
>(
rest_subject,
lib_plankton.http.enum_method.get,
"/calendar/list",
{
"description": "listet alle Kalender auf",
"query_parameters": [
],
"output_schema": () => ({
"type": "array",
"items": {
"type": "object",
"nullable": false,
"additionalProperties": false,
"properties": {
"id": {
"type": "number",
"nullable": false,
},
"preview": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"nullable": false,
},
},
"required": [
"name",
]
}
},
"required": [
"id",
"preview",
],
}
}),
"restriction": restriction_none, // TODO
"execution": () => (
_zeitbild.service.calendar.list(null)
.then(
data => Promise.resolve({
"status_code": 200,
"data": data,
})
)
)
}
);
}
}

View file

@ -0,0 +1,40 @@
namespace _zeitbild.api
{
/**
*/
export function register_meta_ping(
rest_subject : lib_plankton.rest.type_rest
) : void
{
lib_plankton.rest.register<
null,
string
>
(
rest_subject,
lib_plankton.http.enum_method.get,
_zeitbild.conf.get().server.path_base + "/meta/ping",
{
"description": "sendet ein 'pong' zurück; gedacht um die Erreichbarkeit des Backends zu prüfen",
"input_schema": () => ({
"nullable": true,
}),
"output_schema": () => ({
"nullable": false,
"type": "string",
}),
"restriction": restriction_none,
"execution": () => {
return Promise.resolve({
"status_code": 200,
"data": "pong",
});
},
}
);
}
}

View file

@ -0,0 +1,37 @@
namespace _zeitbild.api
{
/**
*/
export function register_meta_spec(
rest_subject : lib_plankton.rest.type_rest
) : void
{
lib_plankton.rest.register<
null,
any
>
(
rest_subject,
lib_plankton.http.enum_method.get,
_zeitbild.conf.get().server.path_base + "/meta/spec",
{
"description": "gibt die API-Spezifikation im OpenAPI-Format aus",
"input_schema": () => ({
"nullable": true,
}),
"output_schema": () => ({
}),
"restriction": restriction_none,
"execution": () => {
return Promise.resolve({
"status_code": 200,
"data": lib_plankton.rest.to_oas(rest_subject),
});
},
}
);
}
}

76
source/api/base.ts Normal file
View file

@ -0,0 +1,76 @@
namespace _zeitbild.api
{
/**
* @todo zu plankton auslagern?
*/
type type_stuff = {
version: (null | string);
headers: Record<string, string>;
path_parameters: Record<string, string>;
query_parameters: Record<string, string>;
};
/**
*/
export async function session_from_stuff(
stuff : {headers : Record<string, string>;}
) : Promise<{key : string; value : lib_plankton.session.type_session}>
{
const key : string = (stuff.headers["X-Session-Key"] || stuff.headers["X-Session-Key".toLowerCase()]);
const value : lib_plankton.session.type_session = await lib_plankton.session.get(key);
return {"key": key, "value": value};
}
/**
*/
export const restriction_none : lib_plankton.rest.type_restriction<any> = (
(stuff) => Promise.resolve<boolean>(true)
);
/**
*/
export function register<type_input, type_output>(
rest_subject : lib_plankton.rest.type_rest,
http_method : lib_plankton.http.enum_method,
path : string,
options : {
active ?: ((version : string) => boolean);
restriction ?: (null | lib_plankton.rest.type_restriction<type_input>);
execution ?: lib_plankton.rest.type_execution<type_input, type_output>;
title ?: (null | string);
description ?: (null | string);
query_parameters ?: Array<
{
name : string;
description : (null | string);
required : boolean;
}
>;
input_schema ?: ((version: (null | string)) => lib_plankton.rest.type_oas_schema);
output_schema ?: ((version: (null | string)) => lib_plankton.rest.type_oas_schema);
request_body_mimetype ?: string;
request_body_decode ?: ((http_request_body : Buffer, http_request_header_content_type : (null | string)) => any);
response_body_mimetype ?: string;
response_body_encode ?: ((output : any) => Buffer);
} = {}
) : void
{
options = Object.assign(
{
},
options
);
lib_plankton.rest.register<type_input, type_output>(
rest_subject,
http_method,
(_zeitbild.conf.get().server.path_base + path),
options
);
}
}

37
source/api/functions.ts Normal file
View file

@ -0,0 +1,37 @@
namespace _zeitbild.api
{
/**
*/
export function make(
) : lib_plankton.rest.type_rest
{
const rest_subject : lib_plankton.rest.type_rest = lib_plankton.rest.make(
{
"title": "zeitbild",
"versioning_method": "header",
"versioning_header_name": "X-Api-Version",
"set_access_control_headers": true,
"authentication": {
"kind": "key_header",
"parameters": {"name": "X-Session-Key"}
},
}
);
// meta
{
_zeitbild.api.register_meta_ping(rest_subject);
_zeitbild.api.register_meta_spec(rest_subject);
}
// calendar
{
_zeitbild.api.register_calendar_list(rest_subject);
}
return rest_subject;
}
}

6
source/backend.ts Normal file
View file

@ -0,0 +1,6 @@
/**
*/
namespace _zeitbild.frontend.resources.backend
{
}

237
source/conf.ts Normal file
View file

@ -0,0 +1,237 @@
namespace _zeitbild.conf
{
/**
*/
type type_log_threshold = (
"debug"
|
"info"
|
"notice"
|
"warning"
|
"error"
);
/**
*/
type type_log_format = (
"jsonl"
|
"human_readable"
);
/**
*/
export type type_conf = {
general : {
language : (null | string);
};
log : Array<
{
kind : "stdout";
data : {
threshold : type_log_threshold;
};
}
|
{
kind : "file";
data : {
threshold : type_log_threshold;
path : string;
};
}
|
{
kind : "email";
data : {
threshold : type_log_threshold;
smtp_credentials : {
host : string;
port : int;
username : string;
password : string;
};
sender : string;
receivers : Array<string>;
};
}
>;
server : {
host : string;
port : int;
path_base : string;
};
database : (
{
kind : "sqlite";
data : {
path : string;
};
}
|
{
kind : "postgresql";
data : {
host : string;
port ?: int;
username : string;
password : string;
schema : string;
};
}
);
session_management : {
in_memory : boolean;
drop_all_at_start : boolean;
lifetime : int;
};
};
/**
*/
var _data : (null | type_conf) = null;
/**
*/
export function inject(
conf_raw : any
) : void
{
const version : int = (conf_raw["version"] ?? 1);
_data = {
"general": (
((node_general) => ({
"language": (node_general["language"] ?? null),
})) (conf_raw["general"] ?? {})
),
"log": (
(() => {
const node_log = (
conf_raw["log"]
??
[
{
"kind": "stdout",
"data": {
}
},
]
);
return (
node_log.map(
(node_log_entry : any) => ({
"kind": node_log_entry["kind"],
"data": Object.assign(
{
"format": "human_readable",
"threshold": "notice",
},
(node_log_entry["data"] ?? {})
)
})
)
);
}) ()
),
"server": (
((node_server) => ({
"host": (() => {
return (node_server["host"] ?? "::");
}) (),
"port": (node_server["port"] ?? 7845),
"path_base": (node_server["path_base"] ?? ""),
})) (conf_raw["server"] ?? {})
),
"database": (
((node_database) => {
const kind : string = (node_database["kind"] ?? "sqlite");
const node_database_data_raw = (node_database["data"] ?? {});
switch (kind) {
case "sqlite": {
return {
"kind": kind,
"data": {
"path": (node_database_data_raw["path"] ?? "data.sqlite"),
}
};
break;
}
case "postgresql": {
return {
"kind": kind,
"data": node_database_data_raw,
};
break;
}
default: {
throw (new Error("unhandled"));
break;
}
}
}) (conf_raw["database"] ?? {})
),
"session_management": (
((node_session_management) => ({
"in_memory": (node_session_management["in_memory"] ?? true),
"drop_all_at_start": (node_session_management["drop_all_at_start"] ?? true),
"lifetime": (node_session_management["lifetime"] ?? 900),
})) (conf_raw["session_management"] ?? {})
),
};
}
/**
* @todo mandatory fields
*/
export async function load(
path : string
) : Promise<void>
{
let conf_raw : any;
if (! (await lib_plankton.file.exists(path))) {
// return Promise.reject<void>(new Error("configuration file not found: " + path + "; using fallback"));
conf_raw = {};
}
else {
try {
conf_raw = lib_plankton.json.decode(await lib_plankton.file.read(path));
}
catch (error) {
conf_raw = null;
}
}
if (conf_raw === null) {
return Promise.reject<void>("configuration file could not be read");
}
else {
inject(conf_raw);
// process.stderr.write(JSON.stringify(_data, undefined, "\t"));
return Promise.resolve<void>(undefined);
}
}
/**
*/
export function get(
) : type_conf
{
if (_data === null) {
throw (new Error("conf not loaded yet"));
}
else {
return _data;
}
}
}

112
source/database.ts Normal file
View file

@ -0,0 +1,112 @@
namespace _zeitbild.database
{
/**
*/
const _compatible_revisions : Array<string> = [
"r1",
];
/**
*/
export function get_implementation(
) : lib_plankton.database.type_database
{
switch (_zeitbild.conf.get().database.kind) {
case "sqlite": {
type type_parameters = {
path : string;
};
const parameters : type_parameters = (_zeitbild.conf.get().database.data as type_parameters);
return lib_plankton.database.sqlite_database(
{
"path": parameters.path,
}
);
break;
}
case "postgresql": {
type type_parameters = {
host : string;
port ?: int;
username : string;
password : string;
schema : string;
};
const parameters : type_parameters = (_zeitbild.conf.get().database.data as type_parameters);
return lib_plankton.database.postgresql_database(
{
"host": parameters.host,
"port": parameters.port,
"username": parameters.username,
"password": parameters.password,
"schema": parameters.schema,
}
);
}
default: {
throw (new Error("database implementation not available: " + _zeitbild.conf.get().database.kind));
}
}
}
/**
*/
function get_revision(
) : Promise<(null | string)>
{
return (
get_implementation().query_select(
{
"source": "_meta",
"fields": ["revision"],
}
)
.then<(null | string)>(
(rows) => Promise.resolve<(null | string)>(rows[0]["revision"])
)
.catch<(null | string)>(
(reason) => {
lib_plankton.log.warning(
"database_get_revision_error",
{
"reason": String(reason),
}
);
return Promise.resolve<(null | string)>(null);
}
)
);
}
/**
*/
export async function check(
) : Promise<void>
{
const revision : (null | string) = await get_revision();
lib_plankton.log.info(
"database_check",
{
"revision_found": revision,
"revisions_compatible": _compatible_revisions,
}
);
if (revision === null) {
return Promise.reject<void>(new Error("database appearently missing"));
}
else {
if (! _compatible_revisions.includes(revision)) {
return Promise.reject<void>(new Error("database revision incompatible; found: " + revision + "; required: " + String(_compatible_revisions)));
}
else {
return Promise.resolve<void>(undefined);
}
}
}
}

View file

@ -1,7 +1,7 @@
/**
*/
namespace _zeitbild.frontend.helpers
namespace _zeitbild.helpers
{
/**

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="logic.js">
</script>
<script type="text/javascript">
document.addEventListener(
"DOMContentLoaded",
() => {
_zeitbild.frontend.main()
.then(
() => {}
)
.catch(
(error) => {console.error(error);}
)
}
);
</script>
</head>
<body>
</body>
</html>

View file

@ -1,333 +0,0 @@
/**
*/
namespace _zeitbild.frontend.resources.backend
{
/**
*/
var _data : _zeitbild.frontend.type_datamodel;
/**
*/
export async function init(
) : Promise<void>
{
const path : string = "data.json";
if (_data === undefined) {
_data = lib_plankton.call.convey(
await lib_plankton.file.read(path),
[
lib_plankton.json.decode,
(data_raw : any) => (
({
"users": data_raw["users"],
"calendars": (
data_raw["calendars"]
.map(
(calendar_entry_raw : any) => ({
"id": calendar_entry_raw["id"],
"object": (
((calendar_object_raw) => {
switch (calendar_object_raw["kind"]) {
default: {
return calendar_object_raw;
break;
}
case "concrete": {
return {
"kind": "concrete",
"data": {
"name": calendar_object_raw["data"]["name"],
"private": (
calendar_object_raw["data"]["private"]
??
false
),
"users": calendar_object_raw["data"]["users"],
"events": (
calendar_object_raw["data"]["events"]
.map(
(event_raw : any) => ({
"name": event_raw["name"],
"begin": event_raw["begin"],
"end": (
(
(
event_raw["end"]
??
null
)
===
null
)
?
null
:
event_raw["end"]
),
"location": (
event_raw["location"]
??
null
),
"description": (
event_raw["description"]
??
null
),
})
)
),
},
};
break;
}
}
}) (calendar_entry_raw["object"])
),
})
)
),
}) as type_datamodel
),
]
);
}
else {
// do nothing
}
return Promise.resolve<void>(undefined);
}
/**
*/
export async function calendar_list(
) : Promise<
Array<
{
key : type_calendar_id;
preview : {
name : string;
}
}
>
>
{
await init();
return Promise.resolve(
_data.calendars
.map(
(calendar_entry) => {
switch (calendar_entry.object.kind) {
case "concrete": {
return {
"key": calendar_entry.id,
"preview": {
"name": calendar_entry.object.data.name,
}
};
break;
}
case "caldav": {
return {
"key": calendar_entry.id,
"preview": {
"name": "(imported)",
}
};
}
}
}
)
);
}
/**
*/
export async function calendar_read(
calendar_id : type_calendar_id
) : Promise<type_calendar_object>
{
await init();
const hits = (
_data.calendars
.filter(
(calendar_entry) => (calendar_entry.id === calendar_id)
)
);
if (hits.length <= 0) {
return Promise.reject<type_calendar_object>(new Error("not found"));
}
else {
return Promise.resolve<type_calendar_object>(hits[0].object);
}
}
/**
* @todo prevent loops
*/
export async function calendar_gather_events(
calendar_ids : Array<type_calendar_id>,
from_pit : _zeitbild.frontend.helpers.type_pit,
to_pit : _zeitbild.frontend.helpers.type_pit
) : Promise<
Array<
{
calendar_id : type_calendar_id;
calendar_name : string;
event : type_event;
}
>
>
{
lib_plankton.log.info(
"calendar_gather_events",
{
"calendar_ids": calendar_ids,
}
);
await init();
let result : Array<
{
calendar_id : type_calendar_id;
calendar_name : string;
event : type_event;
}
> = [];
for await (const calendar_id of calendar_ids) {
const calendar_object : type_calendar_object = await calendar_read(
calendar_id
);
if (calendar_object.data.private) {
lib_plankton.log.info(
"calendar_gather_events_private_calendar_blocked",
{
"calendar_id": calendar_id,
}
);
}
else {
switch (calendar_object.kind) {
case "concrete": {
result = (
result
.concat(
calendar_object.data.events
.filter(
(event) => _zeitbild.frontend.helpers.pit_is_between(
_zeitbild.frontend.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.data.name,
"event": event
})
)
)
);
break;
}
case "caldav": {
const url : lib_plankton.url.type_url = lib_plankton.url.decode(
calendar_object.data.source_url
);
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": ((url.scheme === "https") ? "https" : "http"),
"host": url.host,
"path": (url.path ?? "/"),
"query": url.query,
"method": lib_plankton.http.enum_method.get,
"headers": {},
"body": null,
};
// TODO: cache?
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(
http_request,
{
}
);
const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode(
http_response.body.toString(),
{
}
);
result = (
result
.concat(
vcalendar.vevents
.map(
(vevent : lib_plankton.ical.type_vevent) => (
(vevent.dtstart !== undefined)
?
{
"name": (
(vevent.summary !== undefined)
?
vevent.summary
:
"???"
),
"begin": _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtstart),
"end": (
(vevent.dtend !== undefined)
?
_zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtend)
:
null
),
"location": (
(vevent.location !== undefined)
?
vevent.location
:
null
),
"description": (
(vevent.description !== undefined)
?
vevent.description
:
null
),
}
:
null
)
)
.filter(
(event) => (event !== null)
)
.filter(
(event) => _zeitbild.frontend.helpers.pit_is_between(
_zeitbild.frontend.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.data.name,
"event": event,
})
)
)
);
break;
}
}
}
}
return Promise.resolve(result);
}
}

View file

@ -1,218 +0,0 @@
/**
*/
async function main(
args_raw : Array<string>
) : Promise<void>
{
const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler({
"data_path": lib_plankton.args.class_argument.volatile({
"indicators_long": ["data-path"],
"indicators_short": ["d"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "data.json",
// "info": null,
"name": "data-path",
}),
"timezone_shift": lib_plankton.args.class_argument.volatile({
"indicators_long": ["timezone-shift"],
"indicators_short": ["t"],
"type": lib_plankton.args.enum_type.integer,
"mode": lib_plankton.args.enum_mode.replace,
"default": 0,
// "info": null,
"name": "timezone-shift",
}),
"calendar_id": lib_plankton.args.class_argument.volatile({
"indicators_long": ["calendar"],
"indicators_short": ["c"],
"type": lib_plankton.args.enum_type.integer,
"mode": lib_plankton.args.enum_mode.replace,
"default": 1,
// "info": null,
"name": "calendar_id",
}),
"view_mode": lib_plankton.args.class_argument.volatile({
"indicators_long": ["view-moode"],
"indicators_short": ["m"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "table",
// "info": null,
"name": "view_mode",
}),
"help": lib_plankton.args.class_argument.volatile({
"indicators_long": ["help"],
"indicators_short": ["h"],
"type": lib_plankton.args.enum_type.boolean,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
// "info": null,
"name": "help",
}),
});
const args : Record<string, any> = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" "));
// init
lib_plankton.log.conf_push(
[
// lib_plankton.log.channel_make({"kind": "file", "data": {"threshold": "debug", "path": "/tmp/kalender.log"}}),
lib_plankton.log.channel_make({"kind": "file", "data": {"threshold": "info", "path": "/dev/stderr"}}),
// lib_plankton.log.channel_make({"kind": "stdout", "data": {"threshold": "info"}}),
]
);
// exec
if (args["help"]) {
process.stdout.write(
arg_handler.generate_help(
{
"programname": "kalender",
"description": "Kalender",
"executable": "kalender",
}
)
+
"\n"
);
}
else {
const data : type_datamodel = lib_plankton.call.convey(
await lib_plankton.file.read(args["data_path"]),
[
lib_plankton.json.decode,
(data_raw : any) => (
({
"users": data_raw["users"],
"calendars": (
data_raw["calendars"]
.map(
(calendar_entry_raw : any) => ({
"id": calendar_entry_raw["id"],
"object": (
((calendar_object_raw) => {
switch (calendar_object_raw["kind"]) {
default: {
return calendar_object_raw;
break;
}
case "concrete": {
return {
"kind": "concrete",
"data": {
"name": calendar_object_raw["data"]["name"],
"users": calendar_object_raw["data"]["users"],
"events": (
calendar_object_raw["data"]["events"]
.map(
(event_raw : any) => ({
"name": event_raw["name"],
"begin": event_raw["begin"],
"end": (
(event_raw["end"] === null)
?
null
:
event_raw["end"]
),
"description": event_raw["description"],
})
)
),
},
};
break;
}
}
}) (calendar_entry_raw["object"])
),
})
)
),
}) as type_datamodel
),
]
);
let content : string;
switch (args["view_mode"]) {
default: {
content = "";
throw (new Error("invalid view mode"));
break;
}
case "table": {
content = await calendar_view_table_html(
data,
args.calendar_id,
{
"timezone_shift": args["timezone_shift"],
}
);
break;
}
case "list": {
content = await calendar_view_list_html(
data,
args.calendar_id,
{
"timezone_shift": args["timezone_shift"],
}
);
break;
}
}
const output : string = template_coin(
"main",
{
"content": content,
}
);
process.stdout.write(output);
}
return Promise.resolve<void>(undefined);
}
/*
process.stderr.write(
JSON.stringify(
{
"x1": datetime_from_date_object(
new Date(Date.now()),
{
"timezone_shift": 2,
}
),
"x2": datetime_from_date_object(
new Date(Date.now()),
{
"timezone_shift": 0,
}
),
"x3": pit_from_year_and_week(
2024,
37,
{
"timezone_shift": 0,
}
),
},
undefined,
"\t"
)
+
"\n"
);
*/
(
main(process.argv.slice(2))
.then(
() => {}
)
/*
.catch(
(error) => {process.stderr.write(String(error) + "\n");}
)
*/
);

View file

@ -1,133 +0,0 @@
/**
*/
namespace _zeitbild.frontend
{
/**
*/
type type_conf = {
view_mode : string;
calendar_ids : Array<int>;
timezone_shift : int;
};
/**
*/
async function render(
conf : type_conf,
calendar_ids : Array<type_calendar_id>
) : Promise<void>
{
calendar_ids.sort();
const target : HTMLElement = (document.querySelector("body") as HTMLBodyElement);
switch (conf.view_mode) {
default: {
throw (new Error("invalid view mode"));
break;
}
case "table": {
const content : string = await _zeitbild.frontend.view.calendar_view_table_html(
calendar_ids,
{
"from": {
"year": 2024,
"week": 35
},
"to": {
"year": 2024,
"week": 43
},
"timezone_shift": conf.timezone_shift,
}
);
target.innerHTML = content;
/*
document.querySelectorAll(".tableview-sources-entry").forEach(
(element) => {
element.addEventListener(
"click",
(event) => {
const element_ : HTMLElement = (event.target as HTMLElement);
const calendar_id : type_calendar_id = parseInt(element_.getAttribute("rel") as string);
const active : boolean = element_.classList.toggle("tableview-sources-entry-active");
render(
conf,
lib_plankton.call.convey(
calendar_ids,
[
(x : Array<int>) => (
active
?
calendar_ids.concat([calendar_id])
:
calendar_ids.filter(y => (y !== calendar_id))
),
]
)
);
}
);
}
);
*/
break;
}
case "list": {
const content : string = await _zeitbild.frontend.view.calendar_view_list_html(
calendar_ids,
{
"timezone_shift": conf.timezone_shift,
}
);
target.innerHTML = content;
break;
}
}
return Promise.resolve<void>(undefined);
}
/**
*/
export async function main(
) : Promise<void>
{
// init
lib_plankton.log.conf_push(
[
lib_plankton.log.channel_make({"kind": "console", "data": {"threshold": "info"}}),
]
);
// conf
const conf : type_conf = lib_plankton.json.decode(await lib_plankton.file.read("conf.json"));
// args
const url : URL = new URL(window.location.toString());
const calendar_ids : Array<type_calendar_id> = (
(url.searchParams.get("ids") !== null)
?
lib_plankton.call.convey(
url.searchParams.get("ids"),
[
(x : string) => lib_plankton.string.split(x, ","),
(x : Array<string>) => x.map(y => parseInt(y)),
]
)
:
(await _zeitbild.frontend.resources.backend.calendar_list()).map(x => x.key)
);
// exec
await render(
conf,
calendar_ids
);
return Promise.resolve<void>(undefined);
}
}

View file

@ -1,103 +0,0 @@
/**
*/
namespace _zeitbild.frontend
{
/**
*/
type type_role = (
"editor"
|
"viewer"
);
/**
*/
type type_user_id = int;
/**
*/
type type_user_object = {
name : string;
};
/**
*/
export type type_event = {
name : string;
begin : _zeitbild.frontend.helpers.type_datetime;
end : (
null
|
_zeitbild.frontend.helpers.type_datetime
);
location : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
export type type_calendar_id = int;
/**
*/
export type type_calendar_object = (
{
kind : "concrete";
data : {
name : string;
private : boolean;
users : Array<
{
id : type_user_id;
role : type_role;
}
>;
events : Array<type_event>;
};
}
|
{
kind : "caldav";
data : {
name : string;
private : boolean;
read_only : boolean;
source_url : string;
}
}
);
/**
*/
export type type_datamodel = {
users : Array<
{
id : type_user_id;
object : type_user_object;
}
>;
calendars : Array<
{
id : type_calendar_id;
object : type_calendar_object;
}
>;
};
}

227
source/main.ts Normal file
View file

@ -0,0 +1,227 @@
/**
*/
async function main(
args_raw : Array<string>
) : Promise<void>
{
// init1
lib_plankton.log.conf_push(
[
lib_plankton.log.channel_make({"kind": "stdout", "data": {"threshold": "debug"}}),
]
);
// args
const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler({
"action": lib_plankton.args.class_argument.positional({
"index": 0,
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "serve",
"name": "action",
"info": lib_plankton.string.coin(
"{{description}}:\n{{options}}\n\t\t",
{
"description": "action",
"options": (
[
{
"name": "serve",
"description": "serve"
},
{
"name": "api-doc",
"description": "api-doc"
},
{
"name": "expose-conf",
"description": "expose-conf"
},
{
"name": "help",
"description": "help"
},
]
.map(
entry => lib_plankton.string.coin(
"\t\t- {{name}}\n\t\t\t{{description}}\n",
{
"name": entry.name,
"description": entry.description,
}
)
)
.join("")
),
}
),
}),
"conf_path": lib_plankton.args.class_argument.volatile({
"indicators_long": ["conf_path"],
"indicators_short": ["c"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "conf.json",
// "info": null,
"name": "conf-path",
}),
"data_path": lib_plankton.args.class_argument.volatile({
"indicators_long": ["data-path"],
"indicators_short": ["d"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "data.json",
// "info": null,
"name": "data-path",
}),
"help": lib_plankton.args.class_argument.volatile({
"indicators_long": ["help"],
"indicators_short": ["h"],
"type": lib_plankton.args.enum_type.boolean,
"mode": lib_plankton.args.enum_mode.replace,
"default": false,
// "info": null,
"name": "help",
}),
});
const args : Record<string, any> = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" "));
// init2
await _zeitbild.conf.load(args["conf_path"]);
lib_plankton.log.conf_push(
_zeitbild.conf.get().log.map(
log_output => lib_plankton.log.channel_make(
{
"kind": log_output.kind,
"data": log_output.data
}
)
)
);
// exec
if (args["help"]) {
process.stdout.write(
arg_handler.generate_help(
{
"programname": "zeitbild",
"description": "zeitbild-backend",
"executable": "zeitbild",
}
)
+
"\n"
);
}
else {
switch (args["action"]) {
default: {
lib_plankton.log.error(
"main_invalid_action",
{
"action": args["action"],
}
);
break;
}
case "expose-conf": {
process.stdout.write(
JSON.stringify(
_zeitbild.conf.get(),
undefined,
"\t"
)
+
"\n"
);
break;
}
case "api-doc": {
lib_plankton.log.conf_push([]);
const rest_subject : lib_plankton.rest.type_rest = _zeitbild.api.make();
lib_plankton.log.conf_pop();
process.stdout.write(
JSON.stringify(
lib_plankton.rest.to_oas(rest_subject),
undefined,
"\t"
)
);
break;
}
case "serve": {
// prepare database
await _zeitbild.database.check();
await lib_plankton.session.setup(
{
"data_chest": (
_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.sql_table_common.chest(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "sessions",
"key_names": ["key"],
}
),
[
(core : any) => ({
"setup": (input : any) => core.setup(undefined),
"clear": () => core.clear(),
"write": (key : any, value : any) => core.write([key], {"data": JSON.stringify(value)}),
"delete": (key : any) => core.delete([key]),
"read": (key : any) => core.read([key]).then((row : any) => JSON.parse(row["data"])),
// "search": (term : any) => core.search(term).then(() => []),
"search": (term : any) => Promise.reject(new Error("not implemented")),
}),
]
)
),
"default_lifetime": _zeitbild.conf.get().session_management.lifetime,
}
);
const rest_subject : lib_plankton.rest.type_rest = _zeitbild.api.make();
const server : lib_plankton.server.type_subject = lib_plankton.server.make(
async (input, metadata) => {
const http_request : lib_plankton.http.type_request = lib_plankton.http.decode_request(input.toString());
const http_response : lib_plankton.http.type_response = await lib_plankton.rest.call(
rest_subject,
http_request,
{
"checklevel_restriction": lib_plankton.api.enum_checklevel.hard,
// "checklevel_input": lib_plankton.api.enum_checklevel.soft,
// "checklevel_output": lib_plankton.api.enum_checklevel.soft,
}
);
const output : string = lib_plankton.http.encode_response(http_response);
return output;
},
{
"host": _zeitbild.conf.get().server.host,
"port": _zeitbild.conf.get().server.port,
// DANGER! DANGER!
"threshold": 0.125,
}
);
lib_plankton.server.start(server);
break;
}
}
}
return Promise.resolve<void>(undefined);
}
(
main(process.argv.slice(2))
.then(
() => {}
)
.catch(
(error) => {process.stderr.write(String(error) + "\n");}
)
);

View file

@ -0,0 +1,254 @@
namespace _zeitbild.repository.calendar
{
/**
*/
var _core_store : (
null
|
lib_plankton.storage.type_core_store<
_zeitbild.type.calendar_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
var _event_store : (
null
|
lib_plankton.storage.type_core_store<
_zeitbild.type.event_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
function get_core_store(
) : lib_plankton.storage.type_core_store<
_zeitbild.type.calendar_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_core_store === null) {
_core_store = lib_plankton.storage.sql_table_autokey_core_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "calendars",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _core_store;
}
/**
*/
function get_event_store(
) : lib_plankton.storage.type_core_store<
_zeitbild.type.event_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_event_store === null) {
_event_store = lib_plankton.storage.sql_table_autokey_core_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "events",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _event_store;
}
/**
* @todo use events table
*/
function encode(
object : _zeitbild.type.calendar_object
) : Record<string, any>
{
switch (object.kind) {
/*
case "concrete": {
const data_raw : any = lib_plankton.json.encode(object.data);
data_raw["users"]
return {
"name": object.name,
"private": object.private,
"kind": object.kind,
"data": {
"users": data_raw["users"],
"events": [] // TODO
},
};
}
*/
default: {
return {
"name": object.name,
"private": object.private,
"kind": object.kind,
"data": lib_plankton.json.encode(object.data),
};
}
}
}
/**
*/
function decode(
row : Record<string, any>
) : _zeitbild.type.calendar_object
{
return {
"name": row["name"],
"private": row["private"],
"kind": row["kind"],
"data": lib_plankton.json.decode(row["data"]),
};
}
/**
*/
export async function dump(
) : Promise<
Array<
{
id : _zeitbild.type.calendar_id;
object : _zeitbild.type.calendar_object;
}
>
>
{
return (
(await get_core_store().search(null))
.map(
({"key": key, "preview": preview}) => ({
"id": key,
"object": (preview as _zeitbild.type.calendar_object),
})
)
);
}
/**
* @todo optimize
*/
export async function list(
search_term : (null | string)
) : Promise<
Array<
{
id : _zeitbild.type.calendar_id;
preview : {
name : string;
};
}
>
>
{
return (
(await get_core_store().search(null))
.filter(
({"key": key, "preview": preview}) => (
(
(search_term === null)
||
(search_term.length <= 1)
)
? true
: (
preview["name"].toLowerCase().includes(search_term.toLowerCase())
)
)
)
.map(
({"key": key, "preview": preview}) => ({
"id": key,
"preview": {
"name": preview["name"],
}
})
)
);
}
/**
*/
export async function read(
id : _zeitbild.type.calendar_id
) : Promise<_zeitbild.type.calendar_object>
{
const row : Record<string, any> = await get_core_store().read(id);
return decode(row);
}
/**
*/
export async function create(
value : _zeitbild.type.calendar_object
) : Promise<_zeitbild.type.calendar_id>
{
const row : Record<string, any> = encode(value);
const id : _zeitbild.type.calendar_id = await get_core_store().create(row);
return id;
}
/**
*/
export async function update(
id : _zeitbild.type.calendar_id,
value : _zeitbild.type.calendar_object
) : Promise<void>
{
const row : Record<string, any> = encode(value);
await get_core_store().update(id, row);
}
/**
*/
export async function delete_(
id : _zeitbild.type.calendar_id
) : Promise<void>
{
await get_core_store().delete(id);
}
}

206
source/services/calendar.ts Normal file
View file

@ -0,0 +1,206 @@
namespace _zeitbild.service.calendar
{
/**
*/
export async function list(
search_term : (null | string)
) : Promise<
Array<
{
id : _zeitbild.type.calendar_id;
preview : {
name : string;
}
}
>
>
{
return (
_zeitbild.repository.calendar.list(search_term)
.then(
x => x.map(
(y : any) => ({
"id": y.key,
"preview": y.preview,
})
)
)
);
}
/**
*/
export async function get(
calendar_id : _zeitbild.type.calendar_id
) : Promise<_zeitbild.type.calendar_object>
{
return _zeitbild.repository.calendar.read(calendar_id);
}
/**
* @todo prevent loops
*/
export async function gather_events(
calendar_ids : Array<_zeitbild.type.calendar_id>,
from_pit : _zeitbild.helpers.type_pit,
to_pit : _zeitbild.helpers.type_pit
) : Promise<
Array<
{
calendar_id : _zeitbild.type.calendar_id;
calendar_name : string;
event : _zeitbild.type.event_object;
}
>
>
{
lib_plankton.log.info(
"calendar_gather_events",
{
"calendar_ids": calendar_ids,
}
);
let result : Array<
{
calendar_id : _zeitbild.type.calendar_id;
calendar_name : string;
event : _zeitbild.type.event_object;
}
> = [];
for await (const calendar_id of calendar_ids) {
const calendar_object : _zeitbild.type.calendar_object = await _zeitbild.repository.calendar.read(
calendar_id
);
if (calendar_object.private) {
lib_plankton.log.info(
"calendar_gather_events_private_calendar_blocked",
{
"calendar_id": calendar_id,
}
);
}
else {
switch (calendar_object.kind) {
case "concrete": {
result = (
result
.concat(
calendar_object.data.events
.filter(
(event : _zeitbild.type.event_object) => _zeitbild.helpers.pit_is_between(
_zeitbild.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event : _zeitbild.type.event_object) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.name,
"event": event
})
)
)
);
break;
}
case "caldav": {
const url : lib_plankton.url.type_url = lib_plankton.url.decode(
calendar_object.data.source_url
);
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": ((url.scheme === "https") ? "https" : "http"),
"host": url.host,
"path": (url.path ?? "/"),
"query": url.query,
"method": lib_plankton.http.enum_method.get,
"headers": {},
"body": null,
};
// TODO: cache?
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(
http_request,
{
}
);
const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode(
http_response.body.toString(),
{
}
);
result = (
result
.concat(
vcalendar.vevents
.map(
(vevent : lib_plankton.ical.type_vevent) => (
(vevent.dtstart !== undefined)
?
{
"name": (
(vevent.summary !== undefined)
?
vevent.summary
:
"???"
),
"begin": _zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtstart),
"end": (
(vevent.dtend !== undefined)
?
_zeitbild.helpers.ical_dt_to_own_datetime(vevent.dtend)
:
null
),
"location": (
(vevent.location !== undefined)
?
vevent.location
:
null
),
"description": (
(vevent.description !== undefined)
?
vevent.description
:
null
),
}
:
null
)
)
.filter(
(event) => (event !== null)
)
.filter(
(event) => _zeitbild.helpers.pit_is_between(
_zeitbild.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.name,
"event": event,
})
)
)
);
break;
}
}
}
}
return Promise.resolve(result);
}
}

View file

@ -1,84 +0,0 @@
html {
background-color: #111;
color: #FFF;
font-family: sans-serif;
}
.calendar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.calendar-pane-left {
flex-basis: 12.5%;
}
.calendar-pane-right {
flex-basis: 87.5%;
}
.tableview-sources {
margin: 0;
padding: 0;
list-style-type: none;
font-size: 0.75em;
}
.tableview-sources-entry {
margin: 8px;
padding: 4px;
cursor: pointer;
}
.tableview-sources-entry:not(.tableview-sources-entry-active) {
filter: saturate(0);
}
.calendar table {
width: 100%;
border-collapse: collapse;
}
.calendar-cell {
border: 1px solid #888;
padding: 8px;
vertical-align: top;
}
.calendar-cell-day {
width: 13.5%;
}
.calendar-cell-week {
width: 5.5%;
}
.calendar-cell-regular {
width: 13.5%;
height: 120px;
}
.calendar-cell-today {
outline: 4px solid #FFF;
}
.calendar-day {
font-size: 0.75em;
cursor: help;
}
.calendar-events {
margin: 0; padding: 0;
list-style-type: none;
}
.calendar-event_entry {
margin: 4px;
padding: 4px;
border-radius: 2px;
font-size: 0.75em;
color: #FFF;
font-weight: bold;
cursor: pointer;
}

View file

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
</body>
</html>

View file

@ -1,3 +0,0 @@
<li class="calendar-event_entry" style="background-color: {{color}};" title="{{title}}">
{{name}}
</li>

View file

@ -1,8 +0,0 @@
<td class="calendar-cell calendar-cell-regular{{extra_classes}}">
<span class="calendar-day" title="{{title}}">
{{day}}
</span>
<ul class="calendar-events">
{{entries}}
</ul>
</td>

View file

@ -1,6 +0,0 @@
<tr>
<th class="calendar-cell calendar-cell-week">
{{week}}
</th>
{{cells}}
</tr>

View file

@ -1 +0,0 @@
<li class="tableview-sources-entry tableview-sources-entry-active" style="background-color: {{color}}" rel="{{rel}}">{{name}}</li>

View file

@ -1,27 +0,0 @@
<div class="calendar">
<div class="calendar-pane calendar-pane-left">
<ul class="tableview-sources">
{{sources}}
</ul>
</div>
<div class="calendar-pane calendar-pane-right">
<table>
<thead>
<tr>
<th class="calendar-cell"></th>
<th class="calendar-cell calendar-cell-day">Mo</th>
<th class="calendar-cell calendar-cell-day">Di</th>
<th class="calendar-cell calendar-cell-day">Mi</th>
<th class="calendar-cell calendar-cell-day">Do</th>
<th class="calendar-cell calendar-cell-day">Fr</th>
<th class="calendar-cell calendar-cell-day">Sa</th>
<th class="calendar-cell calendar-cell-day">So</th>
</tr>
</thead>
<tbody>
{{rows}}
</tbody>
</table>
</div>
</div>

93
source/types.ts Normal file
View file

@ -0,0 +1,93 @@
/**
*/
namespace _zeitbild.type
{
/**
*/
type role = (
"editor"
|
"viewer"
);
/**
*/
type user_id = int;
/**
*/
type user_object = {
name : string;
};
/**
*/
export type event_id = int;
/**
*/
export type event_object = {
name : string;
begin : _zeitbild.helpers.type_datetime;
end : (
null
|
_zeitbild.helpers.type_datetime
);
location : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
export type calendar_id = int;
/**
*/
export type calendar_object = (
{
name : string;
private : boolean;
}
&
(
{
kind : "concrete";
data : {
users : Array<
{
id : user_id;
role : role;
}
>;
events : Array<event_object>;
};
}
|
{
kind : "caldav";
data : {
read_only : boolean;
source_url : string;
}
}
)
);
}

View file

@ -12,7 +12,7 @@ def main():
"-o",
"--output-directory",
type = str,
default = "/tmp/kalender",
default = "/tmp/zeitbild",
metavar = "<output-directory>",
help = "output directory",
)

View file

@ -2,7 +2,7 @@
dir_lib := lib
dir_source := source
dir_temp := /tmp/kalender-temp
dir_temp := /tmp/zeitbild-temp
dir_build := build
dir_tools := tools
@ -11,53 +11,42 @@ cmd_chmod := chmod
cmd_cp := cp
cmd_log := echo "--"
cmd_mkdir := mkdir -p
cmd_echo := echo
cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc
## rules
.PHONY: default
default: index templates style logic
default: ${dir_build}/zeitbild
.PHONY: index
index: ${dir_build}/index.html
${dir_build}/index.html: \
${dir_source}/index.html
@ ${cmd_log} "index …"
@ ${cmd_cp} -u -v $^ $@
.PHONY: templates
templates: \
$(wildcard ${dir_source}/templates/*)
@ ${cmd_log} "templates …"
@ ${cmd_mkdir} ${dir_build}/templates
@ ${cmd_cp} -r -u -v ${dir_source}/templates/* ${dir_build}/templates/
.PHONY: style
style: \
$(wildcard ${dir_source}/style/*)
@ ${cmd_log} "style …"
@ ${cmd_mkdir} ${dir_build}
@ ${cmd_cat} ${dir_source}/style/* > ${dir_build}/style.css
.PHONY: logic
logic: ${dir_build}/logic.js
${dir_temp}/logic-unlinked.js: \
${dir_temp}/zeitbild-unlinked.js: \
${dir_lib}/plankton/plankton.d.ts \
${dir_source}/logic/helpers.ts \
${dir_source}/logic/types.ts \
${dir_source}/logic/backend.ts \
${dir_source}/logic/view.ts \
${dir_source}/logic/main.ts
@ ${cmd_log} "logic | compile …"
${dir_source}/helpers.ts \
${dir_source}/conf.ts \
${dir_source}/database.ts \
${dir_source}/types.ts \
${dir_source}/repositories/calendar.ts \
${dir_source}/services/calendar.ts \
${dir_source}/api/base.ts \
${dir_source}/api/actions/meta_ping.ts \
${dir_source}/api/actions/meta_spec.ts \
${dir_source}/api/actions/calendar_list.ts \
${dir_source}/api/functions.ts \
${dir_source}/main.ts
@ ${cmd_log} "compile …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_tsc} --lib dom,es2020 --strict $^ --outFile $@
@ ${cmd_tsc} --lib es2020 --strict $^ --outFile $@
${dir_build}/logic.js: \
${dir_temp}/head.js:
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_echo} "#!/usr/bin/env node" > $@
${dir_build}/zeitbild: \
${dir_temp}/head.js \
${dir_lib}/plankton/plankton.js \
${dir_temp}/logic-unlinked.js
@ ${cmd_log} "logic | link …"
${dir_temp}/zeitbild-unlinked.js
@ ${cmd_log} "link …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_cat} $^ > $@
@ ${cmd_chmod} +x $@

View file

@ -7,22 +7,26 @@ dir=lib/plankton
modules=""
modules="${modules} base"
modules="${modules} call"
modules="${modules} log"
modules="${modules} storage"
modules="${modules} database"
modules="${modules} session"
modules="${modules} file"
modules="${modules} string"
modules="${modules} structures"
modules="${modules} json"
modules="${modules} args"
modules="${modules} string"
modules="${modules} color"
modules="${modules} xml"
modules="${modules} ical"
modules="${modules} http"
modules="${modules} log"
modules="${modules} url"
modules="${modules} http"
modules="${modules} api"
modules="${modules} rest"
modules="${modules} server"
modules="${modules} args"
## exec
mkdir -p ${dir}
cd ${dir}
ptk bundle web ${modules}
ptk bundle node ${modules}
cd - > /dev/null