From 4680b3ac445df974673da4d45217148f40f9d310 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 7 Sep 2026 14:32:06 +0200 Subject: [PATCH] send notifs at launch --- .env.example | 3 +- README.md | 15 +++- index.js | 196 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 143 insertions(+), 71 deletions(-) diff --git a/.env.example b/.env.example index b2a6b51..cb79761 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ NTFY_URL=https://ntfy.sh NTFY_TOPIC= +NTFY_TOKEN= -POLL_INTERVAL_SECONDS=60 \ No newline at end of file +POLL_INTERVAL_SECONDS=60 diff --git a/README.md b/README.md index be74c1a..802b397 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Codex Reset Alerts 🚀 -Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie une notification **ntfy** dĂšs que ton quota est de nouveau disponible. +Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie des notifications **ntfy** au lancement, puis dĂšs que ton quota est de nouveau disponible. L'idĂ©e est simple : lorsque Codex indique que ton quota est Ă©puisĂ©, tu lances le watcher et tu peux passer Ă  autre chose. Il surveille le quota en arriĂšre-plan, t'envoie une notification lors du reset rĂ©el, puis s'arrĂȘte automatiquement. @@ -42,7 +42,7 @@ Le watcher utilise deux mĂ©canismes : Le timestamp `resetsAt` fourni par Codex est Ă©galement surveillĂ©, mais il n'est **pas considĂ©rĂ© comme une garantie** : le quota peut ĂȘtre rĂ©initialisĂ© avant la date annoncĂ©e. -La notification n'est envoyĂ©e que lorsqu'un passage rĂ©el de **quota Ă©puisĂ© → quota disponible** est dĂ©tectĂ©. +Une notification de dĂ©marrage est envoyĂ©e dĂšs le lancement. La notification de reset n'est envoyĂ©e que lorsqu'un passage rĂ©el de **quota Ă©puisĂ© → quota disponible** est dĂ©tectĂ©. ## PrĂ©requis @@ -86,6 +86,7 @@ CrĂ©e un fichier `.env` Ă  la racine : ```env NTFY_URL=https://ntfy.sh NTFY_TOPIC=mon-topic-secret +NTFY_TOKEN= POLL_INTERVAL_SECONDS=60 ``` @@ -102,6 +103,12 @@ https://ntfy.sh Tu peux Ă©galement utiliser une instance ntfy auto-hĂ©bergĂ©e. +### `NTFY_TOKEN` + +Token d'accĂšs pour un serveur ntfy privĂ©. + +Laisse vide si ton topic ne demande pas d'authentification. + ### `NTFY_TOPIC` Topic sur lequel la notification sera envoyĂ©e. @@ -133,6 +140,7 @@ npm start Si le quota est effectivement Ă©puisĂ© : ```text +đŸ“± Notification ntfy de dĂ©marrage envoyĂ©e. 🔌 Connexion Ă  Codex App Server... ✅ ConnectĂ©. @@ -198,8 +206,9 @@ Tu peux vĂ©rifier manuellement que ton tĂ©lĂ©phone reçoit bien les notification curl \ -H "Title: Test Codex" \ -H "Tags: robot" \ + -H "Authorization: Bearer TON_TOKEN" \ -d "Hello depuis Codex Reset Alerts 🚀" \ - https://ntfy.sh/TON_TOPIC + https://ntfy.shinuwa.fr/TON_TOPIC ``` ## Lancer le watcher en arriĂšre-plan diff --git a/index.js b/index.js index dea9cd7..17b61be 100644 --- a/index.js +++ b/index.js @@ -26,6 +26,8 @@ loadEnv(); const NTFY_URL = process.env.NTFY_URL ?? "https://ntfy.sh"; const NTFY_TOPIC = process.env.NTFY_TOPIC; +const NTFY_TOKEN = process.env.NTFY_TOKEN; +const TEST_NTFY = process.argv.includes("--test-ntfy"); // Toutes les 60 secondes par dĂ©faut. const POLL_INTERVAL_MS = @@ -36,17 +38,10 @@ if (!NTFY_TOPIC) { process.exit(1); } -const codex = spawn("codex", ["app-server", "--stdio"], { - stdio: ["pipe", "pipe", "inherit"], -}); - -const lines = readline.createInterface({ - input: codex.stdout, - crlfDelay: Infinity, -}); - let nextId = 1; const pending = new Map(); +let codex = null; +let lines = null; let previousState = null; let refreshInProgress = false; @@ -120,22 +115,26 @@ function formatDuration(ms) { .join(" "); } -async function sendResetNotification(oldState, newState) { +async function sendNtfyNotification({ + title, + tags, + priority = "default", + body, +}) { const url = `${NTFY_URL.replace(/\/$/, "")}/${encodeURIComponent(NTFY_TOPIC)}`; + const headers = { + Title: title, + Tags: tags, + Priority: priority, + "Content-Type": "text/plain; charset=utf-8", + ...(NTFY_TOKEN ? { Authorization: `Bearer ${NTFY_TOKEN}` } : {}), + }; const response = await fetch(url, { method: "POST", - headers: { - Title: "Codex disponible", - Tags: "robot,white_check_mark", - Priority: "high", - "Content-Type": "text/plain; charset=utf-8", - }, - body: - `🚀 Ton quota Codex est de nouveau disponible !\n\n` + - `Avant : ${oldState.usedPercent}% utilisĂ©\n` + - `Maintenant : ${newState.usedPercent}% utilisĂ©`, + headers, + body, }); if (!response.ok) { @@ -143,6 +142,29 @@ async function sendResetNotification(oldState, newState) { } } +async function sendStartupNotification() { + await sendNtfyNotification({ + title: "Codex Reset Alerts lancĂ©", + tags: "robot,eyes", + priority: "default", + body: + `👀 Codex Reset Alerts vient de dĂ©marrer.\n\n` + + `Surveillance du quota Codex en cours.`, + }); +} + +async function sendResetNotification(oldState, newState) { + await sendNtfyNotification({ + title: "Codex disponible", + tags: "robot,white_check_mark", + priority: "high", + body: + `🚀 Ton quota Codex est de nouveau disponible !\n\n` + + `Avant : ${oldState.usedPercent}% utilisĂ©\n` + + `Maintenant : ${newState.usedPercent}% utilisĂ©`, + }); +} + function stop(exitCode = 0) { if (shuttingDown) return; @@ -157,7 +179,7 @@ function stop(exitCode = 0) { } try { - codex.kill(); + codex?.kill(); } catch { // Rien. } @@ -337,53 +359,84 @@ async function refreshQuota(source = "polling") { } } -lines.on("line", async (line) => { - let message; +function startCodexServer() { + codex = spawn("codex", ["app-server", "--stdio"], { + stdio: ["pipe", "pipe", "inherit"], + }); - try { - message = JSON.parse(line); - } catch { - return; - } + lines = readline.createInterface({ + input: codex.stdout, + crlfDelay: Infinity, + }); - if ( - message.id !== undefined && - pending.has(message.id) - ) { - const { resolve, reject } = - pending.get(message.id); + lines.on("line", async (line) => { + let message; - pending.delete(message.id); - - if (message.error) { - reject( - new Error(JSON.stringify(message.error)) - ); - } else { - resolve(message.result); + try { + message = JSON.parse(line); + } catch { + return; } - return; - } + if ( + message.id !== undefined && + pending.has(message.id) + ) { + const { resolve, reject } = + pending.get(message.id); - if ( - message.method === - "account/rateLimits/updated" - ) { - console.log(""); - console.log( - "⚡ Codex signale une modification du quota." - ); + pending.delete(message.id); - await refreshQuota("Ă©vĂ©nement Codex"); - } -}); + if (message.error) { + reject( + new Error(JSON.stringify(message.error)) + ); + } else { + resolve(message.result); + } + + return; + } + + if ( + message.method === + "account/rateLimits/updated" + ) { + console.log(""); + console.log( + "⚡ Codex signale une modification du quota." + ); + + await refreshQuota("Ă©vĂ©nement Codex"); + } + }); + + codex.on("exit", (code) => { + if (!shuttingDown && code !== 0) { + console.error( + `❌ Codex App Server s'est arrĂȘtĂ© avec le code ${code}` + ); + } + }); +} async function main() { + try { + await sendStartupNotification(); + console.log("đŸ“± Notification ntfy de dĂ©marrage envoyĂ©e."); + } catch (error) { + console.error( + "❌ Échec de la notification ntfy de dĂ©marrage :", + error.message + ); + } + console.log( "🔌 Connexion Ă  Codex App Server..." ); + startCodexServer(); + await request("initialize", { clientInfo: { name: "codex-reset-alerts", @@ -428,15 +481,24 @@ process.on("SIGTERM", () => { stop(0); }); -codex.on("exit", (code) => { - if (!shuttingDown && code !== 0) { - console.error( - `❌ Codex App Server s'est arrĂȘtĂ© avec le code ${code}` - ); - } -}); - -main().catch((error) => { - console.error("❌", error); - stop(1); -}); +if (TEST_NTFY) { + sendResetNotification( + { usedPercent: 100 }, + { usedPercent: 0 } + ) + .then(() => { + console.log("đŸ“± Notification ntfy de test envoyĂ©e."); + }) + .catch((error) => { + console.error( + "❌ Échec de la notification ntfy de test :", + error.message + ); + process.exitCode = 1; + }); +} else { + main().catch((error) => { + console.error("❌", error); + stop(1); + }); +}