inital commit

This commit is contained in:
chikovanreuden
2025-10-20 02:05:08 +02:00
commit 2fb5f48a54
19 changed files with 601 additions and 0 deletions

148
app/app.ts Normal file
View File

@@ -0,0 +1,148 @@
import { TEventType } from "./component/event/event.types";
import { db } from "./sql";
import { Event, type TEventEntityNew } from "./component/event/events";
import { sendNotification } from "./sendNotification";
import { createPlaceholders } from "./util";
const argv = require('minimist')(process.argv.slice(2));
console.dir(argv)
const TS_TODAY = new Date();
function pad_l2 ( _thing: string | number ): string {
if ( typeof _thing == "number" ) {
_thing = JSON.stringify(_thing);
};
return _thing.padStart(2, "0");
}
function getTsNow() {
const now = new Date();
const rtn = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
minute: now.getMinutes(),
seconds: now.getSeconds()
}
return rtn;
}
async function main( ) {
const events = await Event.fetch_events( TS_TODAY.getFullYear(), TS_TODAY.getMonth() + 1 , -120 );
// Write to JSON File Section START
// const data = JSON.stringify(events, null, 2);
// const TS = `${TS_TODAY.getFullYear()}-${TS_TODAY.getMonth() + 1}-${TS_TODAY.getDate()}_${TS_TODAY.getHours()}-${TS_TODAY.getMinutes()}-${TS_TODAY.getSeconds()}`;
// await Bun.write(path.join(import.meta.dir, "output", `output_${TS}.json`), data );
// Write to JSON File Section END
const allEventUids = events.map( event => { return event.uid; });
const placeholders = createPlaceholders( allEventUids );
const getAllRelevantEventsQuery = db.query(
`SELECT * FROM events WHERE uid IN (${placeholders}); `
).as(Event );
const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids);
const eventsToInsert: TEventEntityNew[] = [];
for ( const ev of events ) {
const found = AllRelevantEvents.find(event => event.uid === ev.uid);
if ( found ) {
if (
found.title != ev.title ||
found.description != ev.description ||
found.date_at != ev.date_at ||
found.time_start != ev.time_start ||
found.time_end != ev.time_end ||
found.posted_by != ev.posted_by ||
found.location != ev.location ||
found.event_type != ev.event_type ||
found.timezone != ev.timezone ||
found.link != ev.link
) {
const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"};
eventsToInsert.push( newEventToInsert );
}
} else {
const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert );
}
}
Event.insert( eventsToInsert, db);
const list_of_events = Event.get_events(["new", "changed"], db);
for ( const ev of list_of_events ) {
const body = [
`Title: ${ev.title}`,
`Location: ${ev.location}`,
`Type: ${ TEventType[ ev.event_type ] }`,
`Date: ${ev.date_at}`,
`Time: ${ev.time_start}`,
`By: ${ev.posted_by}`,
`Link: ${ev.link}`,
].join("\n");
const notification_prefix = ( (event: Event) => {
switch( event.notification) {
case "new":
return "New";
case "changed":
return "Changed";
case "deleted":
return "Deleted";
default:
return null;
}
} ) ( ev );
const today_prefix = ( (ev: Event) => {
const now = getTsNow();
const [year, month, day] = ev.date_at.split("-")
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
})( ev );
sendNotification(
`${today_prefix ? "TODAY " : ""}${notification_prefix ? notification_prefix + ": " : ""} ${ev.title} (${ TEventType[ ev.event_type ] })`,
`${body}`,
`${ev.link || "https://77th-jsoc.com/#/events"}`
);
ev.set_notification("done", db);
}
// events.forEach( event => {
// 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 ) )
// ) {
// // console.dir( event );
// const body = [
// `Title: ${event.title}`,
// `Location: ${event.location}`,
// `Type: ${ TEventType[ event.event_type ] }`,
// `Date: ${event.date_at}`,
// `Time: ${event.time_start}`,
// `By: ${event.posted_by}`,
// `Link: ${event.link}`,
// ].join("\n");
// sendNotification(
// `TODAY ${ TEventType[ event.event_type ] } - ${event.title}`,
// `${body}`,
// `${event.link || "https://77th-jsoc.com/#/events"}`
// );
// }
// });
};
main();
// do {
// await getEvents(TS_TODAY.getFullYear(), TS_TODAY.getMonth() + 1 , -120);
// await Bun.sleep(1000 * 60 * 60 * 24);
// }
// while( true )

View File

@@ -0,0 +1,19 @@
export const TEventType = {
"1": "Public Event",
"2": "Private Mission",
"3": "Private Meeting"
} as const
export type TEvent = {
uid: string,
title: string,
description: string,
date_at: string,
time_start: string,
time_end: string,
posted_by: string,
location: string,
event_type: keyof typeof TEventType,
timezone: string,
link: string
};

View File

@@ -0,0 +1,130 @@
import { Database } from "bun:sqlite";
import type { TEvent } from "./event.types";
import { transformArray } from "../../util";
const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events";
export type TEventEntity = TEvent & {
event_uid: number
notification: "new" | "changed" | "deleted" | "done"
}
export type TEventEntityNew = Omit<TEventEntity, "event_uid">
export class Event implements TEventEntity {
static table_name: "events"
static createTable (db: Database): void {
const query = db.query(`CREATE TABLE IF NOT EXISTS events (
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 DEFAULT "new"
);`);
query.run();
}
static insert ( events: TEventEntityNew[], db: Database ) {
const insert = db.prepare("INSERT OR REPLACE INTO events (uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification) VALUES ($uid, $title, $date_at, $time_start, $time_end, $posted_by, $location, $event_type, $link, $description, $timezone, $notification)");
const insertEvents = db.transaction(events => {
for (const event of events) insert.run(event);
return events.length;
});
const transforedEventArray = transformArray( events );
const count = insertEvents(transforedEventArray);
console.log(`Inserted ${count} events`);
}
static async fetch_events( _year_: number, _month_: number, timezone: number) {
const url = `${BASE_URL}&year=${_year_}&month=${_month_}&timezone=${timezone}`
const response = await fetch(url, {
method: "GET",
});
const body = await response.json() as {events: TEvent[] };
const events = body.events.sort( ( a, b ) => ( new Date(a.date_at) < new Date(b.date_at ) ) ? -1 : 1 );
return events;
}
static get_events (notification: TEventEntity["notification"][] | null, db: Database ) {
const whereConditions: string[] = [];
if ( notification ) {
whereConditions.push( `notification IN ('${ notification.join("', '") }')` )
}
const where = ( () => {
let str = "WHERE ";
if ( whereConditions.length >= 1 ) {
str += whereConditions.join(" AND ");
}
return str;
})()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
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"]
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"]) {
this.event_uid = event_uid;
this.uid = uid;
this.title = title;
this.description = description;
this.date_at = date_at;
this.time_start = time_start;
this.time_end = time_end;
this.posted_by = posted_by;
this.location = location;
this.event_type = event_type;
this.timezone = timezone;
this.link = link;
this.notification = notification;
}
syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM ${Event.table_name} WHERE event_uid = $event_uid;`).as(Event);
const entity = query.get({$event_uid: this.event_uid });
if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); }
this.uid = entity.uid;
this.title = entity.title;
this.description = entity.description;
this.date_at = entity.date_at;
this.time_start = entity.time_start;
this.time_end = entity.time_end;
this.posted_by = entity.posted_by;
this.location = entity.location;
this.event_type = entity.event_type;
this.timezone = entity.timezone;
this.link = entity.link;
this.notification = entity.notification;
return this;
}
set_notification ( newValue: TEventEntity["notification"], db: Database ) {
const query = db.prepare(
`UPDATE events
SET notification = $notification
WHERE event_uid = $event_uid;`
);
query.get({$notification: newValue, $event_uid: this.event_uid });
}
}

View File

@@ -0,0 +1,2 @@
export * from "./events"
export * from "./event.types";

35
app/notification.py Normal file
View File

@@ -0,0 +1,35 @@
from dotenv import load_dotenv
import os
load_dotenv() # Load environment variables from .env file
ntfy_username = os.getenv('ntfy_username')
ntfy_password = os.getenv('ntfy_password')
ntfy_host = os.getenv('ntfy_host')
ntfy_topic = os.getenv('ntfy_topic')
dc_webhook = os.getenv('dc_webhook')
from argparse import ArgumentParser
import apprise
parser = ArgumentParser()
parser.add_argument("--title")
parser.add_argument("--body")
parser.add_argument("--click")
args = parser.parse_args()
print(args)
apobj = apprise.Apprise()
# config = apprise.AppriseConfig()
# config.add('https://myserver:8080/path/to/config')
if ntfy_host and ntfy_topic:
ntfy_link = f"ntfys://{ntfy_username}:{ntfy_password}@{ntfy_host}/{ntfy_topic}"
if args.click:
ntfy_link = ntfy_link + "?click=" + args.click
apobj.add(ntfy_link)
if dc_webhook:
apobj.add(f"https://discord.com/api/webhooks/{dc_webhook}");
apobj.notify(
body=args.body,
title=args.title
)

14
app/sendNotification.ts Normal file
View File

@@ -0,0 +1,14 @@
import * as Bun from "bun";
export function sendNotification(title: string, body: string, click?: string) {
const command = [
"python",
"./app/notification.py",
`--title=${title}`,
`--body=${body}`,
];
if (click) {
command.push(`--click=${click}`);
}
Bun.spawn(command);
}

13
app/sql.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Database } from "bun:sqlite";
import * as path from "node:path";
import { Event } from "./component/event";
export const db_filename = "77th_eventntfy.db";
export const db_filepath = path.join("data", "db", db_filename);
console.log(db_filepath);
// const db_file = Bun.file(db_filepath);
export const db = new Database(db_filepath);
export function init () {
Event.createTable(db);
}

22
app/util.ts Normal file
View File

@@ -0,0 +1,22 @@
export const createPlaceholders = ( arr: any[] ) => {
return arr.map(() => '?').join(', ');
}
export type AddDollarPrefix<T> = {
[K in keyof T as `$${string & K}`]: T[K];
};
export function prefixKeysWithDollar<T extends Record<string, any>>(obj: T): AddDollarPrefix<T> {
const result = {} as AddDollarPrefix<T>;
for (const key in obj) {
const newKey = `$${key}` as keyof AddDollarPrefix<T>;
result[newKey] = obj[key] as any;
}
return result;
}
export function transformArray<T extends Record<string, any>>(arr: T[]): AddDollarPrefix<T>[] {
return arr.map(prefixKeysWithDollar);
}