@adatechnology/meta-whatsapp-module 0.2.0-rc.0 → 0.2.0-rc.10

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 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
- type: (0, import_pg_core.varchar)("type", {
142
- length: 16
143
- }).notNull().default("text"),
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: 0,
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
- constructor(sessionRepository, messageRepository, realtime) {
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 import_migrator = require("drizzle-orm/bun-sql/migrator");
1436
- async function runMetaWhatsAppMigrations(db) {
1437
- await (0, import_migrator.migrate)(db, {
1438
- migrationsFolder: (0, import_node_path.join)(__dirname, "migrations"),
1439
- migrationsTable: "meta_whatsapp_migrations"
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,