backend/source/services/calendar.ts
2025-10-23 11:34:00 +02:00

612 lines
15 KiB
TypeScript

/*
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
{
/**
*/
async function get_access_level(
calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id)
)
: Promise<_zeitbild.enum_access_level>
{
return _zeitbild.access_level_determine(
calendar_object,
(
(user_id === null)
?
null
:
{
"id": user_id,
"object": (await _zeitbild.service.user.get(user_id)),
}
)
);
}
/**
* checks if a user has a sufficient access level
*/
async function wrap_check_access_level<type_result>(
calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id),
threshold : _zeitbild.enum_access_level,
success_handler : (
(access_level : _zeitbild.enum_access_level)
=>
Promise<type_result>
)
)
: Promise<type_result>
{
const access_level : _zeitbild.enum_access_level = await get_access_level(
calendar_object,
user_id
);
if (! _zeitbild.access_level_order(threshold, access_level))
{
return Promise.reject<type_result>(
new Error(
lib_plankton.string.coin(
"insufficient access level; at least required: {{threshold}}, actual: {{actual}}",
{
"threshold": _zeitbild.access_level_to_string(threshold),
"actual": _zeitbild.access_level_to_string(access_level),
}
)
)
);
}
else
{
return success_handler(access_level);
}
}
/**
*/
export function overview(
user_id : (null | _zeitbild.type_user_id)
) : Promise<
Array<
{
id : _zeitbild.type_calendar_id;
name : string;
access_level : _zeitbild.enum_access_level;
hue : float;
}
>
>
{
return _zeitbild.repository.calendar.overview(user_id);
}
/**
*/
export async function get(
calendar_id : _zeitbild.type_calendar_id,
user_id : _zeitbild.type_user_id
) : Promise<_zeitbild.type_calendar_object>
{
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
return wrap_check_access_level<_zeitbild.type_calendar_object>(
calendar_object,
user_id,
_zeitbild.enum_access_level.view,
() => Promise.resolve(calendar_object)
);
}
/**
*/
export function add(
calendar_object : _zeitbild.type_calendar_object
) : Promise<_zeitbild.type_calendar_id>
{
return _zeitbild.repository.calendar.create(calendar_object);
}
/**
*/
export async function change(
calendar_id : _zeitbild.type_calendar_id,
calendar_object : _zeitbild.type_calendar_object,
user_id : _zeitbild.type_user_id
) : Promise<void>
{
const calendar_object_current : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
return wrap_check_access_level<void>(
calendar_object_current,
user_id,
_zeitbild.enum_access_level.admin,
() => _zeitbild.repository.calendar.update(calendar_id, calendar_object)
);
}
/**
*/
export async function remove(
calendar_id : _zeitbild.type_calendar_id,
user_id : _zeitbild.type_user_id
) : Promise<void>
{
const calendar_object_current : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
return wrap_check_access_level<void>(
calendar_object_current,
user_id,
_zeitbild.enum_access_level.admin,
() => _zeitbild.repository.calendar.delete_(calendar_id)
);
}
/**
*/
export async function event_get(
calendar_id : _zeitbild.type_calendar_id,
local_resource_event_id : _zeitbild.type_local_resource_event_id,
user_id : _zeitbild.type_user_id
) : Promise<_zeitbild.type_event_object>
{
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id
);
return wrap_check_access_level<_zeitbild.type_event_object>(
calendar_object,
user_id,
_zeitbild.enum_access_level.view,
async () => {
const event_object : _zeitbild.type_event_object = await _zeitbild.service.resource.event_get(
calendar_object.resource_id,
local_resource_event_id
);
return Promise.resolve<_zeitbild.type_event_object>(event_object);
}
);
}
/**
*/
export async function event_add(
calendar_id : _zeitbild.type_calendar_id,
event_object : _zeitbild.type_event_object,
user_id : _zeitbild.type_user_id
) : 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(
calendar_id
);
return wrap_check_access_level(
calendar_object,
user_id,
_zeitbild.enum_access_level.edit,
async () => {
const local_resource_event_id : _zeitbild.type_local_resource_event_id = await _zeitbild.service.resource.event_add(
calendar_object.resource_id,
event_object
);
return Promise.resolve(
{
"local_resource_event_id": local_resource_event_id,
"hash": get_event_hash_local(calendar_id, local_resource_event_id),
}
);
}
);
}
/**
*/
export async function event_change(
calendar_id : _zeitbild.type_calendar_id,
local_resource_event_id : _zeitbild.type_local_resource_event_id,
event_object : _zeitbild.type_event_object,
user_id : _zeitbild.type_user_id
) : Promise<void>
{
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id
);
return wrap_check_access_level<void>(
calendar_object,
user_id,
_zeitbild.enum_access_level.edit,
async () => {
await _zeitbild.service.resource.event_change(
calendar_object.resource_id,
local_resource_event_id,
event_object
);
return Promise.resolve<void>(undefined);
}
);
}
/**
*/
export async function event_remove(
calendar_id : _zeitbild.type_calendar_id,
local_resource_event_id : _zeitbild.type_local_resource_event_id,
user_id : _zeitbild.type_user_id
) : Promise<void>
{
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id
);
return wrap_check_access_level<void>(
calendar_object,
user_id,
_zeitbild.enum_access_level.edit,
async () => {
await _zeitbild.service.resource.event_remove(
calendar_object.resource_id,
local_resource_event_id
);
return Promise.resolve<void>(undefined);
}
);
}
/**
*/
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
*/
async function get_events(
calendar_id : _zeitbild.type_calendar_id,
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
user_id : (null | _zeitbild.type_user_id)
) : Promise<
Array<
{
id : (null | _zeitbild.type_local_resource_event_id);
hash : _zeitbild.type_event_hash;
object : _zeitbild.type_event_object;
}
>
>
{
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); hash : _zeitbild.type_event_hash; object : _zeitbild.type_event_object;}>>(
calendar_object,
user_id,
_zeitbild.enum_access_level.view,
async () => {
const resource_object : _zeitbild.type_resource_object = await _zeitbild.repository.resource.read(calendar_object.resource_id);
switch (resource_object.kind) {
case "local": {
return (
Promise.all(
resource_object.data.event_ids
.map(
(event_id) => (
_zeitbild.repository.resource.local_resource_event_read(
calendar_object.resource_id,
event_id
)
.then(
(event_object) => Promise.resolve(
{
"id": event_id,
"hash": get_event_hash_local(
calendar_id,
event_id,
),
"object": event_object,
}
)
)
)
)
)
.then(
(event_entries) => Promise.resolve(
event_entries
.filter(
(event_entry : {id : (null | _zeitbild.type_local_resource_event_id); object : _zeitbild.type_event_object;}) => lib_plankton.pit.is_between(
lib_plankton.pit.from_datetime(event_entry.object.begin),
from_pit,
to_pit
)
)
)
)
);
break;
}
case "ics_feed": {
// TODO readonly
const vcalendar : lib_plankton.ical.type_vcalendar = await lib_plankton.cache.get<lib_plankton.ical.type_vcalendar>(
_zeitbild.cache_external_resources,
resource_object.data.url,
_zeitbild.conf.get().external_resources.lifetime,
async () => {
const url : lib_plankton.url.type_url = lib_plankton.url.decode(
resource_object.data.url
);
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": ((url.scheme === "https") ? "https" : "http"),
"host": url.host,
"path": (url.path ?? "/"),
"query": url.query,
"method": lib_plankton.http.enum_method.get,
"headers": {},
"body": null,
};
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);
}
);
return Promise.resolve(
vcalendar.vevents
.map(
(vevent : lib_plankton.ical.type_vevent) => (
(vevent.dtstart !== undefined)
?
{
"name": (
(vevent.summary !== undefined)
?
vevent.summary
:
"???"
),
"begin": _zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtstart),
"end": (
(vevent.dtend !== undefined)
?
_zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtend)
:
null
),
"location": (
(vevent.location !== undefined)
?
vevent.location
:
null
),
"link": (
(vevent.url !== undefined)
?
vevent.url
:
null
),
"description": (
(vevent.description !== undefined)
?
vevent.description
:
null
),
}
:
null
)
)
.filter(
(event) => (event !== null)
)
.map(
(event) => ({
"id": null,
"hash": get_event_hash_ics_feed(calendar_id, event),
"object": event,
})
)
.filter(
(event_entry) => lib_plankton.pit.is_between(
lib_plankton.pit.from_datetime(event_entry.object.begin),
from_pit,
to_pit
)
)
);
break;
}
default: {
return Promise.reject(
new Error("invalid resource kind: " + resource_object["kind"])
);
break;
}
}
}
);
}
/**
*/
export async function gather_events(
calendar_ids_wanted : (null | Array<_zeitbild.type_calendar_id>),
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
user_id : (null | _zeitbild.type_user_id)
) : Promise<Array<_zeitbild.type_event_extended>>
{
const calendar_ids_allowed : Array<_zeitbild.type_calendar_id> = (
(await overview(user_id))
.map((x : any) => x.id)
);
// use set intersection
const calendar_ids : Array<_zeitbild.type_calendar_id> = (
(
(calendar_ids_wanted === null)
?
calendar_ids_allowed
:
(
calendar_ids_wanted
.filter(
(calendar_id) => calendar_ids_allowed.includes(calendar_id)
)
)
)
);
calendar_ids.sort();
return lib_plankton.cache.get_complex<any, Array<_zeitbild.type_event_extended>>(
_zeitbild.cache_regular,
"gather_events",
{
"user_id": user_id,
"from_pit": from_pit,
"to_pit": to_pit,
"calendar_ids": calendar_ids,
},
/**
* @todo expire?
*/
null,
() => (
Promise.all(
calendar_ids
.map(
async (calendar_id) => {
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id
);
const access_level : _zeitbild.enum_access_level = await get_access_level(
calendar_object,
user_id
);
const events : Array<
{
id : (null | _zeitbild.type_local_resource_event_id);
hash : _zeitbild.type_event_hash;
object : _zeitbild.type_event_object;
}
> = await get_events(
calendar_id,
from_pit,
to_pit,
user_id
);
return Promise.resolve(
events
.map(
(event_entry) => ({
"hash": event_entry.hash,
"calendar_id": calendar_id,
"calendar_name": calendar_object.name,
"hue": calendar_object.hue,
"access_level": access_level,
"event_id": event_entry.id,
"event_object": event_entry.object,
})
)
);
}
)
)
.then(
(sub_results) => sub_results.reduce(
(x, y) => x.concat(y),
[]
)
)
)
);
}
}