diff --git a/bun.lock b/bun.lock index 21b6f11..cc3cda7 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "daily-digest", "dependencies": { "dayjs": "^1.11.20", + "eventsource": "^4.1.0", + "node-cron": "^4.6.0", }, "devDependencies": { "@types/bun": "latest", @@ -23,6 +26,12 @@ "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + "eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "node-cron": ["node-cron@4.6.0", "", {}, "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], diff --git a/fx.ts b/fx.ts new file mode 100644 index 0000000..fa7934b --- /dev/null +++ b/fx.ts @@ -0,0 +1,124 @@ +import { $ } from "bun" + +const API = +"https://api.frankfurter.app/latest?from=USD&to=INR" + +export async function getUSD() { + const res = await fetch(API) + + if (!res.ok) { + throw new Error(`Exchange API failed: ${res.status}`) + } + + const data = await res.json() + + const rate = (data as { rates: { INR: number } }).rates.INR + + if (typeof rate !== "number") { + throw new Error("Invalid exchange rate") + } + + const helions = (rate / 100).toFixed(2) + + return `**📈 Exchange Rate** +$1 = ₹${rate.toFixed(2)} = ⚙${helions} + +` +} + + +export async function getUsage() { + // + // CPU LOAD + // + + const loadavg = await $`cat /proc/loadavg`.text() + const [load1] = loadavg.trim().split(" ") + + // + // MEMORY + // + + const memRaw = await $`free -b`.text() + + const memLine = memRaw + .split("\n") + .find((line) => line.startsWith("Mem:")) + + if (!memLine) { + throw new Error("Failed to parse memory info") + } + + const [ + , + totalMem, + usedMem, + ] = memLine.trim().split(/\s+/) + + // + // DISK + // + + const diskRaw = await $`df -B1 /`.text() + + const diskLine = diskRaw + .split("\n")[1] + + if (!diskLine) { + throw new Error("Failed to parse disk info") + } + + const [ + , + totalDisk, + usedDisk, + ] = diskLine.trim().split(/\s+/) + + const temp = await $`vcgencmd measure_temp`.text() + const tempData = temp.split("=")?.[1]?.replace("'", "°") + + // + // HELPERS + // + + function formatBytesIEC(bytes: number) { + const units = ["B", "KiB", "MiB", "GiB", "TiB"] + + let i = 0 + let value = bytes + + while (value >= 1024 && i < units.length - 1) { + value /= 1024 + i++ + } + + return `${value.toFixed(1)} ${units[i]}` + } + + // + // OUTPUT + // + + return `**💾 Usage** +• **CPU:** ${load1} +• **Memory:** ${formatBytesIEC(Number(usedMem))} / ${formatBytesIEC(Number(totalMem))} +• **Disk:** ${formatBytesIEC(Number(usedDisk))} / ${formatBytesIEC(Number(totalDisk))} +• **Temperature:** ${tempData} +` +} + + +export async function getWeather() { + const res = await fetch(`https://wttr.in/Bengaluru?format="%c%C| %f | %h | %m | %w | %p"`) + const data = await res.text() + const [weather, temp, humidity, moon, winds, preci] = data.replace(/\"/g, '').split("|") + + return `**🌥️ Weather Report** +• **Weather:** ${weather} +• **Temperature:** ${temp} +• **Humidity:** ${humidity} +• **Moon Phase:** ${moon} +• **Winds:** ${winds} +• **Precipitation:** ${preci} +` +} \ No newline at end of file diff --git a/index.ts b/index.ts index af4613b..ee7b4a9 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,9 @@ import { $ } from "bun" +import cron from "node-cron" import dayjs from "dayjs" import "dayjs/locale/es" +import { getUSD, getUsage, getWeather } from "./fx" +import { EventSource } from "eventsource" const TOPIC = "daily-digest" const TOKEN = process.env.NTFY_TOKEN @@ -10,130 +13,6 @@ if (!TOKEN) { process.exit(1) } -const API = -"https://api.frankfurter.app/latest?from=USD&to=INR" - -async function getUSD() { - const res = await fetch(API) - - if (!res.ok) { - throw new Error(`Exchange API failed: ${res.status}`) - } - - const data = await res.json() - - const rate = data.rates.INR - - if (typeof rate !== "number") { - throw new Error("Invalid exchange rate") - } - - const helions = (rate / 100).toFixed(2) - - return `**📈 Exchange Rate** -$1 = ₹${rate.toFixed(2)} = ⚙${helions} - -` -} - - -async function getUsage() { - // - // CPU LOAD - // - - const loadavg = await $`cat /proc/loadavg`.text() - const [load1] = loadavg.trim().split(" ") - - // - // MEMORY - // - - const memRaw = await $`free -b`.text() - - const memLine = memRaw - .split("\n") - .find((line) => line.startsWith("Mem:")) - - if (!memLine) { - throw new Error("Failed to parse memory info") - } - - const [ - , - totalMem, - usedMem, - ] = memLine.trim().split(/\s+/) - - // - // DISK - // - - const diskRaw = await $`df -B1 /`.text() - - const diskLine = diskRaw - .split("\n")[1] - - if (!diskLine) { - throw new Error("Failed to parse disk info") - } - - const [ - , - totalDisk, - usedDisk, - ] = diskLine.trim().split(/\s+/) - - const temp = await $`vcgencmd measure_temp`.text() - const tempData = temp.split("=")[1].replace("'", "°") - - // - // HELPERS - // - - function formatBytesIEC(bytes: number) { - const units = ["B", "KiB", "MiB", "GiB", "TiB"] - - let i = 0 - let value = bytes - - while (value >= 1024 && i < units.length - 1) { - value /= 1024 - i++ - } - - return `${value.toFixed(1)} ${units[i]}` - } - - // - // OUTPUT - // - - return `**💾 Usage** -• **CPU:** ${load1} -• **Memory:** ${formatBytesIEC(Number(usedMem))} / ${formatBytesIEC(Number(totalMem))} -• **Disk:** ${formatBytesIEC(Number(usedDisk))} / ${formatBytesIEC(Number(totalDisk))} -• **Temperature:** ${tempData} -` -} - - -async function getWeather() { - const res = await fetch(`https://wttr.in/Bengaluru?format="%c%C| %f | %h | %m | %w | %p"`) - const data = await res.text() - const [weather, temp, humidity, moon, winds, preci] = data.replace(/\"/g, '').split("|") - - return `**🌥️ Weather Report** -• **Weather:** ${weather} -• **Temperature:** ${temp} -• **Humidity:** ${humidity} -• **Moon Phase:** ${moon} -• **Winds:** ${winds} -• **Precipitation:** ${preci} -` - - console.log(data) -} async function main() { @@ -175,7 +54,34 @@ ${usage} console.log("Notification sent at " + dayjs().format("YYYY-MM-dd HH:mm:ss")) } -main().catch((err) => { - console.error(err) - process.exit(1) +cron.schedule("0 7 * * *", () => { + main().catch((err) => { + console.error(err) + }) }) + + +const es = new EventSource("https://ntfy.hy-pr.link/daily-digest/sse", { + fetch: (input, init) => + fetch(input, { + ...init, + headers: { + ...init?.headers, + Authorization: `Bearer ${TOKEN}`, + }, + }), +}); + +es.onmessage = (e) => { + const msg = JSON.parse(e.data).message as string + + if (msg === "/digest") { + main().catch((err) => { + console.error(err) + }) + } +}; + +es.onerror = (e) => { + console.error(e); +}; diff --git a/package.json b/package.json index 55efa46..05d687d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "typescript": "^5" }, "dependencies": { - "dayjs": "^1.11.20" + "dayjs": "^1.11.20", + "eventsource": "^4.1.0", + "node-cron": "^4.6.0" } }