[task-340] soweit fertig

This commit is contained in:
fenris 2025-06-30 13:02:06 +02:00
parent 37f7a27e95
commit 21c2d9945c
9 changed files with 546 additions and 483 deletions

View file

@ -0,0 +1,218 @@
[
{
"name": "reminder_check.dueness-1",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 15,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": null
},
{
"name": "reminder_check.dueness-2",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 16,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": []
},
{
"name": "reminder_check.dueness-3",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 17,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": null
},
{
"name": "reminder_check.events-1",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
{
"title": "e1",
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
}
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 16,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": []
},
{
"name": "reminder_check.events-2",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
{
"title": "e1",
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 28
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
}
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 16,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": ["e1"]
},
{
"name": "reminder_check.events-3",
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"events": [
{
"title": "e1",
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 29
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
}
],
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 16,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": []
}
]

View file

@ -1,122 +0,0 @@
[
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 0,
"from": 24,
"to": 48
},
"event": {
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 25
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 23
},
"time": {
"hour": 10,
"minute": 0,
"second": 0
}
},
"interval": 1
},
"output": false
},
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 0,
"from": 24,
"to": 48
},
"event": {
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 25
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 24
},
"time": {
"hour": 10,
"minute": 0,
"second": 0
}
},
"interval": 1
},
"output": true
},
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 0,
"from": 24,
"to": 48
},
"event": {
"begin": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 25
},
"time": {
"hour": 12,
"minute": 0,
"second": 0
}
}
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 25
},
"time": {
"hour": 10,
"minute": 0,
"second": 0
}
},
"interval": 1
},
"output": false
}
]

View file

@ -1,77 +0,0 @@
[
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 15,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": false
},
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 16,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": true
},
{
"input": {
"reminder": {
"frequency": "daily",
"offset": 16,
"from": 24,
"to": 48
},
"datetime": {
"timezone_shift": 0,
"date": {
"year": 2025,
"month": 6,
"day": 27
},
"time": {
"hour": 17,
"minute": 30,
"second": 0
}
},
"interval": 1
},
"output": false
}
]

View file

@ -312,6 +312,7 @@ namespace _munin.conf
title : string;
time : string;
location : string;
events : string;
};
};
@ -532,7 +533,7 @@ namespace _munin.conf
}
),
"settings": conf_v4.settings,
"labels": conf_v4.labels,
"labels": Object.assign({"events": "Termine"}, conf_v4.labels),
};
}
@ -1044,6 +1045,11 @@ namespace _munin.conf
"type": "string",
"default": "wo"
},
"events": {
"nullable": false,
"type": "string",
"default": "Termine"
},
},
"additionalProperties": false,
"required": [

View file

@ -67,101 +67,64 @@ namespace _munin.logic
/**
*/
export function reminder_due(
export function reminder_check(
reminder : type_reminder,
events_all : Array<_munin.type_event>,
{
"pit": pit = lib_plankton.pit.now(),
"interval": interval = 1,
} : {
}
: {
pit ?: lib_plankton.pit.type_pit;
interval ?: int;
} = {
}
) : boolean
= {
}
) : (null | Array<_munin.type_event>)
{
const anchor : lib_plankton.pit.type_pit = frequency_anchor(
reminder.frequency,
{"pit": pit}
);
const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
const dueness_window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
(reminder.offset + 0)
);
const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
const dueness_window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
(reminder.offset + interval)
);
const result : boolean = lib_plankton.pit.is_between(
const due : boolean = lib_plankton.pit.is_between(
pit,
window_from,
window_to
dueness_window_from,
dueness_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_covers_event(
reminder : type_reminder,
event : _munin.type_event,
if (! due)
{
"pit": pit = lib_plankton.pit.now(),
} : {
pit ?: lib_plankton.pit.type_pit;
} = {
return null;
}
else
{
const events_window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
reminder.from
);
const events_window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
reminder.to
);
const events : Array<_munin.type_event> = (
events_all
.filter(
(event) => lib_plankton.pit.is_between(
lib_plankton.pit.from_datetime(event.begin),
events_window_from,
events_window_to
)
)
);
return events;
}
) : boolean
{
const anchor : lib_plankton.pit.type_pit = frequency_anchor(
reminder.frequency,
{"pit": pit}
);
const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
reminder.from
);
const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour(
anchor,
reminder.to
);
const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime(
event.begin
);
const result : boolean = lib_plankton.pit.is_between(
event_begin,
window_from,
window_to
);
lib_plankton.log._info(
"munin.logic.reminder_covers_event",
{
"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;
}
@ -173,76 +136,85 @@ namespace _munin.logic
targets : Array<_munin.type_target>,
{
"dry_run": dry_run = false,
} : {
}
: {
dry_run ?: boolean;
} = {
}
= {
}
) : Promise<void>
{
const now : lib_plankton.pit.type_pit = lib_plankton.pit.now();
const events : Array<_munin.type_event> = (
const events_all : Array<_munin.type_event> = (
(await Promise.all(sources.map(source => source.fetch())))
.reduce((x, y) => x.concat(y), [])
);
for (const target of targets) {
for (const reminder of target.reminders) {
const due : boolean = reminder_due(
for (const target of targets)
{
for (const reminder of target.reminders)
{
const events : Array<_munin.type_event> = reminder_check(
reminder,
events_all,
{
"pit": now,
"interval": conf.settings.interval,
}
);
if (! due)
if (events === null)
{
// do nothing
}
else
{
const events_matching : Array<_munin.type_event> = events.filter(
event => reminder_covers_event(
reminder,
event,
{
"pit": now,
lib_plankton.log._info(
"munin.run_iteration.reminder_not_due",
{
"details": {
"reminder": reminder,
}
)
}
);
}
else {
if (events.length <= 0)
{
lib_plankton.log._info(
"munin.run_iteration.no_matching_events",
{
"details": {
"reminder": reminder,
}
}
);
if (events_matching.length <= 0) {
// do nothing
}
else {
lib_plankton.log._info(
"munin.remind.do",
"munin.run_iteration.remind",
{
"details": {
"reminder": reminder,
"events": events,
"target": target.show(),
}
}
);
if (dry_run) {
if (dry_run)
{
// do nothing
}
else {
/**
* @todo bundle?
*/
for (const event of events_matching)
else
{
try
{
try {
await target.send(conf.labels, event);
}
catch (error) {
lib_plankton.log.error(
"munin.remind.error",
{
"details": {
"message": String(error),
}
await target.send(conf.labels, events);
}
catch (error)
{
lib_plankton.log.error(
"munin.remind.error",
{
"details": {
"message": String(error),
}
);
}
}
);
}
}
}

View file

@ -37,10 +37,97 @@ namespace _munin.targets.email
/**
*/
async function send(
function summarize_event(
parameters : type_parameters,
labels : _munin.type_labels,
event : _munin.type_event
) : string
{
return lib_plankton.string.coin(
"[{{head}}] {{date}} : {{macro_tags}}{{title}}",
{
"head": labels.head,
"date": lib_plankton.pit.date_format(
event.begin.date
),
"macro_tags": (
(event.tags === null)
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title": event.title,
}
);
}
/**
*/
function render_event(
parameters : type_parameters,
labels : _munin.type_labels,
event : _munin.type_event
) : string
{
return lib_plankton.string.coin(
"{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}",
{
"title_label": labels.title.toUpperCase(),
"macro_tags": (
(parameters.hide_tags || (event.tags === null))
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title_value": event.title,
"time_label": labels.time.toUpperCase(),
"time_value": lib_plankton.pit.timespan_format(
event.begin,
event.end,
{
"adjust_to_ce": true,
}
),
"macro_location": (
(event.location === null)
?
""
:
lib_plankton.string.coin(
"\n{{location_label}} | {{location_value}}",
{
"location_label": labels.location.toUpperCase(),
"location_value": event.location,
}
)
),
"macro_description": (
(event.description === null)
?
""
:
lib_plankton.string.coin(
"\n\n{{description_value}}",
{
"description_value": event.description,
}
)
),
}
);
}
/**
*/
async function send(
parameters : type_parameters,
labels : _munin.type_labels,
events : Array<_munin.type_event>
) : Promise<void>
{
await lib_plankton.email.send(
@ -52,69 +139,24 @@ namespace _munin.targets.email
},
parameters.sender,
parameters.receivers,
lib_plankton.string.coin(
"[{{head}}] {{date}} : {{macro_tags}}{{title}}",
{
"head": labels.head,
"date": lib_plankton.pit.date_format(
event.begin.date
),
"macro_tags": (
(event.tags === null)
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title": event.title,
}
(
(events.length === 1)
?
summarize_event(parameters, labels, events[0])
:
lib_plankton.string.coin(
"[{{head}}] {{count}} {{events}}",
{
"head": labels.head,
"count": events.length.toFixed(0),
"events": labels.events,
}
)
),
lib_plankton.string.coin(
"{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}",
{
"title_label": labels.title.toUpperCase(),
"macro_tags": (
(parameters.hide_tags || (event.tags === null))
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title_value": event.title,
"time_label": labels.time.toUpperCase(),
"time_value": lib_plankton.pit.timespan_format(
event.begin,
event.end,
{
"adjust_to_ce": true,
}
),
"macro_location": (
(event.location === null)
?
""
:
lib_plankton.string.coin(
"\n{{location_label}} | {{location_value}}",
{
"location_label": labels.location.toUpperCase(),
"location_value": event.location,
}
)
),
"macro_description": (
(event.description === null)
?
""
:
lib_plankton.string.coin(
"\n\n{{description_value}}",
{
"description_value": event.description,
}
)
),
}
(
events
.map(event => render_event(parameters, labels, event))
.join("\n\n---\n\n")
)
);
}
@ -134,7 +176,7 @@ namespace _munin.targets.email
"receivers": parameters.receivers.join(","),
}
),
"send": (labels, event) => send(parameters, labels, event),
"send": (labels, events) => send(parameters, labels, events),
};
}

View file

@ -33,60 +33,94 @@ namespace _munin.targets.telegram_bot
/**
*/
async function send(
function render_event(
parameters : type_parameters,
labels : _munin.type_labels,
event : _munin.type_event
) : string
{
return lib_plankton.string.coin(
"{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}",
{
"macro_tags": (
(parameters.hide_tags || (event.tags === null))
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title_label": labels.title.toUpperCase(),
"title_value": event.title,
"time_label": labels.time.toUpperCase(),
"time_value": lib_plankton.pit.timespan_format(
event.begin,
event.end,
{
"adjust_to_ce": true,
}
),
"macro_location": (
(event.location === null)
?
""
:
lib_plankton.string.coin(
"\n{{location_label}} | {{location_value}}",
{
"location_label": labels.location.toUpperCase(),
"location_value": event.location,
}
)
),
"macro_description": (
(event.description === null)
?
""
:
lib_plankton.string.coin(
"\n\n{{description_value}}",
{
"description_value": event.description,
}
)
),
}
);
}
/**
*/
async function send(
parameters : type_parameters,
labels : _munin.type_labels,
events : Array<_munin.type_event>
) : Promise<void>
{
await lib_plankton.telegram.bot_call_send_message(
parameters.bot_token,
parameters.chat_id,
lib_plankton.string.coin(
"*{{head}}*\n\n\{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}",
"*{{head_core}}{{head_extra}}*\n\n{{events}}",
{
"head": labels.head,
"macro_tags": (
(parameters.hide_tags || (event.tags === null))
?
""
:
(event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ")
),
"title_label": labels.title.toUpperCase(),
"title_value": event.title,
"time_label": labels.time.toUpperCase(),
"time_value": lib_plankton.pit.timespan_format(
event.begin,
event.end,
{
"adjust_to_ce": true,
}
),
"macro_location": (
(event.location === null)
"head_core": labels.head,
"head_extra": (
(events.length <= 1)
?
""
:
lib_plankton.string.coin(
"\n{{location_label}} | {{location_value}}",
" ({{count}} {{events}})",
{
"location_label": labels.location.toUpperCase(),
"location_value": event.location,
"count": events.length.toFixed(0),
"events": labels.events,
}
)
),
"macro_description": (
(event.description === null)
?
""
:
lib_plankton.string.coin(
"\n\n{{description_value}}",
{
"description_value": event.description,
}
)
"events": (
events
.map(event => render_event(parameters, labels, event))
.join("\n\n---\n\n")
),
}
),
@ -111,7 +145,7 @@ namespace _munin.targets.telegram_bot
"chat_id": parameters.chat_id.toFixed(0),
}
),
"send": (labels, event) => send(parameters, labels, event),
"send": (labels, events) => send(parameters, labels, events),
};
}

View file

@ -20,10 +20,28 @@ along with »munin«. If not, see <http://www.gnu.org/licenses/>.
namespace _munin.test
{
/**
* @todo outsource?
*/
function lists_equal<type_element>(
list1 : Array<type_element>,
list2 : Array<type_element>
) : boolean
{
return (
(list1.length === list2.length)
&&
list1.every(
(element, index) => (element === list2[index])
)
);
}
/**
*/
async function reminder_due(
async function reminder_check(
) : Promise<void>
{
type type_input = {
@ -33,73 +51,23 @@ namespace _munin.test
from : int;
to : int;
};
datetime : lib_plankton.pit.type_datetime;
interval : int;
};
type type_output = boolean;
const testcases : Array<
_munin.helpers.test.type_testcase<
type_input,
type_output
>
> = await _munin.helpers.test.get_data<type_input, type_output>(
"data/reminder_due.testdata.json",
{
}
);
for (const testcase of testcases)
{
_munin.helpers.test.start(testcase.name);
// execution
const result : boolean = _munin.logic.reminder_due(
events : Array<
{
"frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency),
"offset": testcase.input.reminder.offset,
"from": testcase.input.reminder.from,
"to": testcase.input.reminder.to,
},
{
"pit": lib_plankton.pit.from_datetime(testcase.input.datetime),
"interval": testcase.input.interval,
title : string;
begin : lib_plankton.pit.type_datetime;
}
);
// assertions
if (result !== testcase.output)
{
_munin.helpers.test.fail(testcase.name);
}
}
}
/**
*/
async function reminder_covers_event(
) : Promise<void>
{
type type_input = {
reminder : {
frequency : string;
offset : int;
from : int;
to : int;
};
event : {
begin : lib_plankton.pit.type_datetime;
};
>;
datetime : lib_plankton.pit.type_datetime;
interval : int;
};
type type_output = boolean;
type type_output = (null | Array<string>);
const testcases : Array<
_munin.helpers.test.type_testcase<
type_input,
type_output
>
> = await _munin.helpers.test.get_data<type_input, type_output>(
"data/reminder_covers_event.testdata.json",
"data/reminder_check.testdata.json",
{
}
);
@ -109,28 +77,50 @@ namespace _munin.test
_munin.helpers.test.start(testcase.name);
// execution
const result : boolean = _munin.logic.reminder_covers_event(
const result : (null | Array<_munin.type_event>) = _munin.logic.reminder_check(
{
"frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency),
"offset": testcase.input.reminder.offset,
"from": testcase.input.reminder.from,
"to": testcase.input.reminder.to,
},
{
"title": "test",
"begin": testcase.input.event.begin,
"end": null,
"description": null,
"location": null,
"tags": null,
},
testcase.input.events.map(
event_raw => ({
"title": event_raw.title,
"begin": event_raw.begin,
"end": null,
"description": null,
"location": null,
"tags": null,
})
),
{
"pit": lib_plankton.pit.from_datetime(testcase.input.datetime),
"interval": testcase.input.interval,
}
);
// assertions
if (result !== testcase.output)
if (
(
(testcase.output === null)
&&
(result === null)
)
||
(
(testcase.output !== null)
&&
lists_equal<string>(
result.map(event => event.title),
testcase.output
)
)
)
{
// success
}
else
{
_munin.helpers.test.fail(testcase.name);
}
@ -143,8 +133,7 @@ namespace _munin.test
export async function all(
) : Promise<void>
{
await reminder_due();
await reminder_covers_event();
await reminder_check();
}
}

View file

@ -38,6 +38,7 @@ namespace _munin
title : string;
time : string;
location : string;
events : string;
};
@ -84,7 +85,7 @@ namespace _munin
send : (
(
labels : type_labels,
event : type_event
events : Array<type_event>
)
=>
Promise<void>