43 Commits
Author SHA1 Message Date
chiko 8e47a3bc8a Version Bump: v0.1.6 2025-11-07 00:42:52 +00:00
chiko eb0c5e1580 Removed Parameter --today from docker-entrypoint.sh
dont query todays Events and send a Notification if the Container starts the first time.
2025-11-07 00:42:32 +00:00
chiko d5d2fa5836 Bugfix "Not sending 'Todays Events'"
Format Month and Day with a Leading 0 for values between 1 to 9 when querying the DB for Todays Events only.
2025-11-07 00:27:45 +00:00
chiko ca102190ea Changed: only get Events from the Db which are not marked as deleted, indicated by having a value for "deleteDate" 2025-11-07 00:25:15 +00:00
chiko 8fee748837 Added "hour" to getTsNow() 2025-11-07 00:24:03 +00:00
chiko c6ec442c2b Merge branch 'main' into dev 2025-11-06 20:16:54 +00:00
chiko 3e5032caf3 Merge branch 'dev' 2025-11-06 20:09:55 +00:00
chiko 2f805c0772 Bugfix: Added "deleted" as Notification State if Event deleteDate is set to 2025-11-06 20:08:08 +00:00
chiko 8aef42396e Merge branch 'version/0.1.5' 2025-11-06 17:48:10 +00:00
chiko 170695f9ff Version Bump: v0.1.5 2025-11-06 17:39:00 +00:00
chiko c703911f85 Bugfix: Events marked as "removed" And deleteDate is set so the Notification does not get sent 2025-11-06 17:36:54 +00:00
chiko 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
chiko 152c1bcba0 Version Bump 0.1.4 2025-11-03 00:35:40 +00:00
chiko 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
chiko 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
chiko 16593e0281 Fixed Syntax Error 2025-11-03 00:24:42 +00:00
chiko 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
chiko 1a7de55da8 Prints "Optime" and a Diff to Optime for a Events thats not on Optime 2025-11-02 21:11:37 +00:00
chiko 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
chiko 4bbda5dcf8 Adding a parameter for the URLs for the Notification URLs of Services. 2025-10-29 23:49:48 +01:00
chiko a57e4efd4c adding a file "config.ts" for adjustable configurations like URLs for Apprise 2025-10-29 23:48:09 +01:00
chiko 9ec83d8b87 adding a Helper Function to create QueryStrings for URLs 2025-10-29 23:47:04 +01:00
chiko 12e57a97f5 some minor fixes to the logging texts 2025-10-29 23:38:45 +01:00
chiko c69eca5c08 updated README 2025-10-27 20:18:40 +01:00
chiko dc76e14c9d changed start script to add env prod and dev 2025-10-27 20:17:52 +01:00
chiko 6e34f30d4a added some scripts for the sqlite db for cleanup 2025-10-27 20:16:53 +01:00
chiko f1bc30a64d exclude any kind of .env from docker and git 2025-10-27 20:11:28 +01:00
chiko d22dbaf971 Event.get_body() does only print the Diff to Optime if its not 00:00 2025-10-27 20:02:21 +01:00
chiko c5c5d872d7 Added Event.toString() 2025-10-27 20:01:29 +01:00
chiko 7b594614c6 Merge branch 'version/0.1.3' into dev 2025-10-27 17:59:23 +01:00
chiko 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
chiko 608608aa56 Bugfix in sendNotification()
URL for Post Request returned a 404 because there was a " too much
2025-10-27 17:54:30 +01:00
chiko 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
chiko 8bcb2618a2 Release Version v0.1.2 2025-10-26 21:15:23 +01:00
chiko 1433d37afa Merge pull request 'dev to main - v0.1.2' (#4) from dev into main
Reviewed-on: #4
2025-10-26 14:22:05 +00:00
chiko c51263c947 Added a Workarond for the DST (European Daylight Saving Time (DST)) 2025-10-26 15:14:57 +01:00
chiko 8c161c6dc5 just moved the properties of Event up in the Class. 2025-10-26 15:12:41 +01:00
chiko c1ad9c7494 Added env vars to function sendNotification 2025-10-26 14:08:10 +01:00
chiko e9ead4e7bf Moved Function to get a Title and Body of a Event to the Event Class. 2025-10-26 14:07:28 +01:00
chiko 420076a8cf Changed Package Name. 2025-10-26 14:06:34 +01:00
chiko d5a1bc9fa7 Added Helper Functions for events.deleteDate. Its stored as integer for unixtime. 2025-10-26 14:05:08 +01:00
chiko 76dfde05f7 added more env vars 2025-10-26 14:03:29 +01:00
chiko 79b7cfae68 Merge pull request 'dev v0.1.1' (#3) from dev into main
Reviewed-on: #3
2025-10-24 23:45:46 +00:00
23 changed files with 367 additions and 106 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ LICENSE
.vscode
Makefile
helm-charts
.env
.env*
.editorconfig
.idea
coverage*
+7 -1
View File
@@ -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
+1
View File
@@ -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
+16 -4
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
services:
app:
image: chiko/77th_eventcalendarntfy:dev
image: chiko/77th_eventcalendarntfy:v0.1.6
build: .
volumes:
- ./data/db:/opt/app/data/db
-1
View File
@@ -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
+9 -1
View File
@@ -7,6 +7,14 @@ 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
apprise_https
apprise_hostname
apprise_port
notification_mock
ntfy_on
ntfy_username
ntfy_password
@@ -27,7 +35,7 @@ for var in "${env_vars[@]}"; do
done
export PATH="/root/.bun/bin:$PATH"
bun run /opt/app/src/app.ts --today
bun run /opt/app/src/app.ts
# Start cron in foreground
exec cron -f
+8 -7
View File
@@ -1,6 +1,6 @@
{
"version": "0.1.1",
"name": "eventcalender",
"version": "0.1.6",
"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_event_calendar_notification",
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_event_calendar_notification",
"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": {
+1 -1
View File
@@ -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!"
+10
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();
File renamed without changes.
+7
View File
@@ -0,0 +1,7 @@
import db from "../src/sql";
db.run(
`UPDATE events
SET notification = 'done'
WHERE deleteDate IS NOT NULL;`
);
+39
View File
@@ -0,0 +1,39 @@
import db from "../src/sql";
const run_migration = db.transaction(() => {
// SQL 1: remove duplicates by uid
db.run(`DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);`);
// SQL 2: create new table with unique key
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();
@@ -0,0 +1 @@
CREATE UNIQUE INDEX idx_events_uid ON events(uid);
@@ -0,0 +1,6 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);
+4
View File
@@ -0,0 +1,4 @@
SELECT uid, COUNT(*) AS count
FROM events
GROUP BY uid
HAVING COUNT(*) > 1;
+29
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;
+19 -56
View File
@@ -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 );
@@ -48,8 +21,10 @@ async function events_update_db() {
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) => {
const events_db_currentMonth = Event.get_events({month: {year: TODAY.year, month: TODAY.month}, deleted: false}, db);
const events_db_nextMonth = Event.get_events({month: {year: TODAY.year, month: (TODAY.month + 1)}, deleted: false}, db);
const events_db = [... events_db_currentMonth, ... events_db_nextMonth];
const events_removed: Event[] = events_db.filter( (ev) => {
return ! events_fetched_list_of_uids.includes(ev.uid);
});
@@ -67,10 +42,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 + " f: " + [ found.title, found.date_at ].join( ", " ) );
if (
found.title != ev.title ||
found.description != ev.description ||
@@ -83,12 +58,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 + " c: " + [ 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 + " n: " + [ ev.title, ev.date_at ].join( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert );
}
@@ -116,39 +91,27 @@ 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;
console.log("loop list_of_events - ev 'title': " + ev.get_title() );
const notificationOptions = {
ntfy: null,
discord: {
avatar_url: ( process.env.dc_avatar_url as string),
botname: ( process.env.dc_botname as string)
}
} ) ( ev );
const title_prefix_arr = [];
if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" );
if ( isEventToday( ev ) ) title_prefix_arr.push( "<TODAY>" )
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);
};
await sendNotification( ev.get_title(), ev.get_body(), notificationOptions );
if ( ev.notification == "removed" ) {
ev.set_deleted( db );
}
} else {
ev.set_notification("done", db);
}
}
}
async function main ( ) {
console.log("Excecuting main()");
await events_update_db();
await events_check_for_notification();
};
main();
+103 -27
View File
@@ -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";
@@ -19,17 +19,32 @@ export type TGetEventsOptions = {
}
export type TEventEntity = TEvent & {
event_uid: number
notification: "new" | "changed" | "removed" | "done"
notification: "new" | "changed" | "removed" | "done" | "deleted"
}
export type TEventEntityNew = Omit<TEventEntity, "event_uid">
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,
"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,
@@ -41,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();
}
@@ -82,10 +95,10 @@ export class Event implements TEventEntity {
whereConditions.push( `notification IN ('${ options.notification.join("', '") }')` )
}
if ( options.date ) {
whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`);
whereConditions.push(`date_at = "${options.date.year}-${pad_l2(options.date.month)}-${pad_l2(options.date.day)}"`);
}
if ( options.month ) {
whereConditions.push( `strftime('%Y-%m', date_at) = '${options.month.year}-${options.month.month}'`)
whereConditions.push( `strftime('%Y-%m', date_at) = '${options.month.year}-${pad_l2(options.month.month)}'`)
}
const where = ( () => {
@@ -101,24 +114,10 @@ export class Event implements TEventEntity {
return null;
})()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
console.dir({ db: { action: {get_events: query} } })
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;
@@ -135,6 +134,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 });
@@ -167,7 +185,8 @@ export class Event implements TEventEntity {
set_deleted ( db: Database ) {
const query = db.prepare(
`UPDATE events
SET deleteDate = $deleteDate
SET notification = 'deleted',
deleteDate = $deleteDate
WHERE event_uid = $event_uid;`
);
query.get({
@@ -176,4 +195,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( "<TODAY>" )
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()} (OP Time${ TimeDiff != "00:00" ? ` ${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;
}
}
+16
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
+29 -6
View File
@@ -1,20 +1,37 @@
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 response = await fetch("http://apprise:8000/notify", {
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",
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}`
`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,
@@ -23,4 +40,10 @@ export async function sendNotification(title: string, body: string, link?: strin
});
const responseBody = await response.json();
return responseBody;
} else {
console.dir({
sendNotification: "mocking"
})
}
}
+2
View File
@@ -8,6 +8,8 @@ console.log(db_filepath);
export const db = new Database(db_filepath);
export default db;
export function init () {
Event.createTable(db);
}
+58
View File
@@ -34,8 +34,66 @@ export function getTsNow() {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
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;
}
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;
}