This commit is contained in:
Christian Fraß 2025-04-24 22:48:05 +00:00
commit a975fa98a0
13 changed files with 11985 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/build/
/temp/
/conf/

2828
libs/plankton/plankton.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

8590
libs/plankton/plankton.js Normal file

File diff suppressed because it is too large Load diff

149
source/conf.ts Normal file
View file

@ -0,0 +1,149 @@
namespace _lixer_event_reminder.conf
{
/**
*/
export type type_conf = {
bot_token : string;
sources : Array<
(
{
kind : "kalender_digital",
data : {
id : string;
filtration : {
category_blacklist : Array<string>;
title_blacklist : Array<string>;
};
};
}
)
>;
targets : Array<
{
chat_id : int;
interval : Array<int>;
}
>;
};
/**
*/
export const schema : lib_plankton.conf.type_schema = {
"nullable": false,
"type": "object",
"properties": {
"version": {
"nullable": false,
"type": "string",
},
"bot_token": {
"nullable": false,
"type": "string",
},
"sources": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"anyOf": [
{
"type": "object",
"properties": {
"kind": {
"nullable": false,
"type": "string",
"enum": ["kalender_digital"]
},
"data": {
"nullable": false,
"type": "object",
"properties": {
"id": {
"nullable": false,
"type": "string"
},
"filtration": {
"nullable": false,
"type": "object",
"properties": {
"category_blacklist": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "string",
}
},
"title_blacklist": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "string",
}
},
},
"additionalProperties": false,
"required": [
"category_blacklist",
"title_blacklist",
]
},
},
"additionalProperties": false,
"required": [
"id",
"filtration",
]
}
},
"additionalProperties": false,
"required": [
"kind",
"data",
]
}
]
}
},
"targets": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "object",
"properties": {
"chat_id": {
"nullable": false,
"type": "integer",
},
"interval": {
"nullable": false,
"type": "array",
"items": {
"nullable": false,
"type": "integer"
}
},
},
"additionalProperties": false,
"required": [
"chat_id",
"interval",
]
}
},
},
"additionalProperties": false,
"required": [
"version",
"bot_token",
"chat_id",
"intval",
],
};
}

164
source/main.ts Normal file
View file

@ -0,0 +1,164 @@
namespace _lixer_event_reminder
{
/**
*/
export async function main(
args_raw : Array<string>
): Promise<void>
{
// 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": "run",
"info": "what to do : help | fetch | send | run",
"name": "action",
}),
"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": "path to configuration file",
"name": "conf-path",
}),
"message": lib_plankton.args.class_argument.volatile({
"indicators_long": ["message"],
"indicators_short": ["m"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "test",
"info": "message to send",
"name": "message",
}),
"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": "alias for action 'help'",
"name": "help",
}),
}
);
const args : Record<string, any> = arg_handler.read(
lib_plankton.args.enum_environment.cli,
args_raw.join(" ")
);
// init
const conf : _lixer_event_reminder.conf.type_conf = await lib_plankton.conf.load<_lixer_event_reminder.conf.type_conf>(
_lixer_event_reminder.conf.schema,
args.conf_path
);
// exec
if (args.help || (args.action === "help")) {
process.stdout.write(
arg_handler.generate_help(
{
"programname": "lixer-event-reminder",
"description": "a telegram bot, which sends reminders about upcoming events",
"executable": "node build/event-reminder",
}
)
);
}
else {
switch (args.action) {
default: {
throw (new Error("unhandled action: " + args.action));
break;
}
case "fetch": {
const updates : Array<any> = await lib_plankton.telegram.bot_call_get_updates(
conf.bot_token
);
process.stdout.write(JSON.stringify(updates, undefined, "\t") + "\n");
break;
}
case "send": {
for (const target of conf.targets) {
const message = await lib_plankton.telegram.bot_call_send_message(
conf.bot_token,
target.chat_id,
args.message
);
}
break;
}
case "run": {
const sources : Array<_lixer_event_reminder.type_source> = conf.sources.map(
source_raw => _lixer_event_reminder.sources.factory(source_raw)
);
const targets : Array<_lixer_event_reminder.type_target> = conf.targets.map(
target_raw => target_raw
);
const now : lib_plankton.pit.type_pit = lib_plankton.pit.now();
const events : Array<_lixer_event_reminder.type_event> = (
(await Promise.all(sources.map(source => source.fetch())))
.reduce((x, y) => x.concat(y), [])
);
for (const event of events) {
const begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime(event.begin);
for (const target of targets) {
for (const hours of target.interval) {
const remind : boolean = lib_plankton.pit.is_between(
lib_plankton.pit.shift_hour(begin, hours),
now,
/**
* @todo parametrize window
*/
lib_plankton.pit.shift_hour(now, +24)
);
if (remind) {
lib_plankton.log._info(
"remind",
{
"details": {
"event": event,
"target": target,
}
}
);
/**
* @todo activate
* @todo format better
*/
const message = await lib_plankton.telegram.bot_call_send_message(
conf.bot_token,
target.chat_id,
lib_plankton.string.coin(
"{{title}} | {{begin}}",
{
"title": event.title,
"begin": lib_plankton.pit.datetime_format(event.begin),
}
)
);
}
}
}
}
break;
}
}
}
return Promise.resolve<void>(undefined);
}
}
_lixer_event_reminder.main(process.argv.slice(2))
.then(() => {})
.catch((reason) => {process.stderr.write(String(reason) + "\n");})
;

View file

@ -0,0 +1,28 @@
namespace _lixer_event_reminder.sources
{
/**
*/
export function factory(
description : {
kind : string,
data : any
}
) : type_source
{
switch (description.kind) {
default: {
throw (new Error("unhandled source kind: " + description.kind));
break;
}
case "kalender_digital": {
return _lixer_event_reminder.sources.kalender_digital.implementation_source(
description.data as _lixer_event_reminder.sources.kalender_digital.type_parameters
);
return
}
}
}
}

View file

@ -0,0 +1,92 @@
namespace _lixer_event_reminder.sources.kalender_digital
{
/**
*/
export type type_parameters = {
id : string;
filtration : {
category_blacklist : Array<string>;
title_blacklist : Array<string>;
};
};
/**
*/
async function fetch(
parameters : type_parameters
) : Promise<Array<_lixer_event_reminder.type_event>>
{
const http_request : lib_plankton.http.type_request = {
"scheme": "https",
"host": "export.kalender.digital",
"path": lib_plankton.string.coin(
"/ics/0/{{id}}/gesamterkalender.ics",
{
"id": parameters.id,
}
),
"version": "HTTP/2",
"method": lib_plankton.http.enum_method.get,
"query": lib_plankton.string.coin(
"?past_months={{past_months}}&future_months={{future_months}}",
{
"past_months": (0).toFixed(0),
"future_months": (2).toFixed(0),
}
),
"headers": {},
"body": null,
};
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(http_request);
const ics : string = http_response.body.toString();
const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode(ics);
const events : Array<_lixer_event_reminder.type_event> = (
vcalendar.vevents
.filter(
vevent => (
vevent.categories.every(category => ! parameters.filtration.category_blacklist.includes(category))
&&
parameters.filtration.title_blacklist.every(title => ! vevent.summary.toLowerCase().includes(title.toLowerCase()))
)
)
.map(
vevent => {
const begin : lib_plankton.pit.type_datetime = {
"timezone_shift": 0,
"date": vevent.dtstart.value.date,
"time": vevent.dtstart.value.time,
};
const end : lib_plankton.pit.type_datetime = {
"timezone_shift": 0,
"date": vevent.dtend.value.date,
"time": vevent.dtend.value.time,
};
const event : _lixer_event_reminder.type_event = {
"title": vevent.summary,
"begin": begin,
"end": end,
"location": vevent.location,
};
return event;
}
)
);
return events;
}
/**
*/
export function implementation_source(
parameters : type_parameters
) : _lixer_event_reminder.type_source
{
return {
"fetch": () => fetch(parameters),
};
}
}

29
source/types.ts Normal file
View file

@ -0,0 +1,29 @@
namespace _lixer_event_reminder
{
/**
*/
export type type_event = {
title : string;
begin : lib_plankton.pit.type_datetime,
end : (null | lib_plankton.pit.type_datetime),
location : (null | string);
};
/**
*/
export type type_source = {
fetch : (() => Promise<Array<type_event>>);
};
/**
*/
export type type_target = {
chat_id : int;
interval : Array<int>;
};
}

16
tools/build Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
import os as _os
import argparse as _argparse
def main():
argument_parser = _argparse.ArgumentParser(
)
args = argument_parser.parse_args()
if True:
_os.system("make -f tools/makefile")
main()

5
tools/clear Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
rm -r -f temp
rm -r -f build

18
tools/deploy Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env sh
## args
# if [ $# -ge 1 ] ; then target=$1 ; else target="pv-svartalfheim-user:~/skripte" ; fi
target=$1
## exec
rsync \
--recursive \
--update \
--delete \
--verbose \
--rsh=ssh \
build/ \
${target}

40
tools/makefile Normal file
View file

@ -0,0 +1,40 @@
## commands
cmd_mkdir := mkdir -p
cmd_cp := cp -r -u
cmd_tsc := tsc
cmd_log := echo "--"
cmd_cat := cat
cmd_echo := echo
cmd_chmod := chmod
## rules
.PHONY: _default
_default: _root
temp/termine-reminder-unlinked.js: \
libs/plankton/plankton.d.ts \
source/types.ts \
source/sources/kalender_digital.ts \
source/sources/_functions.ts \
source/conf.ts \
source/main.ts
@ ${cmd_log} "compiling logic …"
@ ${cmd_mkdir} temp
@ ${cmd_tsc} $^ --lib es2020,dom --target es6 --outFile $@
build/termine-reminder: libs/plankton/plankton.js temp/termine-reminder-unlinked.js
@ ${cmd_log} "linking …"
@ ${cmd_echo} "#!/usr/bin/env node" > temp/head.js
@ ${cmd_mkdir} build
@ ${cmd_cat} temp/head.js $^ > $@
@ ${cmd_chmod} +x $@
.PHONY: sources
sources: build/termine-reminder
.PHONY: _root
_root: sources

23
tools/update-plankton Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env sh
## consts
dir="libs/plankton"
## vars
modules=""
modules="${modules} pit"
modules="${modules} http"
modules="${modules} ical"
modules="${modules} telegram"
modules="${modules} conf"
modules="${modules} args"
## exec
mkdir -p ${dir}
cd ${dir}
ptk bundle node ${modules}