30 Commits

Author SHA1 Message Date
d303560f53 rearranged some code.
splited function main() to 2 seperate functions.
2025-10-24 02:55:33 +02:00
eb6525a66f added 'deleteDate' to Events 2025-10-24 02:51:30 +02:00
f974684945 fixed inconsistent module loading with 'require' 2025-10-24 02:49:27 +02:00
b035c9475d Some Cleanup and minor Changes 2025-10-24 02:48:00 +02:00
f648952fa9 added Linting for TypeScript 2025-10-24 02:38:50 +02:00
5546d5511e replaced /etc/environment with /etc/cron-env.sh to work with cron 2025-10-22 19:10:04 +02:00
4e21b1372f added windows style new line clean up to all script files and linux resources. 2025-10-22 19:09:15 +02:00
f33324e9f8 docker-compose.yml cleanup 2025-10-22 19:07:33 +02:00
f9a1919d08 replaced the old Crontab with commands with a script to execute instead. Better logging for docker logs <container name>.
because cron does not execute script with env vars, they get dumped into a file and set for the runtime of the  script run-taks.sh
2025-10-22 16:28:25 +02:00
96e9e79aeb replaced CMD in the docker file with ENTRYPOINT and docker-entrypoint.sh 2025-10-22 16:26:04 +02:00
1729332373 removed Python and the Python Script, added the apprise-api docker container to the Docker-compose to send notificaiton 2025-10-21 23:34:50 +02:00
8c5d6de5a4 fixed not loading the env file for docker 2025-10-21 02:47:36 +02:00
5f735dce6a container healthchecks.. 2025-10-21 02:35:59 +02:00
de11a6934c fixing docker-compose.yml 2025-10-21 02:29:54 +02:00
457a49e754 fuck python, switching to apprise-api as a docker container 2025-10-21 02:22:51 +02:00
92a2c6956a forgot the ENV for bun 2025-10-21 01:26:18 +02:00
1bf297752d more dockerfile fixing 2025-10-21 01:23:41 +02:00
156a2db485 fixing docker file 2025-10-21 01:21:39 +02:00
714738dcba Fixing python... 2025-10-21 01:16:59 +02:00
66212229f5 fixed python dependency and Dockerfile Volume Bind 2025-10-20 23:48:31 +02:00
3167bd7976 added dotenv as python dependency 2025-10-20 23:24:56 +02:00
877d9e37b3 Udpated Dockerfile, changed Crontab for testing purposes 2025-10-20 23:16:35 +02:00
fd0081d4d0 Fixed dockerfile, added a docker-compose.yml, added db init function to run on every startup 2025-10-20 23:12:38 +02:00
966353de3e Build with Dockerfile works now 2025-10-20 17:00:41 +02:00
ae9ae46fea changed the Working Directory inside the Dockerfile from "/usr/src/app" to "/opt/app" 2025-10-20 16:43:21 +02:00
d8e2027efa changed files depending on the old folder "app" to the new folder "src" 2025-10-20 16:41:14 +02:00
272c9519b9 renamed folder "app" to "src" 2025-10-20 16:38:28 +02:00
1890d28f47 added .env example file 2025-10-20 03:01:58 +02:00
f086fd9792 added --today parameter to send a nofitication for todays events else its change or new events. also fetches current and next month now. 2025-10-20 02:58:16 +02:00
1cdcf2f423 added options to the discord bot 2025-10-20 02:50:02 +02:00
33 changed files with 825 additions and 262 deletions

View File

@@ -12,4 +12,7 @@ helm-charts
.env .env
.editorconfig .editorconfig
.idea .idea
coverage* coverage*
output*
dist
data

10
.env.sample Normal file
View File

@@ -0,0 +1,10 @@
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
dc_avatar_url=https://pico.eix-1.n8x.sx/-tbcjZCuAtj

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
.eslintrc.cjs

17
.eslintrc Normal file
View File

@@ -0,0 +1,17 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": [
"error",
{
"ignoreVoid": false,
"ignoreIIFE": false
}
]
}
}

17
.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
/* eslint-env node */
export default {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
parserOptions: {
project: "./tsconfig.json",
sourceType: "module"
},
rules: {
"@typescript-eslint/no-floating-promises": "error"
}
};

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules
output output
out out
dist dist
build
*.tgz *.tgz
# code coverage # code coverage
@@ -36,3 +37,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
*.db *.db
*.sqlite *.sqlite
data

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"eslint.enable": true,
"eslint.validate": [
"javascript",
"typescript"
]
}

View File

@@ -1,14 +1,32 @@
FROM debian:12 AS base FROM debian:12 AS base
WORKDIR /usr/src/app ARG BUILD_DATE
RUN apt-get update && \ ARG VERSION
apt-get install -y curl unzip ca-certificates python3 python3-pip && \ LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}"
rm -rf /var/lib/apt/lists/* LABEL maintainer="chiko <chiko@xcsone.de>"
ENV TZ=Europe/Berlin
WORKDIR /opt/app
RUN set -eux && \
echo "Updating APT" && \
apt-get update -y -qq && \
apt-get upgrade -y -qq && \
echo "Installing tools" && \
apt-get install -y -qq \
curl unzip cron ca-certificates logrotate dos2unix tzdata && \
echo "Remove exim" && \
apt-get remove -y -qq exim4 exim4-base exim4-daemon-light && \
echo "Cleaning up" && \
apt-get --yes autoremove --purge -qq && \
apt-get clean --yes -qq && \
rm --recursive --force --verbose /var/lib/apt/lists/* && \
rm --recursive --force --verbose /tmp/* && \
rm --recursive --force --verbose /var/tmp/* && \
rm --recursive --force --verbose /var/cache/apt/archives/* && \
truncate --size 0 /var/log/*log
# install BunJs # install BunJs
RUN curl -fsSL https://bun.com/install | bash RUN curl -fsSL https://bun.com/install | bash
ENV PATH="/root/.bun/bin:$PATH" ENV PATH="/root/.bun/bin:$PATH"
# symlink python RUN ln -s /root/.bun/bin/bun /usr/local/bin/bun
RUN ln -s /usr/bin/python3 /usr/bin/python
# install dependencies into temp directory # install dependencies into temp directory
# this will cache them and speed up future builds # this will cache them and speed up future builds
FROM base AS install FROM base AS install
@@ -20,24 +38,27 @@ RUN cd /temp/dev && bun install --frozen-lockfile
RUN mkdir -p /temp/prod RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/ COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production RUN cd /temp/prod && bun install --frozen-lockfile --production
# prepare python packages
RUN pip3 install -r requirements.txt
# copy node_modules from temp directory # copy node_modules from temp directory
# then copy all (non-ignored) project files into the image # then copy all (non-ignored) project files into the image
FROM base AS prerelease FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules COPY --from=install /temp/dev/node_modules node_modules
COPY . . COPY . .
# [optional] tests & build # [optional] tests & build
ENV NODE_ENV=production
# copy production dependencies and source code into final image # copy production dependencies and source code into final image
FROM base AS release FROM base AS release
ENV NODE_ENV=production
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts . # COPY ./docker/cron-bun-log /etc/logrotate.d/
COPY --from=prerelease /usr/src/app/package.json . COPY ./docker/Crontab /etc/cron.d/
RUN chmod 0644 /etc/cron.d/Crontab && \
# run the app chown root:root /etc/cron.d/Crontab
USER bun COPY . .
ENTRYPOINT ["./entrypoint.sh"] RUN dos2unix \
/opt/app/docker/docker-entrypoint.sh \
/opt/app/run-task.sh \
/etc/cron.d/Crontab
RUN chmod +x /opt/app/docker/docker-entrypoint.sh && \
chmod +x /opt/app/run-task.sh
ENTRYPOINT [ "/opt/app/docker/docker-entrypoint.sh" ]

View File

@@ -1,148 +0,0 @@
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

@@ -1,35 +0,0 @@
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
)

View File

@@ -1,14 +0,0 @@
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);
}

254
bun.lock
View File

@@ -8,30 +8,282 @@
"minimist": "^1.2.8", "minimist": "^1.2.8",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.38.0",
"@types/bun": "^1.3.0", "@types/bun": "^1.3.0",
"@types/minimist": "^1.2.5",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
"typescript-eslint": "^8.46.2",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5", "typescript": "^5.9.3",
}, },
}, },
}, },
"packages": { "packages": {
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="],
"@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="],
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], "@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=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/type-utils": "8.46.2", "@typescript-eslint/utils": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.2", "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.2", "", {}, "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.2", "@typescript-eslint/tsconfig-utils": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.46.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
} }
} }

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
services:
app:
image: chiko/77th_eventcalendarntfy:dev
build: .
volumes:
- ./data/db:/opt/app/data/db
env_file:
- path: ./.env
required: true
depends_on:
apprise:
condition: service_healthy
links:
- apprise
apprise:
image: caronc/apprise:1.2.2
hostname: apprise
environment:
- APPRISE_WORKER_COUNT=1
- APPRISE_STATEFUL_MODE=simple
# - PUID=$(id -u)
# - PGID=$(id -g)
volumes:
- ./data/apprise/config:/config
- ./data/apprise/plugin:/plugin
- ./data/apprise/attach:/attach
# ports:
#- 8880:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
interval: 5s
timeout: 3s
retries: 5

5
docker/Crontab Normal file
View File

@@ -0,0 +1,5 @@
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

8
docker/cron-bun-log Normal file
View File

@@ -0,0 +1,8 @@
/var/log/cron.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Create or overwrite the cron env file
cat /dev/null > /etc/cron-env.sh
chmod 600 /etc/cron-env.sh
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=(
ntfy_on
ntfy_username
ntfy_password
ntfy_host
ntfy_topic
dc_on
dc_webhook
dc_botname
dc_avatar_url
)
for var in "${env_vars[@]}"; do
val="${!var}"
if [ -n "$val" ]; then
# Safely export the variable with proper quoting
printf 'export %s=%q\n' "$var" "$val" >> /etc/cron-env.sh
fi
done
export PATH="/root/.bun/bin:$PATH"
bun run /opt/app/src/app.ts --today
# Start cron in foreground
exec cron -f

View File

@@ -1,6 +0,0 @@
#!/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

9
eslint.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
tseslint.configs.recommended,
]);

View File

@@ -1,19 +1,31 @@
{ {
"version": "0.1.1",
"name": "eventcalender", "name": "eventcalender",
"module": "index.ts", "module": "./src/app.ts",
"type": "module", "type": "module",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.0" "@eslint/js": "^9.38.0",
"@types/bun": "^1.3.0",
"@types/minimist": "^1.2.5",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
"typescript-eslint": "^8.46.2"
}, },
"scripts": { "scripts": {
"dev": "bun run ./app/app.ts", "dev": "bun run ./src/app.ts",
"dev:init": "bun run ./app/app.ts --init", "dev:init": "bun run ./src/app.ts --init",
"db:init": "bun run ./run/db_init.ts", "db:init": "bun run ./run/db_init.ts",
"db:deleteall": "bun run ./run/db_deleteall.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",
"docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ."
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.2.3", "dotenv": "^17.2.3",

View File

@@ -1 +0,0 @@
apprise

26
run-task.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
# set -e
export PATH="/root/.bun/bin:$PATH"
set -o allexport
. /etc/cron-env.sh || echo "[WARN] Failed to load env" >> /proc/1/fd/2
set +o allexport
log_info() {
echo "[INFO] $(date) $1" >> /proc/1/fd/1
}
log_error() {
echo "[ERROR] $(date) $1" >> /proc/1/fd/2
}
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
log_info "Task completed successfully."
else
log_error "Task failed!"
exit 1
fi

View File

@@ -1,4 +1,4 @@
import * as db from "../app/sql"; import * as db from "../src/sql";
const query = db.db.query("DELETE FROM events;"); const query = db.db.query("DELETE FROM events;");
query.run(); query.run();

View File

@@ -1,5 +1,5 @@
import { Event } from "../app/component/event/events"; import { Event } from "../src/component/event/events";
import * as db from "../app/sql"; import * as db from "../src/sql";
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
export function init ( db: Database ) { export function init ( db: Database ) {

154
src/app.ts Normal file
View File

@@ -0,0 +1,154 @@
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 { sendNotification } from "./sendNotification";
import minimist from "minimist";
const argv = minimist(process.argv.slice(2))
console.log("App started");
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 );
const events_fetched_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 );
console.log("events_fetched_nextMonth.length: " + events_fetched_nextMonth.length );
const events_fetched = [...events_fetched_currentMonth, ...events_fetched_nextMonth];
console.log("events_fetched.length: " + events_fetched.length );
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) => {
return ! events_fetched_list_of_uids.includes(ev.uid);
});
console.dir({events_removed});
events_removed.forEach( ev => {
ev.set_notification("removed", db);
});
const placeholders = createPlaceholders( events_fetched_list_of_uids );
const getAllRelevantEventsQuery = db.query(
`SELECT * FROM events WHERE uid IN (${placeholders}) AND deleteDate IS NULL;`
).as(Event );
const AllRelevantEvents = getAllRelevantEventsQuery.all(...events_fetched_list_of_uids);
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( ", " ) );
const found = AllRelevantEvents.find(event => event.uid === ev.uid);
if ( found ) {
console.log("loop ev found: " + [ found.uid, found.title, found.date_at ].join( ", " ) );
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
) {
console.log("loop ev different (changed): " + [ ev.uid, 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( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert );
}
}
console.dir({eventsToInsert})
Event.insert( eventsToInsert, db);
}
async function events_check_for_notification() {
const where: TGetEventsOptions = {
notification: ["new", "changed", "removed"],
deleted: false
}
if ( argv.today ) {
where.date = {
year: TODAY.year,
month: TODAY.month,
day: TODAY.day
}
}
const list_of_events = Event.get_events( where, db );
console.dir({
list_of_events,
where
});
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( "<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);
if( ev.notification == "removed" ) {
ev.set_deleted( db );
}
ev.set_notification("done", db);
}
}
async function main ( ) {
console.log("Excecuting main()");
await events_update_db();
await events_check_for_notification();
};
main();

View File

@@ -15,5 +15,6 @@ export type TEvent = {
location: string, location: string,
event_type: keyof typeof TEventType, event_type: keyof typeof TEventType,
timezone: string, timezone: string,
link: string link: string,
deleteDate?: number | null
}; };

View File

@@ -4,9 +4,22 @@ import { transformArray } from "../../util";
const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events"; const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events";
export type TGetEventsOptions = {
notification?: TEventEntity["notification"][] | null,
date?: {
year: number,
month: number,
day: number
},
month?: {
year: number,
month: number,
},
deleted?: boolean
}
export type TEventEntity = TEvent & { export type TEventEntity = TEvent & {
event_uid: number event_uid: number
notification: "new" | "changed" | "deleted" | "done" notification: "new" | "changed" | "removed" | "done"
} }
export type TEventEntityNew = Omit<TEventEntity, "event_uid"> export type TEventEntityNew = Omit<TEventEntity, "event_uid">
@@ -14,26 +27,34 @@ export type TEventEntityNew = Omit<TEventEntity, "event_uid">
export class Event implements TEventEntity { export class Event implements TEventEntity {
static table_name: "events" static table_name: "events"
static createTable (db: Database): void { static createTable (db: Database): void {
const query = db.query(`CREATE TABLE IF NOT EXISTS events ( const query = db.query(`CREATE TABLE IF NOT EXISTS "events" (
event_uid INTEGER PRIMARY KEY, "event_uid" INTEGER NOT NULL,
uid TEXT NOT NULL UNIQUE, "uid" TEXT NOT NULL,
title TEXT NOT NULL, "title" TEXT NOT NULL,
date_at DATETIME NOT NULL, "date_at" DATETIME NOT NULL,
time_start TEXT NOT NULL, "time_start" TEXT NOT NULL,
time_end TEXT NOT NULL, "time_end" TEXT NOT NULL,
posted_by TEXT NOT NULL, "posted_by" TEXT NOT NULL,
location TEXT NOT NULL, "location" TEXT NOT NULL,
event_type TEXT NOT NULL, "event_type" TEXT NOT NULL,
link TEXT NOT NULL, "link" TEXT NOT NULL,
description TEXT NOT NULL, "description" TEXT NOT NULL,
timezone TEXT NOT NULL, "timezone" TEXT NOT NULL,
notification TEXT NOT NULL DEFAULT "new" "notification" TEXT NOT NULL,
);`); "deleteDate" INTEGER NULL,
PRIMARY KEY ("event_uid")
);
CREATE UNIQUE INDEX "sqlite_autoindex_events_1" ON "events" ("uid");`);
query.run(); query.run();
} }
static insert ( events: TEventEntityNew[], db: Database ) { 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 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)"
].join(" "));
const insertEvents = db.transaction(events => { const insertEvents = db.transaction(events => {
for (const event of events) insert.run(event); for (const event of events) insert.run(event);
return events.length; return events.length;
@@ -45,7 +66,7 @@ export class Event implements TEventEntity {
console.log(`Inserted ${count} events`); console.log(`Inserted ${count} events`);
} }
static async fetch_events( _year_: number, _month_: number, timezone: number) { static async fetch_events( _year_: number, _month_: number, timezone: number): Promise<TEvent[]> {
const url = `${BASE_URL}&year=${_year_}&month=${_month_}&timezone=${timezone}` const url = `${BASE_URL}&year=${_year_}&month=${_month_}&timezone=${timezone}`
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@@ -55,17 +76,29 @@ export class Event implements TEventEntity {
return events; return events;
} }
static get_events (notification: TEventEntity["notification"][] | null, db: Database ) { static get_events (options: TGetEventsOptions, db: Database ) {
const whereConditions: string[] = []; const whereConditions: string[] = [];
if ( notification ) { if ( options.notification ) {
whereConditions.push( `notification IN ('${ notification.join("', '") }')` ) whereConditions.push( `notification IN ('${ options.notification.join("', '") }')` )
} }
if (options.date) {
whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`);
}
if ( options.month ) {
whereConditions.push( `strftime('%Y-%m', date_at) = '${options.month.year}-${options.month.month}'`)
}
const where = ( () => { const where = ( () => {
let str = "WHERE "; let str = "WHERE ";
if ( whereConditions.length >= 1 ) { if ( options.deleted === true ) {
str += whereConditions.join(" AND "); str += "deleteDate IS NOT NULL AND ";
} else if ( options.deleted === false ) {
str += "deleteDate IS NULL AND ";
} }
return str; if ( whereConditions.length >= 1 ) {
return str += `( ${ whereConditions.join(" OR ") } )`;
}
return null;
})() })()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event); const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
return query.all(); return query.all();
@@ -83,9 +116,10 @@ export class Event implements TEventEntity {
event_type: TEventEntity["event_type"]; event_type: TEventEntity["event_type"];
timezone: string; timezone: string;
link: string; link: string;
notification: TEventEntity["notification"] 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"]) { 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.event_uid = event_uid;
this.uid = uid; this.uid = uid;
this.title = title; this.title = title;
@@ -99,9 +133,10 @@ export class Event implements TEventEntity {
this.timezone = timezone; this.timezone = timezone;
this.link = link; this.link = link;
this.notification = notification; this.notification = notification;
this.deleteDate = deleteDate;
} }
syncWithDb ( db: Database ) { syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM ${Event.table_name} WHERE event_uid = $event_uid;`).as(Event); const query = db.prepare( `SELECT * FROM events WHERE event_uid = $event_uid;`).as(Event);
const entity = query.get({$event_uid: this.event_uid }); 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!`); } if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); }
this.uid = entity.uid; this.uid = entity.uid;
@@ -116,6 +151,7 @@ export class Event implements TEventEntity {
this.timezone = entity.timezone; this.timezone = entity.timezone;
this.link = entity.link; this.link = entity.link;
this.notification = entity.notification; this.notification = entity.notification;
this.deleteDate = entity.deleteDate;
return this; return this;
} }
@@ -126,5 +162,18 @@ export class Event implements TEventEntity {
WHERE event_uid = $event_uid;` WHERE event_uid = $event_uid;`
); );
query.get({$notification: newValue, $event_uid: this.event_uid }); query.get({$notification: newValue, $event_uid: this.event_uid });
return this.syncWithDb( db );
}
set_deleted ( db: Database ) {
const query = db.prepare(
`UPDATE events
SET deleteDate = $deleteDate
WHERE event_uid = $event_uid;`
);
query.get({
$deleteDate: Math.floor((new Date()).getTime() / 1000),
$event_uid: this.event_uid
});
return this.syncWithDb( db );
} }
} }

41
src/notification.py Normal file
View File

@@ -0,0 +1,41 @@
from dotenv import load_dotenv
import os
def main():
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')
dc_botname = os.getenv('dc_botname')
dc_avatar_url = os.getenv('dc_avatar_url')
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"discord://{dc_webhook}?avatar_url={dc_avatar_url}&botname={dc_botname}");
apobj.notify(
body=args.body,
title=args.title
)
if __name__ == '__main__':
main()

26
src/sendNotification.ts Normal file
View File

@@ -0,0 +1,26 @@
export async function sendNotification(title: string, body: string, link?: string | null) {
console.dir({
sendNotification: {
title,
body,
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"
})
});
const responseBody = await response.json();
return responseBody;
}

16
src/sendNotificationPy.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as Bun from "bun";
export async function sendNotificationPy(title: string, body: string, click?: string | null) {
const command = [
"python3",
"./src/notification.py",
`--title=${title}`,
`--body=${body}`,
];
if ( click ) {
command.push(`--click=${click}`);
}
const proc = Bun.spawn(command);
const text = await proc.stdout.text();
console.log("sendNotification: " + text);
}

View File

@@ -10,4 +10,6 @@ export const db = new Database(db_filepath);
export function init () { export function init () {
Event.createTable(db); Event.createTable(db);
} }
init();

View File

@@ -19,4 +19,23 @@ export function prefixKeysWithDollar<T extends Record<string, any>>(obj: T): Add
export function transformArray<T extends Record<string, any>>(arr: T[]): AddDollarPrefix<T>[] { export function transformArray<T extends Record<string, any>>(arr: T[]): AddDollarPrefix<T>[] {
return arr.map(prefixKeysWithDollar); return arr.map(prefixKeysWithDollar);
}
export function pad_l2 ( _thing: string | number ): string {
if ( typeof _thing == "number" ) {
_thing = JSON.stringify(_thing);
};
return _thing.padStart(2, "0");
}
export 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;
} }

View File

@@ -1,7 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": [
"ESNext",
"es2015.promise"
],
"target": "ESNext", "target": "ESNext",
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",