nonotify 0.1.2 → 0.1.3
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/.github/workflows/release.yml +3 -3
- package/dist/cli.js +448 -0
- package/dist/config.js +53 -0
- package/dist/display.js +24 -0
- package/dist/prompt.js +47 -0
- package/dist/telegram.js +59 -0
- package/nonotify-0.1.2.tgz +0 -0
- package/package.json +8 -2
- package/src/cli.ts +308 -223
- package/src/config.ts +39 -36
- package/src/display.ts +20 -17
- package/src/prompt.ts +36 -27
- package/src/telegram.ts +77 -53
- package/tsconfig.json +1 -3
package/src/config.ts
CHANGED
|
@@ -1,74 +1,77 @@
|
|
|
1
|
-
import { homedir } from
|
|
2
|
-
import { join, resolve } from
|
|
3
|
-
import { mkdir, readFile, writeFile, chmod } from
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
export type TelegramProfile = {
|
|
6
|
-
type:
|
|
7
|
-
name: string
|
|
8
|
-
botToken: string
|
|
9
|
-
chatId: string
|
|
10
|
-
createdAt: string
|
|
11
|
-
}
|
|
6
|
+
type: "telegram";
|
|
7
|
+
name: string;
|
|
8
|
+
botToken: string;
|
|
9
|
+
chatId: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
};
|
|
12
12
|
|
|
13
13
|
export type NntConfig = {
|
|
14
|
-
defaultProfile: string | null
|
|
15
|
-
profiles: Record<string, TelegramProfile
|
|
16
|
-
}
|
|
14
|
+
defaultProfile: string | null;
|
|
15
|
+
profiles: Record<string, TelegramProfile>;
|
|
16
|
+
};
|
|
17
17
|
|
|
18
18
|
const DEFAULT_CONFIG: NntConfig = {
|
|
19
19
|
defaultProfile: null,
|
|
20
20
|
profiles: {},
|
|
21
|
-
}
|
|
21
|
+
};
|
|
22
22
|
|
|
23
23
|
export function getConfigDir(): string {
|
|
24
|
-
const dir = process.env.NNT_CONFIG_DIR
|
|
25
|
-
if (dir && dir.trim() !==
|
|
26
|
-
return resolve(dir)
|
|
24
|
+
const dir = process.env.NNT_CONFIG_DIR;
|
|
25
|
+
if (dir && dir.trim() !== "") {
|
|
26
|
+
return resolve(dir);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return join(homedir(),
|
|
29
|
+
return join(homedir(), ".nnt");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function getConfigPath(): string {
|
|
33
|
-
return join(getConfigDir(),
|
|
33
|
+
return join(getConfigDir(), "config");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function loadConfig(): Promise<NntConfig> {
|
|
37
|
-
const path = getConfigPath()
|
|
37
|
+
const path = getConfigPath();
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
|
-
const raw = await readFile(path,
|
|
41
|
-
const parsed = JSON.parse(raw) as Partial<NntConfig
|
|
40
|
+
const raw = await readFile(path, "utf8");
|
|
41
|
+
const parsed = JSON.parse(raw) as Partial<NntConfig>;
|
|
42
42
|
|
|
43
43
|
return {
|
|
44
|
-
defaultProfile:
|
|
44
|
+
defaultProfile:
|
|
45
|
+
typeof parsed.defaultProfile === "string"
|
|
46
|
+
? parsed.defaultProfile
|
|
47
|
+
: null,
|
|
45
48
|
profiles: parsed.profiles ?? {},
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return { ...DEFAULT_CONFIG }
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
52
|
+
return { ...DEFAULT_CONFIG };
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
throw error
|
|
55
|
+
throw error;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export async function saveConfig(config: NntConfig): Promise<void> {
|
|
58
|
-
const dir = getConfigDir()
|
|
59
|
-
const path = getConfigPath()
|
|
60
|
+
const dir = getConfigDir();
|
|
61
|
+
const path = getConfigPath();
|
|
60
62
|
|
|
61
|
-
await mkdir(dir, { recursive: true })
|
|
62
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
63
|
+
await mkdir(dir, { recursive: true });
|
|
64
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
63
67
|
|
|
64
68
|
try {
|
|
65
|
-
await chmod(path, 0o600)
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
69
|
+
await chmod(path, 0o600);
|
|
70
|
+
} catch {
|
|
68
71
|
// Best effort only.
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
73
|
-
return typeof error ===
|
|
76
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
74
77
|
}
|
package/src/display.ts
CHANGED
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
import Table from
|
|
1
|
+
import Table from "cli-table3";
|
|
2
2
|
|
|
3
3
|
type ProfileRow = {
|
|
4
|
-
name: string
|
|
5
|
-
provider: string
|
|
6
|
-
isDefault: boolean
|
|
7
|
-
}
|
|
4
|
+
name: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
isDefault: boolean;
|
|
7
|
+
};
|
|
8
8
|
|
|
9
9
|
export function printProfilesTable(rows: ProfileRow[]): void {
|
|
10
10
|
if (rows.length === 0) {
|
|
11
|
-
process.stdout.write(
|
|
12
|
-
return
|
|
11
|
+
process.stdout.write("No profiles configured. Run `nnt profile add`.\n");
|
|
12
|
+
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const table = new Table({
|
|
16
|
-
head: [
|
|
17
|
-
})
|
|
16
|
+
head: ["Profile", "Provider", "Default"],
|
|
17
|
+
});
|
|
18
18
|
|
|
19
19
|
for (const row of rows) {
|
|
20
|
-
table.push([row.name, row.provider, row.isDefault ?
|
|
20
|
+
table.push([row.name, row.provider, row.isDefault ? "yes" : ""]);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
process.stdout.write(`${table.toString()}\n`)
|
|
23
|
+
process.stdout.write(`${table.toString()}\n`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function printKeyValueTable(
|
|
27
|
-
|
|
26
|
+
export function printKeyValueTable(
|
|
27
|
+
title: string,
|
|
28
|
+
rows: Array<{ key: string; value: string }>,
|
|
29
|
+
): void {
|
|
30
|
+
process.stdout.write(`${title}\n`);
|
|
28
31
|
|
|
29
32
|
const table = new Table({
|
|
30
|
-
head: [
|
|
31
|
-
})
|
|
33
|
+
head: ["Field", "Value"],
|
|
34
|
+
});
|
|
32
35
|
|
|
33
36
|
for (const row of rows) {
|
|
34
|
-
table.push([row.key, row.value])
|
|
37
|
+
table.push([row.key, row.value]);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
process.stdout.write(`${table.toString()}\n`)
|
|
40
|
+
process.stdout.write(`${table.toString()}\n`);
|
|
38
41
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,67 +1,76 @@
|
|
|
1
|
-
import { cancel, confirm, isCancel, select, text } from
|
|
1
|
+
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
2
2
|
|
|
3
3
|
export async function askRequired(question: string): Promise<string> {
|
|
4
|
-
return askRequiredWithInitial(question)
|
|
4
|
+
return askRequiredWithInitial(question);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export async function askRequiredWithInitial(
|
|
8
|
-
|
|
7
|
+
export async function askRequiredWithInitial(
|
|
8
|
+
question: string,
|
|
9
|
+
initialValue?: string,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const message = normalizeQuestion(question);
|
|
9
12
|
|
|
10
13
|
const value = await text({
|
|
11
14
|
message,
|
|
12
15
|
initialValue,
|
|
13
16
|
validate(input) {
|
|
14
|
-
if (!input || input.trim() ===
|
|
15
|
-
return
|
|
17
|
+
if (!input || input.trim() === "") {
|
|
18
|
+
return "Value cannot be empty";
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
return undefined
|
|
21
|
+
return undefined;
|
|
19
22
|
},
|
|
20
|
-
})
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
if (isCancel(value)) {
|
|
23
|
-
cancel(
|
|
24
|
-
process.exit(1)
|
|
26
|
+
cancel("Operation cancelled.");
|
|
27
|
+
process.exit(1);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
return value.trim()
|
|
30
|
+
return value.trim();
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
export async function askConfirm(
|
|
33
|
+
export async function askConfirm(
|
|
34
|
+
question: string,
|
|
35
|
+
initialValue = false,
|
|
36
|
+
): Promise<boolean> {
|
|
31
37
|
const value = await confirm({
|
|
32
38
|
message: normalizeQuestion(question),
|
|
33
39
|
initialValue,
|
|
34
|
-
})
|
|
40
|
+
});
|
|
35
41
|
|
|
36
42
|
if (isCancel(value)) {
|
|
37
|
-
cancel(
|
|
38
|
-
process.exit(1)
|
|
43
|
+
cancel("Operation cancelled.");
|
|
44
|
+
process.exit(1);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
return value
|
|
47
|
+
return value;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
type SelectOption = {
|
|
45
|
-
value: string
|
|
46
|
-
label: string
|
|
47
|
-
hint?: string
|
|
48
|
-
disabled?: boolean
|
|
49
|
-
}
|
|
51
|
+
value: string;
|
|
52
|
+
label: string;
|
|
53
|
+
hint?: string;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
};
|
|
50
56
|
|
|
51
|
-
export async function askSelect(
|
|
57
|
+
export async function askSelect(
|
|
58
|
+
question: string,
|
|
59
|
+
options: SelectOption[],
|
|
60
|
+
): Promise<string> {
|
|
52
61
|
const value = await select<string>({
|
|
53
62
|
message: normalizeQuestion(question),
|
|
54
63
|
options,
|
|
55
|
-
})
|
|
64
|
+
});
|
|
56
65
|
|
|
57
66
|
if (isCancel(value)) {
|
|
58
|
-
cancel(
|
|
59
|
-
process.exit(1)
|
|
67
|
+
cancel("Operation cancelled.");
|
|
68
|
+
process.exit(1);
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
return value
|
|
71
|
+
return value;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
function normalizeQuestion(question: string): string {
|
|
66
|
-
return question.trim().replace(/:\s*$/,
|
|
75
|
+
return question.trim().replace(/:\s*$/, "");
|
|
67
76
|
}
|
package/src/telegram.ts
CHANGED
|
@@ -1,71 +1,81 @@
|
|
|
1
1
|
type TelegramApiResponse<T> =
|
|
2
2
|
| {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
ok: true;
|
|
4
|
+
result: T;
|
|
5
|
+
}
|
|
6
6
|
| {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
ok: false;
|
|
8
|
+
error_code: number;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
11
|
|
|
12
12
|
type TelegramUpdate = {
|
|
13
|
-
update_id: number
|
|
13
|
+
update_id: number;
|
|
14
14
|
message?: {
|
|
15
15
|
from?: {
|
|
16
|
-
username?: string
|
|
17
|
-
}
|
|
16
|
+
username?: string;
|
|
17
|
+
};
|
|
18
18
|
chat?: {
|
|
19
|
-
id: number
|
|
20
|
-
username?: string
|
|
21
|
-
}
|
|
22
|
-
text?: string
|
|
23
|
-
}
|
|
24
|
-
}
|
|
19
|
+
id: number;
|
|
20
|
+
username?: string;
|
|
21
|
+
};
|
|
22
|
+
text?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
25
|
|
|
26
26
|
export type TelegramConnection = {
|
|
27
|
-
chatId: string
|
|
28
|
-
username: string | null
|
|
29
|
-
}
|
|
27
|
+
chatId: string;
|
|
28
|
+
username: string | null;
|
|
29
|
+
};
|
|
30
30
|
|
|
31
31
|
async function telegramRequest<T>(
|
|
32
32
|
botToken: string,
|
|
33
33
|
method: string,
|
|
34
34
|
payload: Record<string, unknown>,
|
|
35
35
|
): Promise<T> {
|
|
36
|
-
const response = await fetch(
|
|
37
|
-
method
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const response = await fetch(
|
|
37
|
+
`https://api.telegram.org/bot${botToken}/${method}`,
|
|
38
|
+
{
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"content-type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(payload),
|
|
40
44
|
},
|
|
41
|
-
|
|
42
|
-
})
|
|
45
|
+
);
|
|
43
46
|
|
|
44
47
|
if (!response.ok) {
|
|
45
|
-
throw new Error(`Telegram API HTTP ${response.status}`)
|
|
48
|
+
throw new Error(`Telegram API HTTP ${response.status}`);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
const json = await response.json() as TelegramApiResponse<T
|
|
51
|
+
const json = (await response.json()) as TelegramApiResponse<T>;
|
|
49
52
|
|
|
50
53
|
if (!json.ok) {
|
|
51
|
-
throw new Error(json.description)
|
|
54
|
+
throw new Error(json.description);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
return json.result
|
|
57
|
+
return json.result;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export async function getLatestUpdateOffset(botToken: string): Promise<number> {
|
|
58
|
-
const updates = await telegramRequest<TelegramUpdate[]>(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const updates = await telegramRequest<TelegramUpdate[]>(
|
|
62
|
+
botToken,
|
|
63
|
+
"getUpdates",
|
|
64
|
+
{
|
|
65
|
+
timeout: 0,
|
|
66
|
+
allowed_updates: ["message"],
|
|
67
|
+
},
|
|
68
|
+
);
|
|
62
69
|
|
|
63
70
|
if (updates.length === 0) {
|
|
64
|
-
return 0
|
|
71
|
+
return 0;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
const maxUpdateId = updates.reduce(
|
|
68
|
-
|
|
74
|
+
const maxUpdateId = updates.reduce(
|
|
75
|
+
(acc, item) => Math.max(acc, item.update_id),
|
|
76
|
+
0,
|
|
77
|
+
);
|
|
78
|
+
return maxUpdateId + 1;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
export async function waitForChatId(
|
|
@@ -73,37 +83,51 @@ export async function waitForChatId(
|
|
|
73
83
|
offset: number,
|
|
74
84
|
timeoutSeconds = 120,
|
|
75
85
|
): Promise<TelegramConnection> {
|
|
76
|
-
const startedAt = Date.now()
|
|
77
|
-
let currentOffset = offset
|
|
86
|
+
const startedAt = Date.now();
|
|
87
|
+
let currentOffset = offset;
|
|
78
88
|
|
|
79
89
|
while ((Date.now() - startedAt) / 1000 < timeoutSeconds) {
|
|
80
|
-
const remainingSeconds =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const remainingSeconds =
|
|
91
|
+
timeoutSeconds - Math.floor((Date.now() - startedAt) / 1000);
|
|
92
|
+
const pollTimeout = Math.max(1, Math.min(25, remainingSeconds));
|
|
93
|
+
|
|
94
|
+
const updates = await telegramRequest<TelegramUpdate[]>(
|
|
95
|
+
botToken,
|
|
96
|
+
"getUpdates",
|
|
97
|
+
{
|
|
98
|
+
offset: currentOffset,
|
|
99
|
+
timeout: pollTimeout,
|
|
100
|
+
allowed_updates: ["message"],
|
|
101
|
+
},
|
|
102
|
+
);
|
|
88
103
|
|
|
89
104
|
for (const update of updates) {
|
|
90
|
-
currentOffset = Math.max(currentOffset, update.update_id + 1)
|
|
105
|
+
currentOffset = Math.max(currentOffset, update.update_id + 1);
|
|
91
106
|
|
|
92
107
|
if (update.message?.chat?.id !== undefined) {
|
|
93
108
|
return {
|
|
94
109
|
chatId: String(update.message.chat.id),
|
|
95
|
-
username:
|
|
96
|
-
|
|
110
|
+
username:
|
|
111
|
+
update.message.from?.username ??
|
|
112
|
+
update.message.chat.username ??
|
|
113
|
+
null,
|
|
114
|
+
};
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
throw new Error(
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Timed out waiting for Telegram message. Send a message to your bot and try again.",
|
|
121
|
+
);
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
export async function sendTelegramMessage(
|
|
105
|
-
|
|
124
|
+
export async function sendTelegramMessage(
|
|
125
|
+
botToken: string,
|
|
126
|
+
chatId: string,
|
|
127
|
+
text: string,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
await telegramRequest(botToken, "sendMessage", {
|
|
106
130
|
chat_id: chatId,
|
|
107
131
|
text,
|
|
108
|
-
})
|
|
132
|
+
});
|
|
109
133
|
}
|