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 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": { 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..d02b6ec 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, formatTimeDiff, isEuropeanDST, subtractHours } from "../../util"; const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events"; @@ -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; @@ -176,4 +176,61 @@ 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 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.get_time_start()}${ TimeDiff ? ` (Optime ${TimeDiff})` : "" }`, + `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; + } + + 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/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 diff --git a/src/util.ts b/src/util.ts index 6f2f7ed..fff08e5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -38,4 +38,54 @@ export function getTsNow() { seconds: now.getSeconds() } return rtn; +} + +export function unixToDate( unix_timestamp: number ) { return new Date(unix_timestamp * 1000) } +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