@telepapo/sdk 0.3.0 → 0.4.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/package.json +1 -1
- package/src/botoes.js +41 -0
- package/src/index.js +47 -6
- package/src/pedido.js +41 -0
package/package.json
CHANGED
package/src/botoes.js
CHANGED
|
@@ -68,3 +68,44 @@ export function botaoEscolhido(conteudo) {
|
|
|
68
68
|
if (!b || typeof b.id !== 'string' || typeof b.msg !== 'string') return null;
|
|
69
69
|
return { id: b.id.slice(0, 64), msg: b.msg.slice(0, 64) };
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
export const MAX_ITENS_LISTA = 10;
|
|
73
|
+
|
|
74
|
+
const umaLinha = (v) => limparTexto(v).replace(/\s+/g, ' ').trim();
|
|
75
|
+
const tamanho = (s) => [...s].length;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Confere e normaliza uma lista de opções (até 10 itens no total, em seções):
|
|
79
|
+
* { botao: 'Escolher uma opção', secoes: [{ titulo?, itens: [{ id, titulo, descricao? }] }] }
|
|
80
|
+
* O cliente escolhe um item e a resposta chega como a de um botão
|
|
81
|
+
* (`botaoEscolhido(conteudo).id` é o id do item).
|
|
82
|
+
*/
|
|
83
|
+
export function validarLista(lista) {
|
|
84
|
+
const botao = umaLinha(lista?.botao);
|
|
85
|
+
if (!botao || tamanho(botao) > 20) throw new Error('Lista: botao de 1 a 20 caracteres.');
|
|
86
|
+
const secoes = lista.secoes;
|
|
87
|
+
if (!Array.isArray(secoes) || !secoes.length || secoes.length > 10) throw new Error('Lista: de 1 a 10 seções.');
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
let total = 0;
|
|
90
|
+
const saida = secoes.map((s, si) => {
|
|
91
|
+
const titulo = s?.titulo == null ? '' : umaLinha(s.titulo);
|
|
92
|
+
if (tamanho(titulo) > 24) throw new Error(`Seção ${si + 1}: título até 24 caracteres.`);
|
|
93
|
+
if (!Array.isArray(s.itens) || !s.itens.length) throw new Error(`Seção ${si + 1}: pelo menos 1 item.`);
|
|
94
|
+
const itens = s.itens.map((it, ii) => {
|
|
95
|
+
const onde = `Seção ${si + 1}, item ${ii + 1}`;
|
|
96
|
+
const id = String(it?.id ?? '').trim();
|
|
97
|
+
if (!id || id.length > 64) throw new Error(`${onde}: id de 1 a 64 caracteres.`);
|
|
98
|
+
if (ids.has(id)) throw new Error(`${onde}: id "${id}" repetido.`);
|
|
99
|
+
ids.add(id);
|
|
100
|
+
const t = umaLinha(it.titulo);
|
|
101
|
+
if (!t || tamanho(t) > 24) throw new Error(`${onde}: título de 1 a 24 caracteres.`);
|
|
102
|
+
const d = it.descricao == null ? '' : umaLinha(it.descricao);
|
|
103
|
+
if (tamanho(d) > 72) throw new Error(`${onde}: descrição até 72 caracteres.`);
|
|
104
|
+
return { id, titulo: t, ...(d ? { descricao: d } : {}) };
|
|
105
|
+
});
|
|
106
|
+
total += itens.length;
|
|
107
|
+
return { ...(titulo ? { titulo } : {}), itens };
|
|
108
|
+
});
|
|
109
|
+
if (total > MAX_ITENS_LISTA) throw new Error(`Lista: no máximo ${MAX_ITENS_LISTA} itens no total.`);
|
|
110
|
+
return { botao, secoes: saida };
|
|
111
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,14 +10,15 @@ import { ArmazenamentoArquivo, ArmazenamentoMemoria } from './armazenamento.js';
|
|
|
10
10
|
import { verificarAssinatura } from './webhook.js';
|
|
11
11
|
import { categoria, impressao } from './categoria.js';
|
|
12
12
|
import { abrirAnexo, prepararAnexo } from './anexos.js';
|
|
13
|
-
import { validarBotoes, MAX_LEGENDA } from './botoes.js';
|
|
13
|
+
import { validarBotoes, validarLista, MAX_LEGENDA } from './botoes.js';
|
|
14
14
|
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
const { Signal } = require('../signal/telepapo_signal.js');
|
|
17
17
|
|
|
18
18
|
export { ArmazenamentoArquivo, ArmazenamentoMemoria, verificarAssinatura };
|
|
19
19
|
export { cifrarAnexo, decifrarAnexo } from './anexos.js';
|
|
20
|
-
export { validarBotoes, botaoEscolhido, limparTexto } from './botoes.js';
|
|
20
|
+
export { validarBotoes, validarLista, botaoEscolhido, limparTexto } from './botoes.js';
|
|
21
|
+
export { pedidoDe } from './pedido.js';
|
|
21
22
|
|
|
22
23
|
export class ErroTelePapo extends Error {
|
|
23
24
|
constructor(status, corpo) {
|
|
@@ -58,7 +59,7 @@ export class TelePapo {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
async http(metodo, caminho, { corpo, token, basic } = {}) {
|
|
61
|
-
const headers = { 'User-Agent': 'telepapo-sdk/0.
|
|
62
|
+
const headers = { 'User-Agent': 'telepapo-sdk/0.4' };
|
|
62
63
|
if (corpo !== undefined) headers['Content-Type'] = 'application/json';
|
|
63
64
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
64
65
|
if (basic) headers.Authorization = 'Basic ' + Buffer.from(`${this.clientId}:${this.credencial}`).toString('base64');
|
|
@@ -189,6 +190,43 @@ class Conta {
|
|
|
189
190
|
return { id: e.id, nome: e.nome, username: e.username, escopos: e.escopos };
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// ------------------------------------------------------------ catálogo
|
|
194
|
+
// Permissão `catalogo.gerenciar`. O sistema do comércio sincroniza os itens
|
|
195
|
+
// pelo CÓDIGO dele (SKU): salvar de novo o mesmo código atualiza, não duplica.
|
|
196
|
+
|
|
197
|
+
/** Itens do catálogo da conta (com `codigo` e `categoria`). */
|
|
198
|
+
listarCatalogo() {
|
|
199
|
+
return this.api('GET', '/v1/eu/catalogo');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cria ou atualiza o item com este código.
|
|
204
|
+
* @param {string} codigo letras, números, ponto, hífen ou sublinhado (até 64)
|
|
205
|
+
* @param {{nome: string, descricao?: string, precoCentavos: number, precoPromoCentavos?: number|null, disponivel?: boolean, categoria?: string|null}} item
|
|
206
|
+
*/
|
|
207
|
+
salvarItemDoCatalogo(codigo, item) {
|
|
208
|
+
return this.api('PUT', `/v1/eu/catalogo/codigo/${encodeURIComponent(codigo)}`, item);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
removerItemDoCatalogo(codigo) {
|
|
212
|
+
return this.api('DELETE', `/v1/eu/catalogo/codigo/${encodeURIComponent(codigo)}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Foto do item (JPEG, PNG ou WebP, até 2 MB). */
|
|
216
|
+
async trocarFotoDoItem(codigo, imagem, mime) {
|
|
217
|
+
if (!['image/jpeg', 'image/png', 'image/webp'].includes(mime)) throw new Error('Foto: image/jpeg, image/png ou image/webp.');
|
|
218
|
+
const token = await this.token();
|
|
219
|
+
const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/eu/catalogo/codigo/${encodeURIComponent(codigo)}/foto`, {
|
|
220
|
+
method: 'PUT',
|
|
221
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': mime, 'User-Agent': 'telepapo-sdk/0.4' },
|
|
222
|
+
body: imagem,
|
|
223
|
+
});
|
|
224
|
+
if (!r.ok) {
|
|
225
|
+
const t = await r.text();
|
|
226
|
+
throw new ErroTelePapo(r.status, t ? JSON.parse(t) : null);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
192
230
|
// ------------------------------------------------------------ receber
|
|
193
231
|
|
|
194
232
|
/** Busca o que está pendente (alternativa/complemento ao webhook), decifra e confirma. */
|
|
@@ -318,14 +356,17 @@ class Conta {
|
|
|
318
356
|
/**
|
|
319
357
|
* Texto (com *negrito*, _itálico_, ~riscado~ e `código`, que o app mostra
|
|
320
358
|
* formatados). `botoes`: até 3 (só conta comercial; ver validarBotoes).
|
|
359
|
+
* `lista`: menu de até 10 opções atrás de um botão (ver validarLista).
|
|
321
360
|
*/
|
|
322
|
-
enviarTexto(para, texto, { resposta, botoes } = {}) {
|
|
361
|
+
enviarTexto(para, texto, { resposta, botoes, lista } = {}) {
|
|
323
362
|
if (!texto?.trim()) throw new Error('Texto vazio.');
|
|
363
|
+
if (botoes && lista) throw new Error('Use botoes ou lista, não os dois.');
|
|
324
364
|
return this.enviar(para, {
|
|
325
365
|
tipo: 'texto',
|
|
326
366
|
texto: texto.slice(0, 65000),
|
|
327
367
|
...(resposta ? { resposta } : {}),
|
|
328
368
|
...(botoes ? { botoes: validarBotoes(botoes) } : {}),
|
|
369
|
+
...(lista ? { lista: validarLista(lista) } : {}),
|
|
329
370
|
});
|
|
330
371
|
}
|
|
331
372
|
|
|
@@ -364,7 +405,7 @@ class Conta {
|
|
|
364
405
|
const token = await this.token();
|
|
365
406
|
const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${id}/conteudo`, {
|
|
366
407
|
method: 'PUT',
|
|
367
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.
|
|
408
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.4' },
|
|
368
409
|
body: blob,
|
|
369
410
|
});
|
|
370
411
|
if (!r.ok) {
|
|
@@ -379,7 +420,7 @@ class Conta {
|
|
|
379
420
|
async baixarAnexo(anexo) {
|
|
380
421
|
const token = await this.token();
|
|
381
422
|
const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${encodeURIComponent(anexo.id)}`, {
|
|
382
|
-
headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.
|
|
423
|
+
headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.4' },
|
|
383
424
|
});
|
|
384
425
|
if (!r.ok) {
|
|
385
426
|
const texto = await r.text();
|
package/src/pedido.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pedido do carrinho (docs/MENSAGENS-RICAS.md §6 do TelePapo): quem conversa
|
|
3
|
+
* com a conta comercial monta o carrinho com itens do catálogo e envia.
|
|
4
|
+
*
|
|
5
|
+
* O preço é o que o CLIENTE VIU, montado no aparelho dele: confira código,
|
|
6
|
+
* preço e disponibilidade no seu cadastro antes de confirmar o pedido.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const inteiro = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
10
|
+
const texto = (v, max) => (typeof v === 'string' && v.trim() ? v.trim().slice(0, max) : null);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lê o pedido de uma mensagem recebida (`mensagem.conteudo`). Itens
|
|
14
|
+
* malformados são descartados; sem nenhum item válido, devolve null.
|
|
15
|
+
* @returns {null | { loja: string|null, itens: Array<{produtoId: string|null, codigo: string|null, descricao: string, quantidade: number, precoCentavos: number, totalCentavos: number}>, totalCentavos: number, moeda: string, observacao: string|null }}
|
|
16
|
+
*/
|
|
17
|
+
export function pedidoDe(conteudo) {
|
|
18
|
+
const p = conteudo?.pedido;
|
|
19
|
+
if (!p || typeof p !== 'object' || !Array.isArray(p.itens)) return null;
|
|
20
|
+
const itens = p.itens.slice(0, 50).flatMap((i) => {
|
|
21
|
+
const descricao = texto(i?.descricao, 80);
|
|
22
|
+
if (!descricao || !inteiro(i.quantidade, 1, 999) || !inteiro(i.precoCentavos, 0, 100_000_000)) return [];
|
|
23
|
+
return [{
|
|
24
|
+
produtoId: texto(i.produtoId, 64),
|
|
25
|
+
codigo: texto(i.codigo, 64),
|
|
26
|
+
descricao,
|
|
27
|
+
quantidade: i.quantidade,
|
|
28
|
+
precoCentavos: i.precoCentavos,
|
|
29
|
+
// Recalculado aqui: não confie no total que veio.
|
|
30
|
+
totalCentavos: i.quantidade * i.precoCentavos,
|
|
31
|
+
}];
|
|
32
|
+
});
|
|
33
|
+
if (!itens.length) return null;
|
|
34
|
+
return {
|
|
35
|
+
loja: texto(p.loja, 64),
|
|
36
|
+
itens,
|
|
37
|
+
totalCentavos: itens.reduce((s, i) => s + i.totalCentavos, 0),
|
|
38
|
+
moeda: 'BRL',
|
|
39
|
+
observacao: texto(p.observacao, 500),
|
|
40
|
+
};
|
|
41
|
+
}
|