nonotify 0.1.15 → 0.2.0
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/CHANGELOG.md +6 -0
- package/dist/cli.js +170 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/notifier.d.ts +19 -0
- package/dist/notifier.js +94 -1
- package/dist/telegram.d.ts +24 -1
- package/dist/telegram.js +293 -31
- package/package.json +1 -1
- package/src/cli.ts +224 -4
- package/src/index.ts +4 -0
- package/src/notifier.ts +155 -1
- package/src/telegram.ts +451 -48
package/src/notifier.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { getConfigPath, getLegacyConfigPath } from "./config.js";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
answerTelegramCallbackQuery,
|
|
6
|
+
clearTelegramInlineKeyboard,
|
|
7
|
+
getLatestUpdateOffset,
|
|
8
|
+
markTelegramSelectedOption,
|
|
9
|
+
sendTelegramChoiceMessage,
|
|
10
|
+
sendTelegramMessage,
|
|
11
|
+
waitForTelegramCallback,
|
|
12
|
+
} from "./telegram.js";
|
|
4
13
|
|
|
5
14
|
export type NotifierProfile = {
|
|
6
15
|
type: "telegram";
|
|
@@ -36,6 +45,20 @@ export type SendResult = {
|
|
|
36
45
|
provider: "telegram";
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
export type AskInput = {
|
|
49
|
+
message: string;
|
|
50
|
+
options: readonly string[];
|
|
51
|
+
profile?: string;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type AskResult = {
|
|
57
|
+
selected: string;
|
|
58
|
+
profile: string;
|
|
59
|
+
provider: "telegram";
|
|
60
|
+
};
|
|
61
|
+
|
|
39
62
|
export class NotifierError extends Error {
|
|
40
63
|
constructor(message: string) {
|
|
41
64
|
super(message);
|
|
@@ -60,6 +83,20 @@ export class NoProfilesConfiguredError extends NotifierError {
|
|
|
60
83
|
}
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
export class AskTimeoutError extends NotifierError {
|
|
87
|
+
constructor() {
|
|
88
|
+
super("Timed out waiting for Telegram answer.");
|
|
89
|
+
this.name = "AskTimeoutError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class AskAbortedError extends NotifierError {
|
|
94
|
+
constructor() {
|
|
95
|
+
super("Telegram answer wait was aborted.");
|
|
96
|
+
this.name = "AskAbortedError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
63
100
|
type RawEnvConfig = {
|
|
64
101
|
defaultProfile?: unknown;
|
|
65
102
|
profiles?: unknown;
|
|
@@ -151,6 +188,117 @@ export class Notifier {
|
|
|
151
188
|
};
|
|
152
189
|
}
|
|
153
190
|
|
|
191
|
+
async ask(input: AskInput): Promise<AskResult> {
|
|
192
|
+
const message = input.message.trim();
|
|
193
|
+
|
|
194
|
+
if (message === "") {
|
|
195
|
+
throw new NotifierError("Message cannot be empty.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!Array.isArray(input.options)) {
|
|
199
|
+
throw new NotifierError("Options must be an array.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (input.options.length < 1 || input.options.length > 10) {
|
|
203
|
+
throw new NotifierError("Options must contain between 1 and 10 items.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const options = input.options.map((option, index) => {
|
|
207
|
+
if (typeof option !== "string" || option.trim() === "") {
|
|
208
|
+
throw new NotifierError(
|
|
209
|
+
`Invalid option at index ${index}: expected non-empty string.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return option.trim();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
input.timeoutMs !== undefined &&
|
|
218
|
+
(!Number.isFinite(input.timeoutMs) || input.timeoutMs < 0)
|
|
219
|
+
) {
|
|
220
|
+
throw new NotifierError("timeoutMs must be a non-negative number.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const selectedProfile = this.resolveProfile(input.profile);
|
|
224
|
+
const offset = await getLatestUpdateOffset(selectedProfile.botToken);
|
|
225
|
+
const requestId = randomUUID().replaceAll("-", "");
|
|
226
|
+
const callbackOptions = options.map((option, index) => ({
|
|
227
|
+
label: option,
|
|
228
|
+
callbackData: `nnt:${requestId}:${index}`,
|
|
229
|
+
}));
|
|
230
|
+
const sentMessage = await sendTelegramChoiceMessage(
|
|
231
|
+
selectedProfile.botToken,
|
|
232
|
+
selectedProfile.chatId,
|
|
233
|
+
message,
|
|
234
|
+
callbackOptions
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const callback = await waitForTelegramCallback(selectedProfile.botToken, {
|
|
239
|
+
chatId: selectedProfile.chatId,
|
|
240
|
+
messageId: sentMessage.messageId,
|
|
241
|
+
callbackData: callbackOptions.map((option) => option.callbackData),
|
|
242
|
+
offset,
|
|
243
|
+
timeoutMs: input.timeoutMs,
|
|
244
|
+
signal: input.signal,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const selectedIndex = callbackOptions.findIndex(
|
|
248
|
+
(option) => option.callbackData === callback.data
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (selectedIndex === -1) {
|
|
252
|
+
throw new NotifierError("Received an unknown Telegram answer.");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await answerTelegramCallbackQuery(
|
|
256
|
+
selectedProfile.botToken,
|
|
257
|
+
callback.callbackQueryId
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await markTelegramSelectedOption(
|
|
262
|
+
selectedProfile.botToken,
|
|
263
|
+
selectedProfile.chatId,
|
|
264
|
+
sentMessage.messageId,
|
|
265
|
+
callbackOptions,
|
|
266
|
+
callback.data
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
// Keep the selected answer even if the visual update fails.
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
selected: options[selectedIndex],
|
|
274
|
+
profile: selectedProfile.name,
|
|
275
|
+
provider: selectedProfile.type,
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error instanceof Error && error.message === "Timed out waiting") {
|
|
279
|
+
throw new AskTimeoutError();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isAbortError(error)) {
|
|
283
|
+
throw new AskAbortedError();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw error;
|
|
287
|
+
} finally {
|
|
288
|
+
if (input.signal?.aborted) {
|
|
289
|
+
try {
|
|
290
|
+
await clearTelegramInlineKeyboard(
|
|
291
|
+
selectedProfile.botToken,
|
|
292
|
+
selectedProfile.chatId,
|
|
293
|
+
sentMessage.messageId
|
|
294
|
+
);
|
|
295
|
+
} catch {
|
|
296
|
+
// Do not mask the original ask result or failure if cleanup fails.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
154
302
|
private resolveProfile(profileName?: string): Readonly<NotifierProfile> {
|
|
155
303
|
if (profileName) {
|
|
156
304
|
const profile = this.profilesByName.get(profileName);
|
|
@@ -324,3 +472,9 @@ function isConfigLoader(value: unknown): value is NotifierConfigLoader {
|
|
|
324
472
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
325
473
|
return typeof error === "object" && error !== null && "code" in error;
|
|
326
474
|
}
|
|
475
|
+
|
|
476
|
+
function isAbortError(error: unknown): boolean {
|
|
477
|
+
return error instanceof DOMException
|
|
478
|
+
? error.name === "AbortError"
|
|
479
|
+
: error instanceof Error && error.name === "AbortError";
|
|
480
|
+
}
|