diff --git a/Dockerfile b/Dockerfile index 01a20e8..b7bbf35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ ARG BUILD_DATE ARG VERSION LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}" LABEL maintainer="chiko " +ENV TZ=Europe/Berlin WORKDIR /opt/app RUN set -eux && \ echo "Updating APT" && \ @@ -10,12 +11,12 @@ RUN set -eux && \ apt-get upgrade -y -qq && \ echo "Installing tools" && \ apt-get install -y -qq \ - curl unzip cron ca-certificates logrotate dos2unix && \ + curl unzip cron ca-certificates logrotate dos2unix tzdata && \ echo "Remove exim" && \ apt-get remove -y -qq exim4 exim4-base exim4-daemon-light && \ echo "Cleaning up" && \ - apt-get --yes autoremove --purge && \ - apt-get clean --yes && \ + apt-get --yes autoremove --purge -qq && \ + apt-get clean --yes -qq && \ rm --recursive --force --verbose /var/lib/apt/lists/* && \ rm --recursive --force --verbose /tmp/* && \ rm --recursive --force --verbose /var/tmp/* && \ diff --git a/docker-compose.yml b/docker-compose.yml index 8b30198..85a9ba8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: app: + image: chiko/77th_eventcalendarntfy:dev build: . volumes: - ./data/db:/opt/app/data/db @@ -12,7 +13,7 @@ services: links: - apprise apprise: - image: caronc/apprise:latest + image: caronc/apprise:1.2.2 hostname: apprise environment: - APPRISE_WORKER_COUNT=1 @@ -29,8 +30,4 @@ services: test: ["CMD", "curl", "-f", "http://localhost:8000/status"] interval: 5s timeout: 3s - retries: 5 -# networks: -# default: -# external: true -# name: npm \ No newline at end of file + retries: 5 \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 723d77b..bf8b110 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,41 +1,72 @@ -import { TEventType } from "./component/event/event.types"; +import { TEventType, type TEvent } from "./component/event"; import { db } from "./sql"; import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events"; import { createPlaceholders, getTsNow, pad_l2 } from "./util"; import { sendNotification } from "./sendNotification"; - -const argv = require('minimist')(process.argv.slice(2)); +import minimist from "minimist"; +const argv = minimist(process.argv.slice(2)) console.log("App started"); console.dir({argv}) -async function main ( ) { - console.log("Excecuting main()"); - const TODAY = getTsNow(); - 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(); - // Write to JSON File Section START - // const data = JSON.stringify(events, null, 2); - // const TS = `${TS_TODAY.getFullYear()}-${TS_TODAY.getMonth() + 1}-${TS_TODAY.getDate()}_${TS_TODAY.getHours()}-${TS_TODAY.getMinutes()}-${TS_TODAY.getSeconds()}`; - // await Bun.write(path.join(import.meta.dir, "output", `output_${TS}.json`), data ); - // Write to JSON File Section END +const TODAY = getTsNow(); +console.dir({TODAY}); - const allEventUids = events.map( event => { return event.uid; }); - console.dir(allEventUids ); - const placeholders = createPlaceholders( allEventUids ); +function getBodyFromEvent( event: TEvent): string { + const body = [ + `Title: ${event.title}`, + `Date: ${event.date_at}`, + `Time: ${event.time_start}`, + `Type: ${ TEventType[ event.event_type ] }`, + `Location: ${event.location}`, + `By: ${event.posted_by}`, + `Link: ${event.link}`, + ].join("\n"); + return body; +} + +function isEventToday (event: Event | TEvent ) { + const now = getTsNow(); + 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( - `SELECT * FROM events WHERE uid IN (${placeholders}); ` + `SELECT * FROM events WHERE uid IN (${placeholders}) AND deleteDate IS NULL;` ).as(Event ); - const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids); - console.log("AllRelevantEvents.length:" + AllRelevantEvents.length ); + const AllRelevantEvents = getAllRelevantEventsQuery.all(...events_fetched_list_of_uids); + console.log("AllRelevantEvents.length: " + AllRelevantEvents.length ); 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( ", " ) ); const found = AllRelevantEvents.find(event => event.uid === ev.uid); if ( found ) { @@ -62,10 +93,15 @@ async function main ( ) { eventsToInsert.push( newEventToInsert ); } } - console.dir(eventsToInsert) + console.dir({eventsToInsert}) 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 ) { where.date = { year: TODAY.year, @@ -79,46 +115,40 @@ async function main ( ) { where }); 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( ", " ) ); - const body = [ - `Title: ${ev.title}`, - `Location: ${ev.location}`, - `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) { + console.log("loop list_of_events - ev: " + [ ev.uid, ev.title, ev.date_at, "notification: " + ev.notification ].join( ", " ) ); + const body = getBodyFromEvent( ev ); + // console.log("loop list_of_events - ev 'body': " + body ); + const type_of_notification = ( (event: Event) => { + switch ( event.notification ) { case "new": return "New"; case "changed": return "Changed"; - case "deleted": - return "Deleted"; + case "removed": + return "Removed"; default: return null; } } ) ( ev ); - - const today_prefix = ( (ev: Event) => { - const now = getTsNow(); - const [year, month, day] = ev.date_at.split("-") - 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 ] })`; + const title_prefix_arr = []; + if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" ); + if ( isEventToday( ev ) ) title_prefix_arr.push( "" ) + const title = `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${ev.title} (${ TEventType[ ev.event_type ] })`; 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); } +} + +async function main ( ) { + console.log("Excecuting main()"); + + await events_update_db(); + await events_check_for_notification(); }; -main(); \ No newline at end of file + +main(); + diff --git a/src/component/event/event.types.ts b/src/component/event/event.types.ts index 0e64dff..636f029 100644 --- a/src/component/event/event.types.ts +++ b/src/component/event/event.types.ts @@ -15,5 +15,6 @@ export type TEvent = { location: string, event_type: keyof typeof TEventType, timezone: string, - link: string + link: string, + deleteDate?: number | null }; \ No newline at end of file diff --git a/src/component/event/events.ts b/src/component/event/events.ts index c39958d..c9d9ce1 100644 --- a/src/component/event/events.ts +++ b/src/component/event/events.ts @@ -10,11 +10,16 @@ export type TGetEventsOptions = { year: number, month: number, day: number - } + }, + month?: { + year: number, + month: number, + }, + deleted?: boolean } export type TEventEntity = TEvent & { event_uid: number - notification: "new" | "changed" | "deleted" | "done" + notification: "new" | "changed" | "removed" | "done" } export type TEventEntityNew = Omit @@ -22,26 +27,34 @@ export type TEventEntityNew = Omit export class Event implements TEventEntity { static table_name: "events" static createTable (db: Database): void { - const query = db.query(`CREATE TABLE IF NOT EXISTS events ( - event_uid INTEGER PRIMARY KEY, - uid TEXT NOT NULL UNIQUE, - title TEXT NOT NULL, - date_at DATETIME NOT NULL, - time_start TEXT NOT NULL, - time_end TEXT NOT NULL, - posted_by TEXT NOT NULL, - location TEXT NOT NULL, - event_type TEXT NOT NULL, - link TEXT NOT NULL, - description TEXT NOT NULL, - timezone TEXT NOT NULL, - notification TEXT NOT NULL DEFAULT "new" - );`); + const query = db.query(`CREATE TABLE IF NOT EXISTS "events" ( + "event_uid" INTEGER NOT NULL, + "uid" TEXT NOT NULL, + "title" TEXT NOT NULL, + "date_at" DATETIME NOT NULL, + "time_start" TEXT NOT NULL, + "time_end" TEXT NOT NULL, + "posted_by" TEXT NOT NULL, + "location" TEXT NOT NULL, + "event_type" TEXT NOT NULL, + "link" TEXT NOT NULL, + "description" TEXT NOT NULL, + "timezone" TEXT NOT NULL, + "notification" TEXT NOT NULL, + "deleteDate" INTEGER NULL, + PRIMARY KEY ("event_uid") + ); + CREATE UNIQUE INDEX "sqlite_autoindex_events_1" ON "events" ("uid");`); query.run(); } 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 => { for (const event of events) insert.run(event); return events.length; @@ -71,12 +84,21 @@ export class Event implements TEventEntity { if (options.date) { 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 = ( () => { let str = "WHERE "; - if ( whereConditions.length >= 1 ) { - str += whereConditions.join(" OR "); + if ( options.deleted === true ) { + 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); return query.all(); @@ -94,9 +116,10 @@ export class Event implements TEventEntity { event_type: TEventEntity["event_type"]; timezone: 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.uid = uid; this.title = title; @@ -110,9 +133,10 @@ export class Event implements TEventEntity { this.timezone = timezone; this.link = link; this.notification = notification; + this.deleteDate = deleteDate; } 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 }); if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); } this.uid = entity.uid; @@ -127,6 +151,7 @@ export class Event implements TEventEntity { this.timezone = entity.timezone; this.link = entity.link; this.notification = entity.notification; + this.deleteDate = entity.deleteDate; return this; } @@ -137,5 +162,18 @@ export class Event implements TEventEntity { WHERE event_uid = $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 ); } } \ No newline at end of file diff --git a/src/sendNotification.ts b/src/sendNotification.ts index 66e61d4..3f541be 100644 --- a/src/sendNotification.ts +++ b/src/sendNotification.ts @@ -9,7 +9,7 @@ export async function sendNotification(title: string, body: string, link?: strin const response = await fetch("http://apprise:8000/notify", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json" }, body: JSON.stringify({ urls: [ @@ -18,7 +18,7 @@ export async function sendNotification(title: string, body: string, link?: strin ].join(","), title: title, body: body, - format: "text" + format: "markdown" }) }); const responseBody = await response.json();