@telepapo/sdk 0.2.0 → 0.3.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 +70 -0
- package/src/index.js +31 -13
package/package.json
CHANGED
package/src/botoes.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Botões nas mensagens (só conta comercial; ver docs/MENSAGENS-RICAS.md no
|
|
3
|
+
* repositório do TelePapo). As mesmas regras valem no app Android e no web:
|
|
4
|
+
* quem recebe descarta o botão que não passa nelas.
|
|
5
|
+
*
|
|
6
|
+
* { id, tipo: 'resposta' | 'link' | 'telefone', rotulo, url?, telefone? }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MAX_BOTOES = 3;
|
|
10
|
+
export const MAX_ROTULO = 25;
|
|
11
|
+
export const MAX_LEGENDA = 1024;
|
|
12
|
+
|
|
13
|
+
// Controles invisíveis (direção do texto e afins) disfarçam texto e links.
|
|
14
|
+
const CONTROLES = /[\u0000-\u0008\u000B-\u001F\u007F--]/g;
|
|
15
|
+
|
|
16
|
+
/** Tira caracteres de controle e de direção (mantém \n e \t). */
|
|
17
|
+
export function limparTexto(texto) {
|
|
18
|
+
return String(texto ?? '').replace(CONTROLES, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function erro(i, motivo) {
|
|
22
|
+
return new Error(`Botão ${i + 1}: ${motivo}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Confere e normaliza os botões de uma mensagem a enviar. Lança com o motivo
|
|
27
|
+
* (erro de quem chama: melhor saber agora do que o cliente não ver o botão).
|
|
28
|
+
* @returns {Array<{id: string, tipo: string, rotulo: string, url?: string, telefone?: string}>}
|
|
29
|
+
*/
|
|
30
|
+
export function validarBotoes(botoes) {
|
|
31
|
+
if (!Array.isArray(botoes) || !botoes.length) throw new Error('Informe de 1 a 3 botões.');
|
|
32
|
+
if (botoes.length > MAX_BOTOES) throw new Error(`No máximo ${MAX_BOTOES} botões por mensagem.`);
|
|
33
|
+
const ids = new Set();
|
|
34
|
+
return botoes.map((b, i) => {
|
|
35
|
+
const id = String(b?.id ?? '').trim();
|
|
36
|
+
if (!id || id.length > 64) throw erro(i, 'id de 1 a 64 caracteres.');
|
|
37
|
+
if (ids.has(id)) throw erro(i, `id "${id}" repetido.`);
|
|
38
|
+
ids.add(id);
|
|
39
|
+
const rotulo = limparTexto(b.rotulo).replace(/\s+/g, ' ').trim();
|
|
40
|
+
if (!rotulo || [...rotulo].length > MAX_ROTULO) throw erro(i, `rótulo de 1 a ${MAX_ROTULO} caracteres.`);
|
|
41
|
+
switch (b.tipo) {
|
|
42
|
+
case 'resposta':
|
|
43
|
+
return { id, tipo: 'resposta', rotulo };
|
|
44
|
+
case 'link': {
|
|
45
|
+
const url = String(b.url ?? '').trim();
|
|
46
|
+
let u;
|
|
47
|
+
try { u = new URL(url); } catch { throw erro(i, 'url inválida.'); }
|
|
48
|
+
if (!['https:', 'http:'].includes(u.protocol) || url.length > 2000) throw erro(i, 'url precisa ser http(s) e ter até 2000 caracteres.');
|
|
49
|
+
return { id, tipo: 'link', rotulo, url };
|
|
50
|
+
}
|
|
51
|
+
case 'telefone': {
|
|
52
|
+
const telefone = String(b.telefone ?? '').replace(/[^\d+]/g, '');
|
|
53
|
+
if (!/^\+\d{8,15}$/.test(telefone)) throw erro(i, 'telefone no formato +5511912345678.');
|
|
54
|
+
return { id, tipo: 'telefone', rotulo, telefone };
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
throw erro(i, 'tipo deve ser resposta, link ou telefone.');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Botão escolhido numa mensagem recebida (`conteudo.botao`), quando o cliente
|
|
64
|
+
* tocou num botão de resposta: `{ id, msg }` (id do botão e da mensagem).
|
|
65
|
+
*/
|
|
66
|
+
export function botaoEscolhido(conteudo) {
|
|
67
|
+
const b = conteudo?.botao;
|
|
68
|
+
if (!b || typeof b.id !== 'string' || typeof b.msg !== 'string') return null;
|
|
69
|
+
return { id: b.id.slice(0, 64), msg: b.msg.slice(0, 64) };
|
|
70
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,12 +10,14 @@ 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
14
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const { Signal } = require('../signal/telepapo_signal.js');
|
|
16
17
|
|
|
17
18
|
export { ArmazenamentoArquivo, ArmazenamentoMemoria, verificarAssinatura };
|
|
18
19
|
export { cifrarAnexo, decifrarAnexo } from './anexos.js';
|
|
20
|
+
export { validarBotoes, botaoEscolhido, limparTexto } from './botoes.js';
|
|
19
21
|
|
|
20
22
|
export class ErroTelePapo extends Error {
|
|
21
23
|
constructor(status, corpo) {
|
|
@@ -56,7 +58,7 @@ export class TelePapo {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
async http(metodo, caminho, { corpo, token, basic } = {}) {
|
|
59
|
-
const headers = { 'User-Agent': 'telepapo-sdk/0.
|
|
61
|
+
const headers = { 'User-Agent': 'telepapo-sdk/0.3' };
|
|
60
62
|
if (corpo !== undefined) headers['Content-Type'] = 'application/json';
|
|
61
63
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
62
64
|
if (basic) headers.Authorization = 'Basic ' + Buffer.from(`${this.clientId}:${this.credencial}`).toString('base64');
|
|
@@ -313,9 +315,18 @@ class Conta {
|
|
|
313
315
|
});
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Texto (com *negrito*, _itálico_, ~riscado~ e `código`, que o app mostra
|
|
320
|
+
* formatados). `botoes`: até 3 (só conta comercial; ver validarBotoes).
|
|
321
|
+
*/
|
|
322
|
+
enviarTexto(para, texto, { resposta, botoes } = {}) {
|
|
317
323
|
if (!texto?.trim()) throw new Error('Texto vazio.');
|
|
318
|
-
return this.enviar(para, {
|
|
324
|
+
return this.enviar(para, {
|
|
325
|
+
tipo: 'texto',
|
|
326
|
+
texto: texto.slice(0, 65000),
|
|
327
|
+
...(resposta ? { resposta } : {}),
|
|
328
|
+
...(botoes ? { botoes: validarBotoes(botoes) } : {}),
|
|
329
|
+
});
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
/** Avisa quem escreveu que a conta leu (e o celular do dono também). */
|
|
@@ -344,7 +355,7 @@ class Conta {
|
|
|
344
355
|
/**
|
|
345
356
|
* Cifra e sobe um arquivo; devolve o `anexo` para pôr numa mensagem `midia`.
|
|
346
357
|
* @param {Buffer|Uint8Array} dados
|
|
347
|
-
* @param {{mime: string, nome?: string, largura?: number, altura?: number, duracaoMs?: number, miniatura?: string}} info
|
|
358
|
+
* @param {{mime: string, nome?: string, largura?: number, altura?: number, duracaoMs?: number, onda?: string, miniatura?: string}} info
|
|
348
359
|
*/
|
|
349
360
|
async subirAnexo(dados, info = {}) {
|
|
350
361
|
if (!info.mime) throw new Error('Informe o mime do anexo.');
|
|
@@ -353,14 +364,14 @@ class Conta {
|
|
|
353
364
|
const token = await this.token();
|
|
354
365
|
const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${id}/conteudo`, {
|
|
355
366
|
method: 'PUT',
|
|
356
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.
|
|
367
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.3' },
|
|
357
368
|
body: blob,
|
|
358
369
|
});
|
|
359
370
|
if (!r.ok) {
|
|
360
371
|
const texto = await r.text();
|
|
361
372
|
throw new ErroTelePapo(r.status, texto ? JSON.parse(texto) : null);
|
|
362
373
|
}
|
|
363
|
-
const extra = Object.fromEntries(['nome', 'largura', 'altura', 'duracaoMs', 'miniatura'].filter((k) => info[k] != null).map((k) => [k, info[k]]));
|
|
374
|
+
const extra = Object.fromEntries(['nome', 'largura', 'altura', 'duracaoMs', 'onda', 'miniatura'].filter((k) => info[k] != null).map((k) => [k, info[k]]));
|
|
364
375
|
return { id, chave, digest, tamanho: Buffer.byteLength(Buffer.from(dados)), mime: info.mime, ...extra };
|
|
365
376
|
}
|
|
366
377
|
|
|
@@ -368,7 +379,7 @@ class Conta {
|
|
|
368
379
|
async baixarAnexo(anexo) {
|
|
369
380
|
const token = await this.token();
|
|
370
381
|
const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${encodeURIComponent(anexo.id)}`, {
|
|
371
|
-
headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.
|
|
382
|
+
headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.3' },
|
|
372
383
|
});
|
|
373
384
|
if (!r.ok) {
|
|
374
385
|
const texto = await r.text();
|
|
@@ -378,13 +389,20 @@ class Conta {
|
|
|
378
389
|
}
|
|
379
390
|
|
|
380
391
|
/**
|
|
381
|
-
* Envia um arquivo (foto, vídeo, áudio, documento).
|
|
382
|
-
*
|
|
392
|
+
* Envia um arquivo (foto, vídeo, áudio, documento). `legenda` vai na própria
|
|
393
|
+
* mídia (até 1024 caracteres; o app mostra abaixo da imagem/vídeo/documento).
|
|
394
|
+
* Áudio: `duracaoMs` e `onda` (base64 de até 64 barras 0-255) desenham o player.
|
|
383
395
|
*/
|
|
384
|
-
async enviarArquivo(para, dados, { mime, nome, largura, altura, duracaoMs, miniatura, legenda, resposta } = {}) {
|
|
385
|
-
const anexo = await this.subirAnexo(dados, { mime, nome, largura, altura, duracaoMs, miniatura });
|
|
386
|
-
const
|
|
387
|
-
|
|
396
|
+
async enviarArquivo(para, dados, { mime, nome, largura, altura, duracaoMs, onda, miniatura, legenda, resposta, botoes } = {}) {
|
|
397
|
+
const anexo = await this.subirAnexo(dados, { mime, nome, largura, altura, duracaoMs, onda, miniatura });
|
|
398
|
+
const texto = legenda?.trim() ? legenda.trim().slice(0, MAX_LEGENDA) : null;
|
|
399
|
+
const r = await this.enviar(para, {
|
|
400
|
+
tipo: 'midia',
|
|
401
|
+
anexo,
|
|
402
|
+
...(texto ? { texto } : {}),
|
|
403
|
+
...(resposta ? { resposta } : {}),
|
|
404
|
+
...(botoes ? { botoes: validarBotoes(botoes) } : {}),
|
|
405
|
+
});
|
|
388
406
|
return { ...r, anexo };
|
|
389
407
|
}
|
|
390
408
|
|