@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 +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 +26 -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.js
CHANGED
|
@@ -85,9 +85,14 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
85
85
|
length: 12
|
|
86
86
|
}).notNull(),
|
|
87
87
|
agentUserId: uuid("agent_user_id"),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
// 32, não 16: os tipos da própria Meta são curtos ('text', 'interactive'), mas o host rotula
|
|
89
|
+
type: (
|
|
90
|
+
// a saída com o subtipo que enviou ('interactive_buttons' já tem 19). Apertar aqui só
|
|
91
|
+
// transfere para o consumidor a escolha entre truncar o rótulo e estourar o insert.
|
|
92
|
+
varchar("type", {
|
|
93
|
+
length: 32
|
|
94
|
+
}).notNull().default("text")
|
|
95
|
+
),
|
|
91
96
|
content: text("content"),
|
|
92
97
|
// T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por
|
|
93
98
|
// uploadId dentro deste jsonb.
|
|
@@ -101,6 +106,12 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
101
106
|
readAt: timestamp("read_at", {
|
|
102
107
|
withTimezone: true
|
|
103
108
|
}),
|
|
109
|
+
// Moderação de conteúdo. `null` significa NÃO AVALIADO (moderação desligada, ou mensagem
|
|
110
|
+
// anterior ao recurso) — diferente de `false`, que é avaliado e limpo. Colunas em vez de chave
|
|
111
|
+
// dentro de `payload` porque "listar o que foi sinalizado" é consulta de operação, e índice
|
|
112
|
+
// parcial sobre boolean resolve isso sem cavar jsonb.
|
|
113
|
+
moderationFlagged: boolean("moderation_flagged"),
|
|
114
|
+
moderationTerms: jsonb("moderation_terms").$type(),
|
|
104
115
|
createdAt: timestamp("created_at", {
|
|
105
116
|
withTimezone: true
|
|
106
117
|
}).notNull().defaultNow()
|
|
@@ -112,7 +123,10 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
112
123
|
// paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque
|
|
113
124
|
// mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs
|
|
114
125
|
// não podem competir entre si por unicidade.
|
|
115
|
-
uniqueIndex("idx_messages_company_wa_message_id").on(table.companyId, table.waMessageId).where(sql`${table.waMessageId} is not null`)
|
|
126
|
+
uniqueIndex("idx_messages_company_wa_message_id").on(table.companyId, table.waMessageId).where(sql`${table.waMessageId} is not null`),
|
|
127
|
+
// Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do
|
|
128
|
+
// tamanho do transcript.
|
|
129
|
+
index("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(sql`${table.moderationFlagged}`)
|
|
116
130
|
]);
|
|
117
131
|
var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
118
132
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -165,6 +179,31 @@ var settings = metaWhatsAppSchema.table("settings", {
|
|
|
165
179
|
|
|
166
180
|
// src/repositories/SessionRepository.ts
|
|
167
181
|
var DEFAULT_LIMIT = 20;
|
|
182
|
+
var conversationSummaryProjection = {
|
|
183
|
+
lastContent: sql2`(
|
|
184
|
+
select m.content from ${messages} m
|
|
185
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
186
|
+
order by m.created_at desc limit 1
|
|
187
|
+
)`,
|
|
188
|
+
lastDirection: sql2`(
|
|
189
|
+
select m.direction from ${messages} m
|
|
190
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
191
|
+
order by m.created_at desc limit 1
|
|
192
|
+
)`,
|
|
193
|
+
// Entradas do cliente depois da última leitura do atendente. Sessão nunca lida conta
|
|
194
|
+
// tudo — é o comportamento esperado de uma conversa que ninguém abriu ainda.
|
|
195
|
+
unread: sql2`(
|
|
196
|
+
select count(*)::int from ${messages} m
|
|
197
|
+
where m.company_id = ${sessions}.company_id
|
|
198
|
+
and m.session_id = ${sessions}.id
|
|
199
|
+
and m.direction = 'inbound'
|
|
200
|
+
and (${sessions}.last_agent_read_at is null or m.created_at > ${sessions}.last_agent_read_at)
|
|
201
|
+
)`
|
|
202
|
+
};
|
|
203
|
+
function sessionContextPatch(patch) {
|
|
204
|
+
return sql2`${sessions.context} || ${JSON.stringify(patch)}::jsonb`;
|
|
205
|
+
}
|
|
206
|
+
__name(sessionContextPatch, "sessionContextPatch");
|
|
168
207
|
var SessionRepository = class {
|
|
169
208
|
static {
|
|
170
209
|
__name(this, "SessionRepository");
|
|
@@ -195,6 +234,9 @@ var SessionRepository = class {
|
|
|
195
234
|
}).returning();
|
|
196
235
|
return created;
|
|
197
236
|
}
|
|
237
|
+
// O `context` jsonb é o ponto de extensão oficial para estado de sessão por produto: o módulo
|
|
238
|
+
// não conhece a forma, o consumidor a declara em TSessionContext. Este setter SUBSTITUI o
|
|
239
|
+
// objeto inteiro — para acumular respostas ao longo da conversa use patchContext.
|
|
198
240
|
async setState(companyId, whatsappNumber, state, context) {
|
|
199
241
|
await this.db.update(sessions).set({
|
|
200
242
|
currentState: state,
|
|
@@ -205,6 +247,25 @@ var SessionRepository = class {
|
|
|
205
247
|
updatedAt: sql2`now()`
|
|
206
248
|
}).where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)));
|
|
207
249
|
}
|
|
250
|
+
// Mescla parcial do context, feita no banco (`||`) e não por read-modify-write no host: duas
|
|
251
|
+
// mensagens do mesmo cliente processadas em paralelo sobrescreveriam uma à outra, e o campo
|
|
252
|
+
// acumula justamente as respostas coletadas ao longo da conversa. Chave presente no patch
|
|
253
|
+
// vence a existente; as demais permanecem.
|
|
254
|
+
async patchContext(companyId, whatsappNumber, patch) {
|
|
255
|
+
await this.db.update(sessions).set({
|
|
256
|
+
context: sessionContextPatch(patch),
|
|
257
|
+
lastActivity: sql2`now()`,
|
|
258
|
+
updatedAt: sql2`now()`
|
|
259
|
+
}).where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber)));
|
|
260
|
+
}
|
|
261
|
+
// Leitura tipada do estado de sessão do produto. Devolve undefined quando a sessão não existe —
|
|
262
|
+
// distinto de existir com context vazio, que devolve o objeto vazio.
|
|
263
|
+
async readContext(companyId, whatsappNumber) {
|
|
264
|
+
const [row] = await this.db.select({
|
|
265
|
+
context: sessions.context
|
|
266
|
+
}).from(sessions).where(and(eq(sessions.companyId, companyId), eq(sessions.whatsappNumber, whatsappNumber))).limit(1);
|
|
267
|
+
return row?.context;
|
|
268
|
+
}
|
|
208
269
|
// Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.
|
|
209
270
|
// Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).
|
|
210
271
|
async setFlowPosition(companyId, whatsappNumber, flowKey, currentNodeId) {
|
|
@@ -280,17 +341,28 @@ var SessionRepository = class {
|
|
|
280
341
|
currentState: sessions.currentState,
|
|
281
342
|
lastActivity: sessions.lastActivity,
|
|
282
343
|
lastInboundAt: sessions.lastInboundAt,
|
|
283
|
-
humanRequestedAt: sessions.humanRequestedAt
|
|
344
|
+
humanRequestedAt: sessions.humanRequestedAt,
|
|
345
|
+
// Prévia e contagem saem de subquery correlacionada em vez de N+1 na volta: uma inbox
|
|
346
|
+
// lista dezenas de conversas por página, e uma query por linha é o gargalo clássico
|
|
347
|
+
// dessa tela. Ambos os campos são dados do próprio módulo — deixá-los para o host
|
|
348
|
+
// obrigaria todo consumidor a reescrever o mesmo join contra tabelas que não são dele.
|
|
349
|
+
...conversationSummaryProjection
|
|
284
350
|
}).from(sessions).where(and(...conditions)).orderBy(desc(sessions.lastActivity)).limit(limit).offset(offset);
|
|
285
351
|
return rows.map((row) => ({
|
|
286
352
|
id: row.id,
|
|
287
353
|
whatsappNumber: row.whatsappNumber,
|
|
354
|
+
...row.lastContent !== null ? {
|
|
355
|
+
lastContent: row.lastContent
|
|
356
|
+
} : {},
|
|
357
|
+
...row.lastDirection !== null ? {
|
|
358
|
+
lastDirection: row.lastDirection
|
|
359
|
+
} : {},
|
|
288
360
|
lastAt: row.lastActivity.toISOString(),
|
|
289
361
|
lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
|
|
290
362
|
mode: row.mode,
|
|
291
363
|
assignedUserId: row.assignedUserId,
|
|
292
364
|
waitingHuman: row.humanRequestedAt !== null,
|
|
293
|
-
unread:
|
|
365
|
+
unread: Number(row.unread),
|
|
294
366
|
currentState: row.currentState
|
|
295
367
|
}));
|
|
296
368
|
}
|
|
@@ -334,7 +406,9 @@ var MessageRepository = class {
|
|
|
334
406
|
content: params.content ?? null,
|
|
335
407
|
payload: params.payload ?? null,
|
|
336
408
|
waMessageId: params.waMessageId ?? null,
|
|
337
|
-
status: params.status ?? null
|
|
409
|
+
status: params.status ?? null,
|
|
410
|
+
moderationFlagged: params.moderationFlagged ?? null,
|
|
411
|
+
moderationTerms: params.moderationTerms ?? null
|
|
338
412
|
};
|
|
339
413
|
const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning();
|
|
340
414
|
return created;
|
|
@@ -550,16 +624,19 @@ var LogMessageUseCase = class {
|
|
|
550
624
|
sessionRepository;
|
|
551
625
|
messageRepository;
|
|
552
626
|
realtime;
|
|
553
|
-
|
|
627
|
+
moderator;
|
|
628
|
+
constructor(sessionRepository, messageRepository, realtime, moderator) {
|
|
554
629
|
this.sessionRepository = sessionRepository;
|
|
555
630
|
this.messageRepository = messageRepository;
|
|
556
631
|
this.realtime = realtime;
|
|
632
|
+
this.moderator = moderator;
|
|
557
633
|
}
|
|
558
634
|
async execute(params) {
|
|
559
635
|
const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState);
|
|
560
636
|
const saved = await this.messageRepository.insertMessage({
|
|
561
637
|
...params,
|
|
562
|
-
sessionId: session.id
|
|
638
|
+
sessionId: session.id,
|
|
639
|
+
...this.moderationOf(params)
|
|
563
640
|
});
|
|
564
641
|
if (!saved) return void 0;
|
|
565
642
|
if (params.direction === "inbound") {
|
|
@@ -572,6 +649,20 @@ var LogMessageUseCase = class {
|
|
|
572
649
|
this.realtime?.emit("global", "data-changed", {});
|
|
573
650
|
return saved;
|
|
574
651
|
}
|
|
652
|
+
// Só o que o cliente escreveu: marcar o que o próprio atendente ou o bot enviou não sinaliza
|
|
653
|
+
// abuso, apenas sujaria o transcript com etiqueta na resposta de quem atende.
|
|
654
|
+
moderationOf(params) {
|
|
655
|
+
if (!this.moderator || params.direction !== "inbound") return {};
|
|
656
|
+
const text2 = params.content?.trim();
|
|
657
|
+
if (!text2) return {};
|
|
658
|
+
const verdict = this.moderator.inspect(text2);
|
|
659
|
+
return {
|
|
660
|
+
moderationFlagged: verdict.isOffensive,
|
|
661
|
+
moderationTerms: verdict.isOffensive ? [
|
|
662
|
+
...verdict.matchedTerms
|
|
663
|
+
] : null
|
|
664
|
+
};
|
|
665
|
+
}
|
|
575
666
|
};
|
|
576
667
|
|
|
577
668
|
// src/use-cases/SendMessage.use-case.ts
|
|
@@ -1113,6 +1204,8 @@ function toSessionContract(row) {
|
|
|
1113
1204
|
companyId: row.companyId,
|
|
1114
1205
|
whatsappNumber: row.whatsappNumber,
|
|
1115
1206
|
currentState: row.currentState,
|
|
1207
|
+
flowKey: row.flowKey,
|
|
1208
|
+
currentNodeId: row.currentNodeId,
|
|
1116
1209
|
context: row.context,
|
|
1117
1210
|
mode: row.mode,
|
|
1118
1211
|
assignedUserId: row.assignedUserId,
|
|
@@ -1323,7 +1416,7 @@ function createMetaWhatsAppModule(params) {
|
|
|
1323
1416
|
const messageRepository = new MessageRepository(db);
|
|
1324
1417
|
const settingsRepository = new SettingsRepository(db);
|
|
1325
1418
|
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1326
|
-
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime);
|
|
1419
|
+
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
1327
1420
|
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage);
|
|
1328
1421
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
1329
1422
|
appSecret: config.appSecret,
|
|
@@ -1379,11 +1472,15 @@ __name(createMetaWhatsAppModule, "createMetaWhatsAppModule");
|
|
|
1379
1472
|
|
|
1380
1473
|
// src/runMigrations.ts
|
|
1381
1474
|
import { join } from "path";
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1475
|
+
var META_WHATSAPP_MIGRATIONS_TABLE = "meta_whatsapp_migrations";
|
|
1476
|
+
function metaWhatsAppMigrationsFolder() {
|
|
1477
|
+
return join(__dirname, "migrations");
|
|
1478
|
+
}
|
|
1479
|
+
__name(metaWhatsAppMigrationsFolder, "metaWhatsAppMigrationsFolder");
|
|
1480
|
+
async function runMetaWhatsAppMigrations(params) {
|
|
1481
|
+
await params.migrate(params.db, {
|
|
1482
|
+
migrationsFolder: metaWhatsAppMigrationsFolder(),
|
|
1483
|
+
migrationsTable: META_WHATSAPP_MIGRATIONS_TABLE
|
|
1387
1484
|
});
|
|
1388
1485
|
}
|
|
1389
1486
|
__name(runMetaWhatsAppMigrations, "runMetaWhatsAppMigrations");
|
|
@@ -1477,6 +1574,7 @@ export {
|
|
|
1477
1574
|
ListFlowGraphsUseCase,
|
|
1478
1575
|
ListMessagesUseCase,
|
|
1479
1576
|
LogMessageUseCase,
|
|
1577
|
+
META_WHATSAPP_MIGRATIONS_TABLE,
|
|
1480
1578
|
MessageRepository,
|
|
1481
1579
|
OptimisticLockError,
|
|
1482
1580
|
ReceiveWebhookUseCase,
|
|
@@ -1495,6 +1593,7 @@ export {
|
|
|
1495
1593
|
flowGraphs,
|
|
1496
1594
|
issueSseTicket,
|
|
1497
1595
|
messages,
|
|
1596
|
+
metaWhatsAppMigrationsFolder,
|
|
1498
1597
|
metaWhatsAppSchema,
|
|
1499
1598
|
redeemSseTicket,
|
|
1500
1599
|
runMetaWhatsAppMigrations,
|