@erlancarreira/evolution-chat 0.1.0 → 0.1.2
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/api/index.cjs +53 -4
- package/api/index.cjs.map +1 -1
- package/api/index.d.cts +31 -0
- package/api/index.d.ts +31 -0
- package/api/index.js +53 -4
- package/api/index.js.map +1 -1
- package/bridge/index.cjs +192 -6
- package/bridge/index.cjs.map +1 -1
- package/bridge/index.d.cts +40 -2
- package/bridge/index.d.ts +40 -2
- package/bridge/index.js +192 -6
- package/bridge/index.js.map +1 -1
- package/next/index.d.cts +39 -1
- package/next/index.d.ts +39 -1
- package/package.json +1 -1
- package/transports/supabase/index.cjs +24 -1
- package/transports/supabase/index.cjs.map +1 -1
- package/transports/supabase/index.d.cts +4 -2
- package/transports/supabase/index.d.ts +4 -2
- package/transports/supabase/index.js +23 -1
- package/transports/supabase/index.js.map +1 -1
- package/widget/index.cjs +39 -11
- package/widget/index.cjs.map +1 -1
- package/widget/index.d.cts +2 -2
- package/widget/index.d.ts +2 -2
- package/widget/index.js +39 -11
- package/widget/index.js.map +1 -1
- package/widget/styles.css +23 -0
- package/widget/styles.css.map +1 -1
- package/widget-embed/evolution-chat.iife.js +58 -12
package/bridge/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ChatMessageDirection = "visitor" | "owner";
|
|
1
|
+
type ChatMessageDirection = "visitor" | "owner" | "system";
|
|
2
2
|
type ChatMessageStatus = "pending" | "sent" | "failed";
|
|
3
3
|
type ChatSessionStatus = "active" | "closed" | "failed";
|
|
4
4
|
/** Mensagem persistida (domínio). */
|
|
@@ -20,6 +20,8 @@ interface ChatSession {
|
|
|
20
20
|
visitorPhone: string;
|
|
21
21
|
visitorContact: string | null;
|
|
22
22
|
groupJid: string | null;
|
|
23
|
+
/** Modo da conversa: "group" (grupo por visitante, padrão) ou "direct" (1:1 com a plataforma). */
|
|
24
|
+
mode?: "group" | "direct";
|
|
23
25
|
status: ChatSessionStatus;
|
|
24
26
|
createdAt: string;
|
|
25
27
|
lastMessageAt: string | null;
|
|
@@ -39,6 +41,10 @@ interface ChatConfig {
|
|
|
39
41
|
closeHours: number;
|
|
40
42
|
leaveOnClose: boolean;
|
|
41
43
|
webhookToken: string;
|
|
44
|
+
/** true (padrão) = cria um grupo por visitante; false = conversa 1:1 direta com a plataforma. */
|
|
45
|
+
createGroup?: boolean;
|
|
46
|
+
/** URL pública da imagem de capa do grupo (opcional; ignora se vazia). */
|
|
47
|
+
groupImage?: string;
|
|
42
48
|
}
|
|
43
49
|
/** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
|
|
44
50
|
type ChatEvent = {
|
|
@@ -71,6 +77,8 @@ interface SessionStore {
|
|
|
71
77
|
visitorPhone: string;
|
|
72
78
|
visitorContact?: string | null;
|
|
73
79
|
groupJid: string | null;
|
|
80
|
+
/** Modo da conversa: "group" (padrão) ou "direct" (1:1 com a plataforma). Opcional. */
|
|
81
|
+
mode?: "group" | "direct";
|
|
74
82
|
/** Hash não-reversível do IP (LGPD): usado por countRecentSessionsByIpHash. */
|
|
75
83
|
ipHash?: string | null;
|
|
76
84
|
/** User-Agent bruto da requisição (auditoria/forense; nunca exibido). */
|
|
@@ -78,6 +86,8 @@ interface SessionStore {
|
|
|
78
86
|
}): Promise<ChatSession>;
|
|
79
87
|
getSessionByToken(token: string): Promise<ChatSession | null>;
|
|
80
88
|
getSessionByGroupJid(jid: string): Promise<ChatSession | null>;
|
|
89
|
+
/** Lookup da sessão ativa de um visitante pelo telefone (modo direto 1:1). */
|
|
90
|
+
getSessionByVisitorPhone(phone: string): Promise<ChatSession | null>;
|
|
81
91
|
appendMessage(input: {
|
|
82
92
|
sessionId: string;
|
|
83
93
|
direction: ChatMessageDirection;
|
|
@@ -144,7 +154,7 @@ type RouterDecision = {
|
|
|
144
154
|
|
|
145
155
|
declare class ConversationRouter {
|
|
146
156
|
private readonly store;
|
|
147
|
-
constructor(store: Pick<SessionStore, "getSessionByGroupJid" | "isEcho">);
|
|
157
|
+
constructor(store: Pick<SessionStore, "getSessionByGroupJid" | "getSessionByVisitorPhone" | "isEcho">);
|
|
148
158
|
/**
|
|
149
159
|
* `now` faz parte do contrato para que a bridge propague uma única leitura de relógio
|
|
150
160
|
* por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda
|
|
@@ -167,13 +177,25 @@ interface CreateGroupResult {
|
|
|
167
177
|
interface EvolutionClient {
|
|
168
178
|
sendText(instance: string, number: string, text: string): Promise<SendTextResult>;
|
|
169
179
|
createGroup(instance: string, subject: string, participants: string[], description?: string): Promise<CreateGroupResult>;
|
|
180
|
+
setGroupPicture(instance: string, groupJid: string, image: string): Promise<void>;
|
|
170
181
|
leaveGroup(instance: string, groupJid: string): Promise<void>;
|
|
182
|
+
/** Encerra a sessão WhatsApp da instância (desvincula o dispositivo). */
|
|
183
|
+
logout(instance: string): Promise<void>;
|
|
171
184
|
getConnectionState(instance: string): Promise<"open" | "connecting" | "close">;
|
|
172
185
|
connectQR(instance: string): Promise<{
|
|
173
186
|
qrBase64: string | null;
|
|
174
187
|
pairingCode: string | null;
|
|
175
188
|
}>;
|
|
176
189
|
setWebhook(instance: string, url: string, events: string[]): Promise<void>;
|
|
190
|
+
/** Cria a instância na Evolution (one-time). Exige a apiKey com permissão de criação. */
|
|
191
|
+
createInstance(instance: string, integration?: "WHATSAPP-BAILEYS" | "WHATSAPP-BUSINESS"): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Garante que a instância existe (idempotente): se `getConnectionState` responde
|
|
194
|
+
* (qualquer estado), não faz nada; só chama `createInstance` quando a leitura falha
|
|
195
|
+
* (instância inexistente). Assim o LMS gerencia o ciclo de vida sem criar manualmente
|
|
196
|
+
* na Evolution — o único passo humano é escanear o QR de pareamento.
|
|
197
|
+
*/
|
|
198
|
+
ensureInstance(instance: string): Promise<void>;
|
|
177
199
|
}
|
|
178
200
|
|
|
179
201
|
interface StartChatInput {
|
|
@@ -218,6 +240,22 @@ declare class ChatBridge {
|
|
|
218
240
|
handleWebhook(payload: unknown): Promise<{
|
|
219
241
|
handled: boolean;
|
|
220
242
|
}>;
|
|
243
|
+
/**
|
|
244
|
+
* Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é
|
|
245
|
+
* removido do grupo da sessão, registra uma mensagem de sistema no chat (visível
|
|
246
|
+
* para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão
|
|
247
|
+
* — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.
|
|
248
|
+
* NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution
|
|
249
|
+
* não reenviar.
|
|
250
|
+
*/
|
|
251
|
+
private handleGroupParticipants;
|
|
252
|
+
/**
|
|
253
|
+
* Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do
|
|
254
|
+
* lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na
|
|
255
|
+
* Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais
|
|
256
|
+
* (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.
|
|
257
|
+
*/
|
|
258
|
+
private closeSessionCleanup;
|
|
221
259
|
/** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */
|
|
222
260
|
history(token: string, afterIso?: string | null): Promise<{
|
|
223
261
|
session: ChatSession | null;
|
package/bridge/index.js
CHANGED
|
@@ -12,8 +12,14 @@ var ConversationRouter = class {
|
|
|
12
12
|
*/
|
|
13
13
|
async decide(msg, now) {
|
|
14
14
|
void now;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let session = null;
|
|
16
|
+
if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {
|
|
17
|
+
session = await this.store.getSessionByGroupJid(msg.jid);
|
|
18
|
+
} else {
|
|
19
|
+
const phone = msg.jid.split("@")[0] ?? "";
|
|
20
|
+
if (phone.length === 0) return { action: "ignore" };
|
|
21
|
+
session = await this.store.getSessionByVisitorPhone(phone);
|
|
22
|
+
}
|
|
17
23
|
if (session === null) return { action: "unknown_session" };
|
|
18
24
|
if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: "echo" };
|
|
19
25
|
if (typeof msg.text !== "string" || msg.text.length === 0) return { action: "not_text" };
|
|
@@ -159,6 +165,9 @@ function parseWebhookEvent(payload) {
|
|
|
159
165
|
if (typeof state === "string" && state.length > 0) return { kind: "connection", state };
|
|
160
166
|
return { kind: "ignored", reason: "connection.update sem state" };
|
|
161
167
|
}
|
|
168
|
+
if (event === "group-participants.update") {
|
|
169
|
+
return parseGroupParticipants(data);
|
|
170
|
+
}
|
|
162
171
|
if (event !== "messages.upsert") {
|
|
163
172
|
return { kind: "ignored", reason: `evento n\xE3o tratado: ${event}` };
|
|
164
173
|
}
|
|
@@ -171,6 +180,32 @@ function parseWebhookEvent(payload) {
|
|
|
171
180
|
}
|
|
172
181
|
return lastIgnored;
|
|
173
182
|
}
|
|
183
|
+
function parseGroupParticipants(data) {
|
|
184
|
+
if (!isRecord(data)) return { kind: "ignored", reason: "group-participants sem data" };
|
|
185
|
+
const groupJid = data["id"];
|
|
186
|
+
if (typeof groupJid !== "string" || groupJid.length === 0) {
|
|
187
|
+
return { kind: "ignored", reason: "group-participants sem id (groupJid)" };
|
|
188
|
+
}
|
|
189
|
+
const rawParticipants = data["participants"];
|
|
190
|
+
if (!Array.isArray(rawParticipants)) {
|
|
191
|
+
return { kind: "ignored", reason: "group-participants sem participants[]" };
|
|
192
|
+
}
|
|
193
|
+
const participants = [];
|
|
194
|
+
for (const p of rawParticipants) {
|
|
195
|
+
if (typeof p === "string" && p.length > 0) participants.push(p);
|
|
196
|
+
}
|
|
197
|
+
if (participants.length === 0) {
|
|
198
|
+
return { kind: "ignored", reason: "group-participants sem participantes v\xE1lidos" };
|
|
199
|
+
}
|
|
200
|
+
const actionRaw = data["action"];
|
|
201
|
+
const action = typeof actionRaw === "string" ? actionRaw : "unknown";
|
|
202
|
+
const authorRaw = data["author"];
|
|
203
|
+
const author = typeof authorRaw === "string" ? authorRaw : null;
|
|
204
|
+
return {
|
|
205
|
+
kind: "group_participants",
|
|
206
|
+
event: { groupJid, participants, action, author, raw: data }
|
|
207
|
+
};
|
|
208
|
+
}
|
|
174
209
|
|
|
175
210
|
// src/bridge/bridge.ts
|
|
176
211
|
var NAME_MIN = 2;
|
|
@@ -200,6 +235,15 @@ function requireText(value, min, max, field) {
|
|
|
200
235
|
}
|
|
201
236
|
return trimmed;
|
|
202
237
|
}
|
|
238
|
+
function isTargetGoneError(error) {
|
|
239
|
+
if (error === null || typeof error !== "object") return false;
|
|
240
|
+
const status = error.status;
|
|
241
|
+
if (status === 404 || status === 410) return true;
|
|
242
|
+
const text = `${String(error.message ?? "")} ${String(
|
|
243
|
+
error.body ?? ""
|
|
244
|
+
)}`.toLowerCase();
|
|
245
|
+
return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);
|
|
246
|
+
}
|
|
203
247
|
var ChatBridge = class {
|
|
204
248
|
constructor(deps) {
|
|
205
249
|
this.deps = deps;
|
|
@@ -243,12 +287,50 @@ var ChatBridge = class {
|
|
|
243
287
|
);
|
|
244
288
|
}
|
|
245
289
|
}
|
|
290
|
+
const existing = await store.getSessionByVisitorPhone(phone);
|
|
291
|
+
if (existing !== null && existing.status === "active") {
|
|
292
|
+
const firstTarget2 = existing.groupJid === null ? toWhatsappJid(normalizePhone(cfg.platformNumber)) : existing.groupJid;
|
|
293
|
+
try {
|
|
294
|
+
const { waMessageId: waMessageId2 } = await this.deps.client.sendText(
|
|
295
|
+
cfg.instance,
|
|
296
|
+
firstTarget2,
|
|
297
|
+
formatFirstMessage(existing.code, name, message)
|
|
298
|
+
);
|
|
299
|
+
await store.registerSentMessageId(existing.id, waMessageId2);
|
|
300
|
+
const initial2 = await store.appendMessage({
|
|
301
|
+
sessionId: existing.id,
|
|
302
|
+
direction: "visitor",
|
|
303
|
+
body: message,
|
|
304
|
+
waMessageId: waMessageId2,
|
|
305
|
+
status: "sent"
|
|
306
|
+
});
|
|
307
|
+
return { session: existing, messages: [initial2] };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (isTargetGoneError(error)) {
|
|
310
|
+
await this.closeSessionCleanup(existing, "send_target_gone");
|
|
311
|
+
} else {
|
|
312
|
+
await store.markStatus(existing.id, "failed");
|
|
313
|
+
throw new ChatError("Falha ao enviar a primeira mensagem para o grupo", "send_failed", error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
246
317
|
const code = generateSessionCode();
|
|
247
318
|
const visitorJid = toWhatsappJid(phone);
|
|
248
319
|
const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));
|
|
249
320
|
const participants = [.../* @__PURE__ */ new Set([visitorJid, platformJid])];
|
|
250
321
|
const subject = `${cfg.projectName} \u2014 ${name} (#${code})`;
|
|
251
|
-
const
|
|
322
|
+
const direct = cfg.createGroup === false;
|
|
323
|
+
let groupJid = null;
|
|
324
|
+
if (!direct) {
|
|
325
|
+
groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);
|
|
326
|
+
const picture = (cfg.groupImage ?? "").trim();
|
|
327
|
+
if (picture !== "") {
|
|
328
|
+
try {
|
|
329
|
+
await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
252
334
|
const session = await store.createSession({
|
|
253
335
|
code,
|
|
254
336
|
realtimeToken: generateRealtimeToken(),
|
|
@@ -256,14 +338,22 @@ var ChatBridge = class {
|
|
|
256
338
|
visitorPhone: phone,
|
|
257
339
|
visitorContact: input.contact ?? null,
|
|
258
340
|
groupJid,
|
|
341
|
+
mode: direct ? "direct" : "group",
|
|
259
342
|
ipHash: input.ipHash ?? null,
|
|
260
343
|
userAgent: input.userAgent ?? null
|
|
261
344
|
});
|
|
345
|
+
let firstTarget;
|
|
346
|
+
if (direct) {
|
|
347
|
+
firstTarget = platformJid;
|
|
348
|
+
} else {
|
|
349
|
+
if (groupJid === null) throw new ChatError("Falha ao criar o grupo", "group_create_failed");
|
|
350
|
+
firstTarget = groupJid;
|
|
351
|
+
}
|
|
262
352
|
let waMessageId;
|
|
263
353
|
try {
|
|
264
354
|
({ waMessageId } = await this.deps.client.sendText(
|
|
265
355
|
cfg.instance,
|
|
266
|
-
|
|
356
|
+
firstTarget,
|
|
267
357
|
formatFirstMessage(code, name, message)
|
|
268
358
|
));
|
|
269
359
|
} catch (error) {
|
|
@@ -288,7 +378,12 @@ var ChatBridge = class {
|
|
|
288
378
|
if (session === null) throw new ChatError("Sess\xE3o n\xE3o encontrada", "session_not_found");
|
|
289
379
|
if (session.status !== "active") throw new ChatError("Sess\xE3o encerrada", "session_closed");
|
|
290
380
|
const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, "Mensagem");
|
|
291
|
-
|
|
381
|
+
let target;
|
|
382
|
+
if (session.groupJid === null) {
|
|
383
|
+
target = toWhatsappJid(normalizePhone(cfg.platformNumber));
|
|
384
|
+
} else {
|
|
385
|
+
target = session.groupJid;
|
|
386
|
+
}
|
|
292
387
|
const pending = await store.appendMessage({
|
|
293
388
|
sessionId: session.id,
|
|
294
389
|
direction: "visitor",
|
|
@@ -299,11 +394,16 @@ var ChatBridge = class {
|
|
|
299
394
|
try {
|
|
300
395
|
({ waMessageId } = await this.deps.client.sendText(
|
|
301
396
|
cfg.instance,
|
|
302
|
-
|
|
397
|
+
target,
|
|
303
398
|
formatFollowup(session.visitorName, body)
|
|
304
399
|
));
|
|
305
400
|
} catch (error) {
|
|
306
401
|
await store.updateMessageStatus(pending.id, "failed");
|
|
402
|
+
if (isTargetGoneError(error)) {
|
|
403
|
+
await this.closeSessionCleanup(session, "send_target_gone", {
|
|
404
|
+
systemMessage: "N\xE3o foi poss\xEDvel entregar a mensagem: o grupo n\xE3o est\xE1 mais dispon\xEDvel. Conversa encerrada \u2014 inicie uma nova para continuar."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
307
407
|
throw new ChatError("Falha ao enviar mensagem para o grupo", "send_failed", error);
|
|
308
408
|
}
|
|
309
409
|
await store.registerSentMessageId(session.id, waMessageId);
|
|
@@ -318,6 +418,9 @@ var ChatBridge = class {
|
|
|
318
418
|
async handleWebhook(payload) {
|
|
319
419
|
try {
|
|
320
420
|
const parsed = parseWebhookEvent(payload);
|
|
421
|
+
if (parsed.kind === "group_participants") {
|
|
422
|
+
return await this.handleGroupParticipants(parsed.event);
|
|
423
|
+
}
|
|
321
424
|
if (parsed.kind !== "message") return { handled: false };
|
|
322
425
|
const now = this.clock();
|
|
323
426
|
const decision = await this.router.decide(parsed.event, now);
|
|
@@ -341,6 +444,89 @@ var ChatBridge = class {
|
|
|
341
444
|
return { handled: false };
|
|
342
445
|
}
|
|
343
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é
|
|
449
|
+
* removido do grupo da sessão, registra uma mensagem de sistema no chat (visível
|
|
450
|
+
* para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão
|
|
451
|
+
* — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.
|
|
452
|
+
* NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution
|
|
453
|
+
* não reenviar.
|
|
454
|
+
*/
|
|
455
|
+
async handleGroupParticipants(change) {
|
|
456
|
+
try {
|
|
457
|
+
const session = await this.deps.store.getSessionByGroupJid(change.groupJid);
|
|
458
|
+
if (session === null) return { handled: true };
|
|
459
|
+
const phones = change.participants.map((jid) => jid.split("@")[0] ?? jid);
|
|
460
|
+
const visitorLeft = change.participants.some((jid) => {
|
|
461
|
+
const phone = jid.split("@")[0] ?? "";
|
|
462
|
+
return phone !== "" && phone === session.visitorPhone;
|
|
463
|
+
});
|
|
464
|
+
let body;
|
|
465
|
+
switch (change.action) {
|
|
466
|
+
case "leave":
|
|
467
|
+
body = `Participante(s) sa\xEDram do grupo: ${phones.join(", ")}`;
|
|
468
|
+
break;
|
|
469
|
+
case "remove":
|
|
470
|
+
body = `Participante(s) removido(s) do grupo: ${phones.join(", ")}`;
|
|
471
|
+
break;
|
|
472
|
+
case "add":
|
|
473
|
+
body = `Participante(s) adicionado(s) ao grupo: ${phones.join(", ")}`;
|
|
474
|
+
break;
|
|
475
|
+
case "promote":
|
|
476
|
+
body = `Participante(s) promovido(s) a admin: ${phones.join(", ")}`;
|
|
477
|
+
break;
|
|
478
|
+
case "demote":
|
|
479
|
+
body = `Participante(s) rebaixado(s) de admin: ${phones.join(", ")}`;
|
|
480
|
+
break;
|
|
481
|
+
default:
|
|
482
|
+
body = `Altera\xE7\xE3o de participantes no grupo: ${phones.join(", ")}`;
|
|
483
|
+
}
|
|
484
|
+
const now = this.clock().toISOString();
|
|
485
|
+
const message = await this.deps.store.appendMessage({
|
|
486
|
+
sessionId: session.id,
|
|
487
|
+
direction: "system",
|
|
488
|
+
body,
|
|
489
|
+
status: "sent"
|
|
490
|
+
});
|
|
491
|
+
await this.deps.store.touchSession(session.id, now);
|
|
492
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "message", message });
|
|
493
|
+
if (visitorLeft) {
|
|
494
|
+
await this.closeSessionCleanup(session, "visitor_left");
|
|
495
|
+
}
|
|
496
|
+
return { handled: true };
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.error("[evolution-chat] group-participants n\xE3o processado:", error);
|
|
499
|
+
return { handled: true };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do
|
|
504
|
+
* lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na
|
|
505
|
+
* Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais
|
|
506
|
+
* (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.
|
|
507
|
+
*/
|
|
508
|
+
async closeSessionCleanup(session, reason, opts = {}) {
|
|
509
|
+
const now = this.clock().toISOString();
|
|
510
|
+
await this.deps.store.markStatus(session.id, "closed", reason);
|
|
511
|
+
await this.deps.store.touchSession(session.id, now);
|
|
512
|
+
if (opts.systemMessage !== void 0) {
|
|
513
|
+
const notice = await this.deps.store.appendMessage({
|
|
514
|
+
sessionId: session.id,
|
|
515
|
+
direction: "system",
|
|
516
|
+
body: opts.systemMessage,
|
|
517
|
+
status: "sent"
|
|
518
|
+
});
|
|
519
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "message", message: notice });
|
|
520
|
+
}
|
|
521
|
+
await this.deps.transport.publish(session.realtimeToken, { type: "session", status: "closed" });
|
|
522
|
+
if (session.groupJid !== null && (reason === "visitor_left" || reason === "send_target_gone")) {
|
|
523
|
+
try {
|
|
524
|
+
await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error("[evolution-chat] falha ao sair do grupo \xF3rf\xE3o:", error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
344
530
|
/** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */
|
|
345
531
|
async history(token, afterIso) {
|
|
346
532
|
const session = await this.deps.store.getSessionByToken(token);
|
package/bridge/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (ordem fixada pelo plano):\n// 1. jid não termina em \"@g.us\" → ignore (sem tocar no store)\n// 2. sem sessão para o groupJid → unknown_session\n// 3. isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// 4. sem texto → not_text\n// 5. caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(private readonly store: Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n if (!msg.jid.endsWith(GROUP_JID_SUFFIX)) return { action: \"ignore\" };\n\n const session = await this.store.getSessionByGroupJid(msg.jid);\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\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","// 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","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/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 { InboundMessage } from \"../types\";\n\nexport type ParsedWebhook =\n | { kind: \"message\"; event: InboundMessage }\n | { kind: \"connection\"; state: string }\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 !== \"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","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport type { EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n\n const groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n groupJid,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n if (session.groupJid === null) throw new ChatError(\"Sessão sem grupo associado\", \"send_failed\");\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n session.groupJid,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/bridge/router.ts","../src/bridge/format.ts","../src/errors.ts","../src/api/ids.ts","../src/api/phone.ts","../src/api/webhook-parser.ts","../src/bridge/bridge.ts"],"sourcesContent":["// src/bridge/router.ts — SRP: decidir O QUE FAZER com uma mensagem inbound do webhook.\n//\n// Puro: não persiste, não publica, não chama a Evolution. Recebe o recorte mínimo do\n// SessionStore (Pick) e devolve um RouterDecision discriminado; quem executa é a bridge.\n//\n// Regras (modo grupo e modo direto):\n// - jid de grupo (@g.us): sessão por groupJid (comportamento original).\n// - jid pessoal (1:1, modo direto): sessão pelo telefone do visitante.\n// - isEcho(sessionId, waMessageId) → echo (mensagem enviada por nós)\n// - sem texto → not_text\n// - caso contrário → route, direction = fromMe ? owner : visitor\n\nimport type { InboundMessage } from \"../types\";\nimport type { ChatMessageDirection } from \"../types\";\nimport type { RouterDecision, SessionStore } from \"./types\";\n\n/** Sufixo que identifica um Jid de grupo no WhatsApp. */\nconst GROUP_JID_SUFFIX = \"@g.us\";\n\nexport class ConversationRouter {\n constructor(\n private readonly store: Pick<\n SessionStore,\n \"getSessionByGroupJid\" | \"getSessionByVisitorPhone\" | \"isEcho\"\n >,\n ) {}\n\n /**\n * `now` faz parte do contrato para que a bridge propague uma única leitura de relógio\n * por mensagem (touch/close/limite de horário nas próximas tasks); o router puro ainda\n * não a consome.\n */\n async decide(msg: InboundMessage, now: Date): Promise<RouterDecision> {\n void now;\n\n let session: Awaited<ReturnType<SessionStore[\"getSessionByGroupJid\"]>> = null;\n if (msg.jid.endsWith(GROUP_JID_SUFFIX)) {\n session = await this.store.getSessionByGroupJid(msg.jid);\n } else {\n // Modo direto: o jid da conversa 1:1 é o JID pessoal do visitante (ex.: 5511...@s.whatsapp.net).\n const phone = msg.jid.split(\"@\")[0] ?? \"\";\n if (phone.length === 0) return { action: \"ignore\" };\n session = await this.store.getSessionByVisitorPhone(phone);\n }\n\n if (session === null) return { action: \"unknown_session\" };\n\n if (await this.store.isEcho(session.id, msg.waMessageId)) return { action: \"echo\" };\n\n // `text` é string|null no contrato do parser; a checagem defensiva cobre chamadores JS.\n if (typeof msg.text !== \"string\" || msg.text.length === 0) return { action: \"not_text\" };\n\n const direction: ChatMessageDirection = msg.fromMe ? \"owner\" : \"visitor\";\n return { action: \"route\", session, direction, text: msg.text ?? \"\" };\n }\n}\n","// src/bridge/format.ts — Strategy de formatação do texto que entra no grupo WhatsApp.\n//\n// Isolado de propósito: o corpo persistido no store é SEMPRE o texto cru do visitante\n// (é o que o widget mostra), e só o que viaja para a Evolution recebe o prefixo que\n// identifica origem/código/nome. Trocar a identidade visual do relay = mexer aqui.\n\n/** Primeira mensagem da conversa no grupo: destaca código, nome e quebra de linha. */\nexport function formatFirstMessage(code: string, name: string, text: string): string {\n return `🆕 #${code} — ${name} (site):\\n${text}`;\n}\n\n/** Demais mensagens vindas do site: prefixo curto na mesma linha. */\nexport function formatFollowup(name: string, text: string): string {\n return `${name} (site): ${text}`;\n}\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","// 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","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/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","// src/bridge/bridge.ts — orquestração do chat (caso de uso), 100% orientada a portas.\n//\n// O bridge é o único lugar que compõe as peças: valida entrada, aplica anti-abuso,\n// fala com a Evolution (client), persiste (SessionStore) e notifica o widget\n// (RealtimeTransport). Nada disso é importado concretamente — tudo entra por `deps`,\n// o que o torna testável sem rede e sem banco (ver test/bridge/bridge.test.ts).\n//\n// O ConversationRouter (Task 5) é construído a partir do próprio store: ele só enxerga\n// Pick<SessionStore, \"getSessionByGroupJid\" | \"isEcho\">, então não há dependência nova\n// para quem monta o bridge.\n//\n// Convenção de erros: toda falha esperada sai como ChatError com um código de domínio\n// (invalid_input / rate_limited / group_create_failed / send_failed / session_not_found /\n// session_closed / store_error), preservando o erro original em `cause`. As rotas (Task 8)\n// mapeiam esses códigos para status HTTP — `invalid_input` vira 422 de campo (visível ao\n// visitante); falhas de infra/config usam `store_error` (500/502), nunca um 422 indevido.\n\nimport { ChatError } from \"../errors\";\nimport { generateRealtimeToken, generateSessionCode } from \"../api/ids\";\nimport { normalizePhone, toWhatsappJid } from \"../api/phone\";\nimport { parseWebhookEvent } from \"../api/webhook-parser\";\nimport { type EvolutionClient } from \"../api/client\";\nimport type { ChatConfig, ChatMessage, ChatSession, GroupParticipantChange } from \"../types\";\nimport { ConversationRouter } from \"./router\";\nimport { formatFirstMessage, formatFollowup } from \"./format\";\nimport type { Clock, RealtimeTransport, SessionStore } from \"./types\";\n\nexport interface StartChatInput {\n name: string;\n phone: string;\n message: string;\n contact?: string | null;\n ipHash?: string | null;\n userAgent?: string | null;\n honeypot?: string | null;\n}\n\nexport interface ChatBridgeDeps {\n client: EvolutionClient;\n store: SessionStore;\n transport: RealtimeTransport;\n /** Relógio injetável (testes determinísticos). Default: `() => new Date()`. */\n clock?: Clock;\n}\n\n// Limites de entrada (validados aqui, exibidos como 422 pela rota).\nconst NAME_MIN = 2;\nconst NAME_MAX = 60;\nconst MESSAGE_MIN = 1;\nconst MESSAGE_MAX = 1000;\n\n// Anti-abuso: máx. de sessões abertas pelo mesmo IP dentro da janela.\nconst RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;\nconst RATE_LIMIT_MAX_SESSIONS = 5;\n\n/** Sessão fake devolvida quando o honeypot é preenchido (nada é persistido). */\nfunction honeypotSession(atIso: string): ChatSession {\n return {\n id: \"honeypot\",\n code: \"XXXX\",\n realtimeToken: \"\",\n visitorName: \"\",\n visitorPhone: \"\",\n visitorContact: null,\n groupJid: null,\n status: \"closed\",\n createdAt: atIso,\n lastMessageAt: null,\n };\n}\n\n/** Trim + faixa de tamanho; lança ChatError(\"invalid_input\") nomeando o campo. */\nfunction requireText(value: string, min: number, max: number, field: string): string {\n // `typeof` é defensivo: a rota pode entregar undefined/null vindos de JSON.\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (trimmed.length < min || trimmed.length > max) {\n throw new ChatError(`${field} deve ter entre ${min} e ${max} caracteres`, \"invalid_input\");\n }\n return trimmed;\n}\n\n/**\n * Detecta erro da Evolution que indica que o destino sumiu: grupo apagado/saiu,\n * participante removido, número bloqueado, etc. Usado para encerrar a sessão com\n * aviso em vez de deixar o composer travado num envio que nunca chega.\n * Sinais: status 404/410 (grupo inexistente / não é participante) ou palavras-chave\n * no corpo/mensagem da resposta.\n */\nfunction isTargetGoneError(error: unknown): boolean {\n if (error === null || typeof error !== \"object\") return false;\n const status = (error as { status?: unknown }).status;\n if (status === 404 || status === 410) return true;\n const text = `${String((error as { message?: unknown }).message ?? \"\")} ${String(\n (error as { body?: unknown }).body ?? \"\",\n )}`.toLowerCase();\n return /group|participant|recipient|not found|unavailable|left|deleted|blocked|gone/.test(text);\n}\n\nexport class ChatBridge {\n private readonly router: ConversationRouter;\n private readonly clock: Clock;\n private config: ChatConfig | null = null;\n\n constructor(private readonly deps: ChatBridgeDeps) {\n this.router = new ConversationRouter(deps.store);\n this.clock = deps.clock ?? (() => new Date());\n }\n\n /** Config injetada pelas rotas a cada request (setConfig → uso → getConfig). */\n getConfig(): ChatConfig {\n if (this.config === null) {\n // F3: config ausente é falha de infraestrutura, não entrada inválida do visitante.\n // `invalid_input` seria mapeado pela Task 8 a um 422 de campo exibido a quem visita.\n throw new ChatError(\"Chat não configurado\", \"store_error\");\n }\n return this.config;\n }\n\n setConfig(cfg: ChatConfig): void {\n this.config = cfg;\n }\n\n /**\n * Abre uma conversa: cria o grupo na Evolution, envia a primeira mensagem e persiste a\n * sessão. Não publica no canal realtime — quem abriu o chat já tem a própria mensagem.\n */\n async startChat(input: StartChatInput): Promise<{ session: ChatSession; messages: ChatMessage[] }> {\n const atIso = this.clock().toISOString();\n\n // Anti-bot silencioso: campo invisível preenchido → finge sucesso e não toca em nada.\n if (typeof input.honeypot === \"string\" && input.honeypot.trim().length > 0) {\n return { session: honeypotSession(atIso), messages: [] };\n }\n\n const name = requireText(input.name, NAME_MIN, NAME_MAX, \"Nome\");\n const message = requireText(input.message, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n const phone = normalizePhone(input.phone); // ChatError(\"invalid_input\") se não for número útil\n const cfg = this.getConfig();\n const store = this.deps.store;\n\n if (typeof input.ipHash === \"string\" && input.ipHash !== \"\") {\n const recent = await store.countRecentSessionsByIpHash(input.ipHash, RATE_LIMIT_WINDOW_MS);\n if (recent >= RATE_LIMIT_MAX_SESSIONS) {\n throw new ChatError(\n `Limite de ${RATE_LIMIT_MAX_SESSIONS} sessões por IP em 10 minutos excedido`,\n \"rate_limited\",\n );\n }\n }\n\n // Reusa a conversa ativa do mesmo visitante (um grupo/thread por cliente) em vez de\n // abrir um grupo novo a cada reabertura — evita a proliferação de grupos no WhatsApp.\n // Só reaproveita se a sessão estiver ativa; se o grupo sumiu no WhatsApp (visitante\n // saiu/apagou), encerramos a sessão antiga e deixamos o fluxo abaixo recriar um grupo\n // novo para o mesmo visitante (auto-recuperação — sem grupo zumbi preso no histórico).\n const existing = await store.getSessionByVisitorPhone(phone);\n if (existing !== null && existing.status === \"active\") {\n const firstTarget =\n existing.groupJid === null\n ? toWhatsappJid(normalizePhone(cfg.platformNumber))\n : existing.groupJid;\n try {\n const { waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(existing.code, name, message),\n );\n await store.registerSentMessageId(existing.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: existing.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n return { session: existing, messages: [initial] };\n } catch (error) {\n if (isTargetGoneError(error)) {\n // Grupo apagado/saído: encerra a sessão antiga e recria um grupo novo abaixo.\n await this.closeSessionCleanup(existing, \"send_target_gone\");\n } else {\n await store.markStatus(existing.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n }\n }\n\n const code = generateSessionCode();\n const visitorJid = toWhatsappJid(phone);\n const platformJid = toWhatsappJid(normalizePhone(cfg.platformNumber));\n // visitor == platform (a plataforma se auto-atende) → lista dedupe, uma única chamada.\n const participants = [...new Set([visitorJid, platformJid])];\n const subject = `${cfg.projectName} — ${name} (#${code})`;\n const direct = cfg.createGroup === false;\n\n let groupJid: string | null = null;\n if (!direct) {\n groupJid = await this.createGroupWithRetry(cfg.instance, subject, participants, platformJid);\n // Imagem de capa do grupo (logo do site / upload do painel). Cosmético: falha não aborta.\n const picture = (cfg.groupImage ?? \"\").trim();\n if (picture !== \"\") {\n try {\n await this.deps.client.setGroupPicture(cfg.instance, groupJid, picture);\n } catch {\n // ignora falha de imagem — o atendimento segue sem a capa.\n }\n }\n }\n\n const session = await store.createSession({\n code,\n realtimeToken: generateRealtimeToken(),\n visitorName: name,\n visitorPhone: phone,\n visitorContact: input.contact ?? null,\n groupJid,\n mode: direct ? \"direct\" : \"group\",\n ipHash: input.ipHash ?? null,\n userAgent: input.userAgent ?? null,\n });\n\n // Em modo direto a conversa é 1:1 com a plataforma; senão, vai para o grupo.\n let firstTarget: string;\n if (direct) {\n firstTarget = platformJid;\n } else {\n if (groupJid === null) throw new ChatError(\"Falha ao criar o grupo\", \"group_create_failed\");\n firstTarget = groupJid;\n }\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n firstTarget,\n formatFirstMessage(code, name, message),\n ));\n } catch (error) {\n // Grupo existe, mas a conversa não começou: marca failed para não virar sessão zumbi.\n await store.markStatus(session.id, \"failed\");\n throw new ChatError(\"Falha ao enviar a primeira mensagem para o grupo\", \"send_failed\", error);\n }\n\n // Registra o id antes de qualquer eco chegar do webhook.\n await store.registerSentMessageId(session.id, waMessageId);\n const initial = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body: message,\n waMessageId,\n status: \"sent\",\n });\n\n return { session, messages: [initial] };\n }\n\n /** Relay site → grupo: persiste pending, envia, promove para sent (ou failed). */\n async sendVisitorMessage(token: string, text: string): Promise<ChatMessage> {\n const cfg = this.getConfig();\n const { store } = this.deps;\n\n const session = await store.getSessionByToken(token);\n if (session === null) throw new ChatError(\"Sessão não encontrada\", \"session_not_found\");\n if (session.status !== \"active\") throw new ChatError(\"Sessão encerrada\", \"session_closed\");\n const body = requireText(text, MESSAGE_MIN, MESSAGE_MAX, \"Mensagem\");\n\n // Destino: grupo (modo grupo) ou número da plataforma (modo direto 1:1, groupJid nulo).\n let target: string;\n if (session.groupJid === null) {\n target = toWhatsappJid(normalizePhone(cfg.platformNumber));\n } else {\n target = session.groupJid;\n }\n\n const pending = await store.appendMessage({\n sessionId: session.id,\n direction: \"visitor\",\n body,\n status: \"pending\",\n });\n\n let waMessageId: string;\n try {\n ({ waMessageId } = await this.deps.client.sendText(\n cfg.instance,\n target,\n formatFollowup(session.visitorName, body),\n ));\n } catch (error) {\n await store.updateMessageStatus(pending.id, \"failed\");\n // Se o destino sumiu (grupo saiu/apagado, número bloqueado), encerra a sessão com\n // aviso claro e sai do grupo órfão (best-effort) em vez de deixar o composer travado.\n if (isTargetGoneError(error)) {\n await this.closeSessionCleanup(session, \"send_target_gone\", {\n systemMessage:\n \"Não foi possível entregar a mensagem: o grupo não está mais disponível. Conversa encerrada — inicie uma nova para continuar.\",\n });\n }\n throw new ChatError(\"Falha ao enviar mensagem para o grupo\", \"send_failed\", error);\n }\n\n await store.registerSentMessageId(session.id, waMessageId);\n await store.updateMessageStatus(pending.id, \"sent\");\n await store.touchSession(session.id, this.clock().toISOString());\n\n // Devolve o estado já promovido (cópia): o chamador não depende de o store mutar ou não\n // a referência que recebeu no appendMessage.\n return { ...pending, status: \"sent\", waMessageId };\n }\n\n /**\n * Entrada do webhook da Evolution. NUNCA lança: um 500 aqui faz a Evolution reenviar\n * a mesma mensagem para sempre. Qualquer problema é logado e devolvido como não tratado.\n */\n async handleWebhook(payload: unknown): Promise<{ handled: boolean }> {\n try {\n const parsed = parseWebhookEvent(payload);\n if (parsed.kind === \"group_participants\") {\n return await this.handleGroupParticipants(parsed.event);\n }\n if (parsed.kind !== \"message\") return { handled: false }; // connection.update / ignored\n\n const now = this.clock();\n const decision = await this.router.decide(parsed.event, now);\n if (decision.action !== \"route\") return { handled: false }; // echo / unknown_session / not_text / ignore\n\n const { session, direction, text } = decision;\n const waMessageId = parsed.event.waMessageId;\n\n // F2: idempotência de reentrega. A Evolution reenvia a mesma mensagem sempre que\n // devolvemos handled:false (ex.: publish falhou). Sem esta guarda, o reenvio\n // duplicaria append + publish. O isEcho do router continua cobrindo só o eco de\n // mensagens NOSSAS (outbound); aqui é inbound repetido.\n const existing = await this.deps.store.findMessageByWaId(session.id, waMessageId);\n if (existing !== null) return { handled: true }; // já processada: só reconhece\n\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction,\n body: text,\n waMessageId,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now.toISOString());\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] webhook não processado:\", error);\n return { handled: false };\n }\n }\n\n /**\n * Trata `group-participants.update` (Evolution): quando alguém entra, sai ou é\n * removido do grupo da sessão, registra uma mensagem de sistema no chat (visível\n * para visitante e time) e, se quem saiu foi o próprio visitante, encerra a sessão\n * — a conversa 1:1 pelo WhatsApp não chega mais pelo grupo.\n * NUNCA lança: qualquer erro é logado e devolvido como tratado (200) para a Evolution\n * não reenviar.\n */\n private async handleGroupParticipants(\n change: GroupParticipantChange,\n ): Promise<{ handled: boolean }> {\n try {\n const session = await this.deps.store.getSessionByGroupJid(change.groupJid);\n if (session === null) return { handled: true }; // grupo não é desta plataforma\n\n const phones = change.participants.map((jid) => jid.split(\"@\")[0] ?? jid);\n const visitorLeft = change.participants.some((jid) => {\n const phone = jid.split(\"@\")[0] ?? \"\";\n return phone !== \"\" && phone === session.visitorPhone;\n });\n\n let body: string;\n switch (change.action) {\n case \"leave\":\n body = `Participante(s) saíram do grupo: ${phones.join(\", \")}`;\n break;\n case \"remove\":\n body = `Participante(s) removido(s) do grupo: ${phones.join(\", \")}`;\n break;\n case \"add\":\n body = `Participante(s) adicionado(s) ao grupo: ${phones.join(\", \")}`;\n break;\n case \"promote\":\n body = `Participante(s) promovido(s) a admin: ${phones.join(\", \")}`;\n break;\n case \"demote\":\n body = `Participante(s) rebaixado(s) de admin: ${phones.join(\", \")}`;\n break;\n default:\n body = `Alteração de participantes no grupo: ${phones.join(\", \")}`;\n }\n\n const now = this.clock().toISOString();\n const message = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body,\n status: \"sent\",\n });\n await this.deps.store.touchSession(session.id, now);\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message });\n\n if (visitorLeft) {\n await this.closeSessionCleanup(session, \"visitor_left\");\n }\n\n return { handled: true };\n } catch (error) {\n console.error(\"[evolution-chat] group-participants não processado:\", error);\n return { handled: true };\n }\n }\n\n /**\n * Encerra a sessão (status closed + evento realtime) e — só quando o grupo morreu do\n * lado do visitante (ele saiu ou o destino sumiu) — faz a EMPRESA SAIR do grupo na\n * Evolution (best-effort) para não acumular grupos órfãos. Em fechamentos normais\n * (atendente encerrou a conversa) NÃO saímos: senão o visitante ficaria sozinho no grupo.\n */\n private async closeSessionCleanup(\n session: ChatSession,\n reason: string,\n opts: { systemMessage?: string } = {},\n ): Promise<void> {\n const now = this.clock().toISOString();\n await this.deps.store.markStatus(session.id, \"closed\", reason);\n await this.deps.store.touchSession(session.id, now);\n\n if (opts.systemMessage !== undefined) {\n const notice = await this.deps.store.appendMessage({\n sessionId: session.id,\n direction: \"system\",\n body: opts.systemMessage,\n status: \"sent\",\n });\n await this.deps.transport.publish(session.realtimeToken, { type: \"message\", message: notice });\n }\n\n await this.deps.transport.publish(session.realtimeToken, { type: \"session\", status: \"closed\" });\n\n if (\n session.groupJid !== null &&\n (reason === \"visitor_left\" || reason === \"send_target_gone\")\n ) {\n try {\n await this.deps.client.leaveGroup(this.getConfig().instance, session.groupJid);\n } catch (error) {\n console.error(\"[evolution-chat] falha ao sair do grupo órfão:\", error);\n }\n }\n }\n\n /** Replay para o widget: sessão por token + mensagens (opcionalmente após um ISO). */\n async history(\n token: string,\n afterIso?: string | null,\n ): Promise<{ session: ChatSession | null; messages: ChatMessage[] }> {\n const session = await this.deps.store.getSessionByToken(token);\n if (session === null) return { session: null, messages: [] };\n const messages = await this.deps.store.listMessages(session.id, afterIso);\n return { session, messages };\n }\n\n /**\n * createGroup com uma única retratação: um participante inválido (ex.: o número do\n * visitante, vindo de um formulário web) não pode abortar o atendimento — a segunda\n * tentativa cria o grupo só com a plataforma, o número que controlamos e é válido.\n *\n * Atenção (F1): createGroup NÃO é idempotente. Se a 1ª chamada criar o grupo e o erro\n * observado for um timeout, a retratação pode orfanar um segundo grupo; a compensação\n * (limpeza/reconciliação de órfãos) é preocupação das Tasks 7/8, não deste fix.\n */\n private async createGroupWithRetry(\n instance: string,\n subject: string,\n participants: string[],\n platformJid: string,\n ): Promise<string> {\n const attempts: string[][] =\n participants.length > 1 ? [participants, [platformJid]] : [participants];\n let lastError: unknown;\n\n for (const attempt of attempts) {\n try {\n const { groupJid } = await this.deps.client.createGroup(instance, subject, attempt);\n return groupJid;\n } catch (error) {\n lastError = error;\n }\n }\n\n throw new ChatError(\"Falha ao criar o grupo na Evolution\", \"group_create_failed\", lastError);\n }\n}\n"],"mappings":"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","names":["firstTarget","waMessageId","initial"]}
|