@erlancarreira/evolution-chat 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/README.md +69 -13
- package/api/index.cjs +40 -0
- package/api/index.cjs.map +1 -1
- package/api/index.d.cts +56 -30
- package/api/index.d.ts +56 -30
- package/api/index.js +40 -0
- package/api/index.js.map +1 -1
- package/bridge/index.cjs +90 -0
- package/bridge/index.cjs.map +1 -1
- package/bridge/index.d.cts +37 -0
- package/bridge/index.d.ts +37 -0
- package/bridge/index.js +90 -0
- package/bridge/index.js.map +1 -1
- package/next/index.d.cts +37 -0
- package/next/index.d.ts +37 -0
- package/package.json +1 -1
- package/transports/supabase/index.d.cts +8 -0
- package/transports/supabase/index.d.ts +8 -0
- package/widget/index.cjs +115 -28
- package/widget/index.cjs.map +1 -1
- package/widget/index.d.cts +19 -3
- package/widget/index.d.ts +19 -3
- package/widget/index.js +115 -28
- package/widget/index.js.map +1 -1
- package/widget/styles.css +33 -0
- package/widget/styles.css.map +1 -1
- package/widget-embed/evolution-chat.iife.js +113 -27
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @erlancarreira/evolution-chat
|
|
2
2
|
|
|
3
3
|
Widget de chat (React) + bridge para a **Evolution API v2** (WhatsApp): o visitante abre uma
|
|
4
|
-
conversa no site
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
conversa no site e o SDK, por padrão, **cria um grupo no WhatsApp** com o visitante e a
|
|
5
|
+
plataforma — ou, se `createGroup: false`, manda a mensagem **1:1 direto** para o número da
|
|
6
|
+
plataforma. Relaya as mensagens nos dois sentidos e entrega tudo em tempo real no painel, com
|
|
7
|
+
**mensagens de sistema** (entrou/saiu do grupo), encerramento automático e **limpeza de grupos
|
|
8
|
+
órfãos** (a plataforma sai do grupo quando o visitante sai ou o destino some). Polling de 5 s é
|
|
9
|
+
o fallback quando o canal cai. Arquitetura hexagonal: o núcleo (`bridge`) só conhece **portas**
|
|
7
10
|
(`SessionStore`, `RealtimeTransport`, `RealtimeHandle`, `ChatLimiter`) — persistência e
|
|
8
11
|
transporte são injetados; o adapter oficial usa Supabase, mas qualquer banco serve.
|
|
9
12
|
|
|
@@ -19,11 +22,16 @@ transporte são injetados; o adapter oficial usa Supabase, mas qualquer banco se
|
|
|
19
22
|
## Instalação
|
|
20
23
|
|
|
21
24
|
```bash
|
|
22
|
-
npm install @erlancarreira/evolution-chat
|
|
23
|
-
|
|
24
|
-
npm install
|
|
25
|
+
npm install @erlancarreira/evolution-chat # recomendado: pacote publicado no npm
|
|
26
|
+
# alternativas (exigem build local — rode `npm run build` no repo; a saída não é versionada):
|
|
27
|
+
npm install github:erlancarreira/evolution-chat # a partir do repositório
|
|
28
|
+
npm install file:../evolution-chat # cópia local (cuidado: no Windows vira junction e quebra o build no Vercel)
|
|
25
29
|
```
|
|
26
30
|
|
|
31
|
+
> Passo a passo completo de instalação **+ montagem do painel de administração** em outro
|
|
32
|
+
> projeto (Next.js + Supabase), com todas as opções e ações de staff: consulte
|
|
33
|
+
> [`skills/configure-evolution-chat.md`](./skills/configure-evolution-chat.md).
|
|
34
|
+
|
|
27
35
|
`@supabase/supabase-js` é peer **opcional** (só para os adapters de realtime). No `<script>`
|
|
28
36
|
nada disso se aplica: React, react-dom e supabase-js vão embutidos.
|
|
29
37
|
|
|
@@ -123,18 +131,66 @@ export async function getChatConfig(): Promise<ChatConfig> {
|
|
|
123
131
|
apiKey: process.env.EVOLUTION_API_KEY!, // apikey da instância (NUNCA vai ao cliente)
|
|
124
132
|
welcome: s?.chatWelcome ?? "Como podemos ajudar?",
|
|
125
133
|
closeHours: s?.chatCloseHours ?? 24, // inatividade p/ fechar (0 = nunca)
|
|
126
|
-
leaveOnClose: s?.chatLeaveOnClose ?? true, // sai do grupo
|
|
134
|
+
leaveOnClose: s?.chatLeaveOnClose ?? true, // sai do grupo nos fechamentos automáticos (visitante saiu/destino sumiu)
|
|
127
135
|
webhookToken: process.env.CHAT_WEBHOOK_TOKEN!, // segredo do webhook
|
|
136
|
+
createGroup: s?.chatCreateGroup ?? true, // true = grupo por visitante; false = conversa 1:1 direta com a plataforma
|
|
137
|
+
groupImage: s?.chatGroupImage ?? "", // URL pública da imagem de capa do grupo (vazia = padrão do produto)
|
|
128
138
|
};
|
|
129
139
|
}
|
|
130
140
|
```
|
|
131
141
|
|
|
132
|
-
`closeHours
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
`leaveOnClose`/`closeHours` regem o **fechamento manual** (painel do atendente ou cron): ao
|
|
143
|
+
encerrar a sessão (`store.markStatus(id, "closed")`) com `leaveOnClose`, o **hospedeiro** decide
|
|
144
|
+
sair do grupo. Já os fechamentos **automáticos** do SDK **não** dependem do host:
|
|
145
|
+
|
|
146
|
+
- **Visitante sai ou é removido do grupo** (`GROUP_PARTICIPANTS_UPDATE` com `action: leave`/`remove`)
|
|
147
|
+
→ a sessão é fechada e a plataforma **sai do grupo** (`client.leaveGroup`) automaticamente.
|
|
148
|
+
- **O destino some no envio** (Evolution 404/410 ou "group/left/deleted/blocked" no corpo) →
|
|
149
|
+
`send_target_gone`: sessão fechada com aviso "Conversa encerrada — inicie uma nova" e a
|
|
150
|
+
plataforma sai do grupo.
|
|
151
|
+
|
|
152
|
+
Fechamentos manuais **não** saem do grupo (o visitante ficaria sozinho). O widget recebe o status
|
|
153
|
+
via `{ type: "session", status: "closed" }` que o SDK publica no realtime; sem isso, o status
|
|
154
|
+
chega apenas no GET (reabrir o painel ou o polling de fallback).
|
|
155
|
+
|
|
156
|
+
## Ciclo de vida do grupo, mensagens de sistema e auto-recuperação
|
|
157
|
+
|
|
158
|
+
- **Modo grupo (padrão) vs 1:1 direto:** `createGroup` controla. No modo grupo, cada visitante
|
|
159
|
+
ativo ganha seu próprio grupo; no modo direto, a mensagem vai para o número da plataforma.
|
|
160
|
+
- **Reaproveitamento:** `startChat` reusa a **sessão ativa** do mesmo telefone (um grupo/thread
|
|
161
|
+
por cliente) em vez de abrir grupos novos a cada reabertura.
|
|
162
|
+
- **Auto-recuperação de grupo morto:** se a sessão reaproveitada aponta para um grupo que sumiu no
|
|
163
|
+
WhatsApp (visitante apagou/saiu), o envio falha com `target_gone` e o SDK **encerra a sessão
|
|
164
|
+
antiga e cria um grupo novo** para o mesmo visitante — sem grupo zumbi nem histórico travado.
|
|
165
|
+
- **Mensagens de sistema:** mudanças de participantes viram `direction: "system"` no chat
|
|
166
|
+
(ex.: "Fulano saiu do grupo"). O schema deve aceitar `direction IN ('visitor','owner','system')`.
|
|
167
|
+
- **Webhook:** aponte a Evolution para `/api/chat/webhook?token=<webhookToken>` com os eventos
|
|
168
|
+
`MESSAGES_UPSERT`, `GROUP_PARTICIPANTS_UPDATE` **e** `PRESENCE_UPDATE`. O webhook exige URL pública
|
|
169
|
+
(localhost não recebe da Evolution). O `PRESENCE_UPDATE` alimenta o indicador "digitando…" no widget.
|
|
170
|
+
|
|
171
|
+
## Indicador de digitação ("3 pontinhos")
|
|
172
|
+
|
|
173
|
+
O widget mostra os "3 pontinhos" de "digitando…" nos **dois sentidos**, via evento `typing` no canal
|
|
174
|
+
em tempo real da sessão (`{ type: "typing", isTyping, from: "owner" | "visitor" }`):
|
|
175
|
+
|
|
176
|
+
- **WhatsApp → chat (atendente digita):** a Evolution entrega `PRESENCE_UPDATE` no webhook (por isso o
|
|
177
|
+
evento precisa estar subscrito). O SDK resolve a sessão pelo `groupJid` e publica `typing` com
|
|
178
|
+
`from: "owner"` quando quem digita é a **conta da plataforma** (a instância). O widget exibe os
|
|
179
|
+
pontinhos enquanto `isTyping` for `true`; ao enviar a mensagem ou receber `paused`, some.
|
|
180
|
+
- **chat → atendente (visitante digita):** o widget POSTa `{ token, isTyping }` em `${endpoint}/typing`
|
|
181
|
+
(ex.: `/api/chat/typing`) sempre que o visitante digita/para de digitar (com *debounce* de 2,5 s). O
|
|
182
|
+
SDK publica `typing` com `from: "visitor"` no mesmo canal, para que uma **interface de atendente** (que
|
|
183
|
+
assine o canal) exiba o indicador.
|
|
184
|
+
|
|
185
|
+
> **Limitação da presença no WhatsApp:** a Evolution só anuncia a presença da **conta conectada à
|
|
186
|
+
> instância**. Como o atendente desta plataforma opera a própria instância, não é possível fazer o
|
|
187
|
+
> WhatsApp do atendente "ver" a digitação do visitante (isso exigiria uma conta WhatsApp do visitante,
|
|
188
|
+
> que o SDK não controla). Por isso o sentido *chat → WhatsApp* é entregue via o canal em tempo real
|
|
189
|
+
> para um painel de atendente; o sentido *WhatsApp → chat* é nativo.
|
|
190
|
+
|
|
191
|
+
Para expor o sinal "visitante digitando" ao atendente, monte uma rota que chame
|
|
192
|
+
`bridge.setVisitorTyping(token, isTyping)` (já incluso no `ChatBridge`) — o `/api/chat/typing` do LMS é
|
|
193
|
+
um exemplo.
|
|
138
194
|
|
|
139
195
|
## Implementando um `SessionStore` próprio
|
|
140
196
|
|
package/api/index.cjs
CHANGED
|
@@ -158,6 +158,12 @@ function createEvolutionClient(cfg) {
|
|
|
158
158
|
const res = await requestJson("sendText", "POST", `/message/sendText/${instance}`, { number, text });
|
|
159
159
|
return { waMessageId: requireString(res, "key", "id") };
|
|
160
160
|
},
|
|
161
|
+
async sendPresence(instance, number, presence) {
|
|
162
|
+
await request("sendPresence", "POST", `/message/sendPresence/${instance}`, {
|
|
163
|
+
number,
|
|
164
|
+
presence
|
|
165
|
+
});
|
|
166
|
+
},
|
|
161
167
|
async createGroup(instance, subject, participants, description) {
|
|
162
168
|
const payload = description === void 0 ? { subject, participants } : { subject, participants, description };
|
|
163
169
|
const res = await requestJson("createGroup", "POST", `/group/create/${instance}`, payload);
|
|
@@ -275,6 +281,9 @@ function parseWebhookEvent(payload) {
|
|
|
275
281
|
if (event === "group-participants.update") {
|
|
276
282
|
return parseGroupParticipants(data);
|
|
277
283
|
}
|
|
284
|
+
if (event === "PRESENCE_UPDATE" || event === "presence.update") {
|
|
285
|
+
return parsePresence(data);
|
|
286
|
+
}
|
|
278
287
|
if (event !== "messages.upsert") {
|
|
279
288
|
return { kind: "ignored", reason: `evento n\xE3o tratado: ${event}` };
|
|
280
289
|
}
|
|
@@ -313,6 +322,37 @@ function parseGroupParticipants(data) {
|
|
|
313
322
|
event: { groupJid, participants, action, author, raw: data }
|
|
314
323
|
};
|
|
315
324
|
}
|
|
325
|
+
function parsePresence(data) {
|
|
326
|
+
if (!isRecord(data)) return { kind: "ignored", reason: "presence sem data" };
|
|
327
|
+
const groupJid = typeof data["id"] === "string" && data["id"] ? data["id"] : null;
|
|
328
|
+
let participantJid = null;
|
|
329
|
+
let presenceRaw = null;
|
|
330
|
+
const presences = data["presences"];
|
|
331
|
+
if (isRecord(presences)) {
|
|
332
|
+
const entries = Object.entries(presences);
|
|
333
|
+
const first = entries[0];
|
|
334
|
+
if (first !== void 0) {
|
|
335
|
+
const [jid, info] = first;
|
|
336
|
+
participantJid = jid;
|
|
337
|
+
presenceRaw = isRecord(info) ? info["presence"] ?? info["lastKnownPresence"] : info;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
participantJid = typeof data["participant"] === "string" ? data["participant"] : null;
|
|
341
|
+
presenceRaw = data["presence"] ?? data["lastKnownPresence"];
|
|
342
|
+
}
|
|
343
|
+
const presence = normalizePresence(presenceRaw);
|
|
344
|
+
if (presence === null) return { kind: "ignored", reason: "presence sem estado reconhec\xEDvel" };
|
|
345
|
+
return {
|
|
346
|
+
kind: "presence",
|
|
347
|
+
event: { groupJid, participantJid, presence, raw: data }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizePresence(value) {
|
|
351
|
+
if (value === "composing" || value === "recording") return value;
|
|
352
|
+
if (value === "paused") return "paused";
|
|
353
|
+
if (value === "available" || value === "unavailable") return "paused";
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
316
356
|
// Annotate the CommonJS export names for ESM import in node:
|
|
317
357
|
0 && (module.exports = {
|
|
318
358
|
EvolutionApiError,
|
package/api/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/index.ts","../src/errors.ts","../src/api/phone.ts","../src/api/ids.ts","../src/api/client.ts","../src/api/webhook-parser.ts"],"sourcesContent":["// src/api/index.ts — superfície pública do subpacote `evolution-chat/api`.\n//\n// Cliente HTTP tipado e framework-agnóstico da Evolution API v2. Auth por header\n// `apikey`. Sem dependências pesadas (fetch global). O erro de transporte\n// (EvolutionApiError) é exportado daqui para o painel distinguir evolution_error\n// de config_error.\n\nexport * from \"./phone\";\nexport * from \"./ids\";\nexport * from \"./client\";\nexport * from \"./webhook-parser\";\n\n// Tipos canônicos compartilhados (re-export para conveniência).\nexport type { InboundMessage } from \"../types\";\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","// src/api/client.ts — Adapter REST da Evolution API v2 (fetch injetável p/ testes).\nimport { ChatError } from \"../errors\";\n\nexport interface EvolutionFetch { (url: string, init: RequestInit): Promise<Response>; } // ponto de injeção (DI)\n\nexport interface SendTextResult { waMessageId: string; }\nexport interface CreateGroupResult { groupJid: string; }\n\nexport interface EvolutionClient {\n sendText(instance: string, number: string, text: string): Promise<SendTextResult>;\n createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;\n setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;\n leaveGroup(instance: string, groupJid: string): Promise<void>;\n /** Encerra a sessão WhatsApp da instância (desvincula o dispositivo). */\n logout(instance: string): Promise<void>;\n getConnectionState(instance: string): Promise<\"open\" | \"connecting\" | \"close\">;\n connectQR(instance: string): Promise<{ qrBase64: string | null; pairingCode: string | null }>;\n setWebhook(instance: string, url: string, events: string[]): Promise<void>;\n /** Cria a instância na Evolution (one-time). Exige a apiKey com permissão de criação. */\n createInstance(instance: string, integration?: \"WHATSAPP-BAILEYS\" | \"WHATSAPP-BUSINESS\"): Promise<void>;\n /**\n * Garante que a instância existe (idempotente): se `getConnectionState` responde\n * (qualquer estado), não faz nada; só chama `createInstance` quando a leitura falha\n * (instância inexistente). Assim o LMS gerencia o ciclo de vida sem criar manualmente\n * na Evolution — o único passo humano é escanear o QR de pareamento.\n */\n ensureInstance(instance: string): Promise<void>;\n}\n\nexport class EvolutionApiError extends Error {\n constructor(readonly status: number, readonly body: string, operation: string) {\n super(`Evolution ${operation} falhou (${status})`);\n this.name = \"EvolutionApiError\";\n }\n}\n\ninterface JsonOk { operation: string; status: number; text: string; json: Record<string, unknown>; }\n\n// Lê path aninhado (ex.: \"key\", \"id\") sem lançar: devolve undefined se faltar ou não for objeto.\nfunction readPath(json: Record<string, unknown>, ...path: string[]): unknown {\n let cur: unknown = json;\n for (const key of path) {\n if (!cur || typeof cur !== \"object\") return undefined;\n cur = (cur as Record<string, unknown>)[key];\n }\n return cur;\n}\n\nexport function createEvolutionClient(cfg: { baseUrl: string; apiKey: string; fetchImpl?: EvolutionFetch }): EvolutionClient {\n const doFetch: EvolutionFetch = cfg.fetchImpl ?? fetch;\n const base = cfg.baseUrl.replace(/\\/+$/, \"\"); // normaliza p/ evitar double slash na concatenação\n const headers = { \"content-type\": \"application/json\", apikey: cfg.apiKey };\n\n async function request(operation: string, method: string, path: string, body?: unknown): Promise<Response> {\n const response = await doFetch(`${base}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw new EvolutionApiError(response.status, await response.text(), operation);\n return response;\n }\n\n // Contrato: toda falha de HTTP/parse/shape é EvolutionApiError (a ponte captura via instanceof).\n async function requestJson(operation: string, method: string, path: string, body?: unknown): Promise<JsonOk> {\n const response = await request(operation, method, path, body);\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new EvolutionApiError(response.status, text, operation);\n }\n if (!parsed || typeof parsed !== \"object\") throw new EvolutionApiError(response.status, text, operation);\n return { operation, status: response.status, text, json: parsed as Record<string, unknown> };\n }\n\n function requireString(res: JsonOk, ...path: string[]): string {\n const value = readPath(res.json, ...path);\n if (typeof value !== \"string\" || value === \"\") throw new EvolutionApiError(res.status, res.text, res.operation);\n return value;\n }\n\n // Estado da instância (open/connecting/close); lança EvolutionApiError se a resposta\n // for inválida OU a instância não existir (a Evolution devolve erro nesse caso).\n async function fetchConnectionState(instance: string): Promise<\"open\" | \"connecting\" | \"close\"> {\n const res = await requestJson(\"getConnectionState\", \"GET\", `/instance/connectionState/${instance}`);\n const state = readPath(res.json, \"instance\", \"state\");\n if (state === \"open\" || state === \"connecting\" || state === \"close\") return state;\n throw new EvolutionApiError(res.status, res.text, res.operation);\n }\n\n return {\n async sendText(instance, number, text) {\n const res = await requestJson(\"sendText\", \"POST\", `/message/sendText/${instance}`, { number, text });\n return { waMessageId: requireString(res, \"key\", \"id\") };\n },\n\n async createGroup(instance, subject, participants, description) {\n const payload = description === undefined ? { subject, participants } : { subject, participants, description };\n const res = await requestJson(\"createGroup\", \"POST\", `/group/create/${instance}`, payload);\n const groupJid = readPath(res.json, \"group\", \"id\") ?? res.json.id; // mesmo derivador de antes, sem cast\n if (typeof groupJid !== \"string\" || groupJid === \"\") {\n throw new ChatError(\"Evolution createGroup: groupJid ausente/inválido na resposta\", \"send_failed\");\n }\n return { groupJid };\n },\n\n async setGroupPicture(instance, groupJid, image) {\n // Evolution v2: POST /group/updateGroupPicture/{instance} com { groupJid, image }\n // (image = URL pública ou base64). Imagem é cosmética: qualquer falha vira EvolutionApiError.\n await request(\"setGroupPicture\", \"POST\", `/group/updateGroupPicture/${instance}`, { groupJid, image });\n },\n\n async leaveGroup(instance, groupJid) {\n await request(\"leaveGroup\", \"DELETE\", `/group/leave/${instance}`, { groupId: groupJid });\n },\n\n async logout(instance) {\n await request(\"logout\", \"DELETE\", `/instance/logout/${instance}`);\n },\n\n async getConnectionState(instance) {\n return fetchConnectionState(instance);\n },\n\n async connectQR(instance) {\n const res = await requestJson(\"connectQR\", \"GET\", `/instance/connect/${instance}`);\n const base64 = readPath(res.json, \"base64\");\n const pairingCode = readPath(res.json, \"pairingCode\");\n const code = readPath(res.json, \"code\");\n return {\n qrBase64: typeof base64 === \"string\" ? base64 : null,\n pairingCode: typeof pairingCode === \"string\" ? pairingCode : typeof code === \"string\" ? code : null,\n };\n },\n\n async createInstance(instance, integration = \"WHATSAPP-BAILEYS\") {\n await request(\"createInstance\", \"POST\", \"/instance/create\", { instanceName: instance, integration });\n },\n\n async ensureInstance(instance) {\n try {\n await fetchConnectionState(instance); // existe (qualquer estado) → ok, não faz nada\n return;\n } catch {\n // instância inexistente (ou erro de leitura) → tenta criar\n }\n await this.createInstance(instance);\n },\n\n async setWebhook(instance, url, events) {\n await request(\"setWebhook\", \"POST\", `/webhook/set/${instance}`, {\n webhook: { enabled: true, url, events, webhookByEvents: false, webhookBase64: false },\n });\n },\n };\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/api/index.ts","../src/errors.ts","../src/api/phone.ts","../src/api/ids.ts","../src/api/client.ts","../src/api/webhook-parser.ts"],"sourcesContent":["// src/api/index.ts — superfície pública do subpacote `evolution-chat/api`.\n//\n// Cliente HTTP tipado e framework-agnóstico da Evolution API v2. Auth por header\n// `apikey`. Sem dependências pesadas (fetch global). O erro de transporte\n// (EvolutionApiError) é exportado daqui para o painel distinguir evolution_error\n// de config_error.\n\nexport * from \"./phone\";\nexport * from \"./ids\";\nexport * from \"./client\";\nexport * from \"./webhook-parser\";\n\n// Tipos canônicos compartilhados (re-export para conveniência).\nexport type { InboundMessage } from \"../types\";\n","// src/errors.ts — erro de domínio canônico do SDK.\n//\n// ChatError carrega um `code` estável para mapear em HTTP nas rotas sem depender\n// de instanceof entre bundles (cada entry do tsup inline-iza a classe; usamos\n// `.code` para decidir o status). O erro de transporte da Evolution vive em\n// src/api/client.ts (EvolutionApiError), junto do cliente que o lança.\n\nexport type ChatErrorCode =\n | \"invalid_input\"\n | \"rate_limited\"\n | \"group_create_failed\"\n | \"send_failed\"\n | \"session_not_found\"\n | \"session_closed\"\n | \"disabled\"\n | \"unauthorized\"\n | \"store_error\"\n | \"webhook_invalid\";\n\nexport class ChatError extends Error {\n public readonly code: ChatErrorCode;\n public override readonly cause?: unknown;\n\n constructor(message: string, code: ChatErrorCode, cause?: unknown) {\n super(message);\n this.name = \"ChatError\";\n this.code = code;\n this.cause = cause;\n }\n}\n","import { ChatError } from \"../errors\";\n\nconst NON_DIGITS = /\\D+/g;\n\nexport function normalizePhone(raw: string, defaultDdi = \"55\"): string {\n const digits = raw.replace(NON_DIGITS, \"\");\n if (!digits) throw new ChatError(\"Telefone vazio\", \"invalid_input\");\n // Prefixo \"+\" explícito (E.164) sinaliza DDI já presente — ex.: \"+1 415 555 2671\" permanece \"14155552671\".\n const explicitDdi = raw.includes(\"+\");\n const withDdi = digits.length <= 11 && !explicitDdi && !digits.startsWith(defaultDdi) ? defaultDdi + digits : digits;\n if (withDdi.length < 10 || withDdi.length > 15) {\n throw new ChatError(\"Telefone inválido\", \"invalid_input\");\n }\n return withDdi;\n}\nexport function isValidPhone(digits: string): boolean {\n return digits.length >= 10 && digits.length <= 15;\n}\nexport function toWhatsappJid(phoneDigits: string): string {\n return `${phoneDigits}@s.whatsapp.net`;\n}\n","// src/api/ids.ts — geração de ids com webcrypto (isomórfico, sem node:crypto).\n\nconst SESSION_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\"; // sem 0/O/1/I\nconst SESSION_CODE_LENGTH = 4;\n// Rejection sampling: só aceita bytes < 248 (múltiplo de 31 ≤ 256), evitando viés de módulo.\nconst SESSION_ACCEPT_LIMIT = 256 - (256 % SESSION_ALPHABET.length);\n\nconst BASE64URL_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\nconst TOKEN_BYTES = 24; // 24 bytes → exatamente 32 caracteres base64url, sem padding\n\nfunction randomChar(alphabet: string, acceptLimit: number): string {\n const buffer = new Uint8Array(1);\n for (;;) {\n globalThis.crypto.getRandomValues(buffer);\n const byte = buffer[0];\n if (byte !== undefined && byte < acceptLimit) {\n return alphabet.charAt(byte % alphabet.length);\n }\n }\n}\n\nexport function generateSessionCode(): string {\n return Array.from({ length: SESSION_CODE_LENGTH }, () => randomChar(SESSION_ALPHABET, SESSION_ACCEPT_LIMIT)).join(\"\");\n}\n\nexport function generateRealtimeToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTES);\n globalThis.crypto.getRandomValues(bytes);\n let token = \"\";\n let acc = 0;\n let bits = 0;\n for (const byte of bytes) {\n acc = (acc << 8) | byte;\n bits += 8;\n while (bits >= 6) {\n bits -= 6;\n token += BASE64URL_ALPHABET.charAt((acc >>> bits) & 0x3f);\n }\n }\n return token;\n}\n","// src/api/client.ts — Adapter REST da Evolution API v2 (fetch injetável p/ testes).\nimport { ChatError } from \"../errors\";\nimport type { PresenceState } from \"../types\";\n\nexport interface EvolutionFetch { (url: string, init: RequestInit): Promise<Response>; } // ponto de injeção (DI)\n\nexport interface SendTextResult { waMessageId: string; }\nexport interface CreateGroupResult { groupJid: string; }\n\nexport interface EvolutionClient {\n sendText(instance: string, number: string, text: string): Promise<SendTextResult>;\n /** Envia presença de digitação (\"digitando…\") para o número/grupo. `presence` =\n * \"composing\" | \"recording\" (digitando) ou \"paused\" (parou). Best-effort: a\n * presença é anunciada pela CONTA CONECTADA à instância, não por terceiros. */\n sendPresence(instance: string, number: string, presence: PresenceState): Promise<void>;\n createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;\n setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;\n leaveGroup(instance: string, groupJid: string): Promise<void>;\n /** Encerra a sessão WhatsApp da instância (desvincula o dispositivo). */\n logout(instance: string): Promise<void>;\n getConnectionState(instance: string): Promise<\"open\" | \"connecting\" | \"close\">;\n connectQR(instance: string): Promise<{ qrBase64: string | null; pairingCode: string | null }>;\n setWebhook(instance: string, url: string, events: string[]): Promise<void>;\n /** Cria a instância na Evolution (one-time). Exige a apiKey com permissão de criação. */\n createInstance(instance: string, integration?: \"WHATSAPP-BAILEYS\" | \"WHATSAPP-BUSINESS\"): Promise<void>;\n /**\n * Garante que a instância existe (idempotente): se `getConnectionState` responde\n * (qualquer estado), não faz nada; só chama `createInstance` quando a leitura falha\n * (instância inexistente). Assim o LMS gerencia o ciclo de vida sem criar manualmente\n * na Evolution — o único passo humano é escanear o QR de pareamento.\n */\n ensureInstance(instance: string): Promise<void>;\n}\n\nexport class EvolutionApiError extends Error {\n constructor(readonly status: number, readonly body: string, operation: string) {\n super(`Evolution ${operation} falhou (${status})`);\n this.name = \"EvolutionApiError\";\n }\n}\n\ninterface JsonOk { operation: string; status: number; text: string; json: Record<string, unknown>; }\n\n// Lê path aninhado (ex.: \"key\", \"id\") sem lançar: devolve undefined se faltar ou não for objeto.\nfunction readPath(json: Record<string, unknown>, ...path: string[]): unknown {\n let cur: unknown = json;\n for (const key of path) {\n if (!cur || typeof cur !== \"object\") return undefined;\n cur = (cur as Record<string, unknown>)[key];\n }\n return cur;\n}\n\nexport function createEvolutionClient(cfg: { baseUrl: string; apiKey: string; fetchImpl?: EvolutionFetch }): EvolutionClient {\n const doFetch: EvolutionFetch = cfg.fetchImpl ?? fetch;\n const base = cfg.baseUrl.replace(/\\/+$/, \"\"); // normaliza p/ evitar double slash na concatenação\n const headers = { \"content-type\": \"application/json\", apikey: cfg.apiKey };\n\n async function request(operation: string, method: string, path: string, body?: unknown): Promise<Response> {\n const response = await doFetch(`${base}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw new EvolutionApiError(response.status, await response.text(), operation);\n return response;\n }\n\n // Contrato: toda falha de HTTP/parse/shape é EvolutionApiError (a ponte captura via instanceof).\n async function requestJson(operation: string, method: string, path: string, body?: unknown): Promise<JsonOk> {\n const response = await request(operation, method, path, body);\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new EvolutionApiError(response.status, text, operation);\n }\n if (!parsed || typeof parsed !== \"object\") throw new EvolutionApiError(response.status, text, operation);\n return { operation, status: response.status, text, json: parsed as Record<string, unknown> };\n }\n\n function requireString(res: JsonOk, ...path: string[]): string {\n const value = readPath(res.json, ...path);\n if (typeof value !== \"string\" || value === \"\") throw new EvolutionApiError(res.status, res.text, res.operation);\n return value;\n }\n\n // Estado da instância (open/connecting/close); lança EvolutionApiError se a resposta\n // for inválida OU a instância não existir (a Evolution devolve erro nesse caso).\n async function fetchConnectionState(instance: string): Promise<\"open\" | \"connecting\" | \"close\"> {\n const res = await requestJson(\"getConnectionState\", \"GET\", `/instance/connectionState/${instance}`);\n const state = readPath(res.json, \"instance\", \"state\");\n if (state === \"open\" || state === \"connecting\" || state === \"close\") return state;\n throw new EvolutionApiError(res.status, res.text, res.operation);\n }\n\n return {\n async sendText(instance, number, text) {\n const res = await requestJson(\"sendText\", \"POST\", `/message/sendText/${instance}`, { number, text });\n return { waMessageId: requireString(res, \"key\", \"id\") };\n },\n\n async sendPresence(instance, number, presence) {\n // Evolution v2: POST /message/sendPresence/{instance} — anuncia a presença da\n // conta conectada à instância (não de terceiros). A resposta 200 é suficiente.\n await request(\"sendPresence\", \"POST\", `/message/sendPresence/${instance}`, {\n number,\n presence,\n });\n },\n\n async createGroup(instance, subject, participants, description) {\n const payload = description === undefined ? { subject, participants } : { subject, participants, description };\n const res = await requestJson(\"createGroup\", \"POST\", `/group/create/${instance}`, payload);\n const groupJid = readPath(res.json, \"group\", \"id\") ?? res.json.id; // mesmo derivador de antes, sem cast\n if (typeof groupJid !== \"string\" || groupJid === \"\") {\n throw new ChatError(\"Evolution createGroup: groupJid ausente/inválido na resposta\", \"send_failed\");\n }\n return { groupJid };\n },\n\n async setGroupPicture(instance, groupJid, image) {\n // Evolution v2: POST /group/updateGroupPicture/{instance} com { groupJid, image }\n // (image = URL pública ou base64). Imagem é cosmética: qualquer falha vira EvolutionApiError.\n await request(\"setGroupPicture\", \"POST\", `/group/updateGroupPicture/${instance}`, { groupJid, image });\n },\n\n async leaveGroup(instance, groupJid) {\n await request(\"leaveGroup\", \"DELETE\", `/group/leave/${instance}`, { groupId: groupJid });\n },\n\n async logout(instance) {\n await request(\"logout\", \"DELETE\", `/instance/logout/${instance}`);\n },\n\n async getConnectionState(instance) {\n return fetchConnectionState(instance);\n },\n\n async connectQR(instance) {\n const res = await requestJson(\"connectQR\", \"GET\", `/instance/connect/${instance}`);\n const base64 = readPath(res.json, \"base64\");\n const pairingCode = readPath(res.json, \"pairingCode\");\n const code = readPath(res.json, \"code\");\n return {\n qrBase64: typeof base64 === \"string\" ? base64 : null,\n pairingCode: typeof pairingCode === \"string\" ? pairingCode : typeof code === \"string\" ? code : null,\n };\n },\n\n async createInstance(instance, integration = \"WHATSAPP-BAILEYS\") {\n await request(\"createInstance\", \"POST\", \"/instance/create\", { instanceName: instance, integration });\n },\n\n async ensureInstance(instance) {\n try {\n await fetchConnectionState(instance); // existe (qualquer estado) → ok, não faz nada\n return;\n } catch {\n // instância inexistente (ou erro de leitura) → tenta criar\n }\n await this.createInstance(instance);\n },\n\n async setWebhook(instance, url, events) {\n await request(\"setWebhook\", \"POST\", `/webhook/set/${instance}`, {\n webhook: { enabled: true, url, events, webhookByEvents: false, webhookBase64: false },\n });\n },\n };\n}\n","// src/api/webhook-parser.ts — normalização defensiva de webhooks da Evolution API v2.\n//\n// Contrato: parseWebhookEvent(payload: unknown): ParsedWebhook.\n// NUNCA lança exceção — qualquer payload inesperado/inválido vira { kind: \"ignored\", reason }.\n\nimport type { GroupParticipantChange, InboundMessage, PresenceChange, PresenceState } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\n | { kind: \"group_participants\"; event: GroupParticipantChange }\n | { kind: \"presence\"; event: PresenceChange }\n | { kind: \"ignored\"; reason: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n// Texto só sai de `conversation` ou `extendedTextMessage.text`; string vazia/só espaços → null.\nfunction textFromMessage(message: unknown): string | null {\n if (!isRecord(message)) return null;\n const conversation = message[\"conversation\"];\n if (typeof conversation === \"string\") {\n const trimmed = conversation.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const extended = message[\"extendedTextMessage\"];\n if (isRecord(extended)) {\n const text = extended[\"text\"];\n if (typeof text === \"string\") {\n const trimmed = text.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n }\n return null;\n}\n\n// messageTimestamp em segundos (aceita número ou string numérica; ausente/inválido → 0).\nfunction timestampSeconds(value: unknown): number {\n const seconds = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n return Number.isFinite(seconds) ? Math.floor(seconds) : 0;\n}\n\nfunction parseMessageItem(item: unknown): ParsedWebhook {\n if (!isRecord(item)) return { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n\n const key = isRecord(item[\"key\"]) ? item[\"key\"] : null;\n const waMessageId = key?.[\"id\"];\n const jid = key?.[\"remoteJid\"];\n if (typeof waMessageId !== \"string\" || waMessageId.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.id\" };\n }\n if (typeof jid !== \"string\" || jid.length === 0) {\n return { kind: \"ignored\", reason: \"mensagem sem key.remoteJid\" };\n }\n\n const text = textFromMessage(item[\"message\"]);\n if (text === null) {\n return { kind: \"ignored\", reason: \"mensagem sem texto (conversation/extendedTextMessage)\" };\n }\n\n const senderJid = key?.[\"participant\"];\n return {\n kind: \"message\",\n event: {\n waMessageId,\n jid,\n fromMe: key?.[\"fromMe\"] === true,\n senderJid: typeof senderJid === \"string\" ? senderJid : null,\n text,\n timestamp: timestampSeconds(item[\"messageTimestamp\"]),\n raw: item,\n },\n };\n}\n\nexport function parseWebhookEvent(payload: unknown): ParsedWebhook {\n if (!isRecord(payload)) return { kind: \"ignored\", reason: \"payload não é um objeto\" };\n\n const event = payload[\"event\"];\n if (typeof event !== \"string\") return { kind: \"ignored\", reason: \"payload sem campo event\" };\n\n const data = payload[\"data\"];\n\n if (event === \"connection.update\") {\n const state = isRecord(data) ? data[\"state\"] : undefined;\n if (typeof state === \"string\" && state.length > 0) return { kind: \"connection\", state };\n return { kind: \"ignored\", reason: \"connection.update sem state\" };\n }\n\n if (event === \"group-participants.update\") {\n return parseGroupParticipants(data);\n }\n\n if (event === \"PRESENCE_UPDATE\" || event === \"presence.update\") {\n return parsePresence(data);\n }\n\n if (event !== \"messages.upsert\") {\n return { kind: \"ignored\", reason: `evento não tratado: ${event}` };\n }\n\n // Evolution v2 pode mandar `data` como objeto único ou array de mensagens (queue-flush/bulk).\n // Contrato de evento único: devolve o PRIMEIRO item válido, não `items[0]` — senão um item\n // nulo/inválido na frente descartaria silenciosamente as mensagens válidas seguintes.\n const items: readonly unknown[] = Array.isArray(data) ? data : [data];\n let lastIgnored: ParsedWebhook = { kind: \"ignored\", reason: \"messages.upsert sem data válido\" };\n for (const item of items) {\n const parsed = parseMessageItem(item);\n if (parsed.kind !== \"ignored\") return parsed;\n lastIgnored = parsed;\n }\n return lastIgnored;\n}\n\n/**\n * Normaliza o evento `group-participants.update` da Evolution:\n * { id: \"…@g.us\", participants: [\"…@s.whatsapp.net\"], author: \"…@s.whatsapp.net\",\n * action: \"add\" | \"remove\" | \"leave\" | \"promote\" | \"demote\" }\n * Campos faltantes/fora do formato viram { kind: \"ignored\" } (nunca lança).\n */\nfunction parseGroupParticipants(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"group-participants sem data\" };\n\n const groupJid = data[\"id\"];\n if (typeof groupJid !== \"string\" || groupJid.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem id (groupJid)\" };\n }\n\n const rawParticipants = data[\"participants\"];\n if (!Array.isArray(rawParticipants)) {\n return { kind: \"ignored\", reason: \"group-participants sem participants[]\" };\n }\n const participants: string[] = [];\n for (const p of rawParticipants) {\n if (typeof p === \"string\" && p.length > 0) participants.push(p);\n }\n if (participants.length === 0) {\n return { kind: \"ignored\", reason: \"group-participants sem participantes válidos\" };\n }\n\n const actionRaw = data[\"action\"];\n const action = typeof actionRaw === \"string\" ? actionRaw : \"unknown\";\n const authorRaw = data[\"author\"];\n const author = typeof authorRaw === \"string\" ? authorRaw : null;\n\n return {\n kind: \"group_participants\",\n event: { groupJid, participants, action, author, raw: data },\n };\n}\n\n/**\n * Normaliza o evento `PRESENCE_UPDATE` da Evolution (indicador \"digitando…\"):\n * { id: \"…@g.us\", presences: { \"5511…@s.whatsapp.net\": { presence: \"composing\" } } }\n * Variações conhecidas: `presences` como mapa (forma atual) OU `participant`+`presence`\n * diretos (versões mais antigas), e `presence`/`lastKnownPresence` como campo.\n * NUNCA lança: qualquer formato estranho vira { kind: \"ignored\" }.\n */\nfunction parsePresence(data: unknown): ParsedWebhook {\n if (!isRecord(data)) return { kind: \"ignored\", reason: \"presence sem data\" };\n\n const groupJid = typeof data[\"id\"] === \"string\" && data[\"id\"] ? data[\"id\"] : null;\n\n let participantJid: string | null = null;\n let presenceRaw: unknown = null;\n\n const presences = data[\"presences\"];\n if (isRecord(presences)) {\n const entries = Object.entries(presences);\n const first = entries[0];\n if (first !== undefined) {\n const [jid, info] = first;\n participantJid = jid;\n presenceRaw = isRecord(info) ? (info[\"presence\"] ?? info[\"lastKnownPresence\"]) : info;\n }\n } else {\n participantJid = typeof data[\"participant\"] === \"string\" ? data[\"participant\"] : null;\n presenceRaw = data[\"presence\"] ?? data[\"lastKnownPresence\"];\n }\n\n const presence = normalizePresence(presenceRaw);\n if (presence === null) return { kind: \"ignored\", reason: \"presence sem estado reconhecível\" };\n\n return {\n kind: \"presence\",\n event: { groupJid, participantJid, presence, raw: data },\n };\n}\n\nfunction normalizePresence(value: unknown): PresenceState | null {\n if (value === \"composing\" || value === \"recording\") return value;\n if (value === \"paused\") return \"paused\";\n // Algumas versões usam available/unavailable para \"parou de digitar\".\n if (value === \"available\" || value === \"unavailable\") return \"paused\";\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/api/index.d.cts
CHANGED
|
@@ -5,6 +5,55 @@ declare function toWhatsappJid(phoneDigits: string): string;
|
|
|
5
5
|
declare function generateSessionCode(): string;
|
|
6
6
|
declare function generateRealtimeToken(): string;
|
|
7
7
|
|
|
8
|
+
/** Estado de presença (digitação) normalizado da Evolution. */
|
|
9
|
+
type PresenceState = "composing" | "paused" | "recording";
|
|
10
|
+
/**
|
|
11
|
+
* Mensagem de entrada normalizada do webhook da Evolution, independente do
|
|
12
|
+
* formato bruto do payload.
|
|
13
|
+
*/
|
|
14
|
+
interface InboundMessage {
|
|
15
|
+
waMessageId: string;
|
|
16
|
+
jid: string;
|
|
17
|
+
fromMe: boolean;
|
|
18
|
+
senderJid: string | null;
|
|
19
|
+
text: string | null;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
raw: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mudança de participantes em um grupo WhatsApp, normalizada a partir do evento
|
|
25
|
+
* `group-participants.update` da Evolution API. Usada para notificar no chat quando
|
|
26
|
+
* alguém entra, sai ou é removido/adicionado ao grupo da sessão.
|
|
27
|
+
*/
|
|
28
|
+
interface GroupParticipantChange {
|
|
29
|
+
/** JID do grupo (ex.: "120363000000000001@g.us"). */
|
|
30
|
+
groupJid: string;
|
|
31
|
+
/** JIDs dos participantes afetados (ex.: ["5511999998888@s.whatsapp.net"]). */
|
|
32
|
+
participants: string[];
|
|
33
|
+
/** Ação da Evolution: "add" | "remove" | "leave" | "promote" | "demote". */
|
|
34
|
+
action: "add" | "remove" | "leave" | "promote" | "demote" | (string & {});
|
|
35
|
+
/** Quem executou a ação (JID); nulo para "leave" (o próprio participante saiu). */
|
|
36
|
+
author: string | null;
|
|
37
|
+
raw: unknown;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Mudança de presença (digitação) normalizada do evento `PRESENCE_UPDATE` da
|
|
41
|
+
* Evolution API. Usada para mostrar o indicador "digitando…" no widget quando a
|
|
42
|
+
* outra ponta (dona da plataforma ou visitante) está compondo mensagem.
|
|
43
|
+
*
|
|
44
|
+
* - `groupJid`: JID do grupo (modo grupo) ou nulo (modo direto 1:1, em que o
|
|
45
|
+
* próprio número da plataforma é a conversa).
|
|
46
|
+
* - `participantJid`: quem está digitando (em grupo, o participante específico;
|
|
47
|
+
* em direto, costuma vir nulo e tratamos como a dona da plataforma).
|
|
48
|
+
* - `presence`: "composing"/"recording" = está digitando; "paused" = parou.
|
|
49
|
+
*/
|
|
50
|
+
interface PresenceChange {
|
|
51
|
+
groupJid: string | null;
|
|
52
|
+
participantJid: string | null;
|
|
53
|
+
presence: PresenceState;
|
|
54
|
+
raw: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
8
57
|
interface EvolutionFetch {
|
|
9
58
|
(url: string, init: RequestInit): Promise<Response>;
|
|
10
59
|
}
|
|
@@ -16,6 +65,10 @@ interface CreateGroupResult {
|
|
|
16
65
|
}
|
|
17
66
|
interface EvolutionClient {
|
|
18
67
|
sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
|
|
68
|
+
/** Envia presença de digitação ("digitando…") para o número/grupo. `presence` =
|
|
69
|
+
* "composing" | "recording" (digitando) ou "paused" (parou). Best-effort: a
|
|
70
|
+
* presença é anunciada pela CONTA CONECTADA à instância, não por terceiros. */
|
|
71
|
+
sendPresence(instance: string, number: string, presence: PresenceState): Promise<void>;
|
|
19
72
|
createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
|
|
20
73
|
setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;
|
|
21
74
|
leaveGroup(instance: string, groupJid: string): Promise<void>;
|
|
@@ -48,36 +101,6 @@ declare function createEvolutionClient(cfg: {
|
|
|
48
101
|
fetchImpl?: EvolutionFetch;
|
|
49
102
|
}): EvolutionClient;
|
|
50
103
|
|
|
51
|
-
/**
|
|
52
|
-
* Mensagem de entrada normalizada do webhook da Evolution, independente do
|
|
53
|
-
* formato bruto do payload.
|
|
54
|
-
*/
|
|
55
|
-
interface InboundMessage {
|
|
56
|
-
waMessageId: string;
|
|
57
|
-
jid: string;
|
|
58
|
-
fromMe: boolean;
|
|
59
|
-
senderJid: string | null;
|
|
60
|
-
text: string | null;
|
|
61
|
-
timestamp: number;
|
|
62
|
-
raw: unknown;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Mudança de participantes em um grupo WhatsApp, normalizada a partir do evento
|
|
66
|
-
* `group-participants.update` da Evolution API. Usada para notificar no chat quando
|
|
67
|
-
* alguém entra, sai ou é removido/adicionado ao grupo da sessão.
|
|
68
|
-
*/
|
|
69
|
-
interface GroupParticipantChange {
|
|
70
|
-
/** JID do grupo (ex.: "120363000000000001@g.us"). */
|
|
71
|
-
groupJid: string;
|
|
72
|
-
/** JIDs dos participantes afetados (ex.: ["5511999998888@s.whatsapp.net"]). */
|
|
73
|
-
participants: string[];
|
|
74
|
-
/** Ação da Evolution: "add" | "remove" | "leave" | "promote" | "demote". */
|
|
75
|
-
action: "add" | "remove" | "leave" | "promote" | "demote" | (string & {});
|
|
76
|
-
/** Quem executou a ação (JID); nulo para "leave" (o próprio participante saiu). */
|
|
77
|
-
author: string | null;
|
|
78
|
-
raw: unknown;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
104
|
type ParsedWebhook = {
|
|
82
105
|
kind: "message";
|
|
83
106
|
event: InboundMessage;
|
|
@@ -87,6 +110,9 @@ type ParsedWebhook = {
|
|
|
87
110
|
} | {
|
|
88
111
|
kind: "group_participants";
|
|
89
112
|
event: GroupParticipantChange;
|
|
113
|
+
} | {
|
|
114
|
+
kind: "presence";
|
|
115
|
+
event: PresenceChange;
|
|
90
116
|
} | {
|
|
91
117
|
kind: "ignored";
|
|
92
118
|
reason: string;
|
package/api/index.d.ts
CHANGED
|
@@ -5,6 +5,55 @@ declare function toWhatsappJid(phoneDigits: string): string;
|
|
|
5
5
|
declare function generateSessionCode(): string;
|
|
6
6
|
declare function generateRealtimeToken(): string;
|
|
7
7
|
|
|
8
|
+
/** Estado de presença (digitação) normalizado da Evolution. */
|
|
9
|
+
type PresenceState = "composing" | "paused" | "recording";
|
|
10
|
+
/**
|
|
11
|
+
* Mensagem de entrada normalizada do webhook da Evolution, independente do
|
|
12
|
+
* formato bruto do payload.
|
|
13
|
+
*/
|
|
14
|
+
interface InboundMessage {
|
|
15
|
+
waMessageId: string;
|
|
16
|
+
jid: string;
|
|
17
|
+
fromMe: boolean;
|
|
18
|
+
senderJid: string | null;
|
|
19
|
+
text: string | null;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
raw: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mudança de participantes em um grupo WhatsApp, normalizada a partir do evento
|
|
25
|
+
* `group-participants.update` da Evolution API. Usada para notificar no chat quando
|
|
26
|
+
* alguém entra, sai ou é removido/adicionado ao grupo da sessão.
|
|
27
|
+
*/
|
|
28
|
+
interface GroupParticipantChange {
|
|
29
|
+
/** JID do grupo (ex.: "120363000000000001@g.us"). */
|
|
30
|
+
groupJid: string;
|
|
31
|
+
/** JIDs dos participantes afetados (ex.: ["5511999998888@s.whatsapp.net"]). */
|
|
32
|
+
participants: string[];
|
|
33
|
+
/** Ação da Evolution: "add" | "remove" | "leave" | "promote" | "demote". */
|
|
34
|
+
action: "add" | "remove" | "leave" | "promote" | "demote" | (string & {});
|
|
35
|
+
/** Quem executou a ação (JID); nulo para "leave" (o próprio participante saiu). */
|
|
36
|
+
author: string | null;
|
|
37
|
+
raw: unknown;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Mudança de presença (digitação) normalizada do evento `PRESENCE_UPDATE` da
|
|
41
|
+
* Evolution API. Usada para mostrar o indicador "digitando…" no widget quando a
|
|
42
|
+
* outra ponta (dona da plataforma ou visitante) está compondo mensagem.
|
|
43
|
+
*
|
|
44
|
+
* - `groupJid`: JID do grupo (modo grupo) ou nulo (modo direto 1:1, em que o
|
|
45
|
+
* próprio número da plataforma é a conversa).
|
|
46
|
+
* - `participantJid`: quem está digitando (em grupo, o participante específico;
|
|
47
|
+
* em direto, costuma vir nulo e tratamos como a dona da plataforma).
|
|
48
|
+
* - `presence`: "composing"/"recording" = está digitando; "paused" = parou.
|
|
49
|
+
*/
|
|
50
|
+
interface PresenceChange {
|
|
51
|
+
groupJid: string | null;
|
|
52
|
+
participantJid: string | null;
|
|
53
|
+
presence: PresenceState;
|
|
54
|
+
raw: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
8
57
|
interface EvolutionFetch {
|
|
9
58
|
(url: string, init: RequestInit): Promise<Response>;
|
|
10
59
|
}
|
|
@@ -16,6 +65,10 @@ interface CreateGroupResult {
|
|
|
16
65
|
}
|
|
17
66
|
interface EvolutionClient {
|
|
18
67
|
sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
|
|
68
|
+
/** Envia presença de digitação ("digitando…") para o número/grupo. `presence` =
|
|
69
|
+
* "composing" | "recording" (digitando) ou "paused" (parou). Best-effort: a
|
|
70
|
+
* presença é anunciada pela CONTA CONECTADA à instância, não por terceiros. */
|
|
71
|
+
sendPresence(instance: string, number: string, presence: PresenceState): Promise<void>;
|
|
19
72
|
createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
|
|
20
73
|
setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;
|
|
21
74
|
leaveGroup(instance: string, groupJid: string): Promise<void>;
|
|
@@ -48,36 +101,6 @@ declare function createEvolutionClient(cfg: {
|
|
|
48
101
|
fetchImpl?: EvolutionFetch;
|
|
49
102
|
}): EvolutionClient;
|
|
50
103
|
|
|
51
|
-
/**
|
|
52
|
-
* Mensagem de entrada normalizada do webhook da Evolution, independente do
|
|
53
|
-
* formato bruto do payload.
|
|
54
|
-
*/
|
|
55
|
-
interface InboundMessage {
|
|
56
|
-
waMessageId: string;
|
|
57
|
-
jid: string;
|
|
58
|
-
fromMe: boolean;
|
|
59
|
-
senderJid: string | null;
|
|
60
|
-
text: string | null;
|
|
61
|
-
timestamp: number;
|
|
62
|
-
raw: unknown;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Mudança de participantes em um grupo WhatsApp, normalizada a partir do evento
|
|
66
|
-
* `group-participants.update` da Evolution API. Usada para notificar no chat quando
|
|
67
|
-
* alguém entra, sai ou é removido/adicionado ao grupo da sessão.
|
|
68
|
-
*/
|
|
69
|
-
interface GroupParticipantChange {
|
|
70
|
-
/** JID do grupo (ex.: "120363000000000001@g.us"). */
|
|
71
|
-
groupJid: string;
|
|
72
|
-
/** JIDs dos participantes afetados (ex.: ["5511999998888@s.whatsapp.net"]). */
|
|
73
|
-
participants: string[];
|
|
74
|
-
/** Ação da Evolution: "add" | "remove" | "leave" | "promote" | "demote". */
|
|
75
|
-
action: "add" | "remove" | "leave" | "promote" | "demote" | (string & {});
|
|
76
|
-
/** Quem executou a ação (JID); nulo para "leave" (o próprio participante saiu). */
|
|
77
|
-
author: string | null;
|
|
78
|
-
raw: unknown;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
104
|
type ParsedWebhook = {
|
|
82
105
|
kind: "message";
|
|
83
106
|
event: InboundMessage;
|
|
@@ -87,6 +110,9 @@ type ParsedWebhook = {
|
|
|
87
110
|
} | {
|
|
88
111
|
kind: "group_participants";
|
|
89
112
|
event: GroupParticipantChange;
|
|
113
|
+
} | {
|
|
114
|
+
kind: "presence";
|
|
115
|
+
event: PresenceChange;
|
|
90
116
|
} | {
|
|
91
117
|
kind: "ignored";
|
|
92
118
|
reason: string;
|
package/api/index.js
CHANGED
|
@@ -125,6 +125,12 @@ function createEvolutionClient(cfg) {
|
|
|
125
125
|
const res = await requestJson("sendText", "POST", `/message/sendText/${instance}`, { number, text });
|
|
126
126
|
return { waMessageId: requireString(res, "key", "id") };
|
|
127
127
|
},
|
|
128
|
+
async sendPresence(instance, number, presence) {
|
|
129
|
+
await request("sendPresence", "POST", `/message/sendPresence/${instance}`, {
|
|
130
|
+
number,
|
|
131
|
+
presence
|
|
132
|
+
});
|
|
133
|
+
},
|
|
128
134
|
async createGroup(instance, subject, participants, description) {
|
|
129
135
|
const payload = description === void 0 ? { subject, participants } : { subject, participants, description };
|
|
130
136
|
const res = await requestJson("createGroup", "POST", `/group/create/${instance}`, payload);
|
|
@@ -242,6 +248,9 @@ function parseWebhookEvent(payload) {
|
|
|
242
248
|
if (event === "group-participants.update") {
|
|
243
249
|
return parseGroupParticipants(data);
|
|
244
250
|
}
|
|
251
|
+
if (event === "PRESENCE_UPDATE" || event === "presence.update") {
|
|
252
|
+
return parsePresence(data);
|
|
253
|
+
}
|
|
245
254
|
if (event !== "messages.upsert") {
|
|
246
255
|
return { kind: "ignored", reason: `evento n\xE3o tratado: ${event}` };
|
|
247
256
|
}
|
|
@@ -280,6 +289,37 @@ function parseGroupParticipants(data) {
|
|
|
280
289
|
event: { groupJid, participants, action, author, raw: data }
|
|
281
290
|
};
|
|
282
291
|
}
|
|
292
|
+
function parsePresence(data) {
|
|
293
|
+
if (!isRecord(data)) return { kind: "ignored", reason: "presence sem data" };
|
|
294
|
+
const groupJid = typeof data["id"] === "string" && data["id"] ? data["id"] : null;
|
|
295
|
+
let participantJid = null;
|
|
296
|
+
let presenceRaw = null;
|
|
297
|
+
const presences = data["presences"];
|
|
298
|
+
if (isRecord(presences)) {
|
|
299
|
+
const entries = Object.entries(presences);
|
|
300
|
+
const first = entries[0];
|
|
301
|
+
if (first !== void 0) {
|
|
302
|
+
const [jid, info] = first;
|
|
303
|
+
participantJid = jid;
|
|
304
|
+
presenceRaw = isRecord(info) ? info["presence"] ?? info["lastKnownPresence"] : info;
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
participantJid = typeof data["participant"] === "string" ? data["participant"] : null;
|
|
308
|
+
presenceRaw = data["presence"] ?? data["lastKnownPresence"];
|
|
309
|
+
}
|
|
310
|
+
const presence = normalizePresence(presenceRaw);
|
|
311
|
+
if (presence === null) return { kind: "ignored", reason: "presence sem estado reconhec\xEDvel" };
|
|
312
|
+
return {
|
|
313
|
+
kind: "presence",
|
|
314
|
+
event: { groupJid, participantJid, presence, raw: data }
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function normalizePresence(value) {
|
|
318
|
+
if (value === "composing" || value === "recording") return value;
|
|
319
|
+
if (value === "paused") return "paused";
|
|
320
|
+
if (value === "available" || value === "unavailable") return "paused";
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
283
323
|
export {
|
|
284
324
|
EvolutionApiError,
|
|
285
325
|
createEvolutionClient,
|