From 1ade4dec2a44a74229f8cb1c2ecadd603a3ae87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Sat, 28 Jun 2025 04:27:46 +0000 Subject: [PATCH] [task-340] [int] --- source/conf.ts | 345 +++++++++++++++++++++++++++------ source/logic.ts | 194 ++++++++---------- source/main.ts | 3 +- source/targets/email.ts | 5 +- source/targets/telegram_bot.ts | 2 +- source/test.ts | 1 - source/types.ts | 9 +- 7 files changed, 380 insertions(+), 179 deletions(-) diff --git a/source/conf.ts b/source/conf.ts index cd440d7..f492559 100644 --- a/source/conf.ts +++ b/source/conf.ts @@ -24,6 +24,24 @@ along with »munin«. If not, see . namespace _munin.conf { + /** + */ + type type_reminder_raw = { + frequency : ( + "hourly" + | + "daily" + | + "weekly" + | + "monthly" + ); + offset : int; + from : int; + to : int; + }; + + /** */ type type_conf_v1 = { @@ -236,7 +254,71 @@ namespace _munin.conf /** */ - export type type_conf = type_conf_v4; + type type_conf_v5 = { + sources : Array< + ( + { + kind : "ical_feed"; + data : { + url : string; + filtration : { + category_whitelist : (null | Array); + category_blacklist : (null | Array); + title_whitelist : (null | Array); + title_blacklist : (null | Array); + } + } + } + ) + >; + targets : Array< + ( + { + kind : "email"; + data : { + smtp_host : string; + smtp_port : int; + smtp_username : string; + smtp_password : string; + sender : string; + receivers : Array; + hide_tags : boolean; + /** + * in hours + */ + reminders : Array; + } + } + | + { + kind : "telegram_bot"; + data : { + bot_token : string; + chat_id : int; + hide_tags : boolean; + /** + * in hours + */ + reminders : Array; + } + } + ) + >; + settings : { + interval : float; + }; + labels : { + head : string; + title : string; + time : string; + location : string; + }; + }; + + + /** + */ + export type type_conf = type_conf_v5; /** @@ -396,6 +478,65 @@ namespace _munin.conf } + /** + */ + function convert_from_v4( + conf_v4 : type_conf_v4 + ) : type_conf_v5 + { + const map_reminder = hours => ({ + "frequency": "hourly", + "offset": 0, + "from": hours, + "to": (hours + 1), + } as type_reminder_raw); + return { + "sources": conf_v4.sources, + "targets": conf_v4.targets.map( + target => { + switch (target.kind) { + case "email": { + return { + "kind": "email", + "data": { + "smtp_host": target.data.smtp_host, + "smtp_port": target.data.smtp_port, + "smtp_username": target.data.smtp_username, + "smtp_password": target.data.smtp_password, + "sender": target.data.sender, + "receivers": target.data.receivers, + "hide_tags": target.data.hide_tags, + "reminders": target.data.reminders.map(map_reminder), + }, + }; + break; + } + case "telegram_bot": { + return { + "kind": "telegram_bot", + "data": { + "bot_token": target.data.bot_token, + "chat_id": target.data.chat_id, + "hide_tags": target.data.hide_tags, + "reminders": target.data.reminders.map(map_reminder), + }, + }; + break; + } + default: { + // return target; + throw (new Error("unhandled target kind: " + String(target))); + break; + } + } + } + ), + "settings": conf_v4.settings, + "labels": conf_v4.labels, + }; + } + + /** */ function schema_source_kalender_digital( @@ -546,6 +687,136 @@ namespace _munin.conf } + /** + */ + function schema_sources( + version : string + ) : lib_plankton.conf.type_schema + { + switch (version) { + case "1": { + return { + "nullable": false, + "type": "array", + "items": { + "nullable": false, + "anyOf": [ + schema_source_kalender_digital(version), + ], + } + }; + break; + } + default: + case "2": { + return { + "nullable": false, + "type": "array", + "items": { + "nullable": false, + "anyOf": [ + schema_source_ical_feed(version), + ], + } + }; + break; + } + } + } + + + /** + */ + function schema_reminder( + version : string + ) : lib_plankton.conf.type_schema + { + switch (version) + { + case "1": + case "2": + case "3": + case "4": + { + return { + "type": "integer", + }; + } + case "5": + default: + { + return { + "nullable": false, + "type": "object", + "properties": { + "frequency": { + "nullable": false, + "type": "string", + "enum": [ + "hourly", + "daily", + "weekly", + "monthly", + ] + }, + "offset": { + "nullable": false, + "type": "integer", + "default": 0 + }, + "from": { + "nullable": false, + "type": "integer" + }, + "to": { + "nullable": false, + "type": "integer" + }, + }, + "additionalProperties": false, + "required": [ + "frequency", + "from", + "to", + ] + }; + break; + } + } + } + + + /** + */ + function default_reminder( + version : string + ) : any + { + switch (version) + { + case "1": + case "2": + case "3": + case "4": + { + return [24]; + } + case "5": + default: + { + return [ + { + "frequency": "hourly", + "from": 24, + "to": 25 + } + ]; + break; + } + } + } + + /** */ function schema_target_email( @@ -602,11 +873,8 @@ namespace _munin.conf "reminders": { "nullable": false, "type": "array", - "items": { - "nullable": false, - "type": "integer" - }, - "default": [24.0], + "items": schema_reminder(version), + "default": default_reminder(version), }, }, "additionalProperties": false, @@ -628,44 +896,6 @@ namespace _munin.conf } - /** - */ - function schema_sources( - version : string - ) : lib_plankton.conf.type_schema - { - switch (version) { - case "1": { - return { - "nullable": false, - "type": "array", - "items": { - "nullable": false, - "anyOf": [ - schema_source_kalender_digital(version), - ], - } - }; - break; - } - default: - case "2": { - return { - "nullable": false, - "type": "array", - "items": { - "nullable": false, - "anyOf": [ - schema_source_ical_feed(version), - ], - } - }; - break; - } - } - } - - /** */ function schema_target_telegram_bot( @@ -701,11 +931,8 @@ namespace _munin.conf "reminders": { "nullable": false, "type": "array", - "items": { - "nullable": false, - "type": "integer" - }, - "default": [24.0], + "items": schema_reminder(version), + "default": default_reminder(version), }, }, "additionalProperties": false, @@ -737,13 +964,18 @@ namespace _munin.conf "nullable": false, "anyOf": (() => { switch (version) { - default: { + case "1": + case "2": + { return [ schema_target_telegram_bot(version), ]; break; } - case "3": { + case "4": + case "5": + default: + { return [ schema_target_email(version), schema_target_telegram_bot(version), @@ -824,7 +1056,7 @@ namespace _munin.conf /** */ export function schema( - version : string = "4" + version : string = "5" ) : lib_plankton.conf.type_schema { switch (version) { @@ -852,7 +1084,9 @@ namespace _munin.conf } case "2": case "3": - case "4": { + case "4": + case "5": + { return { "nullable": false, "type": "object", @@ -887,7 +1121,8 @@ namespace _munin.conf "1": {"target": "2", "function": convert_from_v1}, "2": {"target": "3", "function": convert_from_v2}, "3": {"target": "4", "function": convert_from_v3}, - "4": null, + "4": {"target": "5", "function": convert_from_v4}, + "5": null, } ); @@ -925,7 +1160,7 @@ namespace _munin.conf } } */ - return (conf_raw.content as type_conf_v4); + return (conf_raw.content as type_conf_v5); } } diff --git a/source/logic.ts b/source/logic.ts index 693811f..a9efba2 100644 --- a/source/logic.ts +++ b/source/logic.ts @@ -21,45 +21,6 @@ along with »munin«. If not, see . namespace _munin.logic { - /** - * @todo Tests schreiben - */ - function shall_remind( - event : _munin.type_event, - reminder : type_reminder, - { - "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, - } : { - pit ?: lib_plankton.pit.type_pit; - interval ?: int; - } = { - } - ) : boolean - { - const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - pit, - 0 - ); - const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - pit, - interval - ); - const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( - event.begin - ); - const reminder_time : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - event_begin, - (-reminder) - ); - return lib_plankton.pit.is_between( - reminder_time, - window_from, - window_to - ); - } - - /** */ function frequency_anchor( @@ -90,7 +51,7 @@ namespace _munin.logic /** */ export function reminder_due( - reminder : type_reminder_new, + reminder : type_reminder, { "pit": pit = lib_plankton.pit.now(), "interval": interval = 1, @@ -105,35 +66,46 @@ namespace _munin.logic reminder.frequency, {"pit": pit} ); - // 0 ≤ ((p - a(p)) - o) < i - const x : float = ( - ( - ( - lib_plankton.pit.to_unix_timestamp(pit) - - - lib_plankton.pit.to_unix_timestamp(anchor) - ) - / - (60 * 60) - ) - - - reminder.offset + const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + (reminder.offset + 0) ); - return ((0 <= x) && (x < interval)); + const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + (reminder.offset + interval) + ); + const result : boolean = lib_plankton.pit.is_between( + pit, + window_from, + window_to + ); + lib_plankton.log._info( + "munin.logic.reminder_due", + { + "details": { + "reminder": reminder, + "datetime": lib_plankton.pit.to_datetime(pit), + "interval": interval, + "anchor": lib_plankton.pit.to_datetime(anchor), + "window_from": lib_plankton.pit.to_datetime(window_from), + "window_to": lib_plankton.pit.to_datetime(window_to), + "result": result, + } + } + ); + return result; } /** */ export function reminder_event_in_window( - reminder : type_reminder_new, + reminder : type_reminder, event : _munin.type_event, { "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, } : { pit ?: lib_plankton.pit.type_pit; - interval ?: int; } = { } ) : boolean @@ -153,41 +125,26 @@ namespace _munin.logic const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( event.begin ); - return lib_plankton.pit.is_between( + const result : boolean = lib_plankton.pit.is_between( event_begin, window_from, window_to ); - } - - - /** - */ - function shall_remind_new( - reminder : type_reminder_new, - event : _munin.type_event, - { - "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, - } : { - pit ?: lib_plankton.pit.type_pit; - interval ?: int; - } = { - } - ) : boolean - { - return ( - reminder_due( - reminder, - {"pit": pit, "interval": interval} - ) - && - reminder_event_in_window( - reminder, - event, - {"pit": pit, "interval": interval} - ) + lib_plankton.log._info( + "munin.logic.reminder_event_in_window", + { + "details": { + "reminder": reminder, + "event": event, + "datetime": lib_plankton.pit.to_datetime(pit), + "anchor": lib_plankton.pit.to_datetime(anchor), + "window_from": lib_plankton.pit.to_datetime(window_from), + "window_to": lib_plankton.pit.to_datetime(window_to), + "result": result, + } + } ); + return result; } @@ -211,17 +168,30 @@ namespace _munin.logic .reduce((x, y) => x.concat(y), []) ); for (const target of targets) { - for (const reminder_hours of target.reminders) { - for (const event of events) { - const remind : boolean = shall_remind( - event, - reminder_hours, - { - "pit": now, - "interval": conf.settings.interval, - } + for (const reminder of target.reminders) { + const due : boolean = reminder_due( + reminder, + { + "pit": now, + "interval": conf.settings.interval, + } + ); + if (! due) + { + // do nothing + } + else + { + const events_matching : Array<_munin.type_event> = events.filter( + event => reminder_event_in_window( + reminder, + event, + { + "pit": now, + } + ) ); - if (! remind) { + if (events_matching.length <= 0) { // do nothing } else { @@ -229,7 +199,7 @@ namespace _munin.logic "munin.remind.do", { "details": { - "event": event, + "events": events, "target": target.show(), } } @@ -238,18 +208,24 @@ namespace _munin.logic // do nothing } else { - try { - await target.send(conf.labels, event); - } - catch (error) { - lib_plankton.log.error( - "munin.remind.error", - { - "details": { - "message": String(error), + /** + * @todo bundle? + */ + for (const event of events_matching) + { + try { + await target.send(conf.labels, event); + } + catch (error) { + lib_plankton.log.error( + "munin.remind.error", + { + "details": { + "message": String(error), + } } - } - ); + ); + } } } } diff --git a/source/main.ts b/source/main.ts index 9fb52fa..af737a4 100644 --- a/source/main.ts +++ b/source/main.ts @@ -158,8 +158,9 @@ namespace _munin "data": { "target": "stdout", "format": { - "kind": "human_readable", + "kind": "jsonl", "data": { + "structured": true } } } diff --git a/source/targets/email.ts b/source/targets/email.ts index b9d33fa..358ebd6 100644 --- a/source/targets/email.ts +++ b/source/targets/email.ts @@ -31,10 +31,7 @@ namespace _munin.targets.email sender : string; receivers : Array; hide_tags : boolean; - /** - * in hours - */ - reminders : Array; + reminders : Array<_munin.type_reminder>; }; diff --git a/source/targets/telegram_bot.ts b/source/targets/telegram_bot.ts index df24da4..44f0dd7 100644 --- a/source/targets/telegram_bot.ts +++ b/source/targets/telegram_bot.ts @@ -27,7 +27,7 @@ namespace _munin.targets.telegram_bot bot_token : string; chat_id : int; hide_tags : boolean; - reminders : Array; + reminders : Array<_munin.type_reminder>; }; diff --git a/source/test.ts b/source/test.ts index 91b0842..91bed40 100644 --- a/source/test.ts +++ b/source/test.ts @@ -359,7 +359,6 @@ namespace _munin.test }, { "pit": datetime_to_pit(testcase.input.datetime), - "interval": testcase.input.interval, } ); diff --git a/source/types.ts b/source/types.ts index e29db21..9c56ef0 100644 --- a/source/types.ts +++ b/source/types.ts @@ -67,15 +67,8 @@ namespace _munin /** * @todo rename - * @todo extend */ - export type type_reminder = int; - - - /** - * @todo rename - */ - export type type_reminder_new = { + export type type_reminder = { frequency : enum_frequency; offset : int; from : int;