@iletai/nzb 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +27 -0
- package/dist/cli.js +34 -5
- package/dist/copilot/tools.js +50 -0
- package/dist/cron/task-runner.js +126 -8
- package/dist/daemon.js +55 -11
- package/dist/telegram/bot.js +28 -0
- package/dist/telegram/handlers/commands.js +1 -0
- package/dist/telegram/handlers/update.js +211 -0
- package/dist/telegram/menus.js +14 -0
- package/dist/update.js +170 -1
- package/package.json +1 -1
package/dist/api/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { listSkills, removeSkill } from "../copilot/skills.js";
|
|
|
7
7
|
import { restartDaemon } from "../daemon.js";
|
|
8
8
|
import { API_TOKEN_PATH, ensureNZBHome } from "../paths.js";
|
|
9
9
|
import { searchMemories } from "../store/memory.js";
|
|
10
|
+
import { sendVoice } from "../telegram/bot.js";
|
|
10
11
|
import { sendPhoto } from "../telegram/bot.js";
|
|
11
12
|
// Ensure token file exists (generate on first run)
|
|
12
13
|
let apiToken = null;
|
|
@@ -211,6 +212,32 @@ app.post("/send-photo", async (req, res) => {
|
|
|
211
212
|
res.status(500).json({ error: msg });
|
|
212
213
|
}
|
|
213
214
|
});
|
|
215
|
+
// Send a voice/audio file to Telegram (protected by bearer token auth middleware)
|
|
216
|
+
app.post("/send-voice", async (req, res) => {
|
|
217
|
+
const { voice, caption, chat_id } = req.body;
|
|
218
|
+
if (!voice || typeof voice !== "string") {
|
|
219
|
+
res.status(400).json({ error: "Missing 'voice' (file path) in request body" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Only allow local file paths (no URLs for voice)
|
|
223
|
+
if (voice.startsWith("http://") || voice.startsWith("https://")) {
|
|
224
|
+
res.status(400).json({ error: "Only local file paths are allowed for voice" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const chatId = chat_id ? parseInt(chat_id, 10) : undefined;
|
|
228
|
+
if (chat_id && (isNaN(chatId) || chatId === 0)) {
|
|
229
|
+
res.status(400).json({ error: "Invalid 'chat_id'" });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await sendVoice(voice, caption, chatId);
|
|
234
|
+
res.json({ status: "sent" });
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
res.status(500).json({ error: msg });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
214
241
|
// Global error handler — catch unhandled Express errors
|
|
215
242
|
app.use((err, _req, res, _next) => {
|
|
216
243
|
console.error("[nzb] Express error:", err.message);
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,8 @@ Commands:
|
|
|
45
45
|
tui Connect to the daemon via terminal UI
|
|
46
46
|
setup Interactive first-run configuration
|
|
47
47
|
update Check for updates and install the latest version
|
|
48
|
+
update check Check for updates without installing
|
|
49
|
+
update --force Force reinstall the latest version
|
|
48
50
|
cron Manage scheduled cron jobs
|
|
49
51
|
help Show this help message
|
|
50
52
|
|
|
@@ -77,21 +79,48 @@ switch (command) {
|
|
|
77
79
|
await import("./setup.js");
|
|
78
80
|
break;
|
|
79
81
|
case "update": {
|
|
80
|
-
const { checkForUpdate, performUpdate } = await import("./update.js");
|
|
82
|
+
const { checkForUpdate, performUpdate, performForceUpdate } = await import("./update.js");
|
|
83
|
+
const updateArgs = args.slice(1);
|
|
84
|
+
const subCmd = updateArgs[0];
|
|
85
|
+
const force = updateArgs.includes("--force");
|
|
86
|
+
// `nzb update check` — check only, don't install
|
|
87
|
+
if (subCmd === "check") {
|
|
88
|
+
const check = await checkForUpdate();
|
|
89
|
+
if (!check.checkSucceeded) {
|
|
90
|
+
console.error("Warning: Could not reach the npm registry. Check your network and try again.");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if (check.updateAvailable) {
|
|
94
|
+
console.log(`Update available: v${check.current} → v${check.latest}`);
|
|
95
|
+
if (check.publishedAt) {
|
|
96
|
+
console.log(`Published: ${new Date(check.publishedAt).toLocaleDateString()}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(`nzb v${check.current} is already the latest version.`);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// `nzb update` or `nzb update --force` — check and install
|
|
81
105
|
const check = await checkForUpdate();
|
|
82
106
|
if (!check.checkSucceeded) {
|
|
83
107
|
console.error("Warning: Could not reach the npm registry. Check your network and try again.");
|
|
84
108
|
process.exit(1);
|
|
85
109
|
}
|
|
86
|
-
if (!check.updateAvailable) {
|
|
110
|
+
if (!check.updateAvailable && !force) {
|
|
87
111
|
console.log(`nzb v${check.current} is already the latest version.`);
|
|
88
112
|
break;
|
|
89
113
|
}
|
|
90
|
-
|
|
114
|
+
if (check.updateAvailable) {
|
|
115
|
+
console.log(`Update available: v${check.current} → v${check.latest}`);
|
|
116
|
+
}
|
|
117
|
+
else if (force) {
|
|
118
|
+
console.log(`Force reinstalling nzb v${check.current}...`);
|
|
119
|
+
}
|
|
91
120
|
console.log("Installing...");
|
|
92
|
-
const result = await performUpdate();
|
|
121
|
+
const result = force ? await performForceUpdate() : await performUpdate();
|
|
93
122
|
if (result.ok) {
|
|
94
|
-
console.log(`Updated to v${check.latest}`);
|
|
123
|
+
console.log(check.updateAvailable ? `Updated to v${check.latest}` : `Reinstalled v${check.current}`);
|
|
95
124
|
}
|
|
96
125
|
else {
|
|
97
126
|
console.error(`Update failed: ${result.output}`);
|
package/dist/copilot/tools.js
CHANGED
|
@@ -855,6 +855,56 @@ export function createTools(deps) {
|
|
|
855
855
|
return `Restarting NZB${reason}. I'll be back in a few seconds.`;
|
|
856
856
|
},
|
|
857
857
|
}),
|
|
858
|
+
defineTool("check_update", {
|
|
859
|
+
description: "Check for NZB updates and optionally apply them. " +
|
|
860
|
+
"Use 'check' to see if updates are available. " +
|
|
861
|
+
"Use 'update' to install the latest version and restart.",
|
|
862
|
+
parameters: z.object({
|
|
863
|
+
action: z.enum(["check", "update"]).describe("'check' to check for updates, 'update' to install and restart"),
|
|
864
|
+
}),
|
|
865
|
+
handler: async (args) => {
|
|
866
|
+
try {
|
|
867
|
+
const { checkForUpdate, performUpdate, getChangelog } = await import("../update.js");
|
|
868
|
+
if (args.action === "check") {
|
|
869
|
+
const result = await checkForUpdate();
|
|
870
|
+
if (!result.checkSucceeded) {
|
|
871
|
+
return "Could not reach the npm registry. Network may be unavailable.";
|
|
872
|
+
}
|
|
873
|
+
if (!result.updateAvailable) {
|
|
874
|
+
return `NZB v${result.current} is already the latest version.`;
|
|
875
|
+
}
|
|
876
|
+
const changelog = await getChangelog(5);
|
|
877
|
+
const changelogText = changelog.length > 0
|
|
878
|
+
? "\n\nRecent versions:\n" + changelog.map((e) => `• v${e.version} (${e.date})`).join("\n")
|
|
879
|
+
: "";
|
|
880
|
+
return `Update available: v${result.current} → v${result.latest}${changelogText}`;
|
|
881
|
+
}
|
|
882
|
+
// action === "update"
|
|
883
|
+
const result = await checkForUpdate();
|
|
884
|
+
if (!result.checkSucceeded) {
|
|
885
|
+
return "Could not reach the npm registry. Network may be unavailable.";
|
|
886
|
+
}
|
|
887
|
+
if (!result.updateAvailable) {
|
|
888
|
+
return `NZB v${result.current} is already the latest version. No update needed.`;
|
|
889
|
+
}
|
|
890
|
+
const updateResult = await performUpdate();
|
|
891
|
+
if (!updateResult.ok) {
|
|
892
|
+
return `Update failed: ${updateResult.output}`;
|
|
893
|
+
}
|
|
894
|
+
// Schedule restart after returning the response
|
|
895
|
+
const { restartDaemon } = await import("../daemon.js");
|
|
896
|
+
setTimeout(() => {
|
|
897
|
+
restartDaemon().catch((err) => {
|
|
898
|
+
console.error("[nzb] Post-update restart failed:", err);
|
|
899
|
+
});
|
|
900
|
+
}, 1000);
|
|
901
|
+
return `Updated NZB to v${result.latest}. Restarting now...`;
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
return `Update error: ${err instanceof Error ? err.message : String(err)}`;
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
}),
|
|
858
908
|
];
|
|
859
909
|
}
|
|
860
910
|
function formatAge(date) {
|
package/dist/cron/task-runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
2
3
|
import { freemem, totalmem } from "os";
|
|
3
4
|
import { join } from "path";
|
|
4
5
|
import { config } from "../config.js";
|
|
@@ -30,6 +31,8 @@ export async function executeCronTask(job) {
|
|
|
30
31
|
return await executeNotificationTask(job, payload);
|
|
31
32
|
case "webhook":
|
|
32
33
|
return await executeWebhookTask(payload, job.timeoutMs);
|
|
34
|
+
case "vocab":
|
|
35
|
+
return await executeVocabTask(job, payload);
|
|
33
36
|
default:
|
|
34
37
|
throw new Error(`Unknown task type: ${job.taskType}`);
|
|
35
38
|
}
|
|
@@ -38,14 +41,12 @@ async function executePromptTask(payload) {
|
|
|
38
41
|
const prompt = payload.prompt || "Scheduled check-in. Anything to report?";
|
|
39
42
|
try {
|
|
40
43
|
const { sendToOrchestrator } = await import("../copilot/orchestrator.js");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
// No internal timeout — the scheduler's withTaskTimeout() handles it
|
|
45
|
+
// using the per-job configurable timeoutMs (default 5min).
|
|
46
|
+
return await new Promise((resolve) => {
|
|
44
47
|
sendToOrchestrator(`[Scheduled task] ${prompt}`, { type: "background" }, (text, done) => {
|
|
45
48
|
if (done) {
|
|
46
|
-
|
|
47
|
-
fullResponse = text;
|
|
48
|
-
resolve(fullResponse);
|
|
49
|
+
resolve(text);
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
52
|
});
|
|
@@ -106,7 +107,6 @@ async function executeBackupTask() {
|
|
|
106
107
|
.sort()
|
|
107
108
|
.reverse();
|
|
108
109
|
for (const old of files.slice(10)) {
|
|
109
|
-
const { unlinkSync } = await import("fs");
|
|
110
110
|
unlinkSync(join(BACKUPS_DIR, old));
|
|
111
111
|
// Also remove corresponding WAL
|
|
112
112
|
const walFile = old + "-wal";
|
|
@@ -158,6 +158,124 @@ async function executeWebhookTask(payload, timeoutMs) {
|
|
|
158
158
|
throw err;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
const VOCAB_PROMPT = `Generate a single advanced English vocabulary word for a Vietnamese learner. Pick an uncommon but useful word.
|
|
162
|
+
|
|
163
|
+
You MUST respond in EXACTLY this format (no extra text, no markdown fences):
|
|
164
|
+
|
|
165
|
+
WORD: <word>
|
|
166
|
+
IPA: <IPA pronunciation>
|
|
167
|
+
POS: <part of speech abbreviation: n., v., adj., adv., etc.>
|
|
168
|
+
VI: <Vietnamese translation>
|
|
169
|
+
EN_EXAMPLE: <example sentence in English using the word>
|
|
170
|
+
VI_EXAMPLE: <Vietnamese translation of the example sentence>`;
|
|
171
|
+
async function executeVocabTask(job, payload) {
|
|
172
|
+
const customPrompt = payload.prompt;
|
|
173
|
+
const prompt = customPrompt || VOCAB_PROMPT;
|
|
174
|
+
// Step 1: Get vocab from AI
|
|
175
|
+
let aiResponse;
|
|
176
|
+
try {
|
|
177
|
+
const { sendToOrchestrator } = await import("../copilot/orchestrator.js");
|
|
178
|
+
aiResponse = await new Promise((resolve) => {
|
|
179
|
+
sendToOrchestrator(`[Scheduled vocab task] ${prompt}`, { type: "background" }, (text, done) => {
|
|
180
|
+
if (done)
|
|
181
|
+
resolve(text);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
throw new Error(`Vocab AI prompt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
// Step 2: Parse the AI response
|
|
189
|
+
const parsed = parseVocabResponse(aiResponse);
|
|
190
|
+
// Step 3: Format the message
|
|
191
|
+
const formattedMessage = formatVocabMessage(parsed);
|
|
192
|
+
// Step 4: Send formatted text to Telegram
|
|
193
|
+
if (config.telegramEnabled) {
|
|
194
|
+
const { sendProactiveMessage } = await import("../telegram/bot.js");
|
|
195
|
+
await sendProactiveMessage(formattedMessage);
|
|
196
|
+
}
|
|
197
|
+
// Step 5: Generate TTS audio and send voice (macOS only)
|
|
198
|
+
if (config.telegramEnabled && parsed.word) {
|
|
199
|
+
try {
|
|
200
|
+
await generateAndSendVocabAudio(parsed.word);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error("[nzb] Vocab TTS failed (non-fatal):", err instanceof Error ? err.message : err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return formattedMessage;
|
|
207
|
+
}
|
|
208
|
+
function parseVocabResponse(response) {
|
|
209
|
+
const get = (key) => {
|
|
210
|
+
const regex = new RegExp(`^${key}:\\s*(.+)$`, "mi");
|
|
211
|
+
const match = response.match(regex);
|
|
212
|
+
return match?.[1]?.trim() ?? "";
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
word: get("WORD"),
|
|
216
|
+
ipa: get("IPA"),
|
|
217
|
+
pos: get("POS"),
|
|
218
|
+
vi: get("VI"),
|
|
219
|
+
enExample: get("EN_EXAMPLE"),
|
|
220
|
+
viExample: get("VI_EXAMPLE"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function formatVocabMessage(v) {
|
|
224
|
+
const lines = ["📖 WORD OF THE MINUTE", ""];
|
|
225
|
+
if (v.word) {
|
|
226
|
+
const ipaPart = v.ipa ? ` • ${v.ipa}` : "";
|
|
227
|
+
lines.push(`✨ ${v.word}${ipaPart}`);
|
|
228
|
+
}
|
|
229
|
+
if (v.pos)
|
|
230
|
+
lines.push(`🏷 ${v.pos}`);
|
|
231
|
+
if (v.vi)
|
|
232
|
+
lines.push(`🇻🇳 ${v.vi}`);
|
|
233
|
+
lines.push("");
|
|
234
|
+
if (v.enExample)
|
|
235
|
+
lines.push(`💬 ${v.enExample}`);
|
|
236
|
+
if (v.viExample)
|
|
237
|
+
lines.push(`🔄 ${v.viExample}`);
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
async function generateAndSendVocabAudio(word) {
|
|
241
|
+
const aiffPath = "/tmp/vocab-word.aiff";
|
|
242
|
+
const m4aPath = "/tmp/vocab-word.m4a";
|
|
243
|
+
// Clean up previous files
|
|
244
|
+
try {
|
|
245
|
+
unlinkSync(aiffPath);
|
|
246
|
+
}
|
|
247
|
+
catch { /* ignore */ }
|
|
248
|
+
try {
|
|
249
|
+
unlinkSync(m4aPath);
|
|
250
|
+
}
|
|
251
|
+
catch { /* ignore */ }
|
|
252
|
+
// Generate TTS with macOS say
|
|
253
|
+
execSync(`say -v Samantha -o "${aiffPath}" "${word.replace(/"/g, '\\"')}"`, {
|
|
254
|
+
timeout: 10_000,
|
|
255
|
+
});
|
|
256
|
+
if (!existsSync(aiffPath)) {
|
|
257
|
+
throw new Error("TTS generation failed: aiff file not created");
|
|
258
|
+
}
|
|
259
|
+
// Convert to m4a
|
|
260
|
+
execSync(`afconvert -f mp4f -d aac "${aiffPath}" "${m4aPath}"`, {
|
|
261
|
+
timeout: 10_000,
|
|
262
|
+
});
|
|
263
|
+
if (!existsSync(m4aPath)) {
|
|
264
|
+
throw new Error("Audio conversion failed: m4a file not created");
|
|
265
|
+
}
|
|
266
|
+
// Send voice via Telegram
|
|
267
|
+
const { sendVoice } = await import("../telegram/bot.js");
|
|
268
|
+
await sendVoice(m4aPath, `🔊 ${word}`);
|
|
269
|
+
// Clean up temp files
|
|
270
|
+
try {
|
|
271
|
+
unlinkSync(aiffPath);
|
|
272
|
+
}
|
|
273
|
+
catch { /* ignore */ }
|
|
274
|
+
try {
|
|
275
|
+
unlinkSync(m4aPath);
|
|
276
|
+
}
|
|
277
|
+
catch { /* ignore */ }
|
|
278
|
+
}
|
|
161
279
|
function formatBytes(bytes) {
|
|
162
280
|
if (bytes < 1024)
|
|
163
281
|
return `${bytes}B`;
|
package/dist/daemon.js
CHANGED
|
@@ -8,7 +8,7 @@ import { PID_FILE_PATH } from "./paths.js";
|
|
|
8
8
|
import { closeDb, getDb } from "./store/db.js";
|
|
9
9
|
import { createBot, sendProactiveMessage, sendWorkerNotification, startBot, stopBot } from "./telegram/bot.js";
|
|
10
10
|
import { startCronScheduler, stopCronScheduler } from "./cron/scheduler.js";
|
|
11
|
-
import { checkForUpdate } from "./update.js";
|
|
11
|
+
import { checkForUpdate, getDismissedVersion, isAutoUpdateEnabled, scheduleUpdateCheck, shouldCheckUpdate, stopUpdateCheck } from "./update.js";
|
|
12
12
|
// Log the active CA bundle (injected by cli.ts via re-exec).
|
|
13
13
|
if (process.env.NODE_EXTRA_CA_CERTS) {
|
|
14
14
|
console.log(`[nzb] Using system CA bundle: ${process.env.NODE_EXTRA_CA_CERTS}`);
|
|
@@ -152,18 +152,60 @@ async function main() {
|
|
|
152
152
|
console.log("[nzb] Telegram user ID missing — skipping bot. Run 'nzb setup' and enter your Telegram user ID (get it from @userinfobot).");
|
|
153
153
|
}
|
|
154
154
|
console.log("[nzb] NZB is fully operational.");
|
|
155
|
-
// Non-blocking update check —
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
// Non-blocking update check — delayed 30s after startup, respects check interval
|
|
156
|
+
setTimeout(async () => {
|
|
157
|
+
try {
|
|
158
|
+
const autoEnabled = await isAutoUpdateEnabled();
|
|
159
|
+
if (!autoEnabled)
|
|
160
|
+
return;
|
|
161
|
+
const shouldCheck = await shouldCheckUpdate();
|
|
162
|
+
if (!shouldCheck)
|
|
163
|
+
return;
|
|
164
|
+
const result = await checkForUpdate();
|
|
165
|
+
if (result.updateAvailable && result.latest) {
|
|
166
|
+
const dismissed = await getDismissedVersion();
|
|
167
|
+
if (dismissed === result.latest)
|
|
168
|
+
return;
|
|
169
|
+
const msg = `⬆ Update available: v${result.current} → v${result.latest} — run \`nzb update\` to install`;
|
|
170
|
+
console.log(`[nzb] ${msg}`);
|
|
171
|
+
if (config.telegramEnabled) {
|
|
172
|
+
try {
|
|
173
|
+
const { sendUpdateNotification } = await import("./telegram/handlers/update.js");
|
|
174
|
+
const tgBot = (await import("./telegram/bot.js")).getBot();
|
|
175
|
+
if (tgBot && config.authorizedUserId !== undefined) {
|
|
176
|
+
await sendUpdateNotification(tgBot, config.authorizedUserId, result);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Fallback to plain text
|
|
181
|
+
sendProactiveMessage(msg).catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
broadcastToSSE(msg);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Silent — network may be unavailable
|
|
189
|
+
}
|
|
190
|
+
}, 30_000);
|
|
191
|
+
// Schedule periodic update checks (every 6 hours)
|
|
192
|
+
scheduleUpdateCheck(async (result) => {
|
|
193
|
+
const msg = `⬆ Update available: v${result.current} → v${result.latest} — run \`nzb update\` to install`;
|
|
194
|
+
console.log(`[nzb] ${msg}`);
|
|
195
|
+
if (config.telegramEnabled) {
|
|
196
|
+
try {
|
|
197
|
+
const { sendUpdateNotification } = await import("./telegram/handlers/update.js");
|
|
198
|
+
const tgBot = (await import("./telegram/bot.js")).getBot();
|
|
199
|
+
if (tgBot && config.authorizedUserId !== undefined) {
|
|
200
|
+
await sendUpdateNotification(tgBot, config.authorizedUserId, result);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
162
204
|
sendProactiveMessage(msg).catch(() => { });
|
|
163
|
-
|
|
205
|
+
}
|
|
164
206
|
}
|
|
165
|
-
|
|
166
|
-
|
|
207
|
+
broadcastToSSE(msg);
|
|
208
|
+
});
|
|
167
209
|
// Notify user if this is a restart (not a fresh start)
|
|
168
210
|
if (config.telegramEnabled && process.env.NZB_RESTARTED === "1") {
|
|
169
211
|
await sendProactiveMessage("I'm back online.").catch(() => { });
|
|
@@ -198,6 +240,7 @@ async function shutdown() {
|
|
|
198
240
|
// Stop health check timer first
|
|
199
241
|
stopHealthCheck();
|
|
200
242
|
stopCronScheduler();
|
|
243
|
+
stopUpdateCheck();
|
|
201
244
|
if (config.telegramEnabled) {
|
|
202
245
|
try {
|
|
203
246
|
await stopBot();
|
|
@@ -225,6 +268,7 @@ export async function restartDaemon() {
|
|
|
225
268
|
console.log("[nzb] Restarting...");
|
|
226
269
|
stopHealthCheck();
|
|
227
270
|
stopCronScheduler();
|
|
271
|
+
stopUpdateCheck();
|
|
228
272
|
const activeWorkers = getWorkers();
|
|
229
273
|
const runningCount = Array.from(activeWorkers.values()).filter((w) => w.status === "running").length;
|
|
230
274
|
if (runningCount > 0) {
|
package/dist/telegram/bot.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerMediaHandlers } from "./handlers/media.js";
|
|
|
17
17
|
import { registerReactionHandlers } from "./handlers/reactions.js";
|
|
18
18
|
import { registerMessageHandler } from "./handlers/streaming.js";
|
|
19
19
|
import { registerSmartSuggestionHandlers } from "./handlers/suggestions.js";
|
|
20
|
+
import { registerUpdateHandlers } from "./handlers/update.js";
|
|
20
21
|
import { initLogChannel, logError, logInfo } from "./log-channel.js";
|
|
21
22
|
import { createMenus } from "./menus.js";
|
|
22
23
|
let bot;
|
|
@@ -106,6 +107,7 @@ export function createBot() {
|
|
|
106
107
|
// --- Handler registrations ---
|
|
107
108
|
registerCallbackHandlers(bot);
|
|
108
109
|
registerCronHandlers(bot);
|
|
110
|
+
registerUpdateHandlers(bot);
|
|
109
111
|
registerInlineQueryHandler(bot);
|
|
110
112
|
registerSmartSuggestionHandlers(bot);
|
|
111
113
|
registerReactionHandlers(bot);
|
|
@@ -155,6 +157,7 @@ export async function startBot() {
|
|
|
155
157
|
{ command: "skills", description: "List installed skills" },
|
|
156
158
|
{ command: "memory", description: "Show stored memories" },
|
|
157
159
|
{ command: "cron", description: "Manage cron jobs" },
|
|
160
|
+
{ command: "update", description: "Check for updates" },
|
|
158
161
|
{ command: "settings", description: "Bot settings" },
|
|
159
162
|
{ command: "restart", description: "Restart NZB" },
|
|
160
163
|
]);
|
|
@@ -315,4 +318,29 @@ export async function sendPhoto(photo, caption) {
|
|
|
315
318
|
throw err;
|
|
316
319
|
}
|
|
317
320
|
}
|
|
321
|
+
/** Send a voice/audio file to the authorized user (or a specific chat). Accepts a local file path. */
|
|
322
|
+
export async function sendVoice(filePath, caption, chatId) {
|
|
323
|
+
if (!bot || config.authorizedUserId === undefined)
|
|
324
|
+
return;
|
|
325
|
+
const targetChat = chatId ?? config.authorizedUserId;
|
|
326
|
+
if (!isAllowedFilePath(filePath)) {
|
|
327
|
+
throw new Error("File path is not within allowed directories");
|
|
328
|
+
}
|
|
329
|
+
const { InputFile } = await import("grammy");
|
|
330
|
+
const input = new InputFile(filePath);
|
|
331
|
+
try {
|
|
332
|
+
// Use sendAudio for .m4a (shows as playable audio with duration),
|
|
333
|
+
// sendVoice for .ogg (shows as voice message bubble)
|
|
334
|
+
if (filePath.endsWith(".ogg")) {
|
|
335
|
+
await bot.api.sendVoice(targetChat, input, { caption });
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
await bot.api.sendAudio(targetChat, input, { caption });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
console.error("[nzb] Failed to send voice:", err instanceof Error ? err.message : err);
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
318
346
|
//# sourceMappingURL=bot.js.map
|
|
@@ -32,6 +32,7 @@ export function registerCommandHandlers(bot, deps) {
|
|
|
32
32
|
"/skills — Installed skills\n" +
|
|
33
33
|
"/workers — Active worker sessions\n" +
|
|
34
34
|
"/cron — Manage cron jobs\n" +
|
|
35
|
+
"/update — Check for updates\n" +
|
|
35
36
|
"/restart — Restart NZB\n\n" +
|
|
36
37
|
"⚡ Breakthrough Features:\n" +
|
|
37
38
|
"• @bot query — Use me inline in any chat!\n" +
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { checkForUpdate, dismissVersion, getChangelog, getCurrentVersion, isAutoUpdateEnabled, performUpdate, toggleAutoUpdate, } from "../../update.js";
|
|
3
|
+
/** Build the update menu inline keyboard. */
|
|
4
|
+
async function buildUpdateKeyboard() {
|
|
5
|
+
const autoEnabled = await isAutoUpdateEnabled();
|
|
6
|
+
return new InlineKeyboard()
|
|
7
|
+
.text("🔍 Check Update", "update:check")
|
|
8
|
+
.text("⬆️ Update Now", "update:now")
|
|
9
|
+
.row()
|
|
10
|
+
.text("📋 Changelog", "update:changelog")
|
|
11
|
+
.text(`⚙️ Auto-Update: ${autoEnabled ? "ON" : "OFF"}`, "update:auto:toggle");
|
|
12
|
+
}
|
|
13
|
+
/** Build the notification keyboard shown when an update is available. */
|
|
14
|
+
export function buildUpdateNotificationKeyboard() {
|
|
15
|
+
return new InlineKeyboard()
|
|
16
|
+
.text("⬆️ Update Now", "update:now")
|
|
17
|
+
.text("📋 Changelog", "update:changelog")
|
|
18
|
+
.row()
|
|
19
|
+
.text("❌ Dismiss", "update:dismiss");
|
|
20
|
+
}
|
|
21
|
+
/** Format an update check result for display. */
|
|
22
|
+
function formatCheckResult(result) {
|
|
23
|
+
if (!result.checkSucceeded) {
|
|
24
|
+
return "❌ Could not reach npm registry. Check your network and try again.";
|
|
25
|
+
}
|
|
26
|
+
if (result.updateAvailable) {
|
|
27
|
+
const published = result.publishedAt
|
|
28
|
+
? `\nPublished: ${new Date(result.publishedAt).toLocaleDateString()}`
|
|
29
|
+
: "";
|
|
30
|
+
return `🆕 Update available!\n\nCurrent: v${result.current}\nLatest: v${result.latest}${published}`;
|
|
31
|
+
}
|
|
32
|
+
return `✅ NZB v${result.current} is up to date.`;
|
|
33
|
+
}
|
|
34
|
+
/** Register the /update command and callback handlers. */
|
|
35
|
+
export function registerUpdateHandlers(bot) {
|
|
36
|
+
// /update command — show update menu
|
|
37
|
+
bot.command("update", async (ctx) => {
|
|
38
|
+
const keyboard = await buildUpdateKeyboard();
|
|
39
|
+
const version = getCurrentVersion();
|
|
40
|
+
await ctx.reply(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
|
|
41
|
+
});
|
|
42
|
+
// Check for updates
|
|
43
|
+
bot.callbackQuery("update:check", async (ctx) => {
|
|
44
|
+
try {
|
|
45
|
+
await ctx.answerCallbackQuery({ text: "Checking for updates..." });
|
|
46
|
+
const result = await checkForUpdate();
|
|
47
|
+
const text = formatCheckResult(result);
|
|
48
|
+
if (result.updateAvailable) {
|
|
49
|
+
await ctx.editMessageText(text, { reply_markup: buildUpdateNotificationKeyboard() });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const keyboard = await buildUpdateKeyboard();
|
|
53
|
+
await ctx.editMessageText(text, { reply_markup: keyboard });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error("[nzb] Update check callback error:", err instanceof Error ? err.message : err);
|
|
58
|
+
await ctx.answerCallbackQuery({
|
|
59
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
60
|
+
show_alert: true,
|
|
61
|
+
}).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Update now — show confirmation dialog
|
|
65
|
+
bot.callbackQuery("update:now", async (ctx) => {
|
|
66
|
+
try {
|
|
67
|
+
await ctx.answerCallbackQuery();
|
|
68
|
+
const result = await checkForUpdate();
|
|
69
|
+
if (!result.checkSucceeded) {
|
|
70
|
+
await ctx.editMessageText("❌ Cannot reach npm registry. Try again later.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!result.updateAvailable) {
|
|
74
|
+
const keyboard = await buildUpdateKeyboard();
|
|
75
|
+
await ctx.editMessageText(`✅ NZB v${result.current} is already the latest version.`, {
|
|
76
|
+
reply_markup: keyboard,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const confirmKeyboard = new InlineKeyboard()
|
|
81
|
+
.text("✅ Yes, update now", "update:confirm")
|
|
82
|
+
.text("❌ Cancel", "update:cancel");
|
|
83
|
+
await ctx.editMessageText(`⚠️ Update NZB?\n\nv${result.current} → v${result.latest}\n\nThis will install the new version and restart the daemon.`, { reply_markup: confirmKeyboard });
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error("[nzb] Update now callback error:", err instanceof Error ? err.message : err);
|
|
87
|
+
await ctx.answerCallbackQuery({
|
|
88
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
89
|
+
show_alert: true,
|
|
90
|
+
}).catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Confirm update — actually perform the update
|
|
94
|
+
bot.callbackQuery("update:confirm", async (ctx) => {
|
|
95
|
+
try {
|
|
96
|
+
await ctx.answerCallbackQuery({ text: "Updating..." });
|
|
97
|
+
await ctx.editMessageText("⏳ Installing update... This may take a minute.");
|
|
98
|
+
const result = await performUpdate();
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
const keyboard = await buildUpdateKeyboard();
|
|
101
|
+
await ctx.editMessageText(`❌ Update failed:\n${result.output}`, { reply_markup: keyboard });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await ctx.editMessageText("✅ Update installed! Restarting NZB...");
|
|
105
|
+
// Restart daemon after a short delay
|
|
106
|
+
setTimeout(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const { restartDaemon } = await import("../../daemon.js");
|
|
109
|
+
await restartDaemon();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error("[nzb] Post-update restart failed:", err);
|
|
113
|
+
}
|
|
114
|
+
}, 1000);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error("[nzb] Update confirm callback error:", err instanceof Error ? err.message : err);
|
|
118
|
+
await ctx.answerCallbackQuery({
|
|
119
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
120
|
+
show_alert: true,
|
|
121
|
+
}).catch(() => { });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Cancel update
|
|
125
|
+
bot.callbackQuery("update:cancel", async (ctx) => {
|
|
126
|
+
try {
|
|
127
|
+
await ctx.answerCallbackQuery({ text: "Cancelled" });
|
|
128
|
+
const keyboard = await buildUpdateKeyboard();
|
|
129
|
+
const version = getCurrentVersion();
|
|
130
|
+
await ctx.editMessageText(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.error("[nzb] Update cancel callback error:", err instanceof Error ? err.message : err);
|
|
134
|
+
await ctx.answerCallbackQuery({
|
|
135
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
136
|
+
show_alert: true,
|
|
137
|
+
}).catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Show changelog
|
|
141
|
+
bot.callbackQuery("update:changelog", async (ctx) => {
|
|
142
|
+
try {
|
|
143
|
+
await ctx.answerCallbackQuery({ text: "Fetching changelog..." });
|
|
144
|
+
const entries = await getChangelog(8);
|
|
145
|
+
if (entries.length === 0) {
|
|
146
|
+
await ctx.editMessageText("📋 Could not fetch version history.", {
|
|
147
|
+
reply_markup: await buildUpdateKeyboard(),
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const current = getCurrentVersion();
|
|
152
|
+
const lines = entries.map((e) => {
|
|
153
|
+
const marker = e.version === current ? " ← current" : "";
|
|
154
|
+
return `• v${e.version} (${e.date})${marker}`;
|
|
155
|
+
});
|
|
156
|
+
const keyboard = await buildUpdateKeyboard();
|
|
157
|
+
await ctx.editMessageText(`📋 Recent versions:\n\n${lines.join("\n")}`, { reply_markup: keyboard });
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error("[nzb] Changelog callback error:", err instanceof Error ? err.message : err);
|
|
161
|
+
await ctx.answerCallbackQuery({
|
|
162
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
163
|
+
show_alert: true,
|
|
164
|
+
}).catch(() => { });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// Toggle auto-update
|
|
168
|
+
bot.callbackQuery("update:auto:toggle", async (ctx) => {
|
|
169
|
+
try {
|
|
170
|
+
const newState = await toggleAutoUpdate();
|
|
171
|
+
await ctx.answerCallbackQuery({ text: `Auto-Update ${newState ? "ON" : "OFF"}` });
|
|
172
|
+
const keyboard = await buildUpdateKeyboard();
|
|
173
|
+
const version = getCurrentVersion();
|
|
174
|
+
await ctx.editMessageText(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error("[nzb] Auto-update toggle error:", err instanceof Error ? err.message : err);
|
|
178
|
+
await ctx.answerCallbackQuery({
|
|
179
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
180
|
+
show_alert: true,
|
|
181
|
+
}).catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Dismiss update notification
|
|
185
|
+
bot.callbackQuery("update:dismiss", async (ctx) => {
|
|
186
|
+
try {
|
|
187
|
+
const result = await checkForUpdate();
|
|
188
|
+
if (result.latest) {
|
|
189
|
+
await dismissVersion(result.latest);
|
|
190
|
+
}
|
|
191
|
+
await ctx.answerCallbackQuery({ text: "Dismissed" });
|
|
192
|
+
await ctx.editMessageText(`📦 NZB v${result.current} — update notification dismissed.`);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error("[nzb] Dismiss callback error:", err instanceof Error ? err.message : err);
|
|
196
|
+
await ctx.answerCallbackQuery({
|
|
197
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
198
|
+
show_alert: true,
|
|
199
|
+
}).catch(() => { });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Send a proactive update notification to a chat. */
|
|
204
|
+
export async function sendUpdateNotification(bot, chatId, result) {
|
|
205
|
+
const keyboard = buildUpdateNotificationKeyboard();
|
|
206
|
+
const published = result.publishedAt
|
|
207
|
+
? ` (${new Date(result.publishedAt).toLocaleDateString()})`
|
|
208
|
+
: "";
|
|
209
|
+
await bot.api.sendMessage(chatId, `🆕 NZB v${result.latest} available!${published}\nCurrent: v${result.current}\n\nRun \`nzb update\` or use the buttons below.`, { reply_markup: keyboard, parse_mode: "Markdown" });
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=update.js.map
|
package/dist/telegram/menus.js
CHANGED
|
@@ -239,6 +239,20 @@ export function createMenus(getUptimeStr) {
|
|
|
239
239
|
});
|
|
240
240
|
// Main interactive menu with navigation
|
|
241
241
|
const mainMenu = new Menu("main-menu")
|
|
242
|
+
.text("⏰ Cron", async (ctx) => {
|
|
243
|
+
try {
|
|
244
|
+
await ctx.answerCallbackQuery();
|
|
245
|
+
const { sendCronMenu } = await import("./handlers/cron.js");
|
|
246
|
+
await sendCronMenu(ctx);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
console.error("[nzb] Menu callback error:", err instanceof Error ? err.message : err);
|
|
250
|
+
await ctx.answerCallbackQuery({
|
|
251
|
+
text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
252
|
+
show_alert: true,
|
|
253
|
+
}).catch(() => { });
|
|
254
|
+
}
|
|
255
|
+
})
|
|
242
256
|
.text("📊 Status", async (ctx) => {
|
|
243
257
|
try {
|
|
244
258
|
const workers = Array.from(getWorkers().values());
|
package/dist/update.js
CHANGED
|
@@ -4,6 +4,9 @@ import { dirname, join } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const PKG_NAME = "@iletai/nzb";
|
|
7
|
+
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}`;
|
|
8
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
9
|
+
let updateCheckTimer;
|
|
7
10
|
function getPackageJson() {
|
|
8
11
|
try {
|
|
9
12
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -54,11 +57,37 @@ function isNewer(local, remote) {
|
|
|
54
57
|
export async function checkForUpdate() {
|
|
55
58
|
const current = getLocalVersion();
|
|
56
59
|
const latest = await getLatestVersion();
|
|
60
|
+
let publishedAt;
|
|
61
|
+
// Try to fetch publish date from registry
|
|
62
|
+
if (latest) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${NPM_REGISTRY_URL}/latest`, { signal: AbortSignal.timeout(10_000) });
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
const data = (await res.json());
|
|
67
|
+
const time = data.time;
|
|
68
|
+
if (time && latest in time) {
|
|
69
|
+
publishedAt = time[latest];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Expected: registry may be unreachable
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Record the check timestamp
|
|
78
|
+
try {
|
|
79
|
+
const { setState } = await import("./store/db.js");
|
|
80
|
+
setState("last_update_check", new Date().toISOString());
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Expected: DB may not be initialized during CLI usage
|
|
84
|
+
}
|
|
57
85
|
return {
|
|
58
86
|
current,
|
|
59
87
|
latest,
|
|
60
88
|
updateAvailable: latest !== null && isNewer(current, latest),
|
|
61
89
|
checkSucceeded: latest !== null,
|
|
90
|
+
publishedAt,
|
|
62
91
|
};
|
|
63
92
|
}
|
|
64
93
|
/** Run `npm install -g <pkg>@latest` and return success/failure. */
|
|
@@ -67,7 +96,23 @@ export async function performUpdate() {
|
|
|
67
96
|
const { name } = getPackageJson();
|
|
68
97
|
const output = execSync(`npm install -g ${name}@latest`, {
|
|
69
98
|
encoding: "utf-8",
|
|
70
|
-
timeout:
|
|
99
|
+
timeout: 120_000,
|
|
100
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
return { ok: true, output: output.trim() };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const msg = err.stderr?.trim() || err.message || "Unknown error";
|
|
106
|
+
return { ok: false, output: msg };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Force update even if the same version is installed. */
|
|
110
|
+
export async function performForceUpdate() {
|
|
111
|
+
try {
|
|
112
|
+
const { name } = getPackageJson();
|
|
113
|
+
const output = execSync(`npm install -g ${name}@latest --force`, {
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
timeout: 120_000,
|
|
71
116
|
stdio: ["ignore", "pipe", "pipe"],
|
|
72
117
|
});
|
|
73
118
|
return { ok: true, output: output.trim() };
|
|
@@ -77,4 +122,128 @@ export async function performUpdate() {
|
|
|
77
122
|
return { ok: false, output: msg };
|
|
78
123
|
}
|
|
79
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if enough time has passed since the last update check.
|
|
127
|
+
* Returns true if we should check now (>= 6 hours since last check).
|
|
128
|
+
*/
|
|
129
|
+
export async function shouldCheckUpdate() {
|
|
130
|
+
try {
|
|
131
|
+
const { getState } = await import("./store/db.js");
|
|
132
|
+
const last = getState("last_update_check");
|
|
133
|
+
if (!last)
|
|
134
|
+
return true;
|
|
135
|
+
const elapsed = Date.now() - new Date(last).getTime();
|
|
136
|
+
return elapsed >= CHECK_INTERVAL_MS;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// DB not ready — check anyway
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if auto-update notifications are enabled.
|
|
145
|
+
* Defaults to true if not explicitly set.
|
|
146
|
+
*/
|
|
147
|
+
export async function isAutoUpdateEnabled() {
|
|
148
|
+
try {
|
|
149
|
+
const { getState } = await import("./store/db.js");
|
|
150
|
+
const val = getState("auto_update_enabled");
|
|
151
|
+
return val !== "false";
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Toggle auto-update notifications on/off. Returns the new state. */
|
|
158
|
+
export async function toggleAutoUpdate() {
|
|
159
|
+
const { getState, setState } = await import("./store/db.js");
|
|
160
|
+
const current = getState("auto_update_enabled");
|
|
161
|
+
const newVal = current === "false" ? "true" : "false";
|
|
162
|
+
setState("auto_update_enabled", newVal);
|
|
163
|
+
return newVal === "true";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the version that the user dismissed (won't be notified about again).
|
|
167
|
+
*/
|
|
168
|
+
export async function getDismissedVersion() {
|
|
169
|
+
try {
|
|
170
|
+
const { getState } = await import("./store/db.js");
|
|
171
|
+
return getState("dismissed_version");
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Dismiss update notifications for a specific version. */
|
|
178
|
+
export async function dismissVersion(version) {
|
|
179
|
+
const { setState } = await import("./store/db.js");
|
|
180
|
+
setState("dismissed_version", version);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Fetch recent version history from npm registry for changelog display.
|
|
184
|
+
* Returns the last N versions with their publish dates.
|
|
185
|
+
*/
|
|
186
|
+
export async function getChangelog(limit = 5) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(NPM_REGISTRY_URL, { signal: AbortSignal.timeout(15_000) });
|
|
189
|
+
if (!res.ok)
|
|
190
|
+
return [];
|
|
191
|
+
const data = (await res.json());
|
|
192
|
+
if (!data.time || !data.versions)
|
|
193
|
+
return [];
|
|
194
|
+
const versions = Object.keys(data.versions)
|
|
195
|
+
.filter((v) => v in data.time && v !== "created" && v !== "modified")
|
|
196
|
+
.sort((a, b) => {
|
|
197
|
+
const dateA = new Date(data.time[a]).getTime();
|
|
198
|
+
const dateB = new Date(data.time[b]).getTime();
|
|
199
|
+
return dateB - dateA;
|
|
200
|
+
})
|
|
201
|
+
.slice(0, limit);
|
|
202
|
+
return versions.map((v) => ({
|
|
203
|
+
version: v,
|
|
204
|
+
date: new Date(data.time[v]).toISOString().split("T")[0],
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Get the current local version. */
|
|
212
|
+
export function getCurrentVersion() {
|
|
213
|
+
return getLocalVersion();
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Schedule periodic update checks (every 6 hours).
|
|
217
|
+
* Calls the provided callback when an update is found.
|
|
218
|
+
*/
|
|
219
|
+
export function scheduleUpdateCheck(onUpdateFound) {
|
|
220
|
+
if (updateCheckTimer)
|
|
221
|
+
return; // already scheduled
|
|
222
|
+
updateCheckTimer = setInterval(async () => {
|
|
223
|
+
try {
|
|
224
|
+
const autoEnabled = await isAutoUpdateEnabled();
|
|
225
|
+
if (!autoEnabled)
|
|
226
|
+
return;
|
|
227
|
+
const result = await checkForUpdate();
|
|
228
|
+
if (result.updateAvailable && result.latest) {
|
|
229
|
+
const dismissed = await getDismissedVersion();
|
|
230
|
+
if (dismissed === result.latest)
|
|
231
|
+
return;
|
|
232
|
+
onUpdateFound(result);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Silent — network may be unavailable
|
|
237
|
+
}
|
|
238
|
+
}, CHECK_INTERVAL_MS);
|
|
239
|
+
updateCheckTimer.unref();
|
|
240
|
+
console.log("[nzb] Update check scheduled (every 6 hours)");
|
|
241
|
+
}
|
|
242
|
+
/** Stop the periodic update check timer. */
|
|
243
|
+
export function stopUpdateCheck() {
|
|
244
|
+
if (updateCheckTimer) {
|
|
245
|
+
clearInterval(updateCheckTimer);
|
|
246
|
+
updateCheckTimer = undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
80
249
|
//# sourceMappingURL=update.js.map
|