@botozap/cli 0.1.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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/client.js +24 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/contacts.js +110 -0
- package/dist/commands/conversations.js +60 -0
- package/dist/commands/customers.js +86 -0
- package/dist/commands/deliveries.js +40 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/logs.js +38 -0
- package/dist/commands/media.js +28 -0
- package/dist/commands/messages.js +113 -0
- package/dist/commands/numbers.js +51 -0
- package/dist/commands/setup-links.js +63 -0
- package/dist/commands/shared.js +37 -0
- package/dist/commands/status.js +32 -0
- package/dist/commands/templates.js +87 -0
- package/dist/commands/users.js +28 -0
- package/dist/commands/webhooks.js +116 -0
- package/dist/config.js +77 -0
- package/dist/index.js +74 -0
- package/dist/output.js +101 -0
- package/dist/render-error.js +100 -0
- package/package.json +47 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printCursorFooter } from "../output.js";
|
|
3
|
+
export function registerLogs(program) {
|
|
4
|
+
const logs = program
|
|
5
|
+
.command("logs")
|
|
6
|
+
.description("Logs de requisições à API (api_logs)");
|
|
7
|
+
logs
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista logs de API (paginação por cursor)")
|
|
10
|
+
.option("--source <fonte>", "filtra por fonte")
|
|
11
|
+
.option("--method <metodo>", "filtra por método HTTP")
|
|
12
|
+
.option("--status-code <codigo>", "filtra por status HTTP")
|
|
13
|
+
.option("--limit <n>", "quantidade por página")
|
|
14
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
15
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
16
|
+
.action(async (opts, cmd) => {
|
|
17
|
+
const { client, format } = context(cmd);
|
|
18
|
+
const res = await client.apiLogs.list({
|
|
19
|
+
source: opts.source,
|
|
20
|
+
method: opts.method,
|
|
21
|
+
status_code: toInt(opts.statusCode),
|
|
22
|
+
limit: toInt(opts.limit),
|
|
23
|
+
after: opts.after,
|
|
24
|
+
before: opts.before,
|
|
25
|
+
});
|
|
26
|
+
if (format === "json")
|
|
27
|
+
return printJson(res);
|
|
28
|
+
printTable(res.data, [
|
|
29
|
+
{ header: "ID", key: "id", max: 36 },
|
|
30
|
+
{ header: "MÉTODO", key: "method" },
|
|
31
|
+
{ header: "ROTA", key: "path", max: 40 },
|
|
32
|
+
{ header: "STATUS", key: "status_code" },
|
|
33
|
+
{ header: "FONTE", key: "source" },
|
|
34
|
+
{ header: "CRIADO", key: "created_at" },
|
|
35
|
+
]);
|
|
36
|
+
printCursorFooter(res.paging);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { context } from "./shared.js";
|
|
2
|
+
import { printJson, printDetail, printLine } from "../output.js";
|
|
3
|
+
export function registerMedia(program) {
|
|
4
|
+
const media = program.command("media").description("Ingestão de mídia");
|
|
5
|
+
media
|
|
6
|
+
.command("ingest")
|
|
7
|
+
.description("Ingere uma mídia a partir de uma URL")
|
|
8
|
+
.requiredOption("--phone-number-id <id>", "número de origem (obrigatório)")
|
|
9
|
+
.requiredOption("--source <url>", "URL pública da mídia (obrigatório)")
|
|
10
|
+
.option("--filename <nome>", "nome do arquivo")
|
|
11
|
+
.option("--mime-type <tipo>", "tipo MIME (ex.: image/jpeg)")
|
|
12
|
+
.option("--delivery <modo>", "modo de entrega")
|
|
13
|
+
.action(async (opts, cmd) => {
|
|
14
|
+
const { client, format } = context(cmd);
|
|
15
|
+
const data = await client.media.upload({
|
|
16
|
+
phone_number_id: opts.phoneNumberId,
|
|
17
|
+
source: opts.source,
|
|
18
|
+
filename: opts.filename,
|
|
19
|
+
mime_type: opts.mimeType,
|
|
20
|
+
// A rota valida o valor; aqui só repassamos a string do flag.
|
|
21
|
+
delivery: opts.delivery,
|
|
22
|
+
});
|
|
23
|
+
if (format === "json")
|
|
24
|
+
return printJson(data);
|
|
25
|
+
printLine("Mídia ingerida.");
|
|
26
|
+
printDetail(data);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { context, toInt } from "./shared.js";
|
|
3
|
+
import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
|
|
4
|
+
/** Lê JSON cru de um arquivo (--input) ou do stdin (--stdin). */
|
|
5
|
+
function readRawBody(file, fromStdin) {
|
|
6
|
+
let raw;
|
|
7
|
+
if (file) {
|
|
8
|
+
raw = readFileSync(file, "utf8");
|
|
9
|
+
}
|
|
10
|
+
else if (fromStdin) {
|
|
11
|
+
raw = readFileSync(0, "utf8");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
throw new Error("Nenhuma fonte de payload informada.");
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error("O payload informado não é um JSON válido.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function registerMessages(program) {
|
|
24
|
+
const messages = program
|
|
25
|
+
.command("messages")
|
|
26
|
+
.description("Enviar e consultar mensagens de WhatsApp");
|
|
27
|
+
messages
|
|
28
|
+
.command("send")
|
|
29
|
+
.description("Envia uma mensagem (texto simples ou payload completo)")
|
|
30
|
+
.option("--to <wa_id>", "destinatário (número E.164 sem +, ex.: 5511999999999)")
|
|
31
|
+
.option("--text <body>", "corpo de uma mensagem de texto")
|
|
32
|
+
.option("--from <phone_number_id>", "número de origem (opcional)")
|
|
33
|
+
.option("--input <arquivo>", "arquivo JSON com o corpo bruto da mensagem")
|
|
34
|
+
.option("--stdin", "lê o corpo bruto da mensagem do stdin")
|
|
35
|
+
.action(async (opts, cmd) => {
|
|
36
|
+
const { client, format } = context(cmd);
|
|
37
|
+
// POST /messages responde o objeto DIRETO (sem envelope `data`); ambos os
|
|
38
|
+
// caminhos devolvem o mesmo shape { id, wamid, to, status }.
|
|
39
|
+
let result;
|
|
40
|
+
if (opts.input || opts.stdin) {
|
|
41
|
+
// Payload cru: contrato de baixo nível do SDK (`request`), sem montar o corpo.
|
|
42
|
+
const body = readRawBody(opts.input, opts.stdin);
|
|
43
|
+
result = await client.request("POST", "/messages", { body });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (!opts.to || !opts.text) {
|
|
47
|
+
throw new Error("Informe --to e --text, ou use --input <arquivo.json> / --stdin para payload completo.");
|
|
48
|
+
}
|
|
49
|
+
result = await client.messages.send({
|
|
50
|
+
to: opts.to,
|
|
51
|
+
text: opts.text,
|
|
52
|
+
...(opts.from ? { from: opts.from } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (format === "json")
|
|
56
|
+
return printJson(result);
|
|
57
|
+
printLine("Mensagem enfileirada.");
|
|
58
|
+
printDetail(result, [
|
|
59
|
+
"id",
|
|
60
|
+
"wamid",
|
|
61
|
+
"to",
|
|
62
|
+
"status",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
messages
|
|
66
|
+
.command("list")
|
|
67
|
+
.description("Lista mensagens (paginação por cursor)")
|
|
68
|
+
.option("--phone-number-id <id>", "filtra por número")
|
|
69
|
+
.option("--conversation-id <id>", "filtra por conversa")
|
|
70
|
+
.option("--direction <dir>", "inbound | outbound")
|
|
71
|
+
.option("--status <status>", "status da mensagem")
|
|
72
|
+
.option("--message-type <tipo>", "tipo (text, image, template, …)")
|
|
73
|
+
.option("--has-media", "apenas mensagens com mídia")
|
|
74
|
+
.option("--limit <n>", "quantidade por página")
|
|
75
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
76
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
77
|
+
.action(async (opts, cmd) => {
|
|
78
|
+
const { client, format } = context(cmd);
|
|
79
|
+
const res = await client.messages.list({
|
|
80
|
+
phone_number_id: opts.phoneNumberId,
|
|
81
|
+
conversation_id: opts.conversationId,
|
|
82
|
+
direction: opts.direction,
|
|
83
|
+
status: opts.status,
|
|
84
|
+
message_type: opts.messageType,
|
|
85
|
+
has_media: opts.hasMedia ? true : undefined,
|
|
86
|
+
limit: toInt(opts.limit),
|
|
87
|
+
after: opts.after,
|
|
88
|
+
before: opts.before,
|
|
89
|
+
});
|
|
90
|
+
if (format === "json")
|
|
91
|
+
return printJson(res);
|
|
92
|
+
printTable(res.data, [
|
|
93
|
+
{ header: "ID", key: "id", max: 36 },
|
|
94
|
+
{ header: "DIREÇÃO", key: "direction" },
|
|
95
|
+
{ header: "TIPO", key: "kind" },
|
|
96
|
+
{ header: "STATUS", key: "status" },
|
|
97
|
+
{ header: "DE", key: "from" },
|
|
98
|
+
{ header: "PARA", key: "to" },
|
|
99
|
+
{ header: "CRIADA", key: "created_at" },
|
|
100
|
+
]);
|
|
101
|
+
printCursorFooter(res.paging);
|
|
102
|
+
});
|
|
103
|
+
messages
|
|
104
|
+
.command("get <id>")
|
|
105
|
+
.description("Detalha uma mensagem por UUID interno ou wamid")
|
|
106
|
+
.action(async (id, _opts, cmd) => {
|
|
107
|
+
const { client, format } = context(cmd);
|
|
108
|
+
const data = await client.messages.get(id);
|
|
109
|
+
if (format === "json")
|
|
110
|
+
return printJson(data);
|
|
111
|
+
printDetail(data);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printOffsetFooter, } from "../output.js";
|
|
3
|
+
export function registerNumbers(program) {
|
|
4
|
+
const numbers = program
|
|
5
|
+
.command("numbers")
|
|
6
|
+
.description("Números de telefone (WhatsApp) conectados");
|
|
7
|
+
numbers
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista números (paginação por offset)")
|
|
10
|
+
.option("--customer-id <id>", "filtra por cliente")
|
|
11
|
+
.option("--page <n>", "página")
|
|
12
|
+
.option("--per-page <n>", "itens por página")
|
|
13
|
+
.action(async (opts, cmd) => {
|
|
14
|
+
const { client, format } = context(cmd);
|
|
15
|
+
const res = await client.phoneNumbers.list({
|
|
16
|
+
customer_id: opts.customerId,
|
|
17
|
+
page: toInt(opts.page),
|
|
18
|
+
per_page: toInt(opts.perPage),
|
|
19
|
+
});
|
|
20
|
+
if (format === "json")
|
|
21
|
+
return printJson(res);
|
|
22
|
+
printTable(res.data, [
|
|
23
|
+
{ header: "ID", key: "id", max: 36 },
|
|
24
|
+
{ header: "NÚMERO", key: "display_phone_number" },
|
|
25
|
+
{ header: "NOME", key: "verified_name" },
|
|
26
|
+
{ header: "STATUS", key: "status" },
|
|
27
|
+
{ header: "QUALIDADE", key: "quality_rating" },
|
|
28
|
+
]);
|
|
29
|
+
printOffsetFooter(res.meta);
|
|
30
|
+
});
|
|
31
|
+
numbers
|
|
32
|
+
.command("get <id>")
|
|
33
|
+
.description("Detalha um número")
|
|
34
|
+
.action(async (id, _opts, cmd) => {
|
|
35
|
+
const { client, format } = context(cmd);
|
|
36
|
+
const data = await client.phoneNumbers.get(id);
|
|
37
|
+
if (format === "json")
|
|
38
|
+
return printJson(data);
|
|
39
|
+
printDetail(data);
|
|
40
|
+
});
|
|
41
|
+
numbers
|
|
42
|
+
.command("health <id>")
|
|
43
|
+
.description("Verifica a saúde de um número")
|
|
44
|
+
.action(async (id, _opts, cmd) => {
|
|
45
|
+
const { client, format } = context(cmd);
|
|
46
|
+
const data = await client.phoneNumbers.health(id);
|
|
47
|
+
if (format === "json")
|
|
48
|
+
return printJson(data);
|
|
49
|
+
printDetail(data);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
|
|
3
|
+
export function registerSetupLinks(program) {
|
|
4
|
+
const links = program
|
|
5
|
+
.command("setup-links")
|
|
6
|
+
.description("Links de configuração (Embedded Signup) de um cliente");
|
|
7
|
+
links
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista os setup links de um cliente (offset)")
|
|
10
|
+
.requiredOption("--customer <id>", "id do cliente (obrigatório)")
|
|
11
|
+
.option("--page <n>", "página")
|
|
12
|
+
.option("--per-page <n>", "itens por página")
|
|
13
|
+
.action(async (opts, cmd) => {
|
|
14
|
+
const { client, format } = context(cmd);
|
|
15
|
+
const res = await client.customers.listSetupLinks(opts.customer, {
|
|
16
|
+
page: toInt(opts.page),
|
|
17
|
+
per_page: toInt(opts.perPage),
|
|
18
|
+
});
|
|
19
|
+
if (format === "json")
|
|
20
|
+
return printJson(res);
|
|
21
|
+
printTable(res.data, [
|
|
22
|
+
{ header: "ID", key: "id", max: 36 },
|
|
23
|
+
{ header: "STATUS", key: "status" },
|
|
24
|
+
{ header: "URL", key: "url", max: 60 },
|
|
25
|
+
{ header: "EXPIRA", key: "expires_at" },
|
|
26
|
+
]);
|
|
27
|
+
printOffsetFooter(res.meta);
|
|
28
|
+
});
|
|
29
|
+
links
|
|
30
|
+
.command("create")
|
|
31
|
+
.description("Cria um setup link para um cliente")
|
|
32
|
+
.requiredOption("--customer <id>", "id do cliente (obrigatório)")
|
|
33
|
+
.action(async (opts, cmd) => {
|
|
34
|
+
const { client, format } = context(cmd);
|
|
35
|
+
const data = await client.customers.createSetupLink(opts.customer);
|
|
36
|
+
if (format === "json")
|
|
37
|
+
return printJson(data);
|
|
38
|
+
printLine("Setup link criado.");
|
|
39
|
+
printDetail(data);
|
|
40
|
+
});
|
|
41
|
+
links
|
|
42
|
+
.command("update <linkId>")
|
|
43
|
+
.description("Atualiza um setup link (status / expiração)")
|
|
44
|
+
.requiredOption("--customer <id>", "id do cliente (obrigatório)")
|
|
45
|
+
.option("--status <status>", "novo status")
|
|
46
|
+
.option("--expires-at <data>", "nova expiração (ISO 8601)")
|
|
47
|
+
.action(async (linkId, opts, cmd) => {
|
|
48
|
+
const { client, format } = context(cmd);
|
|
49
|
+
const body = {};
|
|
50
|
+
if (opts.status !== undefined)
|
|
51
|
+
body.status = opts.status;
|
|
52
|
+
if (opts.expiresAt !== undefined)
|
|
53
|
+
body.expires_at = opts.expiresAt;
|
|
54
|
+
if (Object.keys(body).length === 0) {
|
|
55
|
+
throw new Error("Informe ao menos --status ou --expires-at.");
|
|
56
|
+
}
|
|
57
|
+
const data = await client.customers.updateSetupLink(opts.customer, linkId, body);
|
|
58
|
+
if (format === "json")
|
|
59
|
+
return printJson(data);
|
|
60
|
+
printLine("Setup link atualizado.");
|
|
61
|
+
printDetail(data);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createClient } from "../client.js";
|
|
2
|
+
import { resolveFormat } from "../output.js";
|
|
3
|
+
/** Lê as flags globais (--api-key/--api-url/--output) de qualquer subcomando. */
|
|
4
|
+
export function readGlobals(cmd) {
|
|
5
|
+
const opts = cmd.optsWithGlobals();
|
|
6
|
+
return {
|
|
7
|
+
apiKey: opts.apiKey,
|
|
8
|
+
apiUrl: opts.apiUrl,
|
|
9
|
+
output: opts.output,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Constrói client (SDK) + formato a partir do comando atual. */
|
|
13
|
+
export function context(cmd) {
|
|
14
|
+
const g = readGlobals(cmd);
|
|
15
|
+
const global = { apiKey: g.apiKey, apiUrl: g.apiUrl };
|
|
16
|
+
return {
|
|
17
|
+
client: createClient(global),
|
|
18
|
+
format: resolveFormat(g.output),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Converte string para inteiro, ou undefined se vazio/ inválido. */
|
|
22
|
+
export function toInt(value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const n = Number.parseInt(value, 10);
|
|
26
|
+
return Number.isNaN(n) ? undefined : n;
|
|
27
|
+
}
|
|
28
|
+
/** Booleano a partir de string "true"/"false" (ou undefined). */
|
|
29
|
+
export function toBool(value) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (value === "true")
|
|
33
|
+
return true;
|
|
34
|
+
if (value === "false")
|
|
35
|
+
return false;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createClient } from "../client.js";
|
|
2
|
+
import { resolveAuth } from "../config.js";
|
|
3
|
+
import { resolveFormat, printJson, printLine, dim } from "../output.js";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
export function registerStatus(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("status")
|
|
8
|
+
.description("Verifica a autenticação e a conectividade com a API")
|
|
9
|
+
.action(async (_opts, cmd) => {
|
|
10
|
+
const g = cmd.optsWithGlobals();
|
|
11
|
+
const format = resolveFormat(g.output);
|
|
12
|
+
const auth = resolveAuth({ apiKey: g.apiKey, apiUrl: g.apiUrl });
|
|
13
|
+
const global = { apiKey: g.apiKey, apiUrl: g.apiUrl };
|
|
14
|
+
const client = createClient(global);
|
|
15
|
+
// Sonda leve: 1 número apenas confirma que a chave alcança a conta.
|
|
16
|
+
const res = await client.phoneNumbers.list({ per_page: 1 });
|
|
17
|
+
const total = res.meta?.total_count ?? res.data.length;
|
|
18
|
+
if (format === "json") {
|
|
19
|
+
return printJson({
|
|
20
|
+
ok: true,
|
|
21
|
+
base_url: auth.baseUrl,
|
|
22
|
+
api_key_source: auth.apiKeySource,
|
|
23
|
+
phone_numbers_total: total,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
printLine(pc.green("Autenticado. Conta acessível."));
|
|
27
|
+
printLine(`URL base ${auth.baseUrl}`);
|
|
28
|
+
printLine(`Origem da chave ${auth.apiKeySource}`);
|
|
29
|
+
printLine(`Números ${total}`);
|
|
30
|
+
printLine(dim("Sonda: GET /phone_numbers?per_page=1"));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { context, toInt } from "./shared.js";
|
|
3
|
+
import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
|
|
4
|
+
/** Lê e parseia JSON de um arquivo. */
|
|
5
|
+
function readJsonFile(file) {
|
|
6
|
+
let raw;
|
|
7
|
+
try {
|
|
8
|
+
raw = readFileSync(file, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error(`Não foi possível ler o arquivo: ${file}`);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`Arquivo não é JSON válido: ${file}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function registerTemplates(program) {
|
|
21
|
+
const templates = program
|
|
22
|
+
.command("templates")
|
|
23
|
+
.description("Gerenciar templates de mensagem");
|
|
24
|
+
templates
|
|
25
|
+
.command("list")
|
|
26
|
+
.description("Lista templates (paginação por offset)")
|
|
27
|
+
.option("--status <status>", "filtra por status")
|
|
28
|
+
.option("--category <categoria>", "filtra por categoria")
|
|
29
|
+
.option("--phone-number-id <id>", "filtra por número")
|
|
30
|
+
.option("--page <n>", "página")
|
|
31
|
+
.option("--per-page <n>", "itens por página")
|
|
32
|
+
.action(async (opts, cmd) => {
|
|
33
|
+
const { client, format } = context(cmd);
|
|
34
|
+
const res = await client.templates.list({
|
|
35
|
+
status: opts.status,
|
|
36
|
+
category: opts.category,
|
|
37
|
+
phone_number_id: opts.phoneNumberId,
|
|
38
|
+
page: toInt(opts.page),
|
|
39
|
+
per_page: toInt(opts.perPage),
|
|
40
|
+
});
|
|
41
|
+
if (format === "json")
|
|
42
|
+
return printJson(res);
|
|
43
|
+
printTable(res.data, [
|
|
44
|
+
{ header: "ID", key: "id", max: 36 },
|
|
45
|
+
{ header: "NOME", key: "name" },
|
|
46
|
+
{ header: "IDIOMA", key: "language" },
|
|
47
|
+
{ header: "CATEGORIA", key: "category" },
|
|
48
|
+
{ header: "STATUS", key: "status" },
|
|
49
|
+
]);
|
|
50
|
+
printOffsetFooter(res.meta);
|
|
51
|
+
});
|
|
52
|
+
templates
|
|
53
|
+
.command("get <id>")
|
|
54
|
+
.description("Detalha um template")
|
|
55
|
+
.action(async (id, _opts, cmd) => {
|
|
56
|
+
const { client, format } = context(cmd);
|
|
57
|
+
const data = await client.templates.get(id);
|
|
58
|
+
if (format === "json")
|
|
59
|
+
return printJson(data);
|
|
60
|
+
printDetail(data);
|
|
61
|
+
});
|
|
62
|
+
templates
|
|
63
|
+
.command("create")
|
|
64
|
+
.description("Cria um template (componentes via --components arquivo.json)")
|
|
65
|
+
.requiredOption("--name <nome>", "nome do template")
|
|
66
|
+
.requiredOption("--language <idioma>", "código de idioma (ex.: pt_BR)")
|
|
67
|
+
.requiredOption("--category <categoria>", "categoria (MARKETING, UTILITY, …)")
|
|
68
|
+
.requiredOption("--components <arquivo>", "arquivo JSON com o array de componentes")
|
|
69
|
+
.option("--waba-connection-id <id>", "conexão WABA alvo")
|
|
70
|
+
.option("--phone-number-id <id>", "número alvo")
|
|
71
|
+
.action(async (opts, cmd) => {
|
|
72
|
+
const { client, format } = context(cmd);
|
|
73
|
+
const components = readJsonFile(opts.components);
|
|
74
|
+
const data = await client.templates.create({
|
|
75
|
+
name: opts.name,
|
|
76
|
+
language: opts.language,
|
|
77
|
+
category: opts.category,
|
|
78
|
+
components: components,
|
|
79
|
+
waba_connection_id: opts.wabaConnectionId,
|
|
80
|
+
phone_number_id: opts.phoneNumberId,
|
|
81
|
+
});
|
|
82
|
+
if (format === "json")
|
|
83
|
+
return printJson(data);
|
|
84
|
+
printLine("Template criado.");
|
|
85
|
+
printDetail(data);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printOffsetFooter } from "../output.js";
|
|
3
|
+
export function registerUsers(program) {
|
|
4
|
+
const users = program
|
|
5
|
+
.command("users")
|
|
6
|
+
.description("Usuários do workspace");
|
|
7
|
+
users
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista usuários (paginação por offset)")
|
|
10
|
+
.option("--page <n>", "página")
|
|
11
|
+
.option("--per-page <n>", "itens por página")
|
|
12
|
+
.action(async (opts, cmd) => {
|
|
13
|
+
const { client, format } = context(cmd);
|
|
14
|
+
const res = await client.users.list({
|
|
15
|
+
page: toInt(opts.page),
|
|
16
|
+
per_page: toInt(opts.perPage),
|
|
17
|
+
});
|
|
18
|
+
if (format === "json")
|
|
19
|
+
return printJson(res);
|
|
20
|
+
printTable(res.data, [
|
|
21
|
+
{ header: "ID", key: "id", max: 36 },
|
|
22
|
+
{ header: "NOME", key: "name" },
|
|
23
|
+
{ header: "EMAIL", key: "email" },
|
|
24
|
+
{ header: "PAPEL", key: "role" },
|
|
25
|
+
]);
|
|
26
|
+
printOffsetFooter(res.meta);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { context, toInt, toBool } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
|
|
3
|
+
export function registerWebhooks(program) {
|
|
4
|
+
const webhooks = program
|
|
5
|
+
.command("webhooks")
|
|
6
|
+
.description("Gerenciar endpoints de webhook");
|
|
7
|
+
webhooks
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista webhooks (paginação por cursor)")
|
|
10
|
+
.option("--limit <n>", "quantidade por página")
|
|
11
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
12
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
13
|
+
.action(async (opts, cmd) => {
|
|
14
|
+
const { client, format } = context(cmd);
|
|
15
|
+
const res = await client.webhooks.list({
|
|
16
|
+
limit: toInt(opts.limit),
|
|
17
|
+
after: opts.after,
|
|
18
|
+
before: opts.before,
|
|
19
|
+
});
|
|
20
|
+
if (format === "json")
|
|
21
|
+
return printJson(res);
|
|
22
|
+
printTable(res.data, [
|
|
23
|
+
{ header: "ID", key: "id", max: 36 },
|
|
24
|
+
{ header: "URL", key: "url", max: 50 },
|
|
25
|
+
{ header: "ATIVO", key: "active" },
|
|
26
|
+
{ header: "EVENTOS", key: "events" },
|
|
27
|
+
]);
|
|
28
|
+
printCursorFooter(res.paging);
|
|
29
|
+
});
|
|
30
|
+
webhooks
|
|
31
|
+
.command("get <id>")
|
|
32
|
+
.description("Detalha um webhook")
|
|
33
|
+
.action(async (id, _opts, cmd) => {
|
|
34
|
+
const { client, format } = context(cmd);
|
|
35
|
+
const data = await client.webhooks.get(id);
|
|
36
|
+
if (format === "json")
|
|
37
|
+
return printJson(data);
|
|
38
|
+
printDetail(data);
|
|
39
|
+
});
|
|
40
|
+
webhooks
|
|
41
|
+
.command("create")
|
|
42
|
+
.description("Cria um webhook")
|
|
43
|
+
.requiredOption("--url <url>", "URL de entrega (obrigatório)")
|
|
44
|
+
.requiredOption("--events <lista>", "eventos separados por vírgula (obrigatório)")
|
|
45
|
+
.option("--secret <segredo>", "segredo para assinar as entregas")
|
|
46
|
+
.option("--active <bool>", "true | false")
|
|
47
|
+
.action(async (opts, cmd) => {
|
|
48
|
+
const { client, format } = context(cmd);
|
|
49
|
+
const events = String(opts.events)
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((e) => e.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const data = await client.webhooks.create({
|
|
54
|
+
url: opts.url,
|
|
55
|
+
events,
|
|
56
|
+
secret: opts.secret,
|
|
57
|
+
active: toBool(opts.active),
|
|
58
|
+
});
|
|
59
|
+
if (format === "json")
|
|
60
|
+
return printJson(data);
|
|
61
|
+
printLine("Webhook criado.");
|
|
62
|
+
printDetail(data);
|
|
63
|
+
});
|
|
64
|
+
webhooks
|
|
65
|
+
.command("update <id>")
|
|
66
|
+
.description("Atualiza um webhook")
|
|
67
|
+
.option("--url <url>", "nova URL")
|
|
68
|
+
.option("--events <lista>", "novos eventos (separados por vírgula)")
|
|
69
|
+
.option("--secret <segredo>", "novo segredo")
|
|
70
|
+
.option("--active <bool>", "true | false")
|
|
71
|
+
.action(async (id, opts, cmd) => {
|
|
72
|
+
const { client, format } = context(cmd);
|
|
73
|
+
const body = {};
|
|
74
|
+
if (opts.url !== undefined)
|
|
75
|
+
body.url = opts.url;
|
|
76
|
+
if (opts.events !== undefined) {
|
|
77
|
+
body.events = String(opts.events)
|
|
78
|
+
.split(",")
|
|
79
|
+
.map((e) => e.trim())
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
if (opts.secret !== undefined)
|
|
83
|
+
body.secret = opts.secret;
|
|
84
|
+
if (opts.active !== undefined)
|
|
85
|
+
body.active = toBool(opts.active);
|
|
86
|
+
if (Object.keys(body).length === 0) {
|
|
87
|
+
throw new Error("Informe ao menos um campo para atualizar.");
|
|
88
|
+
}
|
|
89
|
+
const data = await client.webhooks.update(id, body);
|
|
90
|
+
if (format === "json")
|
|
91
|
+
return printJson(data);
|
|
92
|
+
printLine("Webhook atualizado.");
|
|
93
|
+
printDetail(data);
|
|
94
|
+
});
|
|
95
|
+
webhooks
|
|
96
|
+
.command("delete <id>")
|
|
97
|
+
.description("Remove um webhook")
|
|
98
|
+
.action(async (id, _opts, cmd) => {
|
|
99
|
+
const { client, format } = context(cmd);
|
|
100
|
+
await client.webhooks.delete(id);
|
|
101
|
+
if (format === "json")
|
|
102
|
+
return printJson({ deleted: true });
|
|
103
|
+
printLine("Webhook removido.");
|
|
104
|
+
});
|
|
105
|
+
webhooks
|
|
106
|
+
.command("test <id>")
|
|
107
|
+
.description("Dispara uma entrega de teste para o webhook")
|
|
108
|
+
.action(async (id, _opts, cmd) => {
|
|
109
|
+
const { client, format } = context(cmd);
|
|
110
|
+
const result = await client.webhooks.test(id);
|
|
111
|
+
if (format === "json")
|
|
112
|
+
return printJson(result);
|
|
113
|
+
printLine("Entrega de teste disparada.");
|
|
114
|
+
printDetail(result);
|
|
115
|
+
});
|
|
116
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, } from "node:fs";
|
|
4
|
+
/** URL base padrão da API pública do BotoZap. */
|
|
5
|
+
export const DEFAULT_BASE_URL = "https://botozap.com.br/api/v1";
|
|
6
|
+
/** Diretório de configuração: `~/.botozap/cli/`. */
|
|
7
|
+
export function configDir() {
|
|
8
|
+
return join(homedir(), ".botozap", "cli");
|
|
9
|
+
}
|
|
10
|
+
/** Caminho do arquivo de configuração: `~/.botozap/cli/config.json`. */
|
|
11
|
+
export function configPath() {
|
|
12
|
+
return join(configDir(), "config.json");
|
|
13
|
+
}
|
|
14
|
+
/** Lê o config do disco (ou objeto vazio se não existir/inválido). */
|
|
15
|
+
export function readConfig() {
|
|
16
|
+
const path = configPath();
|
|
17
|
+
if (!existsSync(path))
|
|
18
|
+
return {};
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(path, "utf8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Grava o config no disco, criando o diretório com permissão restrita. */
|
|
29
|
+
export function writeConfig(config) {
|
|
30
|
+
const dir = configDir();
|
|
31
|
+
const path = configPath();
|
|
32
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
33
|
+
// Permissões restritas ANTES de gravar o segredo: `mode` do writeFileSync só
|
|
34
|
+
// vale na criação; num arquivo preexistente frouxo (ex.: legado 0644), gravar
|
|
35
|
+
// primeiro abriria uma janela em que a API key fica legível por outros
|
|
36
|
+
// usuários locais (TOCTOU). Endurece → escreve → reforça.
|
|
37
|
+
chmodSync(dir, 0o700);
|
|
38
|
+
if (existsSync(path))
|
|
39
|
+
chmodSync(path, 0o600);
|
|
40
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
|
|
41
|
+
mode: 0o600,
|
|
42
|
+
});
|
|
43
|
+
chmodSync(path, 0o600);
|
|
44
|
+
}
|
|
45
|
+
/** Define uma chave individual no config persistido. */
|
|
46
|
+
export function setConfigValue(key, value) {
|
|
47
|
+
const current = readConfig();
|
|
48
|
+
current[key] = value;
|
|
49
|
+
writeConfig(current);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve credenciais com a prioridade documentada:
|
|
53
|
+
* apiKey: --api-key > BOTOZAP_API_KEY > config.json
|
|
54
|
+
* baseUrl: --api-url > BOTOZAP_API_URL > config.json > DEFAULT_BASE_URL
|
|
55
|
+
*/
|
|
56
|
+
export function resolveAuth(opts) {
|
|
57
|
+
const stored = readConfig();
|
|
58
|
+
let apiKey;
|
|
59
|
+
let apiKeySource = "none";
|
|
60
|
+
if (opts.apiKey) {
|
|
61
|
+
apiKey = opts.apiKey;
|
|
62
|
+
apiKeySource = "flag";
|
|
63
|
+
}
|
|
64
|
+
else if (process.env.BOTOZAP_API_KEY) {
|
|
65
|
+
apiKey = process.env.BOTOZAP_API_KEY;
|
|
66
|
+
apiKeySource = "env";
|
|
67
|
+
}
|
|
68
|
+
else if (stored.apiKey) {
|
|
69
|
+
apiKey = stored.apiKey;
|
|
70
|
+
apiKeySource = "config";
|
|
71
|
+
}
|
|
72
|
+
const baseUrl = opts.apiUrl ||
|
|
73
|
+
process.env.BOTOZAP_API_URL ||
|
|
74
|
+
stored.baseUrl ||
|
|
75
|
+
DEFAULT_BASE_URL;
|
|
76
|
+
return { apiKey, baseUrl: baseUrl.replace(/\/+$/, ""), apiKeySource };
|
|
77
|
+
}
|