From 76dfde05f7907fee41b2a7c562107293406dfe77 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 14:03:29 +0100 Subject: [PATCH 1/7] added more env vars --- .env.sample | 8 +++++++- docker/docker-entrypoint.sh | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index a21ab9f..c4a6fb1 100644 --- a/.env.sample +++ b/.env.sample @@ -1,9 +1,15 @@ +TZ=Europe/Berlin +DB_FILEPATH=./data/db +DB_FILENAME=77th_eventntfy.db +apprise_https=false +apprise_hostname=apprise +apprise_port=8000 +notification_mock=true ntfy_on=true ntfy_username=chiko ntfy_password=Blub ntfy_host=ntfy.some-service.com ntfy_topic=SomeTopic - dc_on=true dc_webhook=123123123123123/ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF dc_botname=Botname Here diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 4138e55..c7fadb5 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -7,6 +7,13 @@ 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=( + TZ + DB_FILEPATH + DB_FILENAME + apprise_https + apprise_hostname + apprise_port + notification_mock ntfy_on ntfy_username ntfy_password -- 2.49.1 From d5a1bc9fa7abbf614378abfbbe7db676239438b9 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 14:05:08 +0100 Subject: [PATCH 2/7] Added Helper Functions for events.deleteDate. Its stored as integer for unixtime. --- src/util.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 6f2f7ed..c8ab635 100644 --- a/src/util.ts +++ b/src/util.ts @@ -38,4 +38,7 @@ export function getTsNow() { seconds: now.getSeconds() } return rtn; -} \ No newline at end of file +} + +export function unixToDate( unix_timestamp: number ) { return new Date(unix_timestamp * 1000) } +export function dateToUnix( date: Date ) { return Math.round( date.getTime()/1000 ) } \ No newline at end of file -- 2.49.1 From 420076a8cf405276bdff682bbf0c210cc0dda299 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 14:06:34 +0100 Subject: [PATCH 3/7] Changed Package Name. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ae09f4b..85bdd4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "version": "0.1.1", - "name": "eventcalender", + "name": "77th_eventcalendernotification", "module": "./src/app.ts", "type": "module", "private": true, @@ -20,8 +20,8 @@ "dev:init": "bun run ./src/app.ts --init", "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_event_calendar_notification", - "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_event_calendar_notification", + "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", "docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ." }, "peerDependencies": { -- 2.49.1 From e9ead4e7bf19afe30ffc1c3995cfc760e3030124 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 14:07:28 +0100 Subject: [PATCH 4/7] Moved Function to get a Title and Body of a Event to the Event Class. --- src/app.ts | 57 +++-------------------------------- src/component/event/events.ts | 47 +++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/src/app.ts b/src/app.ts index bf8b110..0ac2b5a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,6 @@ -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 { createPlaceholders, getTsNow } from "./util"; import { sendNotification } from "./sendNotification"; import minimist from "minimist"; const argv = minimist(process.argv.slice(2)) @@ -11,32 +10,6 @@ console.dir({argv}) const TODAY = getTsNow(); console.dir({TODAY}); -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 ); @@ -116,27 +89,9 @@ 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( ", " ) ); - 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 "removed": - return "Removed"; - default: - return null; - } - } ) ( ev ); - 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); - if( ev.notification == "removed" ) { + console.log("loop list_of_events - ev 'title': " + ev.get_title() ); + await sendNotification( ev.get_title(), ev.get_body() ); + if ( ev.notification == "removed" ) { ev.set_deleted( db ); } ev.set_notification("done", db); @@ -145,10 +100,8 @@ async function events_check_for_notification() { async function main ( ) { console.log("Excecuting main()"); - await events_update_db(); await events_check_for_notification(); }; -main(); - +main(); \ No newline at end of file diff --git a/src/component/event/events.ts b/src/component/event/events.ts index c9d9ce1..2954946 100644 --- a/src/component/event/events.ts +++ b/src/component/event/events.ts @@ -1,6 +1,6 @@ import { Database } from "bun:sqlite"; -import type { TEvent } from "./event.types"; -import { transformArray } from "../../util"; +import { TEventType, type TEvent } from "./event.types"; +import { getTsNow, pad_l2, transformArray } from "../../util"; const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events"; @@ -176,4 +176,47 @@ export class Event implements TEventEntity { }); return this.syncWithDb( db ); } + get_title() { + const type_of_notification = ( (event: Event) => { + switch ( event.notification ) { + case "new": + return "New"; + case "changed": + return "Changed"; + case "removed": + return "Removed"; + default: + return null; + } + } ) ( this ); + const title_prefix_arr = []; + if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" ); + if ( this.isEventToday() ) title_prefix_arr.push( "" ) + return `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${this.title} (${ TEventType[ this.event_type ] })`; + } + get_body() { + const body = [ + `Title: ${this.title}`, + `Date: ${this.date_at}`, + `Time: ${this.time_start}`, + `Type: ${ TEventType[ this.event_type ] }`, + `Location: ${this.location}`, + `By: ${this.posted_by}`, + `Link: ${this.link}`, + ].join("\n"); + return body; + } + + isEventToday ( ) { + const now = getTsNow(); + const [year, month, day] = this.date_at.split("-") + if ( + year == String(now.year) && + month == pad_l2( String(now.month) ) && + day == pad_l2( String( now.day ) ) + ) { + return true; + } + return false; + } } \ No newline at end of file -- 2.49.1 From c1ad9c749489035e74d170a8daf3f8c53fcbea51 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 14:08:10 +0100 Subject: [PATCH 5/7] Added env vars to function sendNotification --- src/sendNotification.ts | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/sendNotification.ts b/src/sendNotification.ts index 3f541be..88746c5 100644 --- a/src/sendNotification.ts +++ b/src/sendNotification.ts @@ -6,21 +6,28 @@ export async function sendNotification(title: string, body: string, link?: strin link } }); - const response = await fetch("http://apprise:8000/notify", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - 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}` - ].join(","), - title: title, - body: body, - format: "markdown" + 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", + headers: { + "Content-Type": "application/json" + }, + 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}` + ].join(","), + title: title, + body: body, + format: "markdown" + }) + }); + const responseBody = await response.json(); + return responseBody; + } else { + console.dir({ + sendNotification: "mocking" }) - }); - const responseBody = await response.json(); - return responseBody; -} \ No newline at end of file + } + +} \ No newline at end of file -- 2.49.1 From 8c161c6dc50d6a863ac95a16aff549c9285c5fe2 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 15:12:41 +0100 Subject: [PATCH 6/7] just moved the properties of Event up in the Class. --- src/component/event/events.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/component/event/events.ts b/src/component/event/events.ts index 2954946..c3ce724 100644 --- a/src/component/event/events.ts +++ b/src/component/event/events.ts @@ -26,6 +26,21 @@ export type TEventEntityNew = Omit export class Event implements TEventEntity { static table_name: "events" + 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"]; + static createTable (db: Database): void { const query = db.query(`CREATE TABLE IF NOT EXISTS "events" ( "event_uid" INTEGER NOT NULL, @@ -104,21 +119,6 @@ export class Event implements TEventEntity { return query.all(); } - 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"]; - 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; -- 2.49.1 From c51263c947b86c37adf43dfb779b5b2a6b8c88e7 Mon Sep 17 00:00:00 2001 From: chiko Date: Sun, 26 Oct 2025 15:14:57 +0100 Subject: [PATCH 7/7] Added a Workarond for the DST (European Daylight Saving Time (DST)) --- src/component/event/events.ts | 18 +++++++++++-- src/util.ts | 49 ++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/component/event/events.ts b/src/component/event/events.ts index c3ce724..d02b6ec 100644 --- a/src/component/event/events.ts +++ b/src/component/event/events.ts @@ -1,6 +1,6 @@ import { Database } from "bun:sqlite"; import { TEventType, type TEvent } from "./event.types"; -import { getTsNow, pad_l2, transformArray } from "../../util"; +import { getTsNow, pad_l2, transformArray, formatTimeDiff, isEuropeanDST, subtractHours } from "../../util"; const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events"; @@ -195,10 +195,13 @@ export class Event implements TEventEntity { return `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${this.title} (${ TEventType[ this.event_type ] })`; } get_body() { + const BaseTime = new Date(`${this.date_at} 21:00`); + const RelativeEventTime = new Date(`${this.date_at} ${this.get_time_start()}`); + const TimeDiff = formatTimeDiff( BaseTime, RelativeEventTime); const body = [ `Title: ${this.title}`, `Date: ${this.date_at}`, - `Time: ${this.time_start}`, + `Time: ${this.get_time_start()}${ TimeDiff ? ` (Optime ${TimeDiff})` : "" }`, `Type: ${ TEventType[ this.event_type ] }`, `Location: ${this.location}`, `By: ${this.posted_by}`, @@ -219,4 +222,15 @@ export class Event implements TEventEntity { } return false; } + + get_time_start () { + const date = new Date( `${this.date_at} ${this.time_start}` ); + if ( ! isEuropeanDST( date ) ) { + const newDate = subtractHours( date, 1); + const hours = newDate.getHours(); + const minutes = newDate.getMinutes(); + return `${pad_l2(hours)}:${pad_l2(minutes)}`; + } + return this.time_start; + } } \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index c8ab635..fff08e5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -41,4 +41,51 @@ export function getTsNow() { } export function unixToDate( unix_timestamp: number ) { return new Date(unix_timestamp * 1000) } -export function dateToUnix( date: Date ) { return Math.round( date.getTime()/1000 ) } \ No newline at end of file +export function dateToUnix( date: Date ) { return Math.round( date.getTime()/1000 ) } + +export function formatTimeDiff(dateA: Date, dateB: Date) { + // Difference in milliseconds + const diffMs = dateB.getTime() - dateA.getTime(); + + // Get sign (+ or -) + const sign = diffMs < 0 ? "-" : ""; + + // Convert to absolute minutes + const diffMinutes = Math.floor(Math.abs(diffMs) / 60000); + + // Split into hours and minutes + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + + // Return formatted string + return `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; +} + +export function subtractHours(date: Date, hours: number) { + // Create a new Date so we don't mutate the original + return new Date(date.getTime() - hours * 60 * 60 * 1000); +} + +// Helper: get last Sunday of a given month +function lastSundayOfMonth(year: number, month: number ) { + const lastDay = new Date(Date.UTC(year, month + 1, 0)); // last day of month + const day = lastDay.getUTCDay(); // 0 = Sunday + const diff = day === 0 ? 0 : day; // how far back to go to reach Sunday + lastDay.setUTCDate(lastDay.getUTCDate() - diff); + return lastDay; +} + +export function isEuropeanDST( date: Date ) { + const year = date.getFullYear(); + + // DST starts: last Sunday in March, 01:00 UTC + const start = lastSundayOfMonth(year, 2); // March (month = 2) + start.setUTCHours(1, 0, 0, 0); + + // DST ends: last Sunday in October, 01:00 UTC + const end = lastSundayOfMonth(year, 9); // October (month = 9) + end.setUTCHours(1, 0, 0, 0); + + // Return true if within DST period + return date >= start && date < end; +} \ No newline at end of file -- 2.49.1