diff --git a/.gitignore b/.gitignore index 398a986..5d4c927 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json *.db *.sqlite +data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c4a8a55..d23c855 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,44 +1,59 @@ FROM debian:12 AS base +ARG BUILD_DATE +ARG VERSION +LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}" +LABEL maintainer="chiko " WORKDIR /opt/app -RUN apt-get update && \ - apt-get install -y curl unzip ca-certificates python3 python3-pip && \ - rm -rf /var/lib/apt/lists/* +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 && \ + echo "Cleaning up" && \ + apt-get --yes autoremove --purge && \ + apt-get clean --yes && \ + 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 RUN curl -fsSL https://bun.com/install | bash ENV PATH="/root/.bun/bin:$PATH" -# symlink python3 to python -RUN ln -s /usr/bin/python3 /usr/bin/python - # install dependencies into temp directory # this will cache them and speed up future builds FROM base AS install RUN mkdir -p /temp/dev COPY package.json bun.lock /temp/dev/ RUN cd /temp/dev && bun install --frozen-lockfile -# and install python dependencies -COPY ./requirements.txt . -RUN pip3 install --break-system-packages -r ./requirements.txt # install with --production (exclude devDependencies) RUN mkdir -p /temp/prod COPY package.json bun.lock /temp/prod/ RUN cd /temp/prod && bun install --frozen-lockfile --production +COPY ./docker/Crontab /etc/cron.d/ +RUN chmod 0644 /etc/cron.d/Crontab +COPY ./docker/cron-bun-log /etc/logrotate.d/ +RUN mkdir /var/log/cron && touch /var/log/cron.log + # copy node_modules from temp directory # then copy all (non-ignored) project files into the image FROM base AS prerelease COPY --from=install /temp/dev/node_modules node_modules -COPY . . - +COPY . ./ # [optional] tests & build ENV NODE_ENV=production # copy production dependencies and source code into final image FROM base AS release COPY --from=install /temp/prod/node_modules node_modules -COPY --from=prerelease /opt/app/src/app.ts . COPY --from=prerelease /opt/app/package.json . -VOLUME ["/opt/app/data/db"] -# run the app -USER bun -ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file +#COPY --from=prerelease .entrypoint.sh . +COPY . ./ +RUN mkdir /var/log/cron && touch /var/log/cron.log +VOLUME /opt/app/data/db +# VOLUME /var/log/cron +CMD bun run ./src/app.ts --today && cron && tail -f /var/log/cron.log \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ebcd829 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + app: + build: . + volumes: + - ./data/db:/opt/app/data/db + - ./data/app/log:/var/log + env_file: + - path: ./.env + required: true + depends_on: + apprise: + condition: service_healthy + links: + - apprise + apprise: + image: caronc/apprise:latest + 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: 10s + timeout: 5s + retries: 5 +# networks: +# default: +# external: true +# name: npm \ No newline at end of file diff --git a/docker/Crontab b/docker/Crontab new file mode 100644 index 0000000..0eee489 --- /dev/null +++ b/docker/Crontab @@ -0,0 +1,2 @@ +8 * * * * bun run ./src/app.ts --today > /var/log/cron.log 2>&1 +*/15 * * * * bun run ./src/app.ts > /var/log/cron.log 2>&1 \ No newline at end of file diff --git a/docker/cron-bun-log b/docker/cron-bun-log new file mode 100644 index 0000000..a5f9fb4 --- /dev/null +++ b/docker/cron-bun-log @@ -0,0 +1,8 @@ +/var/log/cron.log { + daily + rotate 7 + compress + missingok + notifempty + copytruncate +} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index f3fc625..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 \ No newline at end of file diff --git a/package.json b/package.json index 32b5b91..434a70f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dev:init": "bun run ./src/app.ts --init", "db:init": "bun run ./run/db_init.ts", "db:deleteall": "bun run ./run/db_deleteall.ts", - "build": "bun build ./src/app.ts --compile --outfile ./build/77th_event_calendar_notification", + "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": { diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 356ca44..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -apprise \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 70c6df5..723d77b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,31 +1,25 @@ import { TEventType } from "./component/event/event.types"; 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 { createPlaceholders, pad_l2 } from "./util"; const argv = require('minimist')(process.argv.slice(2)); -console.dir(argv) +console.log("App started"); +console.dir({argv}) -// const TS_TODAY = new Date(); - -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( ) { +async function main ( ) { + console.log("Excecuting main()"); const TODAY = getTsNow(); + console.dir(TODAY); const events_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 ); + console.log("events_currentMonth.length:" + events_currentMonth.length ); const events_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 ); + console.log("events_nextMonth.length:" + events_nextMonth.length ); const events = [...events_currentMonth, ...events_nextMonth]; + console.log("events.length:" + events.length ); + + // const TS_TODAY = new Date(); // 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()}`; @@ -33,16 +27,19 @@ async function main( ) { // Write to JSON File Section END const allEventUids = events.map( event => { return event.uid; }); + console.dir(allEventUids ); const placeholders = createPlaceholders( allEventUids ); const getAllRelevantEventsQuery = db.query( `SELECT * FROM events WHERE uid IN (${placeholders}); ` ).as(Event ); const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids); - + console.log("AllRelevantEvents.length:" + AllRelevantEvents.length ); const eventsToInsert: TEventEntityNew[] = []; 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); if ( found ) { + console.log("loop ev found: " + [ found.uid, found.title, found.date_at ].join( ", " ) ); if ( found.title != ev.title || found.description != ev.description || @@ -55,29 +52,34 @@ async function main( ) { 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); - const options: TGetEventsOptions = { - } - if (argv.today) { - options.date = { + const where: TGetEventsOptions = {} + where.notification = ["new", "changed"] + if ( argv.today ) { + where.date = { year: TODAY.year, month: TODAY.month, day: TODAY.day } - } else { - options.notification = ["new", "changed"] } - const list_of_events = Event.get_events( options, db ); + 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 = [ `Title: ${ev.title}`, `Location: ${ev.location}`, @@ -87,6 +89,7 @@ async function main( ) { `By: ${ev.posted_by}`, `Link: ${ev.link}`, ].join("\n"); + console.log("loop list_of_events - ev 'body': " + body ); const notification_prefix = ( (event: Event) => { switch( event.notification) { case "new": @@ -112,11 +115,9 @@ async function main( ) { } 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"}` - ); + const title = `${today_prefix ? "TODAY " : ""}${notification_prefix ? notification_prefix + ": " : ""} ${ev.title} (${ TEventType[ ev.event_type ] })`; + console.log("loop list_of_events - ev 'title': " + title ); + await sendNotification( title, body, ev.link ? ev.link : null); ev.set_notification("done", db); } }; diff --git a/src/notification.py b/src/notification.py index cda75a9..13a3dfb 100644 --- a/src/notification.py +++ b/src/notification.py @@ -1,37 +1,41 @@ 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') -dc_botname = os.getenv('dc_botname') -dc_avatar_url = os.getenv('dc_avatar_url') +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 + 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) + 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 = 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 -) \ No newline at end of file + apobj.notify( + body=args.body, + title=args.title + ) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/sendNotification.ts b/src/sendNotification.ts index cb0a754..66e61d4 100644 --- a/src/sendNotification.ts +++ b/src/sendNotification.ts @@ -1,14 +1,26 @@ -import * as Bun from "bun"; - -export function sendNotification(title: string, body: string, click?: string | null) { - const command = [ - "python", - "./app/notification.py", - `--title=${title}`, - `--body=${body}`, - ]; - if (click) { - command.push(`--click=${click}`); +export async function sendNotification(title: string, body: string, link?: string | null) { + console.dir({ + sendNotification: { + title, + body, + link } - Bun.spawn(command); -} + }); + 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: "text" + }) + }); + const responseBody = await response.json(); + return responseBody; +} \ No newline at end of file diff --git a/src/sendNotificationPy.ts b/src/sendNotificationPy.ts new file mode 100644 index 0000000..2fbf339 --- /dev/null +++ b/src/sendNotificationPy.ts @@ -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); +} diff --git a/src/sql.ts b/src/sql.ts index 04be71c..6d55878 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -10,4 +10,6 @@ export const db = new Database(db_filepath); export function init () { Event.createTable(db); -} \ No newline at end of file +} + +init(); \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 7d3314a..6f2f7ed 100644 --- a/src/util.ts +++ b/src/util.ts @@ -26,4 +26,16 @@ export function pad_l2 ( _thing: string | number ): string { _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; } \ No newline at end of file