1 Commits

Author SHA1 Message Date
999bdf7222 fix-docker (#1)
Reviewed-on: #1
2025-10-21 00:52:32 +00:00
37 changed files with 212 additions and 949 deletions

View File

@@ -9,7 +9,7 @@ LICENSE
.vscode .vscode
Makefile Makefile
helm-charts helm-charts
.env* .env
.editorconfig .editorconfig
.idea .idea
coverage* coverage*

View File

@@ -1,15 +1,9 @@
TZ=Europe/Berlin
DB_FILEPATH=./data/db
DB_FILENAME=77th_eventntfy.db
apprise_https=false
apprise_hostname=apprise
apprise_port=8000
notification_mock=true
ntfy_on=true ntfy_on=true
ntfy_username=chiko ntfy_username=chiko
ntfy_password=Blub ntfy_password=Blub
ntfy_host=ntfy.some-service.com ntfy_host=ntfy.some-service.com
ntfy_topic=SomeTopic ntfy_topic=SomeTopic
dc_on=true dc_on=true
dc_webhook=123123123123123/ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF dc_webhook=123123123123123/ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF
dc_botname=Botname Here dc_botname=Botname Here

View File

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

View File

@@ -1,17 +0,0 @@
{
"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
}
]
}
}

View File

@@ -1,17 +0,0 @@
/* 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

@@ -23,7 +23,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
.env.*
# caches # caches
.eslintcache .eslintcache
@@ -38,4 +37,3 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
*.db *.db
*.sqlite *.sqlite
data

View File

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

2
Crontab Normal file
View File

@@ -0,0 +1,2 @@
1 * * * * bun run ./src/app.ts --today > /dev/null 2>&1
0 * * * * bun run ./src/app.ts > /dev/null 2>&1

View File

@@ -1,32 +1,12 @@
FROM debian:12 AS base FROM debian:12 AS base
ARG BUILD_DATE
ARG VERSION
LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}"
LABEL maintainer="chiko <chiko@xcsone.de>"
ENV TZ=Europe/Berlin
WORKDIR /opt/app WORKDIR /opt/app
RUN set -eux && \ RUN apt-get update && \
echo "Updating APT" && \ # apt-get install -y curl unzip cron ca-certificates python3 python3-pip && \
apt-get update -y -qq && \ apt-get install -y curl unzip cron ca-certificates python3 python3-pip && \
apt-get upgrade -y -qq && \ rm -rf /var/lib/apt/lists/*
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"
RUN ln -s /root/.bun/bin/bun /usr/local/bin/bun
# 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
@@ -39,26 +19,33 @@ 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
# and install python dependencies
# COPY ./requirements.txt .
# RUN python3 -m pip install --break-system-packages -r requirements.txt
# RUN python3 -m pip install -U python-dotenv
# 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 WORKDIR /opt/app
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
# COPY ./docker/cron-bun-log /etc/logrotate.d/ COPY --from=prerelease /opt/app/src/app.ts .
COPY ./docker/Crontab /etc/cron.d/ COPY --from=prerelease /opt/app/package.json .
RUN chmod 0644 /etc/cron.d/Crontab && \ #COPY --from=prerelease .entrypoint.sh .
chown root:root /etc/cron.d/Crontab COPY Crontab /etc/cron.d/
COPY . . RUN chmod 0644 /etc/cron.d/Crontab
RUN dos2unix \ COPY . ./
/opt/app/docker/docker-entrypoint.sh \ # USER bun
/opt/app/run-task.sh \ RUN touch /var/log/cron.log
/etc/cron.d/Crontab # RUN chmod +x entrypoint.sh
RUN chmod +x /opt/app/docker/docker-entrypoint.sh && \ # ENTRYPOINT ["./entrypoint.sh"]
chmod +x /opt/app/run-task.sh VOLUME /opt/app/data/db
ENTRYPOINT [ "/opt/app/docker/docker-entrypoint.sh" ] CMD bun run ./src/app.ts --today && cron && tail -f /var/log/cron.log

View File

@@ -1,27 +1,15 @@
# 77th Event Calendar Notifcations # 77th Event Calender Notifcations
To install dependencies: To install dependencies:
```bash ```bashe
bun install bun install
``` ```
To run: To run:
```bash ```bash
bun run ./src/app.ts bun run index.ts
bun run start
bun run dev
``` ```
## Docker This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
```bash
docker compose build
docker compose up -d
```
## Parameter
### --today
fetch all Events, track all Changes (new, changed and deleted Events) and additionally Send a Notification for todays mission

254
bun.lock
View File

@@ -8,282 +8,30 @@
"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.9.3", "typescript": "^5",
}, },
}, },
}, },
"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=="],
} }
} }

View File

@@ -1,6 +1,5 @@
services: services:
app: app:
image: chiko/77th_eventcalendarntfy:v0.1.5
build: . build: .
volumes: volumes:
- ./data/db:/opt/app/data/db - ./data/db:/opt/app/data/db
@@ -13,7 +12,7 @@ services:
links: links:
- apprise - apprise
apprise: apprise:
image: caronc/apprise:1.2.2 image: caronc/apprise:latest
hostname: apprise hostname: apprise
environment: environment:
- APPRISE_WORKER_COUNT=1 - APPRISE_WORKER_COUNT=1
@@ -24,10 +23,14 @@ services:
- ./data/apprise/config:/config - ./data/apprise/config:/config
- ./data/apprise/plugin:/plugin - ./data/apprise/plugin:/plugin
- ./data/apprise/attach:/attach - ./data/apprise/attach:/attach
# ports: ports:
#- 8880:8000 - 8000:8000
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/status"] test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
interval: 5s interval: 30s
timeout: 3s timeout: 10s
retries: 5 retries: 5
# networks:
# default:
# external: true
# name: npm

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
#!/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=(
NODE_ENV
TZ
DB_FILEPATH
DB_FILENAME
apprise_https
apprise_hostname
apprise_port
notification_mock
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

6
entrypoint.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
crontab -l > mycron
echo "0 8 * * * bun run ./src/app.ts --today > /dev/null 2>&1" >> mycron
echo "0 * * * * bun run ./src/app.ts > /dev/null 2>&1" >> mycron
crontab mycron
rm mycron

View File

@@ -1,9 +0,0 @@
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,32 +1,23 @@
{ {
"version": "0.1.5", "version": "0.1.0",
"name": "77th_eventcalendarnotification", "name": "eventcalender",
"module": "./src/app.ts", "module": "./src/app.ts",
"type": "module", "type": "module",
"private": true, "private": true,
"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"
}, },
"scripts": { "scripts": {
"start": "bun run ./src/app.ts", "dev": "bun run ./src/app.ts",
"dev": "NODE_ENV=development bun ./src/app.ts", "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_event_deleteall.ts", "db:deleteall": "bun run ./run/db_deleteall.ts",
"db:event:dedup": "bun run ./run/db_event_delete_duplicates.ts", "build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_event_calendar_notification",
"build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_eventcalendarnotification", "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_event_calendar_notification",
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_eventcalendarnotification",
"docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ." "docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ."
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.9.3" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.2.3", "dotenv": "^17.2.3",

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
apprise
python-dotenv

View File

@@ -1,26 +0,0 @@
#!/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 start "$@" >> /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,10 +0,0 @@
import * as db from "../src/sql";
const query = db.db.query(`DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);`);
query.run();

View File

@@ -1,7 +0,0 @@
import db from "../src/sql";
db.run(
`UPDATE events
SET notification = 'done'
WHERE deleteDate IS NOT NULL;`
);

View File

@@ -1,39 +0,0 @@
import db from "../src/sql";
const run_migration = db.transaction(() => {
// SQL 1: remove duplicates by uid
db.run(`DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);`);
// SQL 2: create new table with unique key
db.run(`CREATE TABLE events_new (
"event_uid" INTEGER PRIMARY KEY,
"uid" TEXT NOT NULL UNIQUE,
"title" TEXT NOT NULL,
"date_at" DATETIME NOT NULL,
"time_start" TEXT NOT NULL,
"time_end" TEXT NOT NULL,
"posted_by" TEXT NOT NULL,
"location" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"link" TEXT NOT NULL,
"description" TEXT NOT NULL,
"timezone" TEXT NOT NULL,
"notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL
);`);
// SQL 3: Log the transaction
db.run(`INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate)
SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events;
`);
db.run(`DROP TABLE events;
ALTER TABLE events_new RENAME TO events;`);
});
// Run the transaction
run_migration();

View File

@@ -1 +0,0 @@
CREATE UNIQUE INDEX idx_events_uid ON events(uid);

View File

@@ -1,6 +0,0 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);

View File

@@ -1,4 +0,0 @@
SELECT uid, COUNT(*) AS count
FROM events
GROUP BY uid
HAVING COUNT(*) > 1;

View File

@@ -1,29 +0,0 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);
CREATE TABLE events_new (
"event_uid" INTEGER PRIMARY KEY,
"uid" TEXT NOT NULL UNIQUE,
"title" TEXT NOT NULL,
"date_at" DATETIME NOT NULL,
"time_start" TEXT NOT NULL,
"time_end" TEXT NOT NULL,
"posted_by" TEXT NOT NULL,
"location" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"link" TEXT NOT NULL,
"description" TEXT NOT NULL,
"timezone" TEXT NOT NULL,
"notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL
);
INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate)
SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events;
DROP TABLE events;
ALTER TABLE events_new RENAME TO events;

View File

@@ -1,51 +1,48 @@
import { TEventType } from "./component/event/event.types";
import { db } from "./sql"; import { db } from "./sql";
import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events"; import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events";
import { createPlaceholders, getTsNow } from "./util";
import { sendNotification } from "./sendNotification"; import { sendNotification } from "./sendNotification";
import minimist from "minimist"; import { createPlaceholders, pad_l2 } from "./util";
const argv = minimist(process.argv.slice(2))
console.log("App started");
console.dir({argv})
const TODAY = getTsNow(); const argv = require('minimist')(process.argv.slice(2));
console.dir({TODAY}); console.dir(argv)
async function events_update_db() { // const TS_TODAY = new Date();
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; }); function getTsNow() {
console.dir( {events_fetched_list_of_uids} ); const now = new Date();
const rtn = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
minute: now.getMinutes(),
seconds: now.getSeconds()
}
return rtn;
}
const events_db_currentMonth = Event.get_events({month: {year: TODAY.year, month: TODAY.month}}, db); async function main( ) {
const events_db_nextMonth = Event.get_events({month: {year: TODAY.year, month: (TODAY.month + 1)}}, db); const TODAY = getTsNow();
const events_db = [... events_db_currentMonth, ... events_db_nextMonth]; const events_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 );
const events_removed: Event[] = events_db.filter( (ev) => { const events_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 );
return ! events_fetched_list_of_uids.includes(ev.uid); const events = [...events_currentMonth, ...events_nextMonth];
}); // 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
console.dir({events_removed}); const allEventUids = events.map( event => { return event.uid; });
const placeholders = createPlaceholders( allEventUids );
events_removed.forEach( ev => {
ev.set_notification("removed", db);
});
const placeholders = createPlaceholders( events_fetched_list_of_uids );
const getAllRelevantEventsQuery = db.query( const getAllRelevantEventsQuery = db.query(
`SELECT * FROM events WHERE uid IN (${placeholders}) AND deleteDate IS NULL;` `SELECT * FROM events WHERE uid IN (${placeholders}); `
).as(Event ); ).as(Event );
const AllRelevantEvents = getAllRelevantEventsQuery.all(...events_fetched_list_of_uids); const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids);
console.log("AllRelevantEvents.length: " + AllRelevantEvents.length );
const eventsToInsert: TEventEntityNew[] = []; const eventsToInsert: TEventEntityNew[] = [];
for ( const ev of events_fetched ) { for ( const ev of events ) {
console.log("loop ev " + ev.uid + " : " + [ ev.title, ev.date_at ].join( ", " ) );
const found = AllRelevantEvents.find(event => event.uid === ev.uid); const found = AllRelevantEvents.find(event => event.uid === ev.uid);
if ( found ) { if ( found ) {
console.log("loop ev " + ev.uid + " f: " + [ found.title, found.date_at ].join( ", " ) );
if ( if (
found.title != ev.title || found.title != ev.title ||
found.description != ev.description || found.description != ev.description ||
@@ -58,60 +55,87 @@ async function events_update_db() {
found.timezone != ev.timezone || found.timezone != ev.timezone ||
found.link != ev.link found.link != ev.link
) { ) {
console.log("loop ev " + ev.uid + " c: " + [ ev.title, ev.date_at ].join( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} else { } else {
console.log("loop ev " + ev.uid + " n: " + [ ev.title, ev.date_at ].join( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "new"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} }
console.dir({eventsToInsert})
Event.insert( eventsToInsert, db);
}
async function events_check_for_notification() { Event.insert( eventsToInsert, db);
const where: TGetEventsOptions = { const options: TGetEventsOptions = {
notification: ["new", "changed", "removed"],
deleted: false
} }
if ( argv.today ) { if (argv.today) {
where.date = { options.date = {
year: TODAY.year, year: TODAY.year,
month: TODAY.month, month: TODAY.month,
day: TODAY.day day: TODAY.day
} }
} else {
options.notification = ["new", "changed"]
} }
const list_of_events = Event.get_events( where, db ); const list_of_events = Event.get_events( options, db );
console.dir({
list_of_events,
where
});
for ( const ev of list_of_events ) { 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 = [
console.log("loop list_of_events - ev 'title': " + ev.get_title() ); `Title: ${ev.title}`,
const notificationOptions = { `Location: ${ev.location}`,
ntfy: null, `Type: ${ TEventType[ ev.event_type ] }`,
discord: { `Date: ${ev.date_at}`,
avatar_url: ( process.env.dc_avatar_url as string), `Time: ${ev.time_start}`,
botname: ( process.env.dc_botname as string) `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 );
await sendNotification( ev.get_title(), ev.get_body(), notificationOptions );
if ( ev.notification == "removed" ) { const today_prefix = ( (ev: Event) => {
ev.set_deleted( db ); const now = getTsNow();
} else { const [year, month, day] = ev.date_at.split("-")
ev.set_notification("done", db); if (
} year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
})( ev );
const title = `${today_prefix ? "TODAY " : ""}${notification_prefix ? notification_prefix + ": " : ""} ${ev.title} (${ TEventType[ ev.event_type ] })`;
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}${ ev.link ? `?click=${ev.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: "text"
})
});
// await sendNotification(
// title,
// body
// // `${ev.link || "https://77th-jsoc.com/#/events"}`
// );
ev.set_notification("done", db);
} }
}
async function main ( ) {
console.log("Excecuting main()");
await events_update_db();
await events_check_for_notification();
}; };
main(); main();

View File

@@ -15,6 +15,5 @@ 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

@@ -1,6 +1,6 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { TEventType, type TEvent } from "./event.types"; import type { TEvent } from "./event.types";
import { getTsNow, pad_l2, transformArray, formatTimeDiff, isEuropeanDST, subtractHours } from "../../util"; 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";
@@ -10,64 +10,38 @@ export type TGetEventsOptions = {
year: number, year: number,
month: number, month: number,
day: 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" | "removed" | "done" | "deleted" notification: "new" | "changed" | "deleted" | "done"
} }
export type TEventEntityNew = Omit<TEventEntity, "event_uid"> 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"
event_uid: number;
uid: string;
title: string;
description: string;
date_at: string;
time_start: string;
time_end: string;
posted_by: string;
location: string;
event_type: TEventEntity["event_type"];
timezone: string;
link: string;
notification: TEventEntity["notification"];
deleteDate: TEventEntity["deleteDate"];
static createTable (db: Database): void { 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 PRIMARY KEY,
"uid" TEXT NOT NULL UNIQUE, uid TEXT NOT NULL UNIQUE,
"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, notification TEXT NOT NULL DEFAULT "new"
"deleteDate" INTEGER NULL
);`); );`);
query.run(); query.run();
} }
static insert ( events: TEventEntityNew[], db: Database ) { static insert ( events: TEventEntityNew[], db: Database ) {
const insert = db.prepare( [ 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)");
"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;
@@ -89,36 +63,40 @@ export class Event implements TEventEntity {
return events; return events;
} }
static get_events ( options: TGetEventsOptions, db: Database ) { static get_events (options: TGetEventsOptions, db: Database ) {
const whereConditions: string[] = []; const whereConditions: string[] = [];
if ( options.notification ) { if ( options.notification ) {
whereConditions.push( `notification IN ('${ options.notification.join("', '") }')` ) whereConditions.push( `notification IN ('${ options.notification.join("', '") }')` )
} }
if ( options.date ) { if (options.date) {
whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`); 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 ( options.deleted === true ) {
str += "deleteDate IS NOT NULL AND ";
} else if ( options.deleted === false ) {
str += "deleteDate IS NULL AND ";
}
if ( whereConditions.length >= 1 ) { if ( whereConditions.length >= 1 ) {
return str += `( ${ whereConditions.join(" OR ") } )`; str += whereConditions.join(" OR ");
} }
return null; return str;
})() })()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event); const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
console.dir({ db: { action: {get_events: query} } })
return query.all(); return query.all();
} }
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"]) { 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.event_uid = event_uid;
this.uid = uid; this.uid = uid;
this.title = title; this.title = title;
@@ -132,29 +110,9 @@ 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;
} }
toString() {
return {
event_uid: this.event_uid,
uid: this.uid,
title: this.title,
description: this.description,
date_at: this.date_at,
time_start: this.time_start,
time_end: this.time_end,
posted_by: this.posted_by,
location: this.location,
event_type: this.event_type,
timezone: this.timezone,
link: this.link,
notification: this.notification,
deleteDate: this.deleteDate
}
}
syncWithDb ( db: Database ) { syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM events WHERE event_uid = $event_uid;`).as(Event); 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 }); 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;
@@ -169,7 +127,6 @@ 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;
} }
@@ -180,76 +137,5 @@ 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 notification = 'deleted',
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 );
}
get_title() {
const type_of_notification = ( (event: Event) => {
switch ( event.notification ) {
case "new":
return "New";
case "changed":
return "Changed";
case "removed":
return "Removed";
default:
return null;
}
} ) ( this );
const title_prefix_arr = [];
if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" );
if ( this.isEventToday() ) title_prefix_arr.push( "<TODAY>" )
return `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${this.title} (${ TEventType[ this.event_type ] })`;
}
get_body() {
const BaseTime = new Date(`${this.date_at} 21:00`);
const RelativeEventTime = new Date(`${this.date_at} ${this.get_time_start()}`);
const TimeDiff = formatTimeDiff( BaseTime, RelativeEventTime);
const body = [
`Title: ${this.title}`,
`Date: ${this.date_at}`,
`Time: ${this.get_time_start()} (OP Time${ TimeDiff != "00:00" ? ` ${TimeDiff}` : "" })`,
`Type: ${ TEventType[ this.event_type ] }`,
`Location: ${this.location}`,
`By: ${this.posted_by}`,
`Link: ${this.link}`,
].join("\n");
return body;
}
isEventToday ( ) {
const now = getTsNow();
const [year, month, day] = this.date_at.split("-")
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
}
get_time_start () {
const date = new Date( `${this.date_at} ${this.time_start}` );
if ( ! isEuropeanDST( date ) ) {
const newDate = subtractHours( date, 1);
const hours = newDate.getHours();
const minutes = newDate.getMinutes();
return `${pad_l2(hours)}:${pad_l2(minutes)}`;
}
return this.time_start;
} }
} }

View File

@@ -1,16 +0,0 @@
export const config = {
apprise: {
services: {
ntfy: {
url: `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`,
defaults: {
}
}
},
urls: [
`ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`,
`discord://${process.env.dc_webhook}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}`
]
}
} as const

View File

@@ -1,49 +1,16 @@
import { createQS } from "./util"; import * as Bun from "bun";
type TSendNotificationOptions = { export async function sendNotification(title: string, body: string, click?: string | null) {
ntfy: { const command = [
link?: string; "python3",
} | null, "./src/notification.py",
discord: { `--title=${title}`,
href?: string `--body=${body}`,
avatar_url: string, ];
botname: string if ( click ) {
} command.push(`--click=${click}`);
}
export async function sendNotification( title: string, body: string, options: TSendNotificationOptions ) {
console.dir({
sendNotification: {
title,
body
} }
}); const proc = Bun.spawn(command);
const QS = { const text = await proc.stdout.text();
ntfy: options.ntfy ? createQS(options.ntfy) : null, console.log("sendNotification: " + text);
discord: createQS(options.discord)
}
if ( ! ( process.env.notification_mock == "true" ) ) {
const response = await fetch(`${ process.env.apprise_https == "true" ? "https" : "http"}://${process.env.apprise_host ? process.env.apprise_host : "apprise"}:${process.env.apprise_port ? String(process.env.apprise_port) : "80" }/notify`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
urls: [
`ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}${ QS.ntfy ? "?" + QS.ntfy : ""}`,
`discord://${process.env.dc_webhook}?${QS.discord}`
].join(","),
title: title,
body: body,
format: "markdown"
})
});
const responseBody = await response.json();
return responseBody;
} else {
console.dir({
sendNotification: "mocking"
})
}
} }

View File

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

@@ -8,8 +8,6 @@ console.log(db_filepath);
export const db = new Database(db_filepath); export const db = new Database(db_filepath);
export default db;
export function init () { export function init () {
Event.createTable(db); Event.createTable(db);
} }

View File

@@ -27,72 +27,3 @@ export function pad_l2 ( _thing: string | number ): string {
}; };
return _thing.padStart(2, "0"); 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;
}
export function unixToDate( unix_timestamp: number ) { return new Date(unix_timestamp * 1000) }
export function dateToUnix( date: Date ) { return Math.round( date.getTime()/1000 ) }
export function formatTimeDiff(dateA: Date, dateB: Date) {
// Difference in milliseconds
const diffMs = dateB.getTime() - dateA.getTime();
// Get sign (+ or -)
const sign = diffMs < 0 ? "-" : "";
// Convert to absolute minutes
const diffMinutes = Math.floor(Math.abs(diffMs) / 60000);
// Split into hours and minutes
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
// Return formatted string
return `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
}
export function subtractHours(date: Date, hours: number) {
// Create a new Date so we don't mutate the original
return new Date(date.getTime() - hours * 60 * 60 * 1000);
}
// Helper: get last Sunday of a given month
function lastSundayOfMonth(year: number, month: number ) {
const lastDay = new Date(Date.UTC(year, month + 1, 0)); // last day of month
const day = lastDay.getUTCDay(); // 0 = Sunday
const diff = day === 0 ? 0 : day; // how far back to go to reach Sunday
lastDay.setUTCDate(lastDay.getUTCDate() - diff);
return lastDay;
}
export function isEuropeanDST( date: Date ) {
const year = date.getFullYear();
// DST starts: last Sunday in March, 01:00 UTC
const start = lastSundayOfMonth(year, 2); // March (month = 2)
start.setUTCHours(1, 0, 0, 0);
// DST ends: last Sunday in October, 01:00 UTC
const end = lastSundayOfMonth(year, 9); // October (month = 9)
end.setUTCHours(1, 0, 0, 0);
// Return true if within DST period
return date >= start && date < end;
}
export function createQS (params: Record<string, string | number | boolean>): string {
const queryString = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
return queryString;
}

View File

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