@adatechnology/meta-whatsapp-module 0.2.0-rc.1 → 0.2.0-rc.11
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/dist/index.cjs +117 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +417 -206
- package/dist/index.d.ts +417 -206
- package/dist/index.js +114 -15
- package/dist/index.js.map +1 -1
- package/dist/migrations/0004_widen_message_type.sql +1 -0
- package/dist/migrations/0005_message_moderation.sql +3 -0
- package/dist/migrations/meta/_journal.json +48 -0
- package/dist/testing/index.cjs +72 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +34 -0
- package/dist/testing/index.d.ts +34 -0
- package/dist/testing/index.js +40 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +23 -8
- package/dist/migrations/20260725195853_freezing_switch/snapshot.json +0 -555
- package/dist/migrations/20260725210958_military_leper_queen/snapshot.json +0 -742
- package/dist/migrations/20260725214800_illegal_black_tarantula/snapshot.json +0 -810
- package/dist/migrations/20260725234507_normal_viper/snapshot.json +0 -930
- /package/dist/migrations/{20260725195853_freezing_switch/migration.sql → 0000_freezing_switch.sql} +0 -0
- /package/dist/migrations/{20260725210958_military_leper_queen/migration.sql → 0001_military_leper_queen.sql} +0 -0
- /package/dist/migrations/{20260725214800_illegal_black_tarantula/migration.sql → 0002_illegal_black_tarantula.sql} +0 -0
- /package/dist/migrations/{20260725234507_normal_viper/migration.sql → 0003_normal_viper.sql} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -33,6 +34,7 @@ __export(index_exports, {
|
|
|
33
34
|
ListFlowGraphsUseCase: () => ListFlowGraphsUseCase,
|
|
34
35
|
ListMessagesUseCase: () => ListMessagesUseCase,
|
|
35
36
|
LogMessageUseCase: () => LogMessageUseCase,
|
|
37
|
+
META_WHATSAPP_MIGRATIONS_TABLE: () => META_WHATSAPP_MIGRATIONS_TABLE,
|
|
36
38
|
MessageRepository: () => MessageRepository,
|
|
37
39
|
OptimisticLockError: () => OptimisticLockError,
|
|
38
40
|
ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
|
|
@@ -51,6 +53,7 @@ __export(index_exports, {
|
|
|
51
53
|
flowGraphs: () => flowGraphs,
|
|
52
54
|
issueSseTicket: () => issueSseTicket,
|
|
53
55
|
messages: () => messages,
|
|
56
|
+
metaWhatsAppMigrationsFolder: () => metaWhatsAppMigrationsFolder,
|
|
54
57
|
metaWhatsAppSchema: () => metaWhatsAppSchema,
|
|
55
58
|
redeemSseTicket: () => redeemSseTicket,
|
|
56
59
|
runMetaWhatsAppMigrations: () => runMetaWhatsAppMigrations,
|
|
@@ -138,9 +141,14 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
138
141
|
length: 12
|
|
139
142
|
}).notNull(),
|
|
140
143
|
agentUserId: (0, import_pg_core.uuid)("agent_user_id"),
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
// 32, não 16: os tipos da própria Meta são curtos ('text', 'interactive'), mas o host rotula
|
|
145
|
+
type: (
|
|
146
|
+
// a saída com o subtipo que enviou ('interactive_buttons' já tem 19). Apertar aqui só
|
|
147
|
+
// transfere para o consumidor a escolha entre truncar o rótulo e estourar o insert.
|
|
148
|
+
(0, import_pg_core.varchar)("type", {
|
|
149
|
+
length: 32
|
|
150
|
+
}).notNull().default("text")
|
|
151
|
+
),
|
|
144
152
|
content: (0, import_pg_core.text)("content"),
|
|
145
153
|
// T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por
|
|
146
154
|
// uploadId dentro deste jsonb.
|
|
@@ -154,6 +162,12 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
154
162
|
readAt: (0, import_pg_core.timestamp)("read_at", {
|
|
155
163
|
withTimezone: true
|
|
156
164
|
}),
|
|
165
|
+
// Moderação de conteúdo. `null` significa NÃO AVALIADO (moderação desligada, ou mensagem
|
|
166
|
+
// anterior ao recurso) — diferente de `false`, que é avaliado e limpo. Colunas em vez de chave
|
|
167
|
+
// dentro de `payload` porque "listar o que foi sinalizado" é consulta de operação, e índice
|
|
168
|
+
// parcial sobre boolean resolve isso sem cavar jsonb.
|
|
169
|
+
moderationFlagged: (0, import_pg_core.boolean)("moderation_flagged"),
|
|
170
|
+
moderationTerms: (0, import_pg_core.jsonb)("moderation_terms").$type(),
|
|
157
171
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
158
172
|
withTimezone: true
|
|
159
173
|
}).notNull().defaultNow()
|
|
@@ -165,7 +179,10 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
165
179
|
// paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque
|
|
166
180
|
// mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs
|
|
167
181
|
// não podem competir entre si por unicidade.
|
|
168
|
-
(0, import_pg_core.uniqueIndex)("idx_messages_company_wa_message_id").on(table.companyId, table.waMessageId).where(import_drizzle_orm.sql`${table.waMessageId} is not null`)
|
|
182
|
+
(0, import_pg_core.uniqueIndex)("idx_messages_company_wa_message_id").on(table.companyId, table.waMessageId).where(import_drizzle_orm.sql`${table.waMessageId} is not null`),
|
|
183
|
+
// Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do
|
|
184
|
+
// tamanho do transcript.
|
|
185
|
+
(0, import_pg_core.index)("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.moderationFlagged}`)
|
|
169
186
|
]);
|
|
170
187
|
var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
171
188
|
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
@@ -218,6 +235,31 @@ var settings = metaWhatsAppSchema.table("settings", {
|
|
|
218
235
|
|
|
219
236
|
// src/repositories/SessionRepository.ts
|
|
220
237
|
var DEFAULT_LIMIT = 20;
|
|
238
|
+
var conversationSummaryProjection = {
|
|
239
|
+
lastContent: import_drizzle_orm2.sql`(
|
|
240
|
+
select m.content from ${messages} m
|
|
241
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
242
|
+
order by m.created_at desc limit 1
|
|
243
|
+
)`,
|
|
244
|
+
lastDirection: import_drizzle_orm2.sql`(
|
|
245
|
+
select m.direction from ${messages} m
|
|
246
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
247
|
+
order by m.created_at desc limit 1
|
|
248
|
+
)`,
|
|
249
|
+
// Entradas do cliente depois da última leitura do atendente. Sessão nunca lida conta
|
|
250
|
+
// tudo — é o comportamento esperado de uma conversa que ninguém abriu ainda.
|
|
251
|
+
unread: import_drizzle_orm2.sql`(
|
|
252
|
+
select count(*)::int from ${messages} m
|
|
253
|
+
where m.company_id = ${sessions}.company_id
|
|
254
|
+
and m.session_id = ${sessions}.id
|
|
255
|
+
and m.direction = 'inbound'
|
|
256
|
+
and (${sessions}.last_agent_read_at is null or m.created_at > ${sessions}.last_agent_read_at)
|
|
257
|
+
)`
|
|
258
|
+
};
|
|
259
|
+
function sessionContextPatch(patch) {
|
|
260
|
+
return import_drizzle_orm2.sql`${sessions.context} || ${JSON.stringify(patch)}::jsonb`;
|
|
261
|
+
}
|
|
262
|
+
__name(sessionContextPatch, "sessionContextPatch");
|
|
221
263
|
var SessionRepository = class {
|
|
222
264
|
static {
|
|
223
265
|
__name(this, "SessionRepository");
|
|
@@ -248,6 +290,9 @@ var SessionRepository = class {
|
|
|
248
290
|
}).returning();
|
|
249
291
|
return created;
|
|
250
292
|
}
|
|
293
|
+
// O `context` jsonb é o ponto de extensão oficial para estado de sessão por produto: o módulo
|
|
294
|
+
// não conhece a forma, o consumidor a declara em TSessionContext. Este setter SUBSTITUI o
|
|
295
|
+
// objeto inteiro — para acumular respostas ao longo da conversa use patchContext.
|
|
251
296
|
async setState(companyId, whatsappNumber, state, context) {
|
|
252
297
|
await this.db.update(sessions).set({
|
|
253
298
|
currentState: state,
|
|
@@ -258,6 +303,25 @@ var SessionRepository = class {
|
|
|
258
303
|
updatedAt: import_drizzle_orm2.sql`now()`
|
|
259
304
|
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
260
305
|
}
|
|
306
|
+
// Mescla parcial do context, feita no banco (`||`) e não por read-modify-write no host: duas
|
|
307
|
+
// mensagens do mesmo cliente processadas em paralelo sobrescreveriam uma à outra, e o campo
|
|
308
|
+
// acumula justamente as respostas coletadas ao longo da conversa. Chave presente no patch
|
|
309
|
+
// vence a existente; as demais permanecem.
|
|
310
|
+
async patchContext(companyId, whatsappNumber, patch) {
|
|
311
|
+
await this.db.update(sessions).set({
|
|
312
|
+
context: sessionContextPatch(patch),
|
|
313
|
+
lastActivity: import_drizzle_orm2.sql`now()`,
|
|
314
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
315
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
316
|
+
}
|
|
317
|
+
// Leitura tipada do estado de sessão do produto. Devolve undefined quando a sessão não existe —
|
|
318
|
+
// distinto de existir com context vazio, que devolve o objeto vazio.
|
|
319
|
+
async readContext(companyId, whatsappNumber) {
|
|
320
|
+
const [row] = await this.db.select({
|
|
321
|
+
context: sessions.context
|
|
322
|
+
}).from(sessions).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber))).limit(1);
|
|
323
|
+
return row?.context;
|
|
324
|
+
}
|
|
261
325
|
// Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.
|
|
262
326
|
// Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).
|
|
263
327
|
async setFlowPosition(companyId, whatsappNumber, flowKey, currentNodeId) {
|
|
@@ -333,17 +397,28 @@ var SessionRepository = class {
|
|
|
333
397
|
currentState: sessions.currentState,
|
|
334
398
|
lastActivity: sessions.lastActivity,
|
|
335
399
|
lastInboundAt: sessions.lastInboundAt,
|
|
336
|
-
humanRequestedAt: sessions.humanRequestedAt
|
|
400
|
+
humanRequestedAt: sessions.humanRequestedAt,
|
|
401
|
+
// Prévia e contagem saem de subquery correlacionada em vez de N+1 na volta: uma inbox
|
|
402
|
+
// lista dezenas de conversas por página, e uma query por linha é o gargalo clássico
|
|
403
|
+
// dessa tela. Ambos os campos são dados do próprio módulo — deixá-los para o host
|
|
404
|
+
// obrigaria todo consumidor a reescrever o mesmo join contra tabelas que não são dele.
|
|
405
|
+
...conversationSummaryProjection
|
|
337
406
|
}).from(sessions).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(sessions.lastActivity)).limit(limit).offset(offset);
|
|
338
407
|
return rows.map((row) => ({
|
|
339
408
|
id: row.id,
|
|
340
409
|
whatsappNumber: row.whatsappNumber,
|
|
410
|
+
...row.lastContent !== null ? {
|
|
411
|
+
lastContent: row.lastContent
|
|
412
|
+
} : {},
|
|
413
|
+
...row.lastDirection !== null ? {
|
|
414
|
+
lastDirection: row.lastDirection
|
|
415
|
+
} : {},
|
|
341
416
|
lastAt: row.lastActivity.toISOString(),
|
|
342
417
|
lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
|
|
343
418
|
mode: row.mode,
|
|
344
419
|
assignedUserId: row.assignedUserId,
|
|
345
420
|
waitingHuman: row.humanRequestedAt !== null,
|
|
346
|
-
unread:
|
|
421
|
+
unread: Number(row.unread),
|
|
347
422
|
currentState: row.currentState
|
|
348
423
|
}));
|
|
349
424
|
}
|
|
@@ -387,7 +462,9 @@ var MessageRepository = class {
|
|
|
387
462
|
content: params.content ?? null,
|
|
388
463
|
payload: params.payload ?? null,
|
|
389
464
|
waMessageId: params.waMessageId ?? null,
|
|
390
|
-
status: params.status ?? null
|
|
465
|
+
status: params.status ?? null,
|
|
466
|
+
moderationFlagged: params.moderationFlagged ?? null,
|
|
467
|
+
moderationTerms: params.moderationTerms ?? null
|
|
391
468
|
};
|
|
392
469
|
const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning();
|
|
393
470
|
return created;
|
|
@@ -603,16 +680,19 @@ var LogMessageUseCase = class {
|
|
|
603
680
|
sessionRepository;
|
|
604
681
|
messageRepository;
|
|
605
682
|
realtime;
|
|
606
|
-
|
|
683
|
+
moderator;
|
|
684
|
+
constructor(sessionRepository, messageRepository, realtime, moderator) {
|
|
607
685
|
this.sessionRepository = sessionRepository;
|
|
608
686
|
this.messageRepository = messageRepository;
|
|
609
687
|
this.realtime = realtime;
|
|
688
|
+
this.moderator = moderator;
|
|
610
689
|
}
|
|
611
690
|
async execute(params) {
|
|
612
691
|
const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState);
|
|
613
692
|
const saved = await this.messageRepository.insertMessage({
|
|
614
693
|
...params,
|
|
615
|
-
sessionId: session.id
|
|
694
|
+
sessionId: session.id,
|
|
695
|
+
...this.moderationOf(params)
|
|
616
696
|
});
|
|
617
697
|
if (!saved) return void 0;
|
|
618
698
|
if (params.direction === "inbound") {
|
|
@@ -625,6 +705,20 @@ var LogMessageUseCase = class {
|
|
|
625
705
|
this.realtime?.emit("global", "data-changed", {});
|
|
626
706
|
return saved;
|
|
627
707
|
}
|
|
708
|
+
// Só o que o cliente escreveu: marcar o que o próprio atendente ou o bot enviou não sinaliza
|
|
709
|
+
// abuso, apenas sujaria o transcript com etiqueta na resposta de quem atende.
|
|
710
|
+
moderationOf(params) {
|
|
711
|
+
if (!this.moderator || params.direction !== "inbound") return {};
|
|
712
|
+
const text2 = params.content?.trim();
|
|
713
|
+
if (!text2) return {};
|
|
714
|
+
const verdict = this.moderator.inspect(text2);
|
|
715
|
+
return {
|
|
716
|
+
moderationFlagged: verdict.isOffensive,
|
|
717
|
+
moderationTerms: verdict.isOffensive ? [
|
|
718
|
+
...verdict.matchedTerms
|
|
719
|
+
] : null
|
|
720
|
+
};
|
|
721
|
+
}
|
|
628
722
|
};
|
|
629
723
|
|
|
630
724
|
// src/use-cases/SendMessage.use-case.ts
|
|
@@ -1166,6 +1260,8 @@ function toSessionContract(row) {
|
|
|
1166
1260
|
companyId: row.companyId,
|
|
1167
1261
|
whatsappNumber: row.whatsappNumber,
|
|
1168
1262
|
currentState: row.currentState,
|
|
1263
|
+
flowKey: row.flowKey,
|
|
1264
|
+
currentNodeId: row.currentNodeId,
|
|
1169
1265
|
context: row.context,
|
|
1170
1266
|
mode: row.mode,
|
|
1171
1267
|
assignedUserId: row.assignedUserId,
|
|
@@ -1376,7 +1472,7 @@ function createMetaWhatsAppModule(params) {
|
|
|
1376
1472
|
const messageRepository = new MessageRepository(db);
|
|
1377
1473
|
const settingsRepository = new SettingsRepository(db);
|
|
1378
1474
|
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1379
|
-
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime);
|
|
1475
|
+
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
1380
1476
|
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage);
|
|
1381
1477
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
1382
1478
|
appSecret: config.appSecret,
|
|
@@ -1432,11 +1528,15 @@ __name(createMetaWhatsAppModule, "createMetaWhatsAppModule");
|
|
|
1432
1528
|
|
|
1433
1529
|
// src/runMigrations.ts
|
|
1434
1530
|
var import_node_path = require("path");
|
|
1435
|
-
var
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1531
|
+
var META_WHATSAPP_MIGRATIONS_TABLE = "meta_whatsapp_migrations";
|
|
1532
|
+
function metaWhatsAppMigrationsFolder() {
|
|
1533
|
+
return (0, import_node_path.join)(__dirname, "migrations");
|
|
1534
|
+
}
|
|
1535
|
+
__name(metaWhatsAppMigrationsFolder, "metaWhatsAppMigrationsFolder");
|
|
1536
|
+
async function runMetaWhatsAppMigrations(params) {
|
|
1537
|
+
await params.migrate(params.db, {
|
|
1538
|
+
migrationsFolder: metaWhatsAppMigrationsFolder(),
|
|
1539
|
+
migrationsTable: META_WHATSAPP_MIGRATIONS_TABLE
|
|
1440
1540
|
});
|
|
1441
1541
|
}
|
|
1442
1542
|
__name(runMetaWhatsAppMigrations, "runMetaWhatsAppMigrations");
|
|
@@ -1531,6 +1631,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1531
1631
|
ListFlowGraphsUseCase,
|
|
1532
1632
|
ListMessagesUseCase,
|
|
1533
1633
|
LogMessageUseCase,
|
|
1634
|
+
META_WHATSAPP_MIGRATIONS_TABLE,
|
|
1534
1635
|
MessageRepository,
|
|
1535
1636
|
OptimisticLockError,
|
|
1536
1637
|
ReceiveWebhookUseCase,
|
|
@@ -1549,6 +1650,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1549
1650
|
flowGraphs,
|
|
1550
1651
|
issueSseTicket,
|
|
1551
1652
|
messages,
|
|
1653
|
+
metaWhatsAppMigrationsFolder,
|
|
1552
1654
|
metaWhatsAppSchema,
|
|
1553
1655
|
redeemSseTicket,
|
|
1554
1656
|
runMetaWhatsAppMigrations,
|