diff --git a/.dockerignore b/.dockerignore index 9c94fbb..b97a4e1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,7 @@ LICENSE .vscode Makefile helm-charts -.env +.env* .editorconfig .idea coverage* diff --git a/.gitignore b/.gitignore index 5d4c927..38107cf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .env.test.local .env.production.local .env.local +.env.* # caches .eslintcache diff --git a/README.md b/README.md index 8373cfa..72eb260 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,27 @@ -# 77th Event Calender Notifcations +# 77th Event Calendar Notifcations To install dependencies: -```bashe +```bash bun install ``` To run: ```bash -bun run index.ts +bun run ./src/app.ts +bun run start +bun run dev ``` -This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +## Docker + +```bash +docker compose build +docker compose up -d +``` + +## Parameter + +### --today +fetch all Events, track all Changes (new, changed and deleted Events) and additionally Send a Notification for todays mission \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b95a7bb..e541745 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: chiko/77th_eventcalendarntfy:v0.1.3 + image: chiko/77th_eventcalendarntfy:v0.1.4 build: . volumes: - ./data/db:/opt/app/data/db diff --git a/docker/Crontab b/docker/Crontab index 38d1887..593bfcb 100644 --- a/docker/Crontab +++ b/docker/Crontab @@ -2,4 +2,3 @@ SHELL=/bin/bash MAILTO="" 0 8 * * * root . /etc/cron-env.sh && /opt/app/run-task.sh --today >> /proc/1/fd/1 2>&1 */15 * * * * root . /etc/cron-env.sh && /opt/app/run-task.sh >> /proc/1/fd/1 2>&1 -* * * * * root echo "cron test ran at $(date)" >> /proc/1/fd/1 2>&1 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index c7fadb5..c7e20c7 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -7,6 +7,7 @@ chmod +x /etc/cron-env.sh # Write the Env Vars into a file for cron. happens during runtime of the container and not build. # List your environment variables here env_vars=( + NODE_ENV TZ DB_FILEPATH DB_FILENAME diff --git a/package.json b/package.json index 139d992..d8606d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "version": "0.1.3", - "name": "77th_eventcalendernotification", + "version": "0.1.4", + "name": "77th_eventcalendarnotification", "module": "./src/app.ts", "type": "module", "private": true, @@ -16,12 +16,13 @@ "typescript-eslint": "^8.46.2" }, "scripts": { - "dev": "bun run ./src/app.ts", - "dev:init": "bun run ./src/app.ts --init", + "start": "bun run ./src/app.ts", + "dev": "NODE_ENV=development bun ./src/app.ts", "db:init": "bun run ./run/db_init.ts", - "db:deleteall": "bun run ./run/db_deleteall.ts", - "build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_eventcalendernotification", - "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_eventcalendernotification", + "db:deleteall": "bun run ./run/db_event_deleteall.ts", + "db:event:dedup": "bun run ./run/db_event_delete_duplicates.ts", + "build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_eventcalendarnotification", + "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_eventcalendarnotification", "docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ." }, "peerDependencies": { diff --git a/run-task.sh b/run-task.sh index dc5e2bb..41c4fc3 100644 --- a/run-task.sh +++ b/run-task.sh @@ -18,7 +18,7 @@ log_info "Starting task with args: $*" cd /opt/app -if bun run ./src/app.ts "$@" >> /proc/1/fd/1 2>> /proc/1/fd/2; then +if bun run start "$@" >> /proc/1/fd/1 2>> /proc/1/fd/2; then log_info "Task completed successfully." else log_error "Task failed!" diff --git a/run/db_event_delete_duplicates.ts b/run/db_event_delete_duplicates.ts new file mode 100644 index 0000000..16e4e6c --- /dev/null +++ b/run/db_event_delete_duplicates.ts @@ -0,0 +1,10 @@ +import * as db from "../src/sql"; + +const query = db.db.query(`DELETE FROM events +WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM events + GROUP BY uid +);`); + +query.run(); \ No newline at end of file diff --git a/run/db_deleteall.ts b/run/db_event_deleteall.ts similarity index 100% rename from run/db_deleteall.ts rename to run/db_event_deleteall.ts diff --git a/run/db_migration_v0.1.3.ts b/run/db_migration_v0.1.3.ts new file mode 100644 index 0000000..60aea85 --- /dev/null +++ b/run/db_migration_v0.1.3.ts @@ -0,0 +1,39 @@ +import db from "../src/sql"; + +const run_migration = db.transaction(() => { + // SQL 1: Insert a new user + db.run(`DELETE FROM events + WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM events + GROUP BY uid + );`); + + // SQL 2: Update product stock + db.run(`CREATE TABLE events_new ( + "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, + "deleteDate" INTEGER NULL + );`); + + // SQL 3: Log the transaction + db.run(`INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate) + SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events; + `); + db.run(`DROP TABLE events; + ALTER TABLE events_new RENAME TO events;`); +}); + +// Run the transaction +run_migration(); \ No newline at end of file diff --git a/sql/events/events_create_unique_index_uid.sql b/sql/events/events_create_unique_index_uid.sql new file mode 100644 index 0000000..c928e12 --- /dev/null +++ b/sql/events/events_create_unique_index_uid.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX idx_events_uid ON events(uid); \ No newline at end of file diff --git a/sql/events/events_delete_duplicate_rows.sql b/sql/events/events_delete_duplicate_rows.sql new file mode 100644 index 0000000..b1f1234 --- /dev/null +++ b/sql/events/events_delete_duplicate_rows.sql @@ -0,0 +1,6 @@ +DELETE FROM events +WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM events + GROUP BY uid +); \ No newline at end of file diff --git a/sql/events/events_find_duplicate_uid.sql b/sql/events/events_find_duplicate_uid.sql new file mode 100644 index 0000000..331fd14 --- /dev/null +++ b/sql/events/events_find_duplicate_uid.sql @@ -0,0 +1,4 @@ +SELECT uid, COUNT(*) AS count +FROM events +GROUP BY uid +HAVING COUNT(*) > 1; \ No newline at end of file diff --git a/sql/sql_migration_v0.1.3.sql b/sql/sql_migration_v0.1.3.sql new file mode 100644 index 0000000..ae309e4 --- /dev/null +++ b/sql/sql_migration_v0.1.3.sql @@ -0,0 +1,29 @@ +DELETE FROM events +WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM events + GROUP BY uid +); + +CREATE TABLE events_new ( + "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, + "deleteDate" INTEGER NULL +); + +INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate) +SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events; + +DROP TABLE events; +ALTER TABLE events_new RENAME TO events; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 0ac2b5a..84045c1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -40,10 +40,10 @@ async function events_update_db() { console.log("AllRelevantEvents.length: " + AllRelevantEvents.length ); const eventsToInsert: TEventEntityNew[] = []; 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); if ( found ) { - console.log("loop ev found: " + [ found.uid, found.title, found.date_at ].join( ", " ) ); + console.log("loop ev " + ev.uid + " found: " + [ found.title, found.date_at ].join( ", " ) ); if ( found.title != ev.title || found.description != ev.description || @@ -56,12 +56,12 @@ async function events_update_db() { found.timezone != ev.timezone || found.link != ev.link ) { - console.log("loop ev different (changed): " + [ ev.uid, ev.title, ev.date_at ].join( ", " ) ); + console.log("loop ev " + ev.uid + " different (changed): " + [ ev.title, ev.date_at ].join( ", " ) ); const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"}; eventsToInsert.push( newEventToInsert ); } } else { - console.log("loop ev added (new): " + [ ev.uid, ev.title, ev.date_at ].join( ", " ) ); + console.log("loop ev " + ev.uid + " added (new): " + [ ev.title, ev.date_at ].join( ", " ) ); const newEventToInsert: TEventEntityNew = {... ev, notification: "new"}; eventsToInsert.push( newEventToInsert ); } @@ -90,7 +90,14 @@ async function events_check_for_notification() { 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 'title': " + ev.get_title() ); - await sendNotification( ev.get_title(), ev.get_body() ); + const notificationOptions = { + ntfy: null, + discord: { + avatar_url: ( process.env.dc_avatar_url as string), + botname: ( process.env.dc_botname as string) + } + }; + await sendNotification( ev.get_title(), ev.get_body(), notificationOptions ); if ( ev.notification == "removed" ) { ev.set_deleted( db ); } diff --git a/src/component/event/events.ts b/src/component/event/events.ts index d02b6ec..b4ef02b 100644 --- a/src/component/event/events.ts +++ b/src/component/event/events.ts @@ -43,8 +43,8 @@ export class Event implements TEventEntity { static createTable (db: Database): void { const query = db.query(`CREATE TABLE IF NOT EXISTS "events" ( - "event_uid" INTEGER NOT NULL, - "uid" TEXT NOT NULL, + "event_uid" INTEGER PRIMARY KEY, + "uid" TEXT NOT NULL UNIQUE, "title" TEXT NOT NULL, "date_at" DATETIME NOT NULL, "time_start" TEXT NOT NULL, @@ -56,10 +56,8 @@ export class Event implements TEventEntity { "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");`); + "deleteDate" INTEGER NULL + );`); query.run(); } @@ -135,6 +133,25 @@ export class Event implements TEventEntity { this.notification = notification; this.deleteDate = deleteDate; } + toString() { + return { + event_uid: this.event_uid, + uid: this.uid, + title: this.title, + description: this.description, + date_at: this.date_at, + time_start: this.time_start, + time_end: this.time_end, + posted_by: this.posted_by, + location: this.location, + event_type: this.event_type, + timezone: this.timezone, + link: this.link, + notification: this.notification, + deleteDate: this.deleteDate + } + } + syncWithDb ( db: Database ) { const query = db.prepare( `SELECT * FROM events WHERE event_uid = $event_uid;`).as(Event); const entity = query.get({$event_uid: this.event_uid }); @@ -201,7 +218,7 @@ export class Event implements TEventEntity { const body = [ `Title: ${this.title}`, `Date: ${this.date_at}`, - `Time: ${this.get_time_start()}${ TimeDiff ? ` (Optime ${TimeDiff})` : "" }`, + `Time: ${this.get_time_start()} (OP Time${ TimeDiff != "00:00" ? ` ${TimeDiff}` : "" })`, `Type: ${ TEventType[ this.event_type ] }`, `Location: ${this.location}`, `By: ${this.posted_by}`, diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..ec28254 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,16 @@ +export const config = { + apprise: { + services: { + ntfy: { + url: `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`, + defaults: { + + } + } + }, + urls: [ + `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`, + `discord://${process.env.dc_webhook}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}` + ] + } +} as const \ No newline at end of file diff --git a/src/sendNotification.ts b/src/sendNotification.ts index a310afb..f553424 100644 --- a/src/sendNotification.ts +++ b/src/sendNotification.ts @@ -1,11 +1,27 @@ -export async function sendNotification(title: string, body: string, link?: string | null) { +import { createQS } from "./util"; + +type TSendNotificationOptions = { + ntfy: { + link?: string; + } | null, + discord: { + href?: string + avatar_url: string, + botname: string + } +} + +export async function sendNotification( title: string, body: string, options: TSendNotificationOptions ) { console.dir({ sendNotification: { title, - body, - link + body } }); + const QS = { + ntfy: options.ntfy ? createQS(options.ntfy) : null, + discord: createQS(options.discord) + } if ( ! ( process.env.notification_mock == "true" ) ) { const response = await fetch(`${ process.env.apprise_https == "true" ? "https" : "http"}://${process.env.apprise_host ? process.env.apprise_host : "apprise"}:${process.env.apprise_port ? String(process.env.apprise_port) : "80" }/notify`, { method: "POST", @@ -14,8 +30,8 @@ export async function sendNotification(title: string, body: string, link?: strin }, body: JSON.stringify({ urls: [ - `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}${ link ? `?click=${link}`: "?click=https://77th-jsoc.com/#/events" }`, - `discord://${process.env.dc_webhook}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}` + `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}${ QS.ntfy ? "?" + QS.ntfy : ""}`, + `discord://${process.env.dc_webhook}?${QS.discord}` ].join(","), title: title, body: body, diff --git a/src/sql.ts b/src/sql.ts index 6d55878..9429e98 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -8,6 +8,8 @@ console.log(db_filepath); export const db = new Database(db_filepath); +export default db; + export function init () { Event.createTable(db); } diff --git a/src/util.ts b/src/util.ts index fff08e5..74c0126 100644 --- a/src/util.ts +++ b/src/util.ts @@ -88,4 +88,11 @@ export function isEuropeanDST( date: Date ) { // Return true if within DST period return date >= start && date < end; +} + +export function createQS (params: Record): string { + const queryString = Object.entries(params) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join("&"); + return queryString; } \ No newline at end of file