21 Commits

Author SHA1 Message Date
a37d95709f Merge pull request 'version/0.1.4' (#9) from version/0.1.4 into main
Reviewed-on: #9
2025-11-03 00:41:45 +00:00
152c1bcba0 Version Bump 0.1.4 2025-11-03 00:35:40 +00:00
5cdfd0f2e3 Change the Start Script and Added NODE_ENV to the env vars also for the dockerized Version 2025-11-03 00:33:23 +00:00
1c6aad0f3a Merge pull request 'fix/db-event-uid-create-unique-index' (#8) from fix/db-event-uid-create-unique-index into dev
Reviewed-on: #8
2025-11-03 00:28:36 +00:00
16593e0281 Fixed Syntax Error 2025-11-03 00:24:42 +00:00
eea37b3df5 Fixing the uid Column isn't unique.
uid is required to be unique for the the Changed Events (with the new Data) to be inserted without creating new Rows.
2025-11-03 00:22:01 +00:00
1a7de55da8 Prints "Optime" and a Diff to Optime for a Events thats not on Optime 2025-11-02 21:11:37 +00:00
2c34fece2c Merge pull request 'feature/notification-more-options' (#7) from feature/notification-more-options into dev
Reviewed-on: #7
2025-11-02 20:56:14 +00:00
4bbda5dcf8 Adding a parameter for the URLs for the Notification URLs of Services. 2025-10-29 23:49:48 +01:00
a57e4efd4c adding a file "config.ts" for adjustable configurations like URLs for Apprise 2025-10-29 23:48:09 +01:00
9ec83d8b87 adding a Helper Function to create QueryStrings for URLs 2025-10-29 23:47:04 +01:00
12e57a97f5 some minor fixes to the logging texts 2025-10-29 23:38:45 +01:00
c69eca5c08 updated README 2025-10-27 20:18:40 +01:00
dc76e14c9d changed start script to add env prod and dev 2025-10-27 20:17:52 +01:00
6e34f30d4a added some scripts for the sqlite db for cleanup 2025-10-27 20:16:53 +01:00
f1bc30a64d exclude any kind of .env from docker and git 2025-10-27 20:11:28 +01:00
d22dbaf971 Event.get_body() does only print the Diff to Optime if its not 00:00 2025-10-27 20:02:21 +01:00
c5c5d872d7 Added Event.toString() 2025-10-27 20:01:29 +01:00
7b594614c6 Merge branch 'version/0.1.3' into dev 2025-10-27 17:59:23 +01:00
ae569b7739 Merge pull request 'Bugfix in sendNotification()' (#6) from version/0.1.3 into main
Reviewed-on: #6
2025-10-27 16:55:12 +00:00
04ef066158 Merge pull request 'Release Version v0.1.2' (#5) from v0.1.2 into main
Reviewed-on: #5
2025-10-26 20:17:20 +00:00
21 changed files with 200 additions and 32 deletions

View File

@@ -9,7 +9,7 @@ LICENSE
.vscode .vscode
Makefile Makefile
helm-charts helm-charts
.env .env*
.editorconfig .editorconfig
.idea .idea
coverage* coverage*

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
.env.*
# caches # caches
.eslintcache .eslintcache

View File

@@ -1,15 +1,27 @@
# 77th Event Calender Notifcations # 77th Event Calendar Notifcations
To install dependencies: To install dependencies:
```bashe ```bash
bun install bun install
``` ```
To run: To run:
```bash ```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

View File

@@ -1,6 +1,6 @@
services: services:
app: app:
image: chiko/77th_eventcalendarntfy:v0.1.3 image: chiko/77th_eventcalendarntfy:v0.1.4
build: . build: .
volumes: volumes:
- ./data/db:/opt/app/data/db - ./data/db:/opt/app/data/db

View File

@@ -2,4 +2,3 @@ SHELL=/bin/bash
MAILTO="" MAILTO=""
0 8 * * * root . /etc/cron-env.sh && /opt/app/run-task.sh --today >> /proc/1/fd/1 2>&1 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 */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

View File

@@ -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. # Write the Env Vars into a file for cron. happens during runtime of the container and not build.
# List your environment variables here # List your environment variables here
env_vars=( env_vars=(
NODE_ENV
TZ TZ
DB_FILEPATH DB_FILEPATH
DB_FILENAME DB_FILENAME

View File

@@ -1,6 +1,6 @@
{ {
"version": "0.1.3", "version": "0.1.4",
"name": "77th_eventcalendernotification", "name": "77th_eventcalendarnotification",
"module": "./src/app.ts", "module": "./src/app.ts",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -16,12 +16,13 @@
"typescript-eslint": "^8.46.2" "typescript-eslint": "^8.46.2"
}, },
"scripts": { "scripts": {
"dev": "bun run ./src/app.ts", "start": "bun run ./src/app.ts",
"dev:init": "bun run ./src/app.ts --init", "dev": "NODE_ENV=development bun ./src/app.ts",
"db:init": "bun run ./run/db_init.ts", "db:init": "bun run ./run/db_init.ts",
"db:deleteall": "bun run ./run/db_deleteall.ts", "db:deleteall": "bun run ./run/db_event_deleteall.ts",
"build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_eventcalendernotification", "db:event:dedup": "bun run ./run/db_event_delete_duplicates.ts",
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_eventcalendernotification", "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 ." "docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ."
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -18,7 +18,7 @@ log_info "Starting task with args: $*"
cd /opt/app 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." log_info "Task completed successfully."
else else
log_error "Task failed!" log_error "Task failed!"

View File

@@ -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();

View File

@@ -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();

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX idx_events_uid ON events(uid);

View File

@@ -0,0 +1,6 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);

View File

@@ -0,0 +1,4 @@
SELECT uid, COUNT(*) AS count
FROM events
GROUP BY uid
HAVING COUNT(*) > 1;

View File

@@ -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;

View File

@@ -40,10 +40,10 @@ async function events_update_db() {
console.log("AllRelevantEvents.length: " + AllRelevantEvents.length ); console.log("AllRelevantEvents.length: " + AllRelevantEvents.length );
const eventsToInsert: TEventEntityNew[] = []; const eventsToInsert: TEventEntityNew[] = [];
for ( const ev of events_fetched ) { 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 ) {
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 ( if (
found.title != ev.title || found.title != ev.title ||
found.description != ev.description || found.description != ev.description ||
@@ -56,12 +56,12 @@ async function events_update_db() {
found.timezone != ev.timezone || found.timezone != ev.timezone ||
found.link != ev.link 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"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} else { } 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"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
@@ -90,7 +90,14 @@ async function events_check_for_notification() {
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( ", " ) );
console.log("loop list_of_events - ev 'title': " + ev.get_title() ); 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" ) { if ( ev.notification == "removed" ) {
ev.set_deleted( db ); ev.set_deleted( db );
} }

View File

@@ -43,8 +43,8 @@ export class Event implements TEventEntity {
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 NOT NULL, "event_uid" INTEGER PRIMARY KEY,
"uid" TEXT NOT NULL, "uid" TEXT NOT NULL UNIQUE,
"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,
@@ -56,10 +56,8 @@ export class Event implements TEventEntity {
"description" TEXT NOT NULL, "description" TEXT NOT NULL,
"timezone" TEXT NOT NULL, "timezone" TEXT NOT NULL,
"notification" TEXT NOT NULL, "notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL, "deleteDate" INTEGER NULL
PRIMARY KEY ("event_uid") );`);
);
CREATE UNIQUE INDEX "sqlite_autoindex_events_1" ON "events" ("uid");`);
query.run(); query.run();
} }
@@ -135,6 +133,25 @@ export class Event implements TEventEntity {
this.notification = notification; this.notification = notification;
this.deleteDate = deleteDate; 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 ) { syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM events 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 });
@@ -201,7 +218,7 @@ export class Event implements TEventEntity {
const body = [ const body = [
`Title: ${this.title}`, `Title: ${this.title}`,
`Date: ${this.date_at}`, `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 ] }`, `Type: ${ TEventType[ this.event_type ] }`,
`Location: ${this.location}`, `Location: ${this.location}`,
`By: ${this.posted_by}`, `By: ${this.posted_by}`,

16
src/config.ts Normal file
View File

@@ -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

View File

@@ -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({ console.dir({
sendNotification: { sendNotification: {
title, title,
body, body
link
} }
}); });
const QS = {
ntfy: options.ntfy ? createQS(options.ntfy) : null,
discord: createQS(options.discord)
}
if ( ! ( process.env.notification_mock == "true" ) ) { 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`, { 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", method: "POST",
@@ -14,8 +30,8 @@ export async function sendNotification(title: string, body: string, link?: strin
}, },
body: JSON.stringify({ body: JSON.stringify({
urls: [ 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" }`, `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}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}` `discord://${process.env.dc_webhook}?${QS.discord}`
].join(","), ].join(","),
title: title, title: title,
body: body, body: body,

View File

@@ -8,6 +8,8 @@ console.log(db_filepath);
export const db = new Database(db_filepath); export const db = new Database(db_filepath);
export default db;
export function init () { export function init () {
Event.createTable(db); Event.createTable(db);
} }

View File

@@ -89,3 +89,10 @@ export function isEuropeanDST( date: Date ) {
// Return true if within DST period // Return true if within DST period
return date >= start && date < end; return date >= start && date < end;
} }
export function createQS (params: Record<string, string | number | boolean>): string {
const queryString = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
return queryString;
}