inital commit
This commit is contained in:
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
Makefile
|
||||||
|
helm-charts
|
||||||
|
.env
|
||||||
|
.editorconfig
|
||||||
|
.idea
|
||||||
|
coverage*
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
FROM debian:12 AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y curl unzip ca-certificates python3 python3-pip && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
# install BunJs
|
||||||
|
RUN curl -fsSL https://bun.com/install | bash
|
||||||
|
ENV PATH="/root/.bun/bin:$PATH"
|
||||||
|
# symlink python
|
||||||
|
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||||
|
|
||||||
|
# install dependencies into temp directory
|
||||||
|
# this will cache them and speed up future builds
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lock /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# install with --production (exclude devDependencies)
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lock /temp/prod/
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
# prepare python packages
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
# copy node_modules from temp directory
|
||||||
|
# then copy all (non-ignored) project files into the image
|
||||||
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# [optional] tests & build
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# copy production dependencies and source code into final image
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/index.ts .
|
||||||
|
COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
|
||||||
|
# run the app
|
||||||
|
USER bun
|
||||||
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 77th Event Calender Notifcations
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bashe
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
148
app/app.ts
Normal file
148
app/app.ts
Normal 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 )
|
||||||
19
app/component/event/event.types.ts
Normal file
19
app/component/event/event.types.ts
Normal 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
|
||||||
|
};
|
||||||
130
app/component/event/events.ts
Normal file
130
app/component/event/events.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
2
app/component/event/index.ts
Normal file
2
app/component/event/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./events"
|
||||||
|
export * from "./event.types";
|
||||||
35
app/notification.py
Normal file
35
app/notification.py
Normal 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
14
app/sendNotification.ts
Normal 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
13
app/sql.ts
Normal 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
22
app/util.ts
Normal 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);
|
||||||
|
}
|
||||||
37
bun.lock
Normal file
37
bun.lock
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "eventcalender",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||||
|
|
||||||
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
6
entrypoint.sh
Normal file
6
entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
crontab -l > mycron
|
||||||
|
echo "0 8 * * * bun run ./app/app.ts --today > /dev/null 2>&1" >> mycron
|
||||||
|
echo "0 * * * * bun run ./app/app.ts --all > /dev/null 2>&1" >> mycron
|
||||||
|
crontab mycron
|
||||||
|
rm mycron
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "eventcalender",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run ./app/app.ts",
|
||||||
|
"dev:init": "bun run ./app/app.ts --init",
|
||||||
|
"db:init": "bun run ./run/db_init.ts",
|
||||||
|
"db:deleteall": "bun run ./run/db_deleteall.ts"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"minimist": "^1.2.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
apprise
|
||||||
4
run/db_deleteall.ts
Normal file
4
run/db_deleteall.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import * as db from "../app/sql";
|
||||||
|
|
||||||
|
const query = db.db.query("DELETE FROM events;");
|
||||||
|
query.run();
|
||||||
9
run/db_init.ts
Normal file
9
run/db_init.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Event } from "../app/component/event/events";
|
||||||
|
import * as db from "../app/sql";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
|
||||||
|
export function init ( db: Database ) {
|
||||||
|
Event.createTable( db );
|
||||||
|
};
|
||||||
|
|
||||||
|
init(db.db);
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user