5 Commits

Author SHA1 Message Date
79b7cfae68 Merge pull request 'dev v0.1.1' (#3) from dev into main
Reviewed-on: #3
2025-10-24 23:45:46 +00:00
d303560f53 rearranged some code.
splited function main() to 2 seperate functions.
2025-10-24 02:55:33 +02:00
eb6525a66f added 'deleteDate' to Events 2025-10-24 02:51:30 +02:00
f974684945 fixed inconsistent module loading with 'require' 2025-10-24 02:49:27 +02:00
b035c9475d Some Cleanup and minor Changes 2025-10-24 02:48:00 +02:00
6 changed files with 164 additions and 97 deletions

View File

@@ -3,6 +3,7 @@ ARG BUILD_DATE
ARG VERSION ARG VERSION
LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}" LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}"
LABEL maintainer="chiko <chiko@xcsone.de>" LABEL maintainer="chiko <chiko@xcsone.de>"
ENV TZ=Europe/Berlin
WORKDIR /opt/app WORKDIR /opt/app
RUN set -eux && \ RUN set -eux && \
echo "Updating APT" && \ echo "Updating APT" && \
@@ -10,12 +11,12 @@ RUN set -eux && \
apt-get upgrade -y -qq && \ apt-get upgrade -y -qq && \
echo "Installing tools" && \ echo "Installing tools" && \
apt-get install -y -qq \ apt-get install -y -qq \
curl unzip cron ca-certificates logrotate dos2unix && \ curl unzip cron ca-certificates logrotate dos2unix tzdata && \
echo "Remove exim" && \ echo "Remove exim" && \
apt-get remove -y -qq exim4 exim4-base exim4-daemon-light && \ apt-get remove -y -qq exim4 exim4-base exim4-daemon-light && \
echo "Cleaning up" && \ echo "Cleaning up" && \
apt-get --yes autoremove --purge && \ apt-get --yes autoremove --purge -qq && \
apt-get clean --yes && \ apt-get clean --yes -qq && \
rm --recursive --force --verbose /var/lib/apt/lists/* && \ rm --recursive --force --verbose /var/lib/apt/lists/* && \
rm --recursive --force --verbose /tmp/* && \ rm --recursive --force --verbose /tmp/* && \
rm --recursive --force --verbose /var/tmp/* && \ rm --recursive --force --verbose /var/tmp/* && \

View File

@@ -1,5 +1,6 @@
services: services:
app: app:
image: chiko/77th_eventcalendarntfy:dev
build: . build: .
volumes: volumes:
- ./data/db:/opt/app/data/db - ./data/db:/opt/app/data/db
@@ -12,7 +13,7 @@ services:
links: links:
- apprise - apprise
apprise: apprise:
image: caronc/apprise:latest image: caronc/apprise:1.2.2
hostname: apprise hostname: apprise
environment: environment:
- APPRISE_WORKER_COUNT=1 - APPRISE_WORKER_COUNT=1
@@ -30,7 +31,3 @@ services:
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
# networks:
# default:
# external: true
# name: npm

View File

@@ -1,41 +1,72 @@
import { TEventType } from "./component/event/event.types"; import { TEventType, type TEvent } from "./component/event";
import { db } from "./sql"; import { db } from "./sql";
import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events"; import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events";
import { createPlaceholders, getTsNow, pad_l2 } from "./util"; import { createPlaceholders, getTsNow, pad_l2 } from "./util";
import { sendNotification } from "./sendNotification"; import { sendNotification } from "./sendNotification";
import minimist from "minimist";
const argv = require('minimist')(process.argv.slice(2)); const argv = minimist(process.argv.slice(2))
console.log("App started"); console.log("App started");
console.dir({argv}) console.dir({argv})
async function main ( ) {
console.log("Excecuting main()");
const TODAY = getTsNow(); const TODAY = getTsNow();
console.dir(TODAY); console.dir({TODAY});
const events_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 );
console.log("events_currentMonth.length:" + events_currentMonth.length );
const events_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 );
console.log("events_nextMonth.length:" + events_nextMonth.length );
const events = [...events_currentMonth, ...events_nextMonth];
console.log("events.length:" + events.length );
// const TS_TODAY = new Date(); function getBodyFromEvent( event: TEvent): string {
// Write to JSON File Section START const body = [
// const data = JSON.stringify(events, null, 2); `Title: ${event.title}`,
// const TS = `${TS_TODAY.getFullYear()}-${TS_TODAY.getMonth() + 1}-${TS_TODAY.getDate()}_${TS_TODAY.getHours()}-${TS_TODAY.getMinutes()}-${TS_TODAY.getSeconds()}`; `Date: ${event.date_at}`,
// await Bun.write(path.join(import.meta.dir, "output", `output_${TS}.json`), data ); `Time: ${event.time_start}`,
// Write to JSON File Section END `Type: ${ TEventType[ event.event_type ] }`,
`Location: ${event.location}`,
`By: ${event.posted_by}`,
`Link: ${event.link}`,
].join("\n");
return body;
}
const allEventUids = events.map( event => { return event.uid; }); function isEventToday (event: Event | TEvent ) {
console.dir(allEventUids ); const now = getTsNow();
const placeholders = createPlaceholders( allEventUids ); const [year, month, day] = event.date_at.split("-")
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
}
async function events_update_db() {
const events_fetched_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 );
console.log("events_fetched_currentMonth.length: " + events_fetched_currentMonth.length );
const events_fetched_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 );
console.log("events_fetched_nextMonth.length: " + events_fetched_nextMonth.length );
const events_fetched = [...events_fetched_currentMonth, ...events_fetched_nextMonth];
console.log("events_fetched.length: " + events_fetched.length );
const events_fetched_list_of_uids = events_fetched.map( event => { return event.uid; });
console.dir({events_fetched_list_of_uids} );
const events_db_currentMonth = Event.get_events({month: {year: TODAY.year, month: TODAY.month}}, db);
const events_removed: Event[] = events_db_currentMonth.filter( (ev) => {
return ! events_fetched_list_of_uids.includes(ev.uid);
});
console.dir({events_removed});
events_removed.forEach( ev => {
ev.set_notification("removed", db);
});
const placeholders = createPlaceholders( events_fetched_list_of_uids );
const getAllRelevantEventsQuery = db.query( const getAllRelevantEventsQuery = db.query(
`SELECT * FROM events WHERE uid IN (${placeholders}); ` `SELECT * FROM events WHERE uid IN (${placeholders}) AND deleteDate IS NULL;`
).as(Event ); ).as(Event );
const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids); const AllRelevantEvents = getAllRelevantEventsQuery.all(...events_fetched_list_of_uids);
console.log("AllRelevantEvents.length: " + AllRelevantEvents.length ); console.log("AllRelevantEvents.length: " + AllRelevantEvents.length );
const eventsToInsert: TEventEntityNew[] = []; const eventsToInsert: TEventEntityNew[] = [];
for ( const ev of events ) { for ( const ev of events_fetched ) {
console.log("loop ev: " + [ ev.uid, ev.title, ev.date_at ].join( ", " ) ); console.log("loop ev: " + [ ev.uid, ev.title, ev.date_at ].join( ", " ) );
const found = AllRelevantEvents.find(event => event.uid === ev.uid); const found = AllRelevantEvents.find(event => event.uid === ev.uid);
if ( found ) { if ( found ) {
@@ -62,10 +93,15 @@ async function main ( ) {
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} }
console.dir(eventsToInsert) console.dir({eventsToInsert})
Event.insert( eventsToInsert, db); Event.insert( eventsToInsert, db);
const where: TGetEventsOptions = {} }
where.notification = ["new", "changed"]
async function events_check_for_notification() {
const where: TGetEventsOptions = {
notification: ["new", "changed", "removed"],
deleted: false
}
if ( argv.today ) { if ( argv.today ) {
where.date = { where.date = {
year: TODAY.year, year: TODAY.year,
@@ -80,45 +116,39 @@ async function main ( ) {
}); });
for ( const ev of list_of_events ) { for ( const ev of list_of_events ) {
console.log("loop list_of_events - ev: " + [ ev.uid, ev.title, ev.date_at, "notification: " + ev.notification ].join( ", " ) ); console.log("loop list_of_events - ev: " + [ ev.uid, ev.title, ev.date_at, "notification: " + ev.notification ].join( ", " ) );
const body = [ const body = getBodyFromEvent( ev );
`Title: ${ev.title}`, // console.log("loop list_of_events - ev 'body': " + body );
`Location: ${ev.location}`, const type_of_notification = ( (event: Event) => {
`Type: ${ TEventType[ ev.event_type ] }`,
`Date: ${ev.date_at}`,
`Time: ${ev.time_start}`,
`By: ${ev.posted_by}`,
`Link: ${ev.link}`,
].join("\n");
console.log("loop list_of_events - ev 'body': " + body );
const notification_prefix = ( (event: Event) => {
switch ( event.notification ) { switch ( event.notification ) {
case "new": case "new":
return "New"; return "New";
case "changed": case "changed":
return "Changed"; return "Changed";
case "deleted": case "removed":
return "Deleted"; return "Removed";
default: default:
return null; return null;
} }
} ) ( ev ); } ) ( ev );
const title_prefix_arr = [];
const today_prefix = ( (ev: Event) => { if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" );
const now = getTsNow(); if ( isEventToday( ev ) ) title_prefix_arr.push( "<TODAY>" )
const [year, month, day] = ev.date_at.split("-") const title = `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${ev.title} (${ TEventType[ ev.event_type ] })`;
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
})( ev );
const title = `${today_prefix ? "TODAY " : ""}${notification_prefix ? notification_prefix + ": " : ""} ${ev.title} (${ TEventType[ ev.event_type ] })`;
console.log("loop list_of_events - ev 'title': " + title ); console.log("loop list_of_events - ev 'title': " + title );
await sendNotification( title, body, ev.link ? ev.link : null); await sendNotification( title, body);
if( ev.notification == "removed" ) {
ev.set_deleted( db );
}
ev.set_notification("done", db); ev.set_notification("done", db);
} }
}
async function main ( ) {
console.log("Excecuting main()");
await events_update_db();
await events_check_for_notification();
}; };
main(); main();

View File

@@ -15,5 +15,6 @@ export type TEvent = {
location: string, location: string,
event_type: keyof typeof TEventType, event_type: keyof typeof TEventType,
timezone: string, timezone: string,
link: string link: string,
deleteDate?: number | null
}; };

View File

@@ -10,11 +10,16 @@ export type TGetEventsOptions = {
year: number, year: number,
month: number, month: number,
day: number day: number
} },
month?: {
year: number,
month: number,
},
deleted?: boolean
} }
export type TEventEntity = TEvent & { export type TEventEntity = TEvent & {
event_uid: number event_uid: number
notification: "new" | "changed" | "deleted" | "done" notification: "new" | "changed" | "removed" | "done"
} }
export type TEventEntityNew = Omit<TEventEntity, "event_uid"> export type TEventEntityNew = Omit<TEventEntity, "event_uid">
@@ -22,26 +27,34 @@ export type TEventEntityNew = Omit<TEventEntity, "event_uid">
export class Event implements TEventEntity { export class Event implements TEventEntity {
static table_name: "events" static table_name: "events"
static createTable (db: Database): void { static createTable (db: Database): void {
const query = db.query(`CREATE TABLE IF NOT EXISTS events ( const query = db.query(`CREATE TABLE IF NOT EXISTS "events" (
event_uid INTEGER PRIMARY KEY, "event_uid" INTEGER NOT NULL,
uid TEXT NOT NULL UNIQUE, "uid" TEXT NOT NULL,
title TEXT NOT NULL, "title" TEXT NOT NULL,
date_at DATETIME NOT NULL, "date_at" DATETIME NOT NULL,
time_start TEXT NOT NULL, "time_start" TEXT NOT NULL,
time_end TEXT NOT NULL, "time_end" TEXT NOT NULL,
posted_by TEXT NOT NULL, "posted_by" TEXT NOT NULL,
location TEXT NOT NULL, "location" TEXT NOT NULL,
event_type TEXT NOT NULL, "event_type" TEXT NOT NULL,
link TEXT NOT NULL, "link" TEXT NOT NULL,
description TEXT NOT NULL, "description" TEXT NOT NULL,
timezone TEXT NOT NULL, "timezone" TEXT NOT NULL,
notification TEXT NOT NULL DEFAULT "new" "notification" TEXT NOT NULL,
);`); "deleteDate" INTEGER NULL,
PRIMARY KEY ("event_uid")
);
CREATE UNIQUE INDEX "sqlite_autoindex_events_1" ON "events" ("uid");`);
query.run(); query.run();
} }
static insert ( events: TEventEntityNew[], db: Database ) { static insert ( events: TEventEntityNew[], db: Database ) {
const insert = db.prepare("INSERT OR REPLACE INTO events (uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification) VALUES ($uid, $title, $date_at, $time_start, $time_end, $posted_by, $location, $event_type, $link, $description, $timezone, $notification)"); const insert = db.prepare( [
"INSERT OR REPLACE INTO events",
"(uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification)",
"VALUES",
"($uid, $title, $date_at, $time_start, $time_end, $posted_by, $location, $event_type, $link, $description, $timezone, $notification)"
].join(" "));
const insertEvents = db.transaction(events => { const insertEvents = db.transaction(events => {
for (const event of events) insert.run(event); for (const event of events) insert.run(event);
return events.length; return events.length;
@@ -71,12 +84,21 @@ export class Event implements TEventEntity {
if (options.date) { if (options.date) {
whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`); whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`);
} }
if ( options.month ) {
whereConditions.push( `strftime('%Y-%m', date_at) = '${options.month.year}-${options.month.month}'`)
}
const where = ( () => { const where = ( () => {
let str = "WHERE "; let str = "WHERE ";
if ( whereConditions.length >= 1 ) { if ( options.deleted === true ) {
str += whereConditions.join(" OR "); str += "deleteDate IS NOT NULL AND ";
} else if ( options.deleted === false ) {
str += "deleteDate IS NULL AND ";
} }
return str; if ( whereConditions.length >= 1 ) {
return str += `( ${ whereConditions.join(" OR ") } )`;
}
return null;
})() })()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event); const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
return query.all(); return query.all();
@@ -94,9 +116,10 @@ export class Event implements TEventEntity {
event_type: TEventEntity["event_type"]; event_type: TEventEntity["event_type"];
timezone: string; timezone: string;
link: string; link: string;
notification: TEventEntity["notification"] notification: TEventEntity["notification"];
deleteDate: TEventEntity["deleteDate"];
constructor(event_uid: number, uid: string, title: string, description: string, date_at: string, time_start: string, time_end: string, posted_by: string, location: string, event_type: TEventEntity["event_type"], timezone: string, link: string, notification: TEventEntity["notification"]) { constructor(event_uid: number, uid: string, title: string, description: string, date_at: string, time_start: string, time_end: string, posted_by: string, location: string, event_type: TEventEntity["event_type"], timezone: string, link: string, notification: TEventEntity["notification"], deleteDate: TEventEntity["deleteDate"]) {
this.event_uid = event_uid; this.event_uid = event_uid;
this.uid = uid; this.uid = uid;
this.title = title; this.title = title;
@@ -110,9 +133,10 @@ export class Event implements TEventEntity {
this.timezone = timezone; this.timezone = timezone;
this.link = link; this.link = link;
this.notification = notification; this.notification = notification;
this.deleteDate = deleteDate;
} }
syncWithDb ( db: Database ) { syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM ${Event.table_name} WHERE event_uid = $event_uid;`).as(Event); const query = db.prepare( `SELECT * FROM events WHERE event_uid = $event_uid;`).as(Event);
const entity = query.get({$event_uid: this.event_uid }); const entity = query.get({$event_uid: this.event_uid });
if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); } if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); }
this.uid = entity.uid; this.uid = entity.uid;
@@ -127,6 +151,7 @@ export class Event implements TEventEntity {
this.timezone = entity.timezone; this.timezone = entity.timezone;
this.link = entity.link; this.link = entity.link;
this.notification = entity.notification; this.notification = entity.notification;
this.deleteDate = entity.deleteDate;
return this; return this;
} }
@@ -137,5 +162,18 @@ export class Event implements TEventEntity {
WHERE event_uid = $event_uid;` WHERE event_uid = $event_uid;`
); );
query.get({$notification: newValue, $event_uid: this.event_uid }); query.get({$notification: newValue, $event_uid: this.event_uid });
return this.syncWithDb( db );
}
set_deleted ( db: Database ) {
const query = db.prepare(
`UPDATE events
SET deleteDate = $deleteDate
WHERE event_uid = $event_uid;`
);
query.get({
$deleteDate: Math.floor((new Date()).getTime() / 1000),
$event_uid: this.event_uid
});
return this.syncWithDb( db );
} }
} }

View File

@@ -18,7 +18,7 @@ export async function sendNotification(title: string, body: string, link?: strin
].join(","), ].join(","),
title: title, title: title,
body: body, body: body,
format: "text" format: "markdown"
}) })
}); });
const responseBody = await response.json(); const responseBody = await response.json();