@adatechnology/meta-whatsapp-module 0.2.0-rc.3 → 0.2.0-rc.30
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 +1613 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1685 -139
- package/dist/index.d.ts +1685 -139
- package/dist/index.js +1575 -132
- 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/0006_conversation_documents.sql +19 -0
- package/dist/migrations/0007_flow_media.sql +18 -0
- package/dist/migrations/0008_message_transcription.sql +5 -0
- package/dist/migrations/0009_settings_transcription_policy.sql +2 -0
- package/dist/migrations/meta/_journal.json +43 -1
- package/dist/testing/index.cjs +222 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +74 -0
- package/dist/testing/index.d.ts +74 -0
- package/dist/testing/index.js +186 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +16 -4
package/dist/index.cjs
CHANGED
|
@@ -8,11 +8,11 @@ var __export = (target, all) => {
|
|
|
8
8
|
for (var name in all)
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
|
-
var __copyProps = (to, from, except,
|
|
11
|
+
var __copyProps = (to, from, except, desc3) => {
|
|
12
12
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
13
|
for (let key of __getOwnPropNames(from))
|
|
14
14
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc3 = __getOwnPropDesc(from, key)) || desc3.enumerable });
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
@@ -22,14 +22,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
CreateFlowGraphUseCase: () => CreateFlowGraphUseCase,
|
|
25
|
+
DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS: () => DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,
|
|
26
|
+
DeleteConversationUseCase: () => DeleteConversationUseCase,
|
|
25
27
|
DeleteFlowGraphUseCase: () => DeleteFlowGraphUseCase,
|
|
28
|
+
DocumentRepository: () => DocumentRepository,
|
|
26
29
|
ExportConversationUseCase: () => ExportConversationUseCase,
|
|
30
|
+
FlowGraphCache: () => FlowGraphCache,
|
|
27
31
|
FlowGraphRepository: () => FlowGraphRepository,
|
|
28
32
|
FlowInterpreter: () => FlowInterpreter,
|
|
33
|
+
FlowMediaRepository: () => FlowMediaRepository,
|
|
29
34
|
GetFlowGraphUseCase: () => GetFlowGraphUseCase,
|
|
30
35
|
GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
|
|
36
|
+
InboundEffectsDispatcher: () => InboundEffectsDispatcher,
|
|
31
37
|
IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
|
|
32
38
|
InvalidFlowGraphError: () => InvalidFlowGraphError,
|
|
39
|
+
ListCompanyDocumentsUseCase: () => ListCompanyDocumentsUseCase,
|
|
40
|
+
ListConversationDocumentsUseCase: () => ListConversationDocumentsUseCase,
|
|
33
41
|
ListConversationsUseCase: () => ListConversationsUseCase,
|
|
34
42
|
ListFlowGraphsUseCase: () => ListFlowGraphsUseCase,
|
|
35
43
|
ListMessagesUseCase: () => ListMessagesUseCase,
|
|
@@ -37,6 +45,10 @@ __export(index_exports, {
|
|
|
37
45
|
META_WHATSAPP_MIGRATIONS_TABLE: () => META_WHATSAPP_MIGRATIONS_TABLE,
|
|
38
46
|
MessageRepository: () => MessageRepository,
|
|
39
47
|
OptimisticLockError: () => OptimisticLockError,
|
|
48
|
+
PREVIEW_MEDIA_ID_PREFIX: () => import_meta_whatsapp_contracts8.PREVIEW_MEDIA_ID_PREFIX,
|
|
49
|
+
PRODUCT_LIST_LIMIT: () => PRODUCT_LIST_LIMIT,
|
|
50
|
+
ProcessInboundDispatchUseCase: () => ProcessInboundDispatchUseCase,
|
|
51
|
+
PurgeExpiredDocumentsUseCase: () => PurgeExpiredDocumentsUseCase,
|
|
40
52
|
ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
|
|
41
53
|
ReleaseConversationUseCase: () => ReleaseConversationUseCase,
|
|
42
54
|
SaveFlowGraphUseCase: () => SaveFlowGraphUseCase,
|
|
@@ -44,21 +56,41 @@ __export(index_exports, {
|
|
|
44
56
|
SessionRepository: () => SessionRepository,
|
|
45
57
|
SettingsRepository: () => SettingsRepository,
|
|
46
58
|
SseHub: () => SseHub,
|
|
59
|
+
StorePreviewMediaUseCase: () => StorePreviewMediaUseCase,
|
|
60
|
+
TRANSCRIPTION_MODE: () => TRANSCRIPTION_MODE,
|
|
61
|
+
TRANSCRIPTION_STATUS: () => TRANSCRIPTION_STATUS,
|
|
47
62
|
TakeoverConversationUseCase: () => TakeoverConversationUseCase,
|
|
48
|
-
|
|
63
|
+
TranscribeAudioUseCase: () => TranscribeAudioUseCase,
|
|
64
|
+
WEBHOOK_CLAIM_TTL_SECONDS: () => import_meta_graph_core2.WEBHOOK_CLAIM_TTL_SECONDS,
|
|
65
|
+
WEBHOOK_NONCE_TTL_SECONDS: () => import_meta_graph_core2.WEBHOOK_NONCE_TTL_SECONDS,
|
|
49
66
|
WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
|
|
67
|
+
buildInboundJobId: () => buildInboundJobId,
|
|
50
68
|
claimWebhookDelivery: () => claimWebhookDelivery,
|
|
69
|
+
confirmWebhookDelivery: () => confirmWebhookDelivery,
|
|
51
70
|
createMetaWhatsAppModule: () => createMetaWhatsAppModule,
|
|
71
|
+
createSendMediaAction: () => createSendMediaAction,
|
|
72
|
+
createSendProductListAction: () => createSendProductListAction,
|
|
73
|
+
createTranscriptionPolicyResolver: () => createTranscriptionPolicyResolver,
|
|
74
|
+
documents: () => documents,
|
|
52
75
|
extractMediaDescriptor: () => extractMediaDescriptor,
|
|
53
76
|
flowGraphs: () => flowGraphs,
|
|
77
|
+
flowMedia: () => flowMedia,
|
|
78
|
+
isAudioMimeType: () => isAudioMimeType,
|
|
79
|
+
isRetriableTranscriptionError: () => isRetriableTranscriptionError,
|
|
80
|
+
isUnsupportedTranscriptionError: () => isUnsupportedTranscriptionError,
|
|
54
81
|
issueSseTicket: () => issueSseTicket,
|
|
55
82
|
messages: () => messages,
|
|
56
83
|
metaWhatsAppMigrationsFolder: () => metaWhatsAppMigrationsFolder,
|
|
57
84
|
metaWhatsAppSchema: () => metaWhatsAppSchema,
|
|
58
85
|
redeemSseTicket: () => redeemSseTicket,
|
|
86
|
+
resolveFailureStatus: () => resolveFailureStatus,
|
|
87
|
+
resolvePreviewUploadId: () => import_meta_whatsapp_contracts8.resolvePreviewUploadId,
|
|
59
88
|
runMetaWhatsAppMigrations: () => runMetaWhatsAppMigrations,
|
|
60
89
|
sessions: () => sessions,
|
|
61
90
|
settings: () => settings,
|
|
91
|
+
toPreviewMediaId: () => import_meta_whatsapp_contracts8.toPreviewMediaId,
|
|
92
|
+
toSessionContract: () => toSessionContract,
|
|
93
|
+
transcriptionRetryAfterSeconds: () => transcriptionRetryAfterSeconds,
|
|
62
94
|
verifyWebhookChallenge: () => verifyWebhookChallenge,
|
|
63
95
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
64
96
|
});
|
|
@@ -66,6 +98,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
66
98
|
|
|
67
99
|
// src/createMetaWhatsAppModule.ts
|
|
68
100
|
var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
|
|
101
|
+
var import_meta_whatsapp_contracts13 = require("@adatechnology/meta-whatsapp-contracts");
|
|
69
102
|
|
|
70
103
|
// src/repositories/SessionRepository.ts
|
|
71
104
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
@@ -141,9 +174,14 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
141
174
|
length: 12
|
|
142
175
|
}).notNull(),
|
|
143
176
|
agentUserId: (0, import_pg_core.uuid)("agent_user_id"),
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
// 32, não 16: os tipos da própria Meta são curtos ('text', 'interactive'), mas o host rotula
|
|
178
|
+
type: (
|
|
179
|
+
// a saída com o subtipo que enviou ('interactive_buttons' já tem 19). Apertar aqui só
|
|
180
|
+
// transfere para o consumidor a escolha entre truncar o rótulo e estourar o insert.
|
|
181
|
+
(0, import_pg_core.varchar)("type", {
|
|
182
|
+
length: 32
|
|
183
|
+
}).notNull().default("text")
|
|
184
|
+
),
|
|
147
185
|
content: (0, import_pg_core.text)("content"),
|
|
148
186
|
// T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por
|
|
149
187
|
// uploadId dentro deste jsonb.
|
|
@@ -157,6 +195,33 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
157
195
|
readAt: (0, import_pg_core.timestamp)("read_at", {
|
|
158
196
|
withTimezone: true
|
|
159
197
|
}),
|
|
198
|
+
// Moderação de conteúdo. `null` significa NÃO AVALIADO (moderação desligada, ou mensagem
|
|
199
|
+
// anterior ao recurso) — diferente de `false`, que é avaliado e limpo. Colunas em vez de chave
|
|
200
|
+
// dentro de `payload` porque "listar o que foi sinalizado" é consulta de operação, e índice
|
|
201
|
+
// parcial sobre boolean resolve isso sem cavar jsonb.
|
|
202
|
+
moderationFlagged: (0, import_pg_core.boolean)("moderation_flagged"),
|
|
203
|
+
moderationTerms: (0, import_pg_core.jsonb)("moderation_terms").$type(),
|
|
204
|
+
/**
|
|
205
|
+
* Transcrição de áudio. `null` em `transcription_status` significa NÃO AVALIADO — áudio nunca
|
|
206
|
+
* pedido (modo sob demanda), transcrição desligada, mensagem anterior ao recurso, ou mensagem
|
|
207
|
+
* que não é áudio. Diferente de `'done'` com texto vazio, que é áudio em silêncio já processado
|
|
208
|
+
* e que NÃO deve ser reprocessado.
|
|
209
|
+
*
|
|
210
|
+
* Colunas em vez de chave em `payload` pelo mesmo motivo da moderação: "quais áudios ficaram
|
|
211
|
+
* pendentes" e "quais falharam" são consultas de operação, e índice parcial resolve sem cavar
|
|
212
|
+
* jsonb. Buscar texto de áudio também deixa de exigir varredura de payload.
|
|
213
|
+
*/
|
|
214
|
+
transcriptionStatus: (0, import_pg_core.varchar)("transcription_status", {
|
|
215
|
+
length: 16
|
|
216
|
+
}).$type(),
|
|
217
|
+
transcriptionText: (0, import_pg_core.text)("transcription_text"),
|
|
218
|
+
transcriptionLanguage: (0, import_pg_core.varchar)("transcription_language", {
|
|
219
|
+
length: 32
|
|
220
|
+
}),
|
|
221
|
+
/** Qual engine produziu. Com uma cadeia de engines, é o que responde "por que esta saiu ruim". */
|
|
222
|
+
transcriptionEngine: (0, import_pg_core.varchar)("transcription_engine", {
|
|
223
|
+
length: 32
|
|
224
|
+
}),
|
|
160
225
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
161
226
|
withTimezone: true
|
|
162
227
|
}).notNull().defaultNow()
|
|
@@ -168,7 +233,54 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
168
233
|
// paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque
|
|
169
234
|
// mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs
|
|
170
235
|
// não podem competir entre si por unicidade.
|
|
171
|
-
(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`)
|
|
236
|
+
(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`),
|
|
237
|
+
// Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do
|
|
238
|
+
// tamanho do transcript.
|
|
239
|
+
(0, import_pg_core.index)("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.moderationFlagged}`),
|
|
240
|
+
// Alimenta a varredura de retomada: "quais áudios ficaram pendentes de transcrição". Parcial
|
|
241
|
+
// porque pendente é estado transitório e raro — o índice fica do tamanho da fila atrasada, não
|
|
242
|
+
// do transcript inteiro.
|
|
243
|
+
(0, import_pg_core.index)("idx_messages_transcription_pending").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.transcriptionStatus} = 'pending'`)
|
|
244
|
+
]);
|
|
245
|
+
var documents = metaWhatsAppSchema.table("documents", {
|
|
246
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
247
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
248
|
+
sessionId: (0, import_pg_core.uuid)("session_id").notNull().references(() => sessions.id, {
|
|
249
|
+
onDelete: "cascade"
|
|
250
|
+
}),
|
|
251
|
+
// Anulável e `set null`: apagar a mensagem não deve apagar o arquivo da biblioteca, e arquivo
|
|
252
|
+
// do atendente nasce sem mensagem.
|
|
253
|
+
messageId: (0, import_pg_core.uuid)("message_id").references(() => messages.id, {
|
|
254
|
+
onDelete: "set null"
|
|
255
|
+
}),
|
|
256
|
+
uploadId: (0, import_pg_core.varchar)("upload_id", {
|
|
257
|
+
length: 256
|
|
258
|
+
}).notNull(),
|
|
259
|
+
filename: (0, import_pg_core.varchar)("filename", {
|
|
260
|
+
length: 512
|
|
261
|
+
}).notNull(),
|
|
262
|
+
mimeType: (0, import_pg_core.varchar)("mime_type", {
|
|
263
|
+
length: 128
|
|
264
|
+
}).notNull(),
|
|
265
|
+
sizeBytes: (0, import_pg_core.integer)("size_bytes").notNull(),
|
|
266
|
+
// Do provider de storage, que é endereçado por conteúdo — serve para reconciliar storage
|
|
267
|
+
// contra tabela e para detectar o mesmo binário chegando duas vezes.
|
|
268
|
+
sha256: (0, import_pg_core.varchar)("sha256", {
|
|
269
|
+
length: 64
|
|
270
|
+
}),
|
|
271
|
+
source: (0, import_pg_core.varchar)("source", {
|
|
272
|
+
length: 12
|
|
273
|
+
}).notNull(),
|
|
274
|
+
linkedAt: (0, import_pg_core.timestamp)("linked_at", {
|
|
275
|
+
withTimezone: true
|
|
276
|
+
}).notNull().defaultNow()
|
|
277
|
+
}, (table) => [
|
|
278
|
+
(0, import_pg_core.index)("idx_documents_session_linked").on(table.sessionId, table.linkedAt),
|
|
279
|
+
// Para varredura de retenção por idade sem passar por sessão.
|
|
280
|
+
(0, import_pg_core.index)("idx_documents_company_linked").on(table.companyId, table.linkedAt),
|
|
281
|
+
// O mesmo objeto não pode ser linkado duas vezes na mesma empresa: o job de ingestão é
|
|
282
|
+
// reentregue por retry, e sem isto a reentrega criaria linha duplicada no painel.
|
|
283
|
+
(0, import_pg_core.uniqueIndex)("idx_documents_company_upload").on(table.companyId, table.uploadId)
|
|
172
284
|
]);
|
|
173
285
|
var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
174
286
|
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
@@ -199,6 +311,46 @@ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
|
199
311
|
}, (table) => [
|
|
200
312
|
(0, import_pg_core.uniqueIndex)("idx_flow_graphs_company_key").on(table.companyId, table.key)
|
|
201
313
|
]);
|
|
314
|
+
var flowMedia = metaWhatsAppSchema.table("flow_media", {
|
|
315
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
316
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
317
|
+
flowKey: (0, import_pg_core.varchar)("flow_key", {
|
|
318
|
+
length: 64
|
|
319
|
+
}).notNull(),
|
|
320
|
+
nodeId: (0, import_pg_core.varchar)("node_id", {
|
|
321
|
+
length: 64
|
|
322
|
+
}).notNull(),
|
|
323
|
+
uploadId: (0, import_pg_core.varchar)("upload_id", {
|
|
324
|
+
length: 256
|
|
325
|
+
}).notNull(),
|
|
326
|
+
filename: (0, import_pg_core.varchar)("filename", {
|
|
327
|
+
length: 512
|
|
328
|
+
}).notNull(),
|
|
329
|
+
mimeType: (0, import_pg_core.varchar)("mime_type", {
|
|
330
|
+
length: 128
|
|
331
|
+
}).notNull(),
|
|
332
|
+
sizeBytes: (0, import_pg_core.integer)("size_bytes").notNull(),
|
|
333
|
+
// Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e
|
|
334
|
+
// um folder precisa de textos diferentes para cada um.
|
|
335
|
+
caption: (0, import_pg_core.text)("caption"),
|
|
336
|
+
// Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e "tabela antes
|
|
337
|
+
// do folder" é decisão de quem edita, não do banco.
|
|
338
|
+
sortOrder: (0, import_pg_core.integer)("sort_order").notNull().default(0),
|
|
339
|
+
// Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
|
|
340
|
+
// perderia a ordem e a legenda já ajustadas.
|
|
341
|
+
active: (0, import_pg_core.boolean)("active").notNull().default(true),
|
|
342
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
343
|
+
withTimezone: true
|
|
344
|
+
}).notNull().defaultNow(),
|
|
345
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", {
|
|
346
|
+
withTimezone: true
|
|
347
|
+
}).notNull().defaultNow()
|
|
348
|
+
}, (table) => [
|
|
349
|
+
(0, import_pg_core.index)("idx_flow_media_node").on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),
|
|
350
|
+
// O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente
|
|
351
|
+
// receberia o documento repetido.
|
|
352
|
+
(0, import_pg_core.uniqueIndex)("idx_flow_media_node_upload").on(table.companyId, table.flowKey, table.nodeId, table.uploadId)
|
|
353
|
+
]);
|
|
202
354
|
var settings = metaWhatsAppSchema.table("settings", {
|
|
203
355
|
companyId: (0, import_pg_core.uuid)("company_id").primaryKey(),
|
|
204
356
|
templateName: (0, import_pg_core.varchar)("template_name", {
|
|
@@ -211,6 +363,15 @@ var settings = metaWhatsAppSchema.table("settings", {
|
|
|
211
363
|
templateVariables: (0, import_pg_core.jsonb)("template_variables").$type().notNull().default([]),
|
|
212
364
|
welcomeMessage: (0, import_pg_core.text)("welcome_message"),
|
|
213
365
|
farewellMessage: (0, import_pg_core.text)("farewell_message"),
|
|
366
|
+
/**
|
|
367
|
+
* Política de transcrição desta empresa. Nulo é significativo: "o painel não decidiu", e aí vale
|
|
368
|
+
* o padrão que o host injetou. Sem a distinção, atualizar o módulo desligaria a transcrição de
|
|
369
|
+
* quem já a tinha ligada por ambiente.
|
|
370
|
+
*/
|
|
371
|
+
transcriptionEnabled: (0, import_pg_core.boolean)("transcription_enabled"),
|
|
372
|
+
transcriptionMode: (0, import_pg_core.varchar)("transcription_mode", {
|
|
373
|
+
length: 16
|
|
374
|
+
}).$type(),
|
|
214
375
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
215
376
|
withTimezone: true
|
|
216
377
|
}).notNull().defaultNow(),
|
|
@@ -221,6 +382,31 @@ var settings = metaWhatsAppSchema.table("settings", {
|
|
|
221
382
|
|
|
222
383
|
// src/repositories/SessionRepository.ts
|
|
223
384
|
var DEFAULT_LIMIT = 20;
|
|
385
|
+
var conversationSummaryProjection = {
|
|
386
|
+
lastContent: import_drizzle_orm2.sql`(
|
|
387
|
+
select m.content from ${messages} m
|
|
388
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
389
|
+
order by m.created_at desc limit 1
|
|
390
|
+
)`,
|
|
391
|
+
lastDirection: import_drizzle_orm2.sql`(
|
|
392
|
+
select m.direction from ${messages} m
|
|
393
|
+
where m.company_id = ${sessions}.company_id and m.session_id = ${sessions}.id
|
|
394
|
+
order by m.created_at desc limit 1
|
|
395
|
+
)`,
|
|
396
|
+
// Entradas do cliente depois da última leitura do atendente. Sessão nunca lida conta
|
|
397
|
+
// tudo — é o comportamento esperado de uma conversa que ninguém abriu ainda.
|
|
398
|
+
unread: import_drizzle_orm2.sql`(
|
|
399
|
+
select count(*)::int from ${messages} m
|
|
400
|
+
where m.company_id = ${sessions}.company_id
|
|
401
|
+
and m.session_id = ${sessions}.id
|
|
402
|
+
and m.direction = 'inbound'
|
|
403
|
+
and (${sessions}.last_agent_read_at is null or m.created_at > ${sessions}.last_agent_read_at)
|
|
404
|
+
)`
|
|
405
|
+
};
|
|
406
|
+
function sessionContextPatch(patch) {
|
|
407
|
+
return import_drizzle_orm2.sql`${sessions.context} || ${JSON.stringify(patch)}::jsonb`;
|
|
408
|
+
}
|
|
409
|
+
__name(sessionContextPatch, "sessionContextPatch");
|
|
224
410
|
var SessionRepository = class {
|
|
225
411
|
static {
|
|
226
412
|
__name(this, "SessionRepository");
|
|
@@ -251,6 +437,9 @@ var SessionRepository = class {
|
|
|
251
437
|
}).returning();
|
|
252
438
|
return created;
|
|
253
439
|
}
|
|
440
|
+
// O `context` jsonb é o ponto de extensão oficial para estado de sessão por produto: o módulo
|
|
441
|
+
// não conhece a forma, o consumidor a declara em TSessionContext. Este setter SUBSTITUI o
|
|
442
|
+
// objeto inteiro — para acumular respostas ao longo da conversa use patchContext.
|
|
254
443
|
async setState(companyId, whatsappNumber, state, context) {
|
|
255
444
|
await this.db.update(sessions).set({
|
|
256
445
|
currentState: state,
|
|
@@ -261,6 +450,25 @@ var SessionRepository = class {
|
|
|
261
450
|
updatedAt: import_drizzle_orm2.sql`now()`
|
|
262
451
|
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
263
452
|
}
|
|
453
|
+
// Mescla parcial do context, feita no banco (`||`) e não por read-modify-write no host: duas
|
|
454
|
+
// mensagens do mesmo cliente processadas em paralelo sobrescreveriam uma à outra, e o campo
|
|
455
|
+
// acumula justamente as respostas coletadas ao longo da conversa. Chave presente no patch
|
|
456
|
+
// vence a existente; as demais permanecem.
|
|
457
|
+
async patchContext(companyId, whatsappNumber, patch) {
|
|
458
|
+
await this.db.update(sessions).set({
|
|
459
|
+
context: sessionContextPatch(patch),
|
|
460
|
+
lastActivity: import_drizzle_orm2.sql`now()`,
|
|
461
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
462
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
463
|
+
}
|
|
464
|
+
// Leitura tipada do estado de sessão do produto. Devolve undefined quando a sessão não existe —
|
|
465
|
+
// distinto de existir com context vazio, que devolve o objeto vazio.
|
|
466
|
+
async readContext(companyId, whatsappNumber) {
|
|
467
|
+
const [row] = await this.db.select({
|
|
468
|
+
context: sessions.context
|
|
469
|
+
}).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);
|
|
470
|
+
return row?.context;
|
|
471
|
+
}
|
|
264
472
|
// Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.
|
|
265
473
|
// Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).
|
|
266
474
|
async setFlowPosition(companyId, whatsappNumber, flowKey, currentNodeId) {
|
|
@@ -303,6 +511,16 @@ var SessionRepository = class {
|
|
|
303
511
|
async release(companyId, whatsappNumber) {
|
|
304
512
|
await this.setMode(companyId, whatsappNumber, "bot", null);
|
|
305
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Apaga a sessão; a cascata das FKs leva mensagens e documentos.
|
|
516
|
+
*
|
|
517
|
+
* Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é
|
|
518
|
+
* chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os
|
|
519
|
+
* objetos antes deixa mídia órfã sendo cobrada para sempre.
|
|
520
|
+
*/
|
|
521
|
+
async deleteByNumber(companyId, whatsappNumber) {
|
|
522
|
+
await this.db.delete(sessions).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
523
|
+
}
|
|
306
524
|
async requestHuman(companyId, whatsappNumber) {
|
|
307
525
|
await this.db.update(sessions).set({
|
|
308
526
|
humanRequestedAt: import_drizzle_orm2.sql`now()`,
|
|
@@ -336,17 +554,28 @@ var SessionRepository = class {
|
|
|
336
554
|
currentState: sessions.currentState,
|
|
337
555
|
lastActivity: sessions.lastActivity,
|
|
338
556
|
lastInboundAt: sessions.lastInboundAt,
|
|
339
|
-
humanRequestedAt: sessions.humanRequestedAt
|
|
557
|
+
humanRequestedAt: sessions.humanRequestedAt,
|
|
558
|
+
// Prévia e contagem saem de subquery correlacionada em vez de N+1 na volta: uma inbox
|
|
559
|
+
// lista dezenas de conversas por página, e uma query por linha é o gargalo clássico
|
|
560
|
+
// dessa tela. Ambos os campos são dados do próprio módulo — deixá-los para o host
|
|
561
|
+
// obrigaria todo consumidor a reescrever o mesmo join contra tabelas que não são dele.
|
|
562
|
+
...conversationSummaryProjection
|
|
340
563
|
}).from(sessions).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(sessions.lastActivity)).limit(limit).offset(offset);
|
|
341
564
|
return rows.map((row) => ({
|
|
342
565
|
id: row.id,
|
|
343
566
|
whatsappNumber: row.whatsappNumber,
|
|
567
|
+
...row.lastContent !== null ? {
|
|
568
|
+
lastContent: row.lastContent
|
|
569
|
+
} : {},
|
|
570
|
+
...row.lastDirection !== null ? {
|
|
571
|
+
lastDirection: row.lastDirection
|
|
572
|
+
} : {},
|
|
344
573
|
lastAt: row.lastActivity.toISOString(),
|
|
345
574
|
lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
|
|
346
575
|
mode: row.mode,
|
|
347
576
|
assignedUserId: row.assignedUserId,
|
|
348
577
|
waitingHuman: row.humanRequestedAt !== null,
|
|
349
|
-
unread:
|
|
578
|
+
unread: Number(row.unread),
|
|
350
579
|
currentState: row.currentState
|
|
351
580
|
}));
|
|
352
581
|
}
|
|
@@ -390,7 +619,9 @@ var MessageRepository = class {
|
|
|
390
619
|
content: params.content ?? null,
|
|
391
620
|
payload: params.payload ?? null,
|
|
392
621
|
waMessageId: params.waMessageId ?? null,
|
|
393
|
-
status: params.status ?? null
|
|
622
|
+
status: params.status ?? null,
|
|
623
|
+
moderationFlagged: params.moderationFlagged ?? null,
|
|
624
|
+
moderationTerms: params.moderationTerms ?? null
|
|
394
625
|
};
|
|
395
626
|
const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning();
|
|
396
627
|
return created;
|
|
@@ -404,6 +635,58 @@ var MessageRepository = class {
|
|
|
404
635
|
}).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, companyId), (0, import_drizzle_orm3.eq)(messages.waMessageId, waMessageId))).returning();
|
|
405
636
|
return updated;
|
|
406
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.
|
|
640
|
+
*
|
|
641
|
+
* Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para
|
|
642
|
+
* responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever
|
|
643
|
+
* o mesmo áudio uma segunda vez só para o painel ver.
|
|
644
|
+
*
|
|
645
|
+
* Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são
|
|
646
|
+
* corridas normais, não erro.
|
|
647
|
+
*/
|
|
648
|
+
async saveTranscriptionByWaMessageId(params) {
|
|
649
|
+
const [updated] = await this.db.update(messages).set({
|
|
650
|
+
transcriptionStatus: params.status,
|
|
651
|
+
...params.text !== void 0 ? {
|
|
652
|
+
transcriptionText: params.text
|
|
653
|
+
} : {},
|
|
654
|
+
...params.language !== void 0 ? {
|
|
655
|
+
transcriptionLanguage: params.language
|
|
656
|
+
} : {},
|
|
657
|
+
...params.engine !== void 0 ? {
|
|
658
|
+
transcriptionEngine: params.engine
|
|
659
|
+
} : {}
|
|
660
|
+
}).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm3.eq)(messages.waMessageId, params.waMessageId))).returning();
|
|
661
|
+
return updated;
|
|
662
|
+
}
|
|
663
|
+
async findById(companyId, messageId) {
|
|
664
|
+
const [found] = await this.db.select().from(messages).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, companyId), (0, import_drizzle_orm3.eq)(messages.id, messageId))).limit(1);
|
|
665
|
+
return found;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada
|
|
669
|
+
* entre o enfileiramento e a execução do job) — não é erro, é corrida normal.
|
|
670
|
+
*
|
|
671
|
+
* `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar
|
|
672
|
+
* atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de
|
|
673
|
+
* outro engine da cadeia.
|
|
674
|
+
*/
|
|
675
|
+
async saveTranscription(params) {
|
|
676
|
+
const [updated] = await this.db.update(messages).set({
|
|
677
|
+
transcriptionStatus: params.status,
|
|
678
|
+
...params.text !== void 0 ? {
|
|
679
|
+
transcriptionText: params.text
|
|
680
|
+
} : {},
|
|
681
|
+
...params.language !== void 0 ? {
|
|
682
|
+
transcriptionLanguage: params.language
|
|
683
|
+
} : {},
|
|
684
|
+
...params.engine !== void 0 ? {
|
|
685
|
+
transcriptionEngine: params.engine
|
|
686
|
+
} : {}
|
|
687
|
+
}).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm3.eq)(messages.id, params.messageId))).returning();
|
|
688
|
+
return updated;
|
|
689
|
+
}
|
|
407
690
|
async listByConversation(params) {
|
|
408
691
|
const conditions = [
|
|
409
692
|
(0, import_drizzle_orm3.eq)(messages.companyId, params.companyId),
|
|
@@ -453,12 +736,21 @@ var FlowGraphRepository = class {
|
|
|
453
736
|
__name(this, "FlowGraphRepository");
|
|
454
737
|
}
|
|
455
738
|
db;
|
|
456
|
-
|
|
739
|
+
cache;
|
|
740
|
+
// Cache opcional: sem ele o repositório se comporta exatamente como antes, lendo sempre do
|
|
741
|
+
// banco. É o host que decide se quer cachear e com qual provedor (ver CacheInterface).
|
|
742
|
+
constructor(db, cache) {
|
|
457
743
|
this.db = db;
|
|
744
|
+
this.cache = cache;
|
|
458
745
|
}
|
|
459
746
|
async get(companyId, key) {
|
|
747
|
+
const cached = await this.cache?.read(companyId, key);
|
|
748
|
+
if (cached) return cached;
|
|
460
749
|
const [row] = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key))).limit(1);
|
|
461
|
-
|
|
750
|
+
if (!row) return void 0;
|
|
751
|
+
const graph = toContractGraph(row);
|
|
752
|
+
await this.cache?.write(companyId, graph);
|
|
753
|
+
return graph;
|
|
462
754
|
}
|
|
463
755
|
async list(companyId) {
|
|
464
756
|
const rows = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId));
|
|
@@ -488,7 +780,9 @@ var FlowGraphRepository = class {
|
|
|
488
780
|
showInMenu: graph.showInMenu ?? false,
|
|
489
781
|
menuOptionLabel: graph.menuOptionLabel
|
|
490
782
|
}).returning();
|
|
491
|
-
|
|
783
|
+
const createdGraph = toContractGraph(created);
|
|
784
|
+
await this.cache?.invalidate(companyId, createdGraph.key);
|
|
785
|
+
return createdGraph;
|
|
492
786
|
}
|
|
493
787
|
// Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —
|
|
494
788
|
// senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).
|
|
@@ -502,10 +796,12 @@ var FlowGraphRepository = class {
|
|
|
502
796
|
updatedAt: /* @__PURE__ */ new Date()
|
|
503
797
|
}).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, graph.key), (0, import_drizzle_orm4.eq)(flowGraphs.version, expectedVersion))).returning();
|
|
504
798
|
if (rows.length === 0) throw new OptimisticLockError(graph.key);
|
|
799
|
+
await this.cache?.invalidate(companyId, graph.key);
|
|
505
800
|
return toContractGraph(rows[0]);
|
|
506
801
|
}
|
|
507
802
|
async delete(companyId, key) {
|
|
508
803
|
await this.db.delete(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key)));
|
|
804
|
+
await this.cache?.invalidate(companyId, key);
|
|
509
805
|
}
|
|
510
806
|
// T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as
|
|
511
807
|
// colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,
|
|
@@ -525,6 +821,48 @@ var FlowGraphRepository = class {
|
|
|
525
821
|
}
|
|
526
822
|
};
|
|
527
823
|
|
|
824
|
+
// src/repositories/FlowGraphCache.ts
|
|
825
|
+
var KEY_PREFIX = "meta-whatsapp:flow-graph";
|
|
826
|
+
var DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300;
|
|
827
|
+
var FlowGraphCache = class {
|
|
828
|
+
static {
|
|
829
|
+
__name(this, "FlowGraphCache");
|
|
830
|
+
}
|
|
831
|
+
provider;
|
|
832
|
+
ttlSeconds;
|
|
833
|
+
constructor(provider, ttlSeconds = DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS) {
|
|
834
|
+
this.provider = provider;
|
|
835
|
+
this.ttlSeconds = ttlSeconds;
|
|
836
|
+
}
|
|
837
|
+
// companyId na chave, e não só a flowKey: a chave do fluxo é escolhida por quem edita e se
|
|
838
|
+
// repete entre empresas — 'consorcio' existe em todas — então uma chave sem tenant serviria o
|
|
839
|
+
// grafo de uma empresa para a conversa de outra.
|
|
840
|
+
keyFor(companyId, flowKey) {
|
|
841
|
+
return `${KEY_PREFIX}:${companyId}:${flowKey}`;
|
|
842
|
+
}
|
|
843
|
+
async read(companyId, flowKey) {
|
|
844
|
+
try {
|
|
845
|
+
const cached = await this.provider.get(this.keyFor(companyId, flowKey));
|
|
846
|
+
if (!cached) return void 0;
|
|
847
|
+
return JSON.parse(cached);
|
|
848
|
+
} catch {
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async write(companyId, graph) {
|
|
853
|
+
try {
|
|
854
|
+
await this.provider.set(this.keyFor(companyId, graph.key), JSON.stringify(graph), this.ttlSeconds);
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async invalidate(companyId, flowKey) {
|
|
859
|
+
try {
|
|
860
|
+
await this.provider.delete(this.keyFor(companyId, flowKey));
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
528
866
|
// src/repositories/SettingsRepository.ts
|
|
529
867
|
var import_drizzle_orm5 = require("drizzle-orm");
|
|
530
868
|
function toContractSettings(row) {
|
|
@@ -533,7 +871,11 @@ function toContractSettings(row) {
|
|
|
533
871
|
templateLanguage: row.templateLanguage,
|
|
534
872
|
templateVariables: row.templateVariables,
|
|
535
873
|
welcomeMessage: row.welcomeMessage ?? "",
|
|
536
|
-
farewellMessage: row.farewellMessage ?? ""
|
|
874
|
+
farewellMessage: row.farewellMessage ?? "",
|
|
875
|
+
// `?? null` e não `?? false`: nulo é "o painel não decidiu", e é o que faz valer o padrão do
|
|
876
|
+
// host. Colapsar para `false` desligaria quem ligou por ambiente.
|
|
877
|
+
transcriptionEnabled: row.transcriptionEnabled ?? null,
|
|
878
|
+
transcriptionMode: row.transcriptionMode ?? null
|
|
537
879
|
};
|
|
538
880
|
}
|
|
539
881
|
__name(toContractSettings, "toContractSettings");
|
|
@@ -542,7 +884,9 @@ var EMPTY_SETTINGS = {
|
|
|
542
884
|
templateLanguage: "pt_BR",
|
|
543
885
|
templateVariables: [],
|
|
544
886
|
welcomeMessage: "",
|
|
545
|
-
farewellMessage: ""
|
|
887
|
+
farewellMessage: "",
|
|
888
|
+
transcriptionEnabled: null,
|
|
889
|
+
transcriptionMode: null
|
|
546
890
|
};
|
|
547
891
|
var SettingsRepository = class {
|
|
548
892
|
static {
|
|
@@ -571,7 +915,12 @@ var SettingsRepository = class {
|
|
|
571
915
|
templateLanguage: merged.templateLanguage,
|
|
572
916
|
templateVariables: merged.templateVariables,
|
|
573
917
|
welcomeMessage: merged.welcomeMessage || null,
|
|
574
|
-
farewellMessage: merged.farewellMessage || null
|
|
918
|
+
farewellMessage: merged.farewellMessage || null,
|
|
919
|
+
// Sem `|| null`: `false` aqui é decisão explícita do painel ("desligado para esta empresa"),
|
|
920
|
+
// e colapsá-lo para nulo faria a empresa voltar a herdar o padrão do host — exatamente o
|
|
921
|
+
// oposto do que o operador acabou de pedir.
|
|
922
|
+
transcriptionEnabled: merged.transcriptionEnabled,
|
|
923
|
+
transcriptionMode: merged.transcriptionMode
|
|
575
924
|
}).onConflictDoUpdate({
|
|
576
925
|
target: settings.companyId,
|
|
577
926
|
set: {
|
|
@@ -580,6 +929,8 @@ var SettingsRepository = class {
|
|
|
580
929
|
templateVariables: merged.templateVariables,
|
|
581
930
|
welcomeMessage: merged.welcomeMessage || null,
|
|
582
931
|
farewellMessage: merged.farewellMessage || null,
|
|
932
|
+
transcriptionEnabled: merged.transcriptionEnabled,
|
|
933
|
+
transcriptionMode: merged.transcriptionMode,
|
|
583
934
|
updatedAt: /* @__PURE__ */ new Date()
|
|
584
935
|
}
|
|
585
936
|
}).returning();
|
|
@@ -606,16 +957,19 @@ var LogMessageUseCase = class {
|
|
|
606
957
|
sessionRepository;
|
|
607
958
|
messageRepository;
|
|
608
959
|
realtime;
|
|
609
|
-
|
|
960
|
+
moderator;
|
|
961
|
+
constructor(sessionRepository, messageRepository, realtime, moderator) {
|
|
610
962
|
this.sessionRepository = sessionRepository;
|
|
611
963
|
this.messageRepository = messageRepository;
|
|
612
964
|
this.realtime = realtime;
|
|
965
|
+
this.moderator = moderator;
|
|
613
966
|
}
|
|
614
967
|
async execute(params) {
|
|
615
968
|
const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState);
|
|
616
969
|
const saved = await this.messageRepository.insertMessage({
|
|
617
970
|
...params,
|
|
618
|
-
sessionId: session.id
|
|
971
|
+
sessionId: session.id,
|
|
972
|
+
...this.moderationOf(params)
|
|
619
973
|
});
|
|
620
974
|
if (!saved) return void 0;
|
|
621
975
|
if (params.direction === "inbound") {
|
|
@@ -628,6 +982,20 @@ var LogMessageUseCase = class {
|
|
|
628
982
|
this.realtime?.emit("global", "data-changed", {});
|
|
629
983
|
return saved;
|
|
630
984
|
}
|
|
985
|
+
// Só o que o cliente escreveu: marcar o que o próprio atendente ou o bot enviou não sinaliza
|
|
986
|
+
// abuso, apenas sujaria o transcript com etiqueta na resposta de quem atende.
|
|
987
|
+
moderationOf(params) {
|
|
988
|
+
if (!this.moderator || params.direction !== "inbound") return {};
|
|
989
|
+
const text2 = params.content?.trim();
|
|
990
|
+
if (!text2) return {};
|
|
991
|
+
const verdict = this.moderator.inspect(text2);
|
|
992
|
+
return {
|
|
993
|
+
moderationFlagged: verdict.isOffensive,
|
|
994
|
+
moderationTerms: verdict.isOffensive ? [
|
|
995
|
+
...verdict.matchedTerms
|
|
996
|
+
] : null
|
|
997
|
+
};
|
|
998
|
+
}
|
|
631
999
|
};
|
|
632
1000
|
|
|
633
1001
|
// src/use-cases/SendMessage.use-case.ts
|
|
@@ -641,11 +1009,13 @@ var SendMessageUseCase = class {
|
|
|
641
1009
|
sessionRepository;
|
|
642
1010
|
logMessage;
|
|
643
1011
|
objectStorage;
|
|
644
|
-
|
|
1012
|
+
documentRepository;
|
|
1013
|
+
constructor(channel, sessionRepository, logMessage, objectStorage, documentRepository) {
|
|
645
1014
|
this.channel = channel;
|
|
646
1015
|
this.sessionRepository = sessionRepository;
|
|
647
1016
|
this.logMessage = logMessage;
|
|
648
1017
|
this.objectStorage = objectStorage;
|
|
1018
|
+
this.documentRepository = documentRepository;
|
|
649
1019
|
}
|
|
650
1020
|
async assertWithinWindow(companyId, whatsappNumber) {
|
|
651
1021
|
const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber);
|
|
@@ -682,7 +1052,7 @@ var SendMessageUseCase = class {
|
|
|
682
1052
|
mimeType: params.mimeType,
|
|
683
1053
|
key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`
|
|
684
1054
|
})).uploadId : void 0;
|
|
685
|
-
|
|
1055
|
+
const saved = await this.logMessage.execute({
|
|
686
1056
|
companyId: params.companyId,
|
|
687
1057
|
whatsappNumber: params.whatsappNumber,
|
|
688
1058
|
direction: "outbound",
|
|
@@ -701,6 +1071,19 @@ var SendMessageUseCase = class {
|
|
|
701
1071
|
status: "sent",
|
|
702
1072
|
startState: params.startState
|
|
703
1073
|
});
|
|
1074
|
+
if (saved && uploadId) {
|
|
1075
|
+
await this.documentRepository?.link({
|
|
1076
|
+
companyId: params.companyId,
|
|
1077
|
+
sessionId: saved.sessionId,
|
|
1078
|
+
messageId: saved.id,
|
|
1079
|
+
uploadId,
|
|
1080
|
+
filename: params.filename,
|
|
1081
|
+
mimeType: params.mimeType,
|
|
1082
|
+
sizeBytes: params.buffer.length,
|
|
1083
|
+
source: params.sender
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
return saved;
|
|
704
1087
|
}
|
|
705
1088
|
// Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece
|
|
706
1089
|
// para reabri-la.
|
|
@@ -818,8 +1201,330 @@ var ListMessagesUseCase = class {
|
|
|
818
1201
|
}
|
|
819
1202
|
};
|
|
820
1203
|
|
|
821
|
-
// src/use-cases/
|
|
1204
|
+
// src/use-cases/ListConversationDocuments.use-case.ts
|
|
822
1205
|
var import_meta_whatsapp_contracts4 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1206
|
+
var ListConversationDocumentsUseCase = class {
|
|
1207
|
+
static {
|
|
1208
|
+
__name(this, "ListConversationDocumentsUseCase");
|
|
1209
|
+
}
|
|
1210
|
+
sessionRepository;
|
|
1211
|
+
documentRepository;
|
|
1212
|
+
constructor(sessionRepository, documentRepository) {
|
|
1213
|
+
this.sessionRepository = sessionRepository;
|
|
1214
|
+
this.documentRepository = documentRepository;
|
|
1215
|
+
}
|
|
1216
|
+
async execute(params) {
|
|
1217
|
+
const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
|
|
1218
|
+
if (!session) throw new import_meta_whatsapp_contracts4.SessionNotFoundError(params.whatsappNumber);
|
|
1219
|
+
const { rows, total } = await this.documentRepository.listByConversation({
|
|
1220
|
+
companyId: params.companyId,
|
|
1221
|
+
sessionId: session.id,
|
|
1222
|
+
...params.search ? {
|
|
1223
|
+
search: params.search
|
|
1224
|
+
} : {},
|
|
1225
|
+
...params.sources && params.sources.length > 0 ? {
|
|
1226
|
+
sources: params.sources
|
|
1227
|
+
} : {},
|
|
1228
|
+
...params.sortDirection ? {
|
|
1229
|
+
sortDirection: params.sortDirection
|
|
1230
|
+
} : {},
|
|
1231
|
+
...params.page ? {
|
|
1232
|
+
page: params.page
|
|
1233
|
+
} : {},
|
|
1234
|
+
...params.limit ? {
|
|
1235
|
+
limit: params.limit
|
|
1236
|
+
} : {}
|
|
1237
|
+
});
|
|
1238
|
+
return {
|
|
1239
|
+
documents: rows.map((row) => ({
|
|
1240
|
+
id: row.uploadId,
|
|
1241
|
+
filename: row.filename,
|
|
1242
|
+
mimeType: row.mimeType,
|
|
1243
|
+
sizeBytes: row.sizeBytes,
|
|
1244
|
+
source: row.source,
|
|
1245
|
+
linkedAt: row.linkedAt.toISOString()
|
|
1246
|
+
})),
|
|
1247
|
+
total
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/use-cases/ListCompanyDocuments.use-case.ts
|
|
1253
|
+
var ListCompanyDocumentsUseCase = class {
|
|
1254
|
+
static {
|
|
1255
|
+
__name(this, "ListCompanyDocumentsUseCase");
|
|
1256
|
+
}
|
|
1257
|
+
documentRepository;
|
|
1258
|
+
constructor(documentRepository) {
|
|
1259
|
+
this.documentRepository = documentRepository;
|
|
1260
|
+
}
|
|
1261
|
+
async execute(params) {
|
|
1262
|
+
const { rows, total } = await this.documentRepository.listByCompany({
|
|
1263
|
+
companyId: params.companyId,
|
|
1264
|
+
...params.search ? {
|
|
1265
|
+
search: params.search
|
|
1266
|
+
} : {},
|
|
1267
|
+
...params.sources && params.sources.length > 0 ? {
|
|
1268
|
+
sources: params.sources
|
|
1269
|
+
} : {},
|
|
1270
|
+
...params.sortDirection ? {
|
|
1271
|
+
sortDirection: params.sortDirection
|
|
1272
|
+
} : {},
|
|
1273
|
+
...params.page ? {
|
|
1274
|
+
page: params.page
|
|
1275
|
+
} : {},
|
|
1276
|
+
...params.limit ? {
|
|
1277
|
+
limit: params.limit
|
|
1278
|
+
} : {}
|
|
1279
|
+
});
|
|
1280
|
+
return {
|
|
1281
|
+
documents: rows.map((row) => ({
|
|
1282
|
+
id: row.uploadId,
|
|
1283
|
+
conversationId: row.whatsappNumber,
|
|
1284
|
+
filename: row.filename,
|
|
1285
|
+
mimeType: row.mimeType,
|
|
1286
|
+
sizeBytes: row.sizeBytes,
|
|
1287
|
+
source: row.source,
|
|
1288
|
+
linkedAt: row.linkedAt.toISOString()
|
|
1289
|
+
})),
|
|
1290
|
+
total
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
// src/use-cases/DeleteConversation.use-case.ts
|
|
1296
|
+
var import_meta_whatsapp_contracts5 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1297
|
+
var DeleteConversationUseCase = class {
|
|
1298
|
+
static {
|
|
1299
|
+
__name(this, "DeleteConversationUseCase");
|
|
1300
|
+
}
|
|
1301
|
+
sessionRepository;
|
|
1302
|
+
documentRepository;
|
|
1303
|
+
objectStorage;
|
|
1304
|
+
constructor(sessionRepository, documentRepository, objectStorage) {
|
|
1305
|
+
this.sessionRepository = sessionRepository;
|
|
1306
|
+
this.documentRepository = documentRepository;
|
|
1307
|
+
this.objectStorage = objectStorage;
|
|
1308
|
+
}
|
|
1309
|
+
async execute(params) {
|
|
1310
|
+
const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
|
|
1311
|
+
if (!session) throw new import_meta_whatsapp_contracts5.SessionNotFoundError(params.whatsappNumber);
|
|
1312
|
+
const uploadIds = await this.documentRepository.listUploadIdsBySession(params.companyId, session.id);
|
|
1313
|
+
if (uploadIds.length > 0 && !this.objectStorage?.delete) {
|
|
1314
|
+
throw new Error("storage_delete_unsupported: a conversa tem arquivos e o storage injetado n\xE3o implementa delete");
|
|
1315
|
+
}
|
|
1316
|
+
const failedObjects = [];
|
|
1317
|
+
let deletedObjects = 0;
|
|
1318
|
+
for (const uploadId of uploadIds) {
|
|
1319
|
+
try {
|
|
1320
|
+
await this.objectStorage?.delete?.(uploadId);
|
|
1321
|
+
deletedObjects++;
|
|
1322
|
+
} catch {
|
|
1323
|
+
failedObjects.push(uploadId);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (failedObjects.length > 0) return {
|
|
1327
|
+
deletedObjects,
|
|
1328
|
+
failedObjects
|
|
1329
|
+
};
|
|
1330
|
+
await this.sessionRepository.deleteByNumber(params.companyId, params.whatsappNumber);
|
|
1331
|
+
return {
|
|
1332
|
+
deletedObjects,
|
|
1333
|
+
failedObjects: []
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// src/use-cases/PurgeExpiredDocuments.use-case.ts
|
|
1339
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
1340
|
+
var HOURS_IN_DAY = 24;
|
|
1341
|
+
var MILLISECONDS_IN_HOUR = 60 * 60 * 1e3;
|
|
1342
|
+
var PurgeExpiredDocumentsUseCase = class {
|
|
1343
|
+
static {
|
|
1344
|
+
__name(this, "PurgeExpiredDocumentsUseCase");
|
|
1345
|
+
}
|
|
1346
|
+
documentRepository;
|
|
1347
|
+
objectStorage;
|
|
1348
|
+
constructor(documentRepository, objectStorage) {
|
|
1349
|
+
this.documentRepository = documentRepository;
|
|
1350
|
+
this.objectStorage = objectStorage;
|
|
1351
|
+
}
|
|
1352
|
+
async execute(params) {
|
|
1353
|
+
if (!this.objectStorage?.delete) {
|
|
1354
|
+
throw new Error("storage_delete_unsupported: reten\xE7\xE3o exige um storage que implemente delete");
|
|
1355
|
+
}
|
|
1356
|
+
const reference = params.now ?? /* @__PURE__ */ new Date();
|
|
1357
|
+
const olderThan = new Date(reference.getTime() - params.retentionDays * HOURS_IN_DAY * MILLISECONDS_IN_HOUR);
|
|
1358
|
+
const expired = await this.documentRepository.listExpired(params.companyId, olderThan, params.batchSize ?? DEFAULT_BATCH_SIZE);
|
|
1359
|
+
const failed = [];
|
|
1360
|
+
let purged = 0;
|
|
1361
|
+
for (const document of expired) {
|
|
1362
|
+
try {
|
|
1363
|
+
await this.objectStorage.delete(document.uploadId);
|
|
1364
|
+
} catch {
|
|
1365
|
+
failed.push(document.uploadId);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
await this.documentRepository.deleteById(params.companyId, document.id);
|
|
1369
|
+
purged++;
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
purged,
|
|
1373
|
+
failed
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// src/repositories/DocumentRepository.ts
|
|
1379
|
+
var import_drizzle_orm6 = require("drizzle-orm");
|
|
1380
|
+
var DEFAULT_LIMIT3 = 50;
|
|
1381
|
+
function companyDocumentSearch(search) {
|
|
1382
|
+
const term = search?.trim();
|
|
1383
|
+
if (!term) return void 0;
|
|
1384
|
+
const digits = term.replace(/\D/g, "");
|
|
1385
|
+
const byFilename = (0, import_drizzle_orm6.ilike)(documents.filename, `%${term}%`);
|
|
1386
|
+
if (!digits) return byFilename;
|
|
1387
|
+
return (0, import_drizzle_orm6.or)(byFilename, (0, import_drizzle_orm6.ilike)(sessions.whatsappNumber, `%${digits}%`));
|
|
1388
|
+
}
|
|
1389
|
+
__name(companyDocumentSearch, "companyDocumentSearch");
|
|
1390
|
+
var DocumentRepository = class {
|
|
1391
|
+
static {
|
|
1392
|
+
__name(this, "DocumentRepository");
|
|
1393
|
+
}
|
|
1394
|
+
db;
|
|
1395
|
+
constructor(db) {
|
|
1396
|
+
this.db = db;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o
|
|
1400
|
+
* job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma
|
|
1401
|
+
* checagem, duplicando a linha no painel.
|
|
1402
|
+
*
|
|
1403
|
+
* Devolve `undefined` quando o documento já estava linkado.
|
|
1404
|
+
*/
|
|
1405
|
+
async link(params) {
|
|
1406
|
+
const values = {
|
|
1407
|
+
companyId: params.companyId,
|
|
1408
|
+
sessionId: params.sessionId,
|
|
1409
|
+
messageId: params.messageId ?? null,
|
|
1410
|
+
uploadId: params.uploadId,
|
|
1411
|
+
filename: params.filename,
|
|
1412
|
+
mimeType: params.mimeType,
|
|
1413
|
+
sizeBytes: params.sizeBytes,
|
|
1414
|
+
sha256: params.sha256 ?? null,
|
|
1415
|
+
source: params.source
|
|
1416
|
+
};
|
|
1417
|
+
const [created] = await this.db.insert(documents).values(values).onConflictDoNothing().returning();
|
|
1418
|
+
return created;
|
|
1419
|
+
}
|
|
1420
|
+
async listByConversation(params) {
|
|
1421
|
+
const filters = [
|
|
1422
|
+
(0, import_drizzle_orm6.eq)(documents.companyId, params.companyId),
|
|
1423
|
+
(0, import_drizzle_orm6.eq)(documents.sessionId, params.sessionId)
|
|
1424
|
+
];
|
|
1425
|
+
if (params.search) filters.push((0, import_drizzle_orm6.ilike)(documents.filename, `%${params.search}%`));
|
|
1426
|
+
if (params.sources && params.sources.length > 0) {
|
|
1427
|
+
filters.push((0, import_drizzle_orm6.inArray)(documents.source, [
|
|
1428
|
+
...params.sources
|
|
1429
|
+
]));
|
|
1430
|
+
}
|
|
1431
|
+
const where = (0, import_drizzle_orm6.and)(...filters);
|
|
1432
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
1433
|
+
const page = params.page && params.page > 0 ? params.page : 1;
|
|
1434
|
+
const orderBy = params.sortDirection === "asc" ? (0, import_drizzle_orm6.asc)(documents.linkedAt) : (0, import_drizzle_orm6.desc)(documents.linkedAt);
|
|
1435
|
+
const [rows, counted] = await Promise.all([
|
|
1436
|
+
this.db.select().from(documents).where(where).orderBy(orderBy).limit(limit).offset((page - 1) * limit),
|
|
1437
|
+
this.db.select({
|
|
1438
|
+
value: (0, import_drizzle_orm6.count)()
|
|
1439
|
+
}).from(documents).where(where)
|
|
1440
|
+
]);
|
|
1441
|
+
return {
|
|
1442
|
+
rows,
|
|
1443
|
+
total: counted[0]?.value ?? 0
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* A biblioteca da EMPRESA inteira, não de uma conversa.
|
|
1448
|
+
*
|
|
1449
|
+
* A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.
|
|
1450
|
+
*
|
|
1451
|
+
* Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,
|
|
1452
|
+
* arquivo sem essa referência é inútil: o atendente vê "comprovante.pdf" e não sabe de quem.
|
|
1453
|
+
*
|
|
1454
|
+
* Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a
|
|
1455
|
+
* varredura de retenção.
|
|
1456
|
+
*/
|
|
1457
|
+
async listByCompany(params) {
|
|
1458
|
+
const filters = [
|
|
1459
|
+
(0, import_drizzle_orm6.eq)(documents.companyId, params.companyId)
|
|
1460
|
+
];
|
|
1461
|
+
const search = companyDocumentSearch(params.search);
|
|
1462
|
+
if (search) filters.push(search);
|
|
1463
|
+
if (params.sources && params.sources.length > 0) {
|
|
1464
|
+
filters.push((0, import_drizzle_orm6.inArray)(documents.source, [
|
|
1465
|
+
...params.sources
|
|
1466
|
+
]));
|
|
1467
|
+
}
|
|
1468
|
+
const where = (0, import_drizzle_orm6.and)(...filters);
|
|
1469
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
1470
|
+
const page = params.page && params.page > 0 ? params.page : 1;
|
|
1471
|
+
const orderBy = params.sortDirection === "asc" ? (0, import_drizzle_orm6.asc)(documents.linkedAt) : (0, import_drizzle_orm6.desc)(documents.linkedAt);
|
|
1472
|
+
const [rows, counted] = await Promise.all([
|
|
1473
|
+
this.db.select({
|
|
1474
|
+
id: documents.id,
|
|
1475
|
+
uploadId: documents.uploadId,
|
|
1476
|
+
filename: documents.filename,
|
|
1477
|
+
mimeType: documents.mimeType,
|
|
1478
|
+
sizeBytes: documents.sizeBytes,
|
|
1479
|
+
source: documents.source,
|
|
1480
|
+
linkedAt: documents.linkedAt,
|
|
1481
|
+
// Só o número: o NOME do cliente é dado do produto (tabela própria dele), não do
|
|
1482
|
+
// módulo. Quem quiser exibir "Marina Alves" enriquece na borda.
|
|
1483
|
+
whatsappNumber: sessions.whatsappNumber
|
|
1484
|
+
}).from(documents).innerJoin(sessions, (0, import_drizzle_orm6.eq)(documents.sessionId, sessions.id)).where(where).orderBy(orderBy).limit(limit).offset((page - 1) * limit),
|
|
1485
|
+
// O mesmo join da listagem, e não só `from(documents)`: a busca pode citar
|
|
1486
|
+
// `sessions.whatsapp_number`, e uma contagem sem a tabela na cláusula não compila — pior,
|
|
1487
|
+
// se compilasse, o total divergiria das linhas e a paginação prometeria páginas vazias.
|
|
1488
|
+
this.db.select({
|
|
1489
|
+
value: (0, import_drizzle_orm6.count)()
|
|
1490
|
+
}).from(documents).innerJoin(sessions, (0, import_drizzle_orm6.eq)(documents.sessionId, sessions.id)).where(where)
|
|
1491
|
+
]);
|
|
1492
|
+
return {
|
|
1493
|
+
rows,
|
|
1494
|
+
total: counted[0]?.value ?? 0
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o
|
|
1499
|
+
* download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.
|
|
1500
|
+
*/
|
|
1501
|
+
async findByUploadId(companyId, uploadId) {
|
|
1502
|
+
const [row] = await this.db.select().from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.uploadId, uploadId))).limit(1);
|
|
1503
|
+
return row;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Os objetos a apagar no storage antes de a linha sumir.
|
|
1507
|
+
*
|
|
1508
|
+
* Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa
|
|
1509
|
+
* precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.
|
|
1510
|
+
*/
|
|
1511
|
+
async listUploadIdsBySession(companyId, sessionId) {
|
|
1512
|
+
const rows = await this.db.select({
|
|
1513
|
+
uploadId: documents.uploadId
|
|
1514
|
+
}).from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.sessionId, sessionId)));
|
|
1515
|
+
return rows.map((row) => row.uploadId);
|
|
1516
|
+
}
|
|
1517
|
+
/** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */
|
|
1518
|
+
async listExpired(companyId, olderThan, limit = DEFAULT_LIMIT3) {
|
|
1519
|
+
return this.db.select().from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.lt)(documents.linkedAt, olderThan))).orderBy(documents.linkedAt).limit(limit);
|
|
1520
|
+
}
|
|
1521
|
+
async deleteById(companyId, id) {
|
|
1522
|
+
await this.db.delete(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.id, id)));
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/use-cases/ExportConversation.use-case.ts
|
|
1527
|
+
var import_meta_whatsapp_contracts6 = require("@adatechnology/meta-whatsapp-contracts");
|
|
823
1528
|
var ExportConversationUseCase = class {
|
|
824
1529
|
static {
|
|
825
1530
|
__name(this, "ExportConversationUseCase");
|
|
@@ -830,7 +1535,7 @@ var ExportConversationUseCase = class {
|
|
|
830
1535
|
}
|
|
831
1536
|
async execute(params) {
|
|
832
1537
|
const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber);
|
|
833
|
-
if (!result) throw new
|
|
1538
|
+
if (!result) throw new import_meta_whatsapp_contracts6.SessionNotFoundError(params.whatsappNumber);
|
|
834
1539
|
return result;
|
|
835
1540
|
}
|
|
836
1541
|
};
|
|
@@ -910,7 +1615,7 @@ var GetLiveFlowPositionsUseCase = class {
|
|
|
910
1615
|
};
|
|
911
1616
|
|
|
912
1617
|
// src/flows/FlowInterpreter.ts
|
|
913
|
-
var
|
|
1618
|
+
var import_meta_whatsapp_contracts7 = require("@adatechnology/meta-whatsapp-contracts");
|
|
914
1619
|
var DEFAULT_MAX_STEPS = 50;
|
|
915
1620
|
function evaluateCondition(node, context) {
|
|
916
1621
|
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false;
|
|
@@ -1011,9 +1716,9 @@ var FlowInterpreter = class {
|
|
|
1011
1716
|
kind: "terminal",
|
|
1012
1717
|
context: input.context
|
|
1013
1718
|
};
|
|
1014
|
-
if ((0,
|
|
1719
|
+
if ((0, import_meta_whatsapp_contracts7.isCrossFlowTarget)(nextNodeId)) return {
|
|
1015
1720
|
kind: "cross-flow",
|
|
1016
|
-
flowKey: (0,
|
|
1721
|
+
flowKey: (0, import_meta_whatsapp_contracts7.crossFlowKey)(nextNodeId),
|
|
1017
1722
|
context: input.context
|
|
1018
1723
|
};
|
|
1019
1724
|
if (!input.graph.nodes[nextNodeId]) return {
|
|
@@ -1067,22 +1772,243 @@ var FlowInterpreter = class {
|
|
|
1067
1772
|
}
|
|
1068
1773
|
};
|
|
1069
1774
|
|
|
1775
|
+
// src/flows/createSendMediaAction.ts
|
|
1776
|
+
function mediaTypeFor2(mimeType) {
|
|
1777
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
1778
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
1779
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
1780
|
+
return "document";
|
|
1781
|
+
}
|
|
1782
|
+
__name(mediaTypeFor2, "mediaTypeFor");
|
|
1783
|
+
function createSendMediaAction(params) {
|
|
1784
|
+
return async ({ node, session, channel }) => {
|
|
1785
|
+
if (!session.flowKey) return;
|
|
1786
|
+
const location = {
|
|
1787
|
+
companyId: session.companyId,
|
|
1788
|
+
flowKey: session.flowKey,
|
|
1789
|
+
nodeId: node.id
|
|
1790
|
+
};
|
|
1791
|
+
const attachments = await params.flowMediaRepository.listActive(location);
|
|
1792
|
+
for (const attachment of attachments) {
|
|
1793
|
+
try {
|
|
1794
|
+
const buffer = await params.objectStorage.getObject(attachment.uploadId);
|
|
1795
|
+
const { externalMessageId } = await channel.sendMedia({
|
|
1796
|
+
to: session.whatsappNumber,
|
|
1797
|
+
buffer,
|
|
1798
|
+
mimeType: attachment.mimeType,
|
|
1799
|
+
filename: attachment.filename,
|
|
1800
|
+
caption: attachment.caption ?? void 0
|
|
1801
|
+
});
|
|
1802
|
+
await params.logMessage.execute({
|
|
1803
|
+
companyId: session.companyId,
|
|
1804
|
+
whatsappNumber: session.whatsappNumber,
|
|
1805
|
+
direction: "outbound",
|
|
1806
|
+
sender: "bot",
|
|
1807
|
+
agentUserId: null,
|
|
1808
|
+
type: mediaTypeFor2(attachment.mimeType),
|
|
1809
|
+
content: attachment.caption ?? attachment.filename,
|
|
1810
|
+
payload: {
|
|
1811
|
+
filename: attachment.filename,
|
|
1812
|
+
mimeType: attachment.mimeType,
|
|
1813
|
+
uploadId: attachment.uploadId,
|
|
1814
|
+
flowMediaId: attachment.id
|
|
1815
|
+
},
|
|
1816
|
+
waMessageId: externalMessageId,
|
|
1817
|
+
status: "sent",
|
|
1818
|
+
startState: params.startState
|
|
1819
|
+
});
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
params.onError?.(error, {
|
|
1822
|
+
flowKey: location.flowKey,
|
|
1823
|
+
nodeId: node.id,
|
|
1824
|
+
uploadId: attachment.uploadId
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
__name(createSendMediaAction, "createSendMediaAction");
|
|
1831
|
+
|
|
1832
|
+
// src/flows/createSendProductListAction.ts
|
|
1833
|
+
var PRODUCT_LIST_LIMIT = {
|
|
1834
|
+
ITEMS: 30,
|
|
1835
|
+
SECTIONS: 10
|
|
1836
|
+
};
|
|
1837
|
+
function createSendProductListAction(params) {
|
|
1838
|
+
return async ({ node, session, channel }) => {
|
|
1839
|
+
if (!channel.sendProductList) return;
|
|
1840
|
+
const actionParams = node.actionParams ?? {};
|
|
1841
|
+
try {
|
|
1842
|
+
const available = await listAvailableProducts({
|
|
1843
|
+
catalog: params.catalog,
|
|
1844
|
+
catalogId: params.catalogId,
|
|
1845
|
+
...actionParams.search ? {
|
|
1846
|
+
search: actionParams.search
|
|
1847
|
+
} : {}
|
|
1848
|
+
});
|
|
1849
|
+
if (available.length === 0) return;
|
|
1850
|
+
const bodyText = actionParams.bodyText ?? "Veja o que temos dispon\xEDvel:";
|
|
1851
|
+
const { externalMessageId } = await channel.sendProductList({
|
|
1852
|
+
to: session.whatsappNumber,
|
|
1853
|
+
headerText: actionParams.headerText ?? "Nossos produtos",
|
|
1854
|
+
body: bodyText,
|
|
1855
|
+
...actionParams.footerText ? {
|
|
1856
|
+
footerText: actionParams.footerText
|
|
1857
|
+
} : {},
|
|
1858
|
+
sections: [
|
|
1859
|
+
{
|
|
1860
|
+
title: actionParams.sectionTitle ?? "Dispon\xEDveis",
|
|
1861
|
+
retailerIds: available.map((product) => product.retailerId)
|
|
1862
|
+
}
|
|
1863
|
+
]
|
|
1864
|
+
});
|
|
1865
|
+
await params.logMessage.execute({
|
|
1866
|
+
companyId: session.companyId,
|
|
1867
|
+
whatsappNumber: session.whatsappNumber,
|
|
1868
|
+
direction: "outbound",
|
|
1869
|
+
sender: "bot",
|
|
1870
|
+
agentUserId: null,
|
|
1871
|
+
type: "interactive",
|
|
1872
|
+
content: bodyText,
|
|
1873
|
+
payload: {
|
|
1874
|
+
kind: "product_list",
|
|
1875
|
+
productCount: available.length
|
|
1876
|
+
},
|
|
1877
|
+
waMessageId: externalMessageId,
|
|
1878
|
+
status: "sent",
|
|
1879
|
+
startState: params.startState
|
|
1880
|
+
});
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
params.onError?.(error, {
|
|
1883
|
+
flowKey: session.flowKey ?? "",
|
|
1884
|
+
nodeId: node.id
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
__name(createSendProductListAction, "createSendProductListAction");
|
|
1890
|
+
async function listAvailableProducts(params) {
|
|
1891
|
+
const products = await params.catalog.listProducts({
|
|
1892
|
+
catalogId: params.catalogId,
|
|
1893
|
+
...params.search ? {
|
|
1894
|
+
search: params.search
|
|
1895
|
+
} : {}
|
|
1896
|
+
});
|
|
1897
|
+
return products.filter((product) => product.availability === "in stock").slice(0, PRODUCT_LIST_LIMIT.ITEMS);
|
|
1898
|
+
}
|
|
1899
|
+
__name(listAvailableProducts, "listAvailableProducts");
|
|
1900
|
+
|
|
1901
|
+
// src/repositories/FlowMediaRepository.ts
|
|
1902
|
+
var import_drizzle_orm7 = require("drizzle-orm");
|
|
1903
|
+
var FlowMediaRepository = class {
|
|
1904
|
+
static {
|
|
1905
|
+
__name(this, "FlowMediaRepository");
|
|
1906
|
+
}
|
|
1907
|
+
db;
|
|
1908
|
+
constructor(db) {
|
|
1909
|
+
this.db = db;
|
|
1910
|
+
}
|
|
1911
|
+
locationFilter(location) {
|
|
1912
|
+
return (0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, location.companyId), (0, import_drizzle_orm7.eq)(flowMedia.flowKey, location.flowKey), (0, import_drizzle_orm7.eq)(flowMedia.nodeId, location.nodeId));
|
|
1913
|
+
}
|
|
1914
|
+
// O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:
|
|
1915
|
+
// é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente
|
|
1916
|
+
// justamente o material que alguém desligou.
|
|
1917
|
+
async listActive(location) {
|
|
1918
|
+
return this.db.select().from(flowMedia).where((0, import_drizzle_orm7.and)(this.locationFilter(location), (0, import_drizzle_orm7.eq)(flowMedia.active, true))).orderBy((0, import_drizzle_orm7.asc)(flowMedia.sortOrder), (0, import_drizzle_orm7.asc)(flowMedia.createdAt));
|
|
1919
|
+
}
|
|
1920
|
+
// Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para
|
|
1921
|
+
// poder ser religado.
|
|
1922
|
+
async listAll(location) {
|
|
1923
|
+
return this.db.select().from(flowMedia).where(this.locationFilter(location)).orderBy((0, import_drizzle_orm7.asc)(flowMedia.sortOrder), (0, import_drizzle_orm7.asc)(flowMedia.createdAt));
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Anexa um arquivo já existente no storage ao nó.
|
|
1927
|
+
*
|
|
1928
|
+
* `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
|
|
1929
|
+
* editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
|
|
1930
|
+
* quem está montando o fluxo.
|
|
1931
|
+
*/
|
|
1932
|
+
async attach(params) {
|
|
1933
|
+
const [row] = await this.db.insert(flowMedia).values({
|
|
1934
|
+
companyId: params.companyId,
|
|
1935
|
+
flowKey: params.flowKey,
|
|
1936
|
+
nodeId: params.nodeId,
|
|
1937
|
+
uploadId: params.uploadId,
|
|
1938
|
+
filename: params.filename,
|
|
1939
|
+
mimeType: params.mimeType,
|
|
1940
|
+
sizeBytes: params.sizeBytes,
|
|
1941
|
+
caption: params.caption ?? null,
|
|
1942
|
+
sortOrder: params.sortOrder ?? 0
|
|
1943
|
+
}).onConflictDoUpdate({
|
|
1944
|
+
target: [
|
|
1945
|
+
flowMedia.companyId,
|
|
1946
|
+
flowMedia.flowKey,
|
|
1947
|
+
flowMedia.nodeId,
|
|
1948
|
+
flowMedia.uploadId
|
|
1949
|
+
],
|
|
1950
|
+
set: {
|
|
1951
|
+
caption: params.caption ?? null,
|
|
1952
|
+
sortOrder: params.sortOrder ?? 0,
|
|
1953
|
+
active: true,
|
|
1954
|
+
updatedAt: import_drizzle_orm7.sql`now()`
|
|
1955
|
+
}
|
|
1956
|
+
}).returning();
|
|
1957
|
+
return row;
|
|
1958
|
+
}
|
|
1959
|
+
async update(params) {
|
|
1960
|
+
const [row] = await this.db.update(flowMedia).set({
|
|
1961
|
+
...params.caption !== void 0 ? {
|
|
1962
|
+
caption: params.caption
|
|
1963
|
+
} : {},
|
|
1964
|
+
...params.sortOrder !== void 0 ? {
|
|
1965
|
+
sortOrder: params.sortOrder
|
|
1966
|
+
} : {},
|
|
1967
|
+
...params.active !== void 0 ? {
|
|
1968
|
+
active: params.active
|
|
1969
|
+
} : {},
|
|
1970
|
+
updatedAt: import_drizzle_orm7.sql`now()`
|
|
1971
|
+
}).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.id, params.id))).returning();
|
|
1972
|
+
return row;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
|
|
1976
|
+
* outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
|
|
1977
|
+
* host, que é dono da biblioteca de arquivos.
|
|
1978
|
+
*/
|
|
1979
|
+
async detach(params) {
|
|
1980
|
+
await this.db.delete(flowMedia).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.id, params.id)));
|
|
1981
|
+
}
|
|
1982
|
+
// Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.
|
|
1983
|
+
async detachRemovedNodes(params) {
|
|
1984
|
+
const condition = params.existingNodeIds.length === 0 ? void 0 : import_drizzle_orm7.sql`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`;
|
|
1985
|
+
await this.db.delete(flowMedia).where((0, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(flowMedia.companyId, params.companyId), (0, import_drizzle_orm7.eq)(flowMedia.flowKey, params.flowKey), condition));
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1070
1989
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1071
1990
|
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1072
|
-
var
|
|
1991
|
+
var import_meta_whatsapp_contracts9 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1992
|
+
|
|
1993
|
+
// src/channel/previewMedia.ts
|
|
1994
|
+
var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1995
|
+
|
|
1996
|
+
// src/channel/WhatsAppChannelAdapter.ts
|
|
1073
1997
|
var WhatsAppChannelAdapter = class {
|
|
1074
1998
|
static {
|
|
1075
1999
|
__name(this, "WhatsAppChannelAdapter");
|
|
1076
2000
|
}
|
|
1077
2001
|
messages;
|
|
1078
|
-
|
|
2002
|
+
previewMedia;
|
|
2003
|
+
constructor(messages2, previewMedia) {
|
|
1079
2004
|
this.messages = messages2;
|
|
2005
|
+
this.previewMedia = previewMedia;
|
|
1080
2006
|
}
|
|
1081
2007
|
async translateErrors(operation) {
|
|
1082
2008
|
try {
|
|
1083
2009
|
return await operation();
|
|
1084
2010
|
} catch (error) {
|
|
1085
|
-
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new
|
|
2011
|
+
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new import_meta_whatsapp_contracts9.WindowExpiredError();
|
|
1086
2012
|
throw error;
|
|
1087
2013
|
}
|
|
1088
2014
|
}
|
|
@@ -1120,55 +2046,413 @@ var WhatsAppChannelAdapter = class {
|
|
|
1120
2046
|
externalMessageId: result.waMessageId
|
|
1121
2047
|
};
|
|
1122
2048
|
}
|
|
2049
|
+
async sendInteractiveButtons(params) {
|
|
2050
|
+
const result = await this.translateErrors(() => this.messages.sendInteractiveButtons({
|
|
2051
|
+
to: params.to,
|
|
2052
|
+
bodyText: params.body,
|
|
2053
|
+
buttons: params.buttons
|
|
2054
|
+
}));
|
|
2055
|
+
return {
|
|
2056
|
+
externalMessageId: result.waMessageId
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
async sendProductList(params) {
|
|
2060
|
+
const result = await this.translateErrors(() => this.messages.sendProductListMessage({
|
|
2061
|
+
to: params.to,
|
|
2062
|
+
headerText: params.headerText,
|
|
2063
|
+
bodyText: params.body,
|
|
2064
|
+
...params.footerText ? {
|
|
2065
|
+
footerText: params.footerText
|
|
2066
|
+
} : {},
|
|
2067
|
+
sections: params.sections
|
|
2068
|
+
}));
|
|
2069
|
+
return {
|
|
2070
|
+
externalMessageId: result.waMessageId
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.
|
|
2075
|
+
*
|
|
2076
|
+
* O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e
|
|
2077
|
+
* tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.
|
|
2078
|
+
*/
|
|
1123
2079
|
async fetchMediaAsBase64(mediaId) {
|
|
2080
|
+
const uploadId = this.previewMedia?.isEnabled ? (0, import_meta_whatsapp_contracts8.resolvePreviewUploadId)(mediaId) : void 0;
|
|
2081
|
+
if (uploadId) {
|
|
2082
|
+
const buffer = await this.previewMedia.objectStorage.getObject(uploadId);
|
|
2083
|
+
return {
|
|
2084
|
+
data: buffer.toString("base64"),
|
|
2085
|
+
mimeType: this.previewMedia.defaultMimeType ?? "audio/ogg"
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
1124
2088
|
return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId));
|
|
1125
2089
|
}
|
|
1126
2090
|
};
|
|
1127
2091
|
|
|
1128
2092
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
1129
|
-
var
|
|
2093
|
+
var import_meta_whatsapp_contracts12 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1130
2094
|
|
|
1131
2095
|
// src/channel/webhookSecurity.ts
|
|
1132
|
-
var
|
|
1133
|
-
var
|
|
1134
|
-
var
|
|
1135
|
-
function safeEqualStrings(left, right) {
|
|
1136
|
-
const leftDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(left).digest();
|
|
1137
|
-
const rightDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(right).digest();
|
|
1138
|
-
return (0, import_node_crypto.timingSafeEqual)(leftDigest, rightDigest);
|
|
1139
|
-
}
|
|
1140
|
-
__name(safeEqualStrings, "safeEqualStrings");
|
|
2096
|
+
var import_meta_graph_core2 = require("@adatechnology/meta-graph-core");
|
|
2097
|
+
var import_meta_whatsapp_contracts10 = require("@adatechnology/meta-whatsapp-contracts");
|
|
2098
|
+
var WEBHOOK_NONCE_NAMESPACE = "meta-whatsapp";
|
|
1141
2099
|
function verifyWebhookChallenge(params) {
|
|
1142
|
-
if (params
|
|
1143
|
-
throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1144
|
-
}
|
|
1145
|
-
if (!safeEqualStrings(params.token, params.expectedToken)) {
|
|
1146
|
-
throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1147
|
-
}
|
|
2100
|
+
if (!(0, import_meta_graph_core2.isValidWebhookChallenge)(params)) throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1148
2101
|
return params.challenge;
|
|
1149
2102
|
}
|
|
1150
2103
|
__name(verifyWebhookChallenge, "verifyWebhookChallenge");
|
|
1151
2104
|
function verifyWebhookSignature(params) {
|
|
1152
|
-
|
|
1153
|
-
if (!signatureHeader?.startsWith("sha256=")) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1154
|
-
const expected = (0, import_node_crypto.createHmac)("sha256", appSecret).update(rawBody).digest("hex");
|
|
1155
|
-
const received = signatureHeader.slice("sha256=".length);
|
|
1156
|
-
if (!safeEqualStrings(received, expected)) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
2105
|
+
if (!(0, import_meta_graph_core2.isValidWebhookSignature)(params)) throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1157
2106
|
}
|
|
1158
2107
|
__name(verifyWebhookSignature, "verifyWebhookSignature");
|
|
2108
|
+
function deliveryKey(signatureHeader) {
|
|
2109
|
+
return (0, import_meta_graph_core2.buildWebhookDeliveryKey)({
|
|
2110
|
+
namespace: WEBHOOK_NONCE_NAMESPACE,
|
|
2111
|
+
signatureHeader
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
__name(deliveryKey, "deliveryKey");
|
|
1159
2115
|
async function claimWebhookDelivery(params) {
|
|
1160
|
-
|
|
1161
|
-
return params.nonceStore.setIfAbsent(key, params.ttlSeconds ?? WEBHOOK_NONCE_TTL_SECONDS);
|
|
2116
|
+
return params.nonceStore.setIfAbsent(deliveryKey(params.signatureHeader), params.ttlSeconds ?? import_meta_graph_core2.WEBHOOK_CLAIM_TTL_SECONDS);
|
|
1162
2117
|
}
|
|
1163
2118
|
__name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
2119
|
+
async function confirmWebhookDelivery(params) {
|
|
2120
|
+
await params.nonceStore.confirm?.(deliveryKey(params.signatureHeader), params.ttlSeconds ?? import_meta_graph_core2.WEBHOOK_NONCE_TTL_SECONDS);
|
|
2121
|
+
}
|
|
2122
|
+
__name(confirmWebhookDelivery, "confirmWebhookDelivery");
|
|
1164
2123
|
|
|
1165
|
-
// src/channel/
|
|
2124
|
+
// src/channel/IngestInboundMedia.use-case.ts
|
|
2125
|
+
var import_drizzle_orm8 = require("drizzle-orm");
|
|
2126
|
+
|
|
2127
|
+
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2128
|
+
var import_meta_whatsapp_contracts11 = require("@adatechnology/meta-whatsapp-contracts");
|
|
2129
|
+
|
|
2130
|
+
// src/transcription.types.ts
|
|
2131
|
+
var TRANSCRIPTION_STATUS = {
|
|
2132
|
+
/** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */
|
|
2133
|
+
PENDING: "pending",
|
|
2134
|
+
/** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */
|
|
2135
|
+
DONE: "done",
|
|
2136
|
+
/** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */
|
|
2137
|
+
FAILED: "failed",
|
|
2138
|
+
/** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */
|
|
2139
|
+
UNSUPPORTED: "unsupported"
|
|
2140
|
+
};
|
|
2141
|
+
var TRANSCRIPTION_MODE = {
|
|
2142
|
+
AUTO: "auto",
|
|
2143
|
+
ON_DEMAND: "onDemand"
|
|
2144
|
+
};
|
|
2145
|
+
function isRetriableTranscriptionError(error) {
|
|
2146
|
+
if (typeof error !== "object" || error === null) return true;
|
|
2147
|
+
const isRetriable = error.isRetriable;
|
|
2148
|
+
return typeof isRetriable === "boolean" ? isRetriable : true;
|
|
2149
|
+
}
|
|
2150
|
+
__name(isRetriableTranscriptionError, "isRetriableTranscriptionError");
|
|
2151
|
+
function isUnsupportedTranscriptionError(error) {
|
|
2152
|
+
return typeof error === "object" && error !== null && error.name === "TranscriptionUnsupportedError";
|
|
2153
|
+
}
|
|
2154
|
+
__name(isUnsupportedTranscriptionError, "isUnsupportedTranscriptionError");
|
|
2155
|
+
function transcriptionRetryAfterSeconds(error) {
|
|
2156
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
2157
|
+
const retryAfter = error.retryAfterSeconds;
|
|
2158
|
+
return typeof retryAfter === "number" ? retryAfter : void 0;
|
|
2159
|
+
}
|
|
2160
|
+
__name(transcriptionRetryAfterSeconds, "transcriptionRetryAfterSeconds");
|
|
2161
|
+
function isAudioMimeType(mimeType) {
|
|
2162
|
+
return typeof mimeType === "string" && mimeType.trim().toLowerCase().startsWith("audio/");
|
|
2163
|
+
}
|
|
2164
|
+
__name(isAudioMimeType, "isAudioMimeType");
|
|
2165
|
+
|
|
2166
|
+
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2167
|
+
var TranscribeAudioUseCase = class {
|
|
2168
|
+
static {
|
|
2169
|
+
__name(this, "TranscribeAudioUseCase");
|
|
2170
|
+
}
|
|
2171
|
+
dependencies;
|
|
2172
|
+
constructor(dependencies) {
|
|
2173
|
+
this.dependencies = dependencies;
|
|
2174
|
+
}
|
|
2175
|
+
async execute(params) {
|
|
2176
|
+
const message = await this.dependencies.messageRepository.findById(params.companyId, params.messageId);
|
|
2177
|
+
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para transcri\xE7\xE3o`);
|
|
2178
|
+
if (message.transcriptionStatus === TRANSCRIPTION_STATUS.DONE && !params.force) {
|
|
2179
|
+
return {
|
|
2180
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2181
|
+
text: message.transcriptionText,
|
|
2182
|
+
language: message.transcriptionLanguage,
|
|
2183
|
+
engine: message.transcriptionEngine,
|
|
2184
|
+
alreadyTranscribed: true
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
if (this.dependencies.resolvePolicy) {
|
|
2188
|
+
const policy = await this.dependencies.resolvePolicy(params.companyId);
|
|
2189
|
+
if (!policy.isEnabled) throw new import_meta_whatsapp_contracts11.TranscriptionDisabledError();
|
|
2190
|
+
}
|
|
2191
|
+
const audio = extractAudioReference(message);
|
|
2192
|
+
const buffer = await this.dependencies.objectStorage.getObject(audio.uploadId);
|
|
2193
|
+
return this.transcribeBuffer({
|
|
2194
|
+
...params,
|
|
2195
|
+
buffer,
|
|
2196
|
+
mimeType: audio.mimeType,
|
|
2197
|
+
uploadId: audio.uploadId,
|
|
2198
|
+
message
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
async transcribeBuffer(context) {
|
|
2202
|
+
try {
|
|
2203
|
+
const result = await this.dependencies.transcriber.transcribe({
|
|
2204
|
+
buffer: context.buffer,
|
|
2205
|
+
mimeType: context.mimeType,
|
|
2206
|
+
...this.dependencies.languageHint ? {
|
|
2207
|
+
languageHint: this.dependencies.languageHint
|
|
2208
|
+
} : {}
|
|
2209
|
+
});
|
|
2210
|
+
await this.dependencies.messageRepository.saveTranscription({
|
|
2211
|
+
companyId: context.companyId,
|
|
2212
|
+
messageId: context.messageId,
|
|
2213
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2214
|
+
text: result.text,
|
|
2215
|
+
language: result.language ?? null,
|
|
2216
|
+
engine: result.engine
|
|
2217
|
+
});
|
|
2218
|
+
return {
|
|
2219
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2220
|
+
text: result.text,
|
|
2221
|
+
language: result.language ?? null,
|
|
2222
|
+
engine: result.engine,
|
|
2223
|
+
alreadyTranscribed: false
|
|
2224
|
+
};
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
await this.persistFailure(context, error);
|
|
2227
|
+
throw error;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:
|
|
2232
|
+
* reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.
|
|
2233
|
+
*/
|
|
2234
|
+
async persistFailure(context, error) {
|
|
2235
|
+
const status = resolveFailureStatus(error);
|
|
2236
|
+
await this.dependencies.messageRepository.saveTranscription({
|
|
2237
|
+
companyId: context.companyId,
|
|
2238
|
+
messageId: context.messageId,
|
|
2239
|
+
status
|
|
2240
|
+
});
|
|
2241
|
+
if (status !== TRANSCRIPTION_STATUS.PENDING) return;
|
|
2242
|
+
const retryAfterSeconds = transcriptionRetryAfterSeconds(error);
|
|
2243
|
+
await this.dependencies.hooks?.onTranscriptionDeferred?.({
|
|
2244
|
+
companyId: context.companyId,
|
|
2245
|
+
messageId: context.messageId,
|
|
2246
|
+
whatsappNumber: context.message.whatsappNumber,
|
|
2247
|
+
uploadId: context.uploadId,
|
|
2248
|
+
...retryAfterSeconds !== void 0 ? {
|
|
2249
|
+
retryAfterSeconds
|
|
2250
|
+
} : {},
|
|
2251
|
+
reason: retryAfterSeconds !== void 0 ? "rate-limited" : "transient-failure",
|
|
2252
|
+
error
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
function resolveFailureStatus(error) {
|
|
2257
|
+
if (isUnsupportedTranscriptionError(error)) return TRANSCRIPTION_STATUS.UNSUPPORTED;
|
|
2258
|
+
return isRetriableTranscriptionError(error) ? TRANSCRIPTION_STATUS.PENDING : TRANSCRIPTION_STATUS.FAILED;
|
|
2259
|
+
}
|
|
2260
|
+
__name(resolveFailureStatus, "resolveFailureStatus");
|
|
2261
|
+
function extractAudioReference(message) {
|
|
2262
|
+
const payload = message.payload ?? {};
|
|
2263
|
+
const audio = payload["audio"];
|
|
2264
|
+
const mimeType = typeof payload["mimeType"] === "string" ? payload["mimeType"] : audio?.mime_type;
|
|
2265
|
+
if (!isAudioMimeType(mimeType) && !audio) {
|
|
2266
|
+
throw new import_meta_whatsapp_contracts11.MessageNotAudioError(message.id, message.type);
|
|
2267
|
+
}
|
|
2268
|
+
const uploadId = payload["uploadId"];
|
|
2269
|
+
if (typeof uploadId !== "string" || uploadId.length === 0) {
|
|
2270
|
+
throw new import_meta_whatsapp_contracts11.AudioNotIngestedError(message.id);
|
|
2271
|
+
}
|
|
2272
|
+
return {
|
|
2273
|
+
uploadId,
|
|
2274
|
+
mimeType: mimeType ?? "audio/ogg"
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
__name(extractAudioReference, "extractAudioReference");
|
|
2278
|
+
|
|
2279
|
+
// src/channel/IngestInboundMedia.use-case.ts
|
|
2280
|
+
var IngestInboundMediaUseCase = class {
|
|
2281
|
+
static {
|
|
2282
|
+
__name(this, "IngestInboundMediaUseCase");
|
|
2283
|
+
}
|
|
2284
|
+
db;
|
|
2285
|
+
channel;
|
|
2286
|
+
objectStorage;
|
|
2287
|
+
documentRepository;
|
|
2288
|
+
transcription;
|
|
2289
|
+
constructor(db, channel, objectStorage, documentRepository, transcription) {
|
|
2290
|
+
this.db = db;
|
|
2291
|
+
this.channel = channel;
|
|
2292
|
+
this.objectStorage = objectStorage;
|
|
2293
|
+
this.documentRepository = documentRepository;
|
|
2294
|
+
this.transcription = transcription;
|
|
2295
|
+
}
|
|
2296
|
+
async execute(params) {
|
|
2297
|
+
const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm8.and)((0, import_drizzle_orm8.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm8.eq)(messages.id, params.messageId))).limit(1);
|
|
2298
|
+
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
2299
|
+
const payload = message.payload ?? {};
|
|
2300
|
+
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
2301
|
+
return {
|
|
2302
|
+
uploadId: String(payload["uploadId"]),
|
|
2303
|
+
alreadyIngested: true
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
|
|
2307
|
+
const buffer = Buffer.from(data, "base64");
|
|
2308
|
+
const { uploadId } = await this.objectStorage.upload({
|
|
2309
|
+
buffer,
|
|
2310
|
+
mimeType: mimeType || params.mimeType,
|
|
2311
|
+
key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`
|
|
2312
|
+
});
|
|
2313
|
+
const updatedPayload = {
|
|
2314
|
+
...payload,
|
|
2315
|
+
uploadId,
|
|
2316
|
+
sourceMediaId: params.sourceMediaId,
|
|
2317
|
+
mimeType: mimeType || params.mimeType,
|
|
2318
|
+
// O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o
|
|
2319
|
+
// exibe ao lado do tipo. Sem gravar aqui, a UI mostraria "PDF" sem o "· 180 KB", e buscá-lo
|
|
2320
|
+
// depois custaria uma consulta à tabela de documentos por mensagem renderizada.
|
|
2321
|
+
sizeBytes: buffer.length,
|
|
2322
|
+
...params.filename ? {
|
|
2323
|
+
filename: params.filename
|
|
2324
|
+
} : {}
|
|
2325
|
+
};
|
|
2326
|
+
await this.db.update(messages).set({
|
|
2327
|
+
payload: updatedPayload
|
|
2328
|
+
}).where((0, import_drizzle_orm8.and)((0, import_drizzle_orm8.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm8.eq)(messages.id, params.messageId)));
|
|
2329
|
+
await this.documentRepository?.link({
|
|
2330
|
+
companyId: params.companyId,
|
|
2331
|
+
sessionId: message.sessionId,
|
|
2332
|
+
messageId: message.id,
|
|
2333
|
+
uploadId,
|
|
2334
|
+
// Áudio e sticker chegam sem nome; sem um rótulo o painel mostraria linha vazia.
|
|
2335
|
+
filename: params.filename ?? `${params.sourceMediaId}`,
|
|
2336
|
+
mimeType: mimeType || params.mimeType,
|
|
2337
|
+
sizeBytes: buffer.length,
|
|
2338
|
+
source: message.sender
|
|
2339
|
+
});
|
|
2340
|
+
const transcription = await this.transcribeIfAuto({
|
|
2341
|
+
companyId: params.companyId,
|
|
2342
|
+
message,
|
|
2343
|
+
uploadId,
|
|
2344
|
+
buffer,
|
|
2345
|
+
mimeType: mimeType || params.mimeType
|
|
2346
|
+
});
|
|
2347
|
+
return {
|
|
2348
|
+
uploadId,
|
|
2349
|
+
alreadyIngested: false,
|
|
2350
|
+
...transcription ? {
|
|
2351
|
+
transcription
|
|
2352
|
+
} : {}
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Transcreve o áudio recém-baixado, quando o modo é `auto`.
|
|
2357
|
+
*
|
|
2358
|
+
* **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da
|
|
2359
|
+
* conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o
|
|
2360
|
+
* retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir
|
|
2361
|
+
* um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`
|
|
2362
|
+
* avisa quem sabe reenfileirar.
|
|
2363
|
+
*/
|
|
2364
|
+
async transcribeIfAuto(context) {
|
|
2365
|
+
const transcription = this.transcription;
|
|
2366
|
+
if (!transcription) return void 0;
|
|
2367
|
+
if (!isAudioMimeType(context.mimeType)) return void 0;
|
|
2368
|
+
const policy = await transcription.resolvePolicy(context.companyId);
|
|
2369
|
+
if (!policy.isEnabled || policy.mode !== TRANSCRIPTION_MODE.AUTO) return void 0;
|
|
2370
|
+
const current = await transcription.messageRepository.findById(context.companyId, context.message.id);
|
|
2371
|
+
if (current?.transcriptionStatus === TRANSCRIPTION_STATUS.DONE) return void 0;
|
|
2372
|
+
try {
|
|
2373
|
+
const result = await transcription.transcriber.transcribe({
|
|
2374
|
+
buffer: context.buffer,
|
|
2375
|
+
mimeType: context.mimeType,
|
|
2376
|
+
...transcription.languageHint ? {
|
|
2377
|
+
languageHint: transcription.languageHint
|
|
2378
|
+
} : {}
|
|
2379
|
+
});
|
|
2380
|
+
await transcription.messageRepository.saveTranscription({
|
|
2381
|
+
companyId: context.companyId,
|
|
2382
|
+
messageId: context.message.id,
|
|
2383
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2384
|
+
text: result.text,
|
|
2385
|
+
language: result.language ?? null,
|
|
2386
|
+
engine: result.engine
|
|
2387
|
+
});
|
|
2388
|
+
return {
|
|
2389
|
+
status: TRANSCRIPTION_STATUS.DONE
|
|
2390
|
+
};
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
return this.recordTranscriptionFailure(context, error);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async recordTranscriptionFailure(context, error) {
|
|
2396
|
+
const status = resolveFailureStatus(error);
|
|
2397
|
+
await this.transcription?.messageRepository.saveTranscription({
|
|
2398
|
+
companyId: context.companyId,
|
|
2399
|
+
messageId: context.message.id,
|
|
2400
|
+
status
|
|
2401
|
+
});
|
|
2402
|
+
if (status === TRANSCRIPTION_STATUS.PENDING) {
|
|
2403
|
+
const retryAfterSeconds = transcriptionRetryAfterSeconds(error);
|
|
2404
|
+
await this.transcription?.hooks?.onTranscriptionDeferred?.({
|
|
2405
|
+
companyId: context.companyId,
|
|
2406
|
+
messageId: context.message.id,
|
|
2407
|
+
whatsappNumber: context.message.whatsappNumber,
|
|
2408
|
+
uploadId: context.uploadId,
|
|
2409
|
+
...retryAfterSeconds !== void 0 ? {
|
|
2410
|
+
retryAfterSeconds
|
|
2411
|
+
} : {},
|
|
2412
|
+
reason: retryAfterSeconds !== void 0 ? "rate-limited" : "transient-failure",
|
|
2413
|
+
error
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
status
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
function extractMediaDescriptor(message) {
|
|
2422
|
+
const payload = message.payload ?? {};
|
|
2423
|
+
for (const key of [
|
|
2424
|
+
"image",
|
|
2425
|
+
"audio",
|
|
2426
|
+
"video",
|
|
2427
|
+
"document",
|
|
2428
|
+
"sticker"
|
|
2429
|
+
]) {
|
|
2430
|
+
const media = payload[key];
|
|
2431
|
+
if (media?.id) {
|
|
2432
|
+
return {
|
|
2433
|
+
sourceMediaId: media.id,
|
|
2434
|
+
mimeType: media.mime_type ?? "application/octet-stream",
|
|
2435
|
+
filename: media.filename
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return void 0;
|
|
2440
|
+
}
|
|
2441
|
+
__name(extractMediaDescriptor, "extractMediaDescriptor");
|
|
2442
|
+
|
|
2443
|
+
// src/channel/inboundDispatch.ts
|
|
2444
|
+
function buildInboundJobId(job) {
|
|
2445
|
+
return job.kind === "message" ? `wa-inbound-message:${job.message.id}` : `wa-inbound-status:${job.status.id}:${job.status.status}`;
|
|
2446
|
+
}
|
|
2447
|
+
__name(buildInboundJobId, "buildInboundJobId");
|
|
1166
2448
|
function toSessionContract(row) {
|
|
1167
2449
|
return {
|
|
1168
2450
|
id: row.id,
|
|
1169
2451
|
companyId: row.companyId,
|
|
1170
2452
|
whatsappNumber: row.whatsappNumber,
|
|
1171
2453
|
currentState: row.currentState,
|
|
2454
|
+
flowKey: row.flowKey,
|
|
2455
|
+
currentNodeId: row.currentNodeId,
|
|
1172
2456
|
context: row.context,
|
|
1173
2457
|
mode: row.mode,
|
|
1174
2458
|
assignedUserId: row.assignedUserId,
|
|
@@ -1181,6 +2465,65 @@ function toSessionContract(row) {
|
|
|
1181
2465
|
};
|
|
1182
2466
|
}
|
|
1183
2467
|
__name(toSessionContract, "toSessionContract");
|
|
2468
|
+
function extractAnswer(message) {
|
|
2469
|
+
const interactive = message.interactive;
|
|
2470
|
+
if (interactive?.button_reply) return interactive.button_reply.id;
|
|
2471
|
+
if (interactive?.list_reply) return interactive.list_reply.id;
|
|
2472
|
+
return message.text?.body;
|
|
2473
|
+
}
|
|
2474
|
+
__name(extractAnswer, "extractAnswer");
|
|
2475
|
+
var InboundEffectsDispatcher = class {
|
|
2476
|
+
static {
|
|
2477
|
+
__name(this, "InboundEffectsDispatcher");
|
|
2478
|
+
}
|
|
2479
|
+
params;
|
|
2480
|
+
constructor(params) {
|
|
2481
|
+
this.params = params;
|
|
2482
|
+
}
|
|
2483
|
+
async run(job) {
|
|
2484
|
+
if (job.kind === "message") return this.runMessageEffects(job);
|
|
2485
|
+
return this.runStatusEffects(job);
|
|
2486
|
+
}
|
|
2487
|
+
async runMessageEffects(job) {
|
|
2488
|
+
const { companyId, message, savedMessageId, media } = job;
|
|
2489
|
+
if (media) {
|
|
2490
|
+
await this.params.hooks?.onMediaReceived?.({
|
|
2491
|
+
companyId,
|
|
2492
|
+
messageId: savedMessageId,
|
|
2493
|
+
whatsappNumber: message.from,
|
|
2494
|
+
sourceMediaId: media.sourceMediaId,
|
|
2495
|
+
mimeType: media.mimeType,
|
|
2496
|
+
...media.filename ? {
|
|
2497
|
+
filename: media.filename
|
|
2498
|
+
} : {}
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from);
|
|
2502
|
+
if (!sessionRow) return;
|
|
2503
|
+
if (sessionRow.mode === "human") return;
|
|
2504
|
+
const outcome = await this.params.hooks?.onMessageReceived?.(message, toSessionContract(sessionRow));
|
|
2505
|
+
if (outcome?.outcome === "handled") return;
|
|
2506
|
+
void extractAnswer(message);
|
|
2507
|
+
}
|
|
2508
|
+
async runStatusEffects(job) {
|
|
2509
|
+
const sessionRow = await this.params.sessionRepository.getContext(job.companyId, job.whatsappNumber);
|
|
2510
|
+
await this.params.hooks?.onStatusUpdate?.(job.status, sessionRow ? toSessionContract(sessionRow) : null);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
var ProcessInboundDispatchUseCase = class {
|
|
2514
|
+
static {
|
|
2515
|
+
__name(this, "ProcessInboundDispatchUseCase");
|
|
2516
|
+
}
|
|
2517
|
+
dispatcher;
|
|
2518
|
+
constructor(dispatcher) {
|
|
2519
|
+
this.dispatcher = dispatcher;
|
|
2520
|
+
}
|
|
2521
|
+
async execute(job) {
|
|
2522
|
+
await this.dispatcher.run(job);
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
// src/channel/ReceiveWebhook.use-case.ts
|
|
1184
2527
|
function extractContent(message) {
|
|
1185
2528
|
if (message.text?.body) return message.text.body;
|
|
1186
2529
|
const interactive = message.interactive;
|
|
@@ -1193,13 +2536,6 @@ function extractContent(message) {
|
|
|
1193
2536
|
return message.image?.caption ?? message.document?.caption ?? null;
|
|
1194
2537
|
}
|
|
1195
2538
|
__name(extractContent, "extractContent");
|
|
1196
|
-
function extractAnswer(message) {
|
|
1197
|
-
const interactive = message.interactive;
|
|
1198
|
-
if (interactive?.button_reply) return interactive.button_reply.id;
|
|
1199
|
-
if (interactive?.list_reply) return interactive.list_reply.id;
|
|
1200
|
-
return message.text?.body;
|
|
1201
|
-
}
|
|
1202
|
-
__name(extractAnswer, "extractAnswer");
|
|
1203
2539
|
function extractPayload(message) {
|
|
1204
2540
|
const payload = {};
|
|
1205
2541
|
if (message.interactive) payload["interactive"] = message.interactive;
|
|
@@ -1220,7 +2556,17 @@ var ReceiveWebhookUseCase = class {
|
|
|
1220
2556
|
params;
|
|
1221
2557
|
constructor(params) {
|
|
1222
2558
|
this.params = params;
|
|
2559
|
+
this.dispatcher = new InboundEffectsDispatcher({
|
|
2560
|
+
sessionRepository: params.sessionRepository,
|
|
2561
|
+
...params.hooks ? {
|
|
2562
|
+
hooks: params.hooks
|
|
2563
|
+
} : {},
|
|
2564
|
+
...params.realtime ? {
|
|
2565
|
+
realtime: params.realtime
|
|
2566
|
+
} : {}
|
|
2567
|
+
});
|
|
1223
2568
|
}
|
|
2569
|
+
dispatcher;
|
|
1224
2570
|
async execute(input) {
|
|
1225
2571
|
verifyWebhookSignature({
|
|
1226
2572
|
rawBody: input.rawBody,
|
|
@@ -1234,14 +2580,21 @@ var ReceiveWebhookUseCase = class {
|
|
|
1234
2580
|
if (!claimed) return {
|
|
1235
2581
|
duplicate: true,
|
|
1236
2582
|
messagesProcessed: 0,
|
|
1237
|
-
statusesProcessed: 0
|
|
2583
|
+
statusesProcessed: 0,
|
|
2584
|
+
ignoredForeignNumber: 0
|
|
1238
2585
|
};
|
|
1239
2586
|
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
1240
|
-
const payload =
|
|
2587
|
+
const payload = import_meta_whatsapp_contracts12.whatsAppWebhookPayloadSchema.parse(JSON.parse(rawText));
|
|
1241
2588
|
let messagesProcessed = 0;
|
|
1242
2589
|
let statusesProcessed = 0;
|
|
2590
|
+
let ignoredForeignNumber = 0;
|
|
1243
2591
|
for (const entry of payload.entry) {
|
|
1244
2592
|
for (const change of entry.changes) {
|
|
2593
|
+
const targetNumber = change.value.metadata?.phone_number_id;
|
|
2594
|
+
if (targetNumber && targetNumber !== this.params.phoneNumberId) {
|
|
2595
|
+
ignoredForeignNumber++;
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
1245
2598
|
for (const message of change.value.messages ?? []) {
|
|
1246
2599
|
await this.handleMessage(input.companyId, message);
|
|
1247
2600
|
messagesProcessed++;
|
|
@@ -1252,10 +2605,15 @@ var ReceiveWebhookUseCase = class {
|
|
|
1252
2605
|
}
|
|
1253
2606
|
}
|
|
1254
2607
|
}
|
|
2608
|
+
await confirmWebhookDelivery({
|
|
2609
|
+
nonceStore: this.params.nonceStore,
|
|
2610
|
+
signatureHeader: input.signatureHeader
|
|
2611
|
+
});
|
|
1255
2612
|
return {
|
|
1256
2613
|
duplicate: false,
|
|
1257
2614
|
messagesProcessed,
|
|
1258
|
-
statusesProcessed
|
|
2615
|
+
statusesProcessed,
|
|
2616
|
+
ignoredForeignNumber
|
|
1259
2617
|
};
|
|
1260
2618
|
}
|
|
1261
2619
|
async handleMessage(companyId, message) {
|
|
@@ -1272,12 +2630,17 @@ var ReceiveWebhookUseCase = class {
|
|
|
1272
2630
|
startState: this.params.startState
|
|
1273
2631
|
});
|
|
1274
2632
|
if (!saved) return;
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
2633
|
+
const media = extractMediaDescriptor(saved);
|
|
2634
|
+
await this.dispatch({
|
|
2635
|
+
kind: "message",
|
|
2636
|
+
companyId,
|
|
2637
|
+
message,
|
|
2638
|
+
savedMessageId: saved.id,
|
|
2639
|
+
...media ? {
|
|
2640
|
+
media
|
|
2641
|
+
} : {},
|
|
2642
|
+
receivedAt: Date.now()
|
|
2643
|
+
});
|
|
1281
2644
|
}
|
|
1282
2645
|
async handleStatus(companyId, status) {
|
|
1283
2646
|
const updated = await this.params.messageRepository.updateMessageStatus(companyId, status.id, status.status);
|
|
@@ -1286,81 +2649,71 @@ var ReceiveWebhookUseCase = class {
|
|
|
1286
2649
|
waMessageId: status.id,
|
|
1287
2650
|
status: status.status
|
|
1288
2651
|
});
|
|
1289
|
-
|
|
1290
|
-
|
|
2652
|
+
await this.dispatch({
|
|
2653
|
+
kind: "status",
|
|
2654
|
+
companyId,
|
|
2655
|
+
status,
|
|
2656
|
+
whatsappNumber: updated.whatsappNumber,
|
|
2657
|
+
receivedAt: Date.now()
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
// Com fila configurada, os efeitos saem da requisição do webhook; sem ela, rodam aqui mesmo e o
|
|
2661
|
+
// comportamento é o de sempre. É o mesmo `InboundEffectsDispatcher` nos dois caminhos.
|
|
2662
|
+
async dispatch(job) {
|
|
2663
|
+
if (this.params.inboundQueue) {
|
|
2664
|
+
await this.params.inboundQueue.enqueue(job, {
|
|
2665
|
+
jobId: buildInboundJobId(job)
|
|
2666
|
+
});
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
await this.dispatcher.run(job);
|
|
1291
2670
|
}
|
|
1292
2671
|
};
|
|
1293
2672
|
|
|
1294
|
-
// src/
|
|
1295
|
-
|
|
1296
|
-
|
|
2673
|
+
// src/use-cases/resolveTranscriptionPolicy.ts
|
|
2674
|
+
function createTranscriptionPolicyResolver(dependencies) {
|
|
2675
|
+
return /* @__PURE__ */ __name(async function resolveTranscriptionPolicy(companyId) {
|
|
2676
|
+
const settings2 = await dependencies.settingsRepository.get(companyId);
|
|
2677
|
+
return {
|
|
2678
|
+
// `??` e não `||`: `false` gravado é decisão explícita de desligar, e `||` a trocaria pelo
|
|
2679
|
+
// padrão do host — desligar no painel não faria nada num deploy com transcrição ligada.
|
|
2680
|
+
isEnabled: settings2.transcriptionEnabled ?? dependencies.defaults.isEnabled,
|
|
2681
|
+
mode: normalizeMode(settings2.transcriptionMode) ?? dependencies.defaults.mode
|
|
2682
|
+
};
|
|
2683
|
+
}, "resolveTranscriptionPolicy");
|
|
2684
|
+
}
|
|
2685
|
+
__name(createTranscriptionPolicyResolver, "createTranscriptionPolicyResolver");
|
|
2686
|
+
function normalizeMode(value) {
|
|
2687
|
+
if (value === TRANSCRIPTION_MODE.AUTO) return TRANSCRIPTION_MODE.AUTO;
|
|
2688
|
+
if (value === TRANSCRIPTION_MODE.ON_DEMAND) return TRANSCRIPTION_MODE.ON_DEMAND;
|
|
2689
|
+
return void 0;
|
|
2690
|
+
}
|
|
2691
|
+
__name(normalizeMode, "normalizeMode");
|
|
2692
|
+
|
|
2693
|
+
// src/use-cases/StorePreviewMedia.use-case.ts
|
|
2694
|
+
var StorePreviewMediaUseCase = class {
|
|
1297
2695
|
static {
|
|
1298
|
-
__name(this, "
|
|
2696
|
+
__name(this, "StorePreviewMediaUseCase");
|
|
1299
2697
|
}
|
|
1300
|
-
db;
|
|
1301
|
-
channel;
|
|
1302
2698
|
objectStorage;
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
this.channel = channel;
|
|
2699
|
+
generateKeySuffix;
|
|
2700
|
+
constructor(objectStorage, generateKeySuffix) {
|
|
1306
2701
|
this.objectStorage = objectStorage;
|
|
2702
|
+
this.generateKeySuffix = generateKeySuffix;
|
|
1307
2703
|
}
|
|
1308
2704
|
async execute(params) {
|
|
1309
|
-
const
|
|
1310
|
-
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1311
|
-
const payload = message.payload ?? {};
|
|
1312
|
-
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
1313
|
-
return {
|
|
1314
|
-
uploadId: String(payload["uploadId"]),
|
|
1315
|
-
alreadyIngested: true
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
|
|
1319
|
-
const buffer = Buffer.from(data, "base64");
|
|
2705
|
+
const key = `meta-whatsapp/${params.companyId}/preview/${this.generateKeySuffix()}`;
|
|
1320
2706
|
const { uploadId } = await this.objectStorage.upload({
|
|
1321
|
-
buffer,
|
|
1322
|
-
mimeType:
|
|
1323
|
-
key
|
|
2707
|
+
buffer: params.buffer,
|
|
2708
|
+
mimeType: params.mimeType,
|
|
2709
|
+
key
|
|
1324
2710
|
});
|
|
1325
|
-
const updatedPayload = {
|
|
1326
|
-
...payload,
|
|
1327
|
-
uploadId,
|
|
1328
|
-
sourceMediaId: params.sourceMediaId,
|
|
1329
|
-
mimeType: mimeType || params.mimeType,
|
|
1330
|
-
...params.filename ? {
|
|
1331
|
-
filename: params.filename
|
|
1332
|
-
} : {}
|
|
1333
|
-
};
|
|
1334
|
-
await this.db.update(messages).set({
|
|
1335
|
-
payload: updatedPayload
|
|
1336
|
-
}).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId)));
|
|
1337
2711
|
return {
|
|
1338
|
-
uploadId,
|
|
1339
|
-
|
|
2712
|
+
mediaId: (0, import_meta_whatsapp_contracts8.toPreviewMediaId)(uploadId),
|
|
2713
|
+
uploadId
|
|
1340
2714
|
};
|
|
1341
2715
|
}
|
|
1342
2716
|
};
|
|
1343
|
-
function extractMediaDescriptor(message) {
|
|
1344
|
-
const payload = message.payload ?? {};
|
|
1345
|
-
for (const key of [
|
|
1346
|
-
"image",
|
|
1347
|
-
"audio",
|
|
1348
|
-
"video",
|
|
1349
|
-
"document",
|
|
1350
|
-
"sticker"
|
|
1351
|
-
]) {
|
|
1352
|
-
const media = payload[key];
|
|
1353
|
-
if (media?.id) {
|
|
1354
|
-
return {
|
|
1355
|
-
sourceMediaId: media.id,
|
|
1356
|
-
mimeType: media.mime_type ?? "application/octet-stream",
|
|
1357
|
-
filename: media.filename
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return void 0;
|
|
1362
|
-
}
|
|
1363
|
-
__name(extractMediaDescriptor, "extractMediaDescriptor");
|
|
1364
2717
|
|
|
1365
2718
|
// src/createMetaWhatsAppModule.ts
|
|
1366
2719
|
function createMetaWhatsAppModule(params) {
|
|
@@ -1374,15 +2727,24 @@ function createMetaWhatsAppModule(params) {
|
|
|
1374
2727
|
apiVersion: config.apiVersion,
|
|
1375
2728
|
baseUrl: config.baseUrl
|
|
1376
2729
|
});
|
|
1377
|
-
const
|
|
2730
|
+
const previewMediaSupport = params.features?.previewMedia && providers.objectStorage?.getObject ? {
|
|
2731
|
+
isEnabled: true,
|
|
2732
|
+
objectStorage: providers.objectStorage
|
|
2733
|
+
} : void 0;
|
|
2734
|
+
const channel = new WhatsAppChannelAdapter(messageProvider, previewMediaSupport);
|
|
1378
2735
|
const sessionRepository = new SessionRepository(db);
|
|
1379
2736
|
const messageRepository = new MessageRepository(db);
|
|
1380
2737
|
const settingsRepository = new SettingsRepository(db);
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
2738
|
+
const flowGraphCacheFeature = params.features?.flowGraphCache ?? false;
|
|
2739
|
+
const flowGraphCache = flowGraphCacheFeature && providers.cache ? new FlowGraphCache(providers.cache, typeof flowGraphCacheFeature === "object" ? flowGraphCacheFeature.ttlSeconds ?? DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS : DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS) : void 0;
|
|
2740
|
+
const flowGraphRepository = new FlowGraphRepository(db, flowGraphCache);
|
|
2741
|
+
const documentRepository = new DocumentRepository(db);
|
|
2742
|
+
const flowMediaRepository = new FlowMediaRepository(db);
|
|
2743
|
+
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
2744
|
+
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
|
|
1384
2745
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
1385
2746
|
appSecret: config.appSecret,
|
|
2747
|
+
phoneNumberId: config.phoneNumberId,
|
|
1386
2748
|
nonceStore,
|
|
1387
2749
|
sessionRepository,
|
|
1388
2750
|
messageRepository,
|
|
@@ -1392,7 +2754,58 @@ function createMetaWhatsAppModule(params) {
|
|
|
1392
2754
|
realtime: providers.realtime
|
|
1393
2755
|
});
|
|
1394
2756
|
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
1395
|
-
|
|
2757
|
+
if (flowInterpreter && providers.objectStorage?.getObject) {
|
|
2758
|
+
flowInterpreter.registerFlowAction(import_meta_whatsapp_contracts13.FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
|
|
2759
|
+
flowMediaRepository,
|
|
2760
|
+
objectStorage: providers.objectStorage,
|
|
2761
|
+
logMessage,
|
|
2762
|
+
startState,
|
|
2763
|
+
onError: hooks?.onFlowMediaError
|
|
2764
|
+
}));
|
|
2765
|
+
}
|
|
2766
|
+
if (flowInterpreter && providers.catalog && config.catalogId) {
|
|
2767
|
+
flowInterpreter.registerFlowAction(import_meta_whatsapp_contracts13.FLOW_ACTION_KIND.SEND_PRODUCT_LIST, createSendProductListAction({
|
|
2768
|
+
catalog: providers.catalog,
|
|
2769
|
+
catalogId: config.catalogId,
|
|
2770
|
+
logMessage,
|
|
2771
|
+
startState,
|
|
2772
|
+
onError: hooks?.onFlowProductListError
|
|
2773
|
+
}));
|
|
2774
|
+
}
|
|
2775
|
+
const transcriptionMode = providers.transcription?.mode ?? TRANSCRIPTION_MODE.ON_DEMAND;
|
|
2776
|
+
const resolveTranscriptionPolicy = providers.transcription ? createTranscriptionPolicyResolver({
|
|
2777
|
+
settingsRepository,
|
|
2778
|
+
defaults: {
|
|
2779
|
+
isEnabled: providers.transcription.isEnabledByDefault ?? true,
|
|
2780
|
+
mode: transcriptionMode
|
|
2781
|
+
}
|
|
2782
|
+
}) : void 0;
|
|
2783
|
+
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository, providers.transcription && resolveTranscriptionPolicy ? {
|
|
2784
|
+
transcriber: providers.transcription.transcriber,
|
|
2785
|
+
resolvePolicy: resolveTranscriptionPolicy,
|
|
2786
|
+
messageRepository,
|
|
2787
|
+
...providers.transcription.languageHint ? {
|
|
2788
|
+
languageHint: providers.transcription.languageHint
|
|
2789
|
+
} : {},
|
|
2790
|
+
...hooks ? {
|
|
2791
|
+
hooks
|
|
2792
|
+
} : {}
|
|
2793
|
+
} : void 0) : void 0;
|
|
2794
|
+
const transcribeAudio = providers.transcription && providers.objectStorage?.getObject ? new TranscribeAudioUseCase({
|
|
2795
|
+
messageRepository,
|
|
2796
|
+
objectStorage: providers.objectStorage,
|
|
2797
|
+
transcriber: providers.transcription.transcriber,
|
|
2798
|
+
...resolveTranscriptionPolicy ? {
|
|
2799
|
+
resolvePolicy: resolveTranscriptionPolicy
|
|
2800
|
+
} : {},
|
|
2801
|
+
...providers.transcription.languageHint ? {
|
|
2802
|
+
languageHint: providers.transcription.languageHint
|
|
2803
|
+
} : {},
|
|
2804
|
+
...hooks ? {
|
|
2805
|
+
hooks
|
|
2806
|
+
} : {}
|
|
2807
|
+
}) : void 0;
|
|
2808
|
+
const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
|
|
1396
2809
|
return {
|
|
1397
2810
|
channel,
|
|
1398
2811
|
// undefined quando providers.objectStorage não foi injetado.
|
|
@@ -1404,10 +2817,36 @@ function createMetaWhatsAppModule(params) {
|
|
|
1404
2817
|
release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),
|
|
1405
2818
|
list: new ListConversationsUseCase(sessionRepository),
|
|
1406
2819
|
listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),
|
|
2820
|
+
listDocuments,
|
|
2821
|
+
// Biblioteca da empresa inteira, para uma tela de Documentos fora da conversa.
|
|
2822
|
+
listCompanyDocuments: new ListCompanyDocumentsUseCase(documentRepository),
|
|
2823
|
+
// Apaga a mídia no storage antes das linhas — a cascata da FK sozinha deixaria os binários
|
|
2824
|
+
// órfãos, já que a lista de uploadId vive justamente nas linhas que ela derruba.
|
|
2825
|
+
delete: new DeleteConversationUseCase(sessionRepository, documentRepository, providers.objectStorage),
|
|
2826
|
+
purgeExpiredDocuments: new PurgeExpiredDocumentsUseCase(documentRepository, providers.objectStorage),
|
|
1407
2827
|
export: new ExportConversationUseCase(sessionRepository),
|
|
1408
|
-
|
|
2828
|
+
// undefined quando transcrição não foi injetada, ou quando o storage não sabe ler de volta.
|
|
2829
|
+
// O painel consulta a ausência para decidir se desenha o botão "transcrever".
|
|
2830
|
+
transcribeAudio,
|
|
2831
|
+
repository: sessionRepository,
|
|
2832
|
+
messageRepository,
|
|
2833
|
+
documentRepository
|
|
1409
2834
|
},
|
|
2835
|
+
/**
|
|
2836
|
+
* `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a
|
|
2837
|
+
* capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —
|
|
2838
|
+
* é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.
|
|
2839
|
+
*/
|
|
2840
|
+
transcription: providers.transcription && resolveTranscriptionPolicy ? {
|
|
2841
|
+
defaultMode: transcriptionMode,
|
|
2842
|
+
resolvePolicy: resolveTranscriptionPolicy
|
|
2843
|
+
} : void 0,
|
|
1410
2844
|
settings: settingsRepository,
|
|
2845
|
+
/**
|
|
2846
|
+
* `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a
|
|
2847
|
+
* ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.
|
|
2848
|
+
*/
|
|
2849
|
+
previewMedia: previewMediaSupport ? new StorePreviewMediaUseCase(providers.objectStorage, () => `${Date.now()}-${Math.random().toString(36).slice(2)}`) : void 0,
|
|
1411
2850
|
webhook: {
|
|
1412
2851
|
receive: receiveWebhook,
|
|
1413
2852
|
// GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.
|
|
@@ -1426,7 +2865,11 @@ function createMetaWhatsAppModule(params) {
|
|
|
1426
2865
|
save: new SaveFlowGraphUseCase(flowGraphRepository),
|
|
1427
2866
|
delete: new DeleteFlowGraphUseCase(flowGraphRepository),
|
|
1428
2867
|
livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
|
|
1429
|
-
repository: flowGraphRepository
|
|
2868
|
+
repository: flowGraphRepository,
|
|
2869
|
+
// Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor
|
|
2870
|
+
// (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é
|
|
2871
|
+
// consultar a tabela, e só o ENVIO precisa dos bytes.
|
|
2872
|
+
mediaRepository: flowMediaRepository
|
|
1430
2873
|
} : void 0,
|
|
1431
2874
|
catalog: providers.catalog
|
|
1432
2875
|
};
|
|
@@ -1449,7 +2892,7 @@ async function runMetaWhatsAppMigrations(params) {
|
|
|
1449
2892
|
__name(runMetaWhatsAppMigrations, "runMetaWhatsAppMigrations");
|
|
1450
2893
|
|
|
1451
2894
|
// src/realtime/SseHub.ts
|
|
1452
|
-
var
|
|
2895
|
+
var import_node_crypto = require("crypto");
|
|
1453
2896
|
var TICKET_TTL_SECONDS = 60;
|
|
1454
2897
|
var SseHub = class {
|
|
1455
2898
|
static {
|
|
@@ -1462,7 +2905,7 @@ var SseHub = class {
|
|
|
1462
2905
|
relaySubscriptions = /* @__PURE__ */ new Map();
|
|
1463
2906
|
// Identifica esta instância do processo para descartar o próprio eco (pub/sub entrega a
|
|
1464
2907
|
// mensagem a todos os inscritos, inclusive quem publicou).
|
|
1465
|
-
instanceId = (0,
|
|
2908
|
+
instanceId = (0, import_node_crypto.randomUUID)();
|
|
1466
2909
|
constructor(relay) {
|
|
1467
2910
|
this.relay = relay;
|
|
1468
2911
|
}
|
|
@@ -1508,7 +2951,7 @@ var SseHub = class {
|
|
|
1508
2951
|
}
|
|
1509
2952
|
};
|
|
1510
2953
|
async function issueSseTicket(store, companyId, whatsappNumber) {
|
|
1511
|
-
const ticket = (0,
|
|
2954
|
+
const ticket = (0, import_node_crypto.randomUUID)();
|
|
1512
2955
|
await store.set(`sse:ticket:${ticket}`, JSON.stringify({
|
|
1513
2956
|
companyId,
|
|
1514
2957
|
whatsappNumber
|
|
@@ -1526,14 +2969,22 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1526
2969
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1527
2970
|
0 && (module.exports = {
|
|
1528
2971
|
CreateFlowGraphUseCase,
|
|
2972
|
+
DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS,
|
|
2973
|
+
DeleteConversationUseCase,
|
|
1529
2974
|
DeleteFlowGraphUseCase,
|
|
2975
|
+
DocumentRepository,
|
|
1530
2976
|
ExportConversationUseCase,
|
|
2977
|
+
FlowGraphCache,
|
|
1531
2978
|
FlowGraphRepository,
|
|
1532
2979
|
FlowInterpreter,
|
|
2980
|
+
FlowMediaRepository,
|
|
1533
2981
|
GetFlowGraphUseCase,
|
|
1534
2982
|
GetLiveFlowPositionsUseCase,
|
|
2983
|
+
InboundEffectsDispatcher,
|
|
1535
2984
|
IngestInboundMediaUseCase,
|
|
1536
2985
|
InvalidFlowGraphError,
|
|
2986
|
+
ListCompanyDocumentsUseCase,
|
|
2987
|
+
ListConversationDocumentsUseCase,
|
|
1537
2988
|
ListConversationsUseCase,
|
|
1538
2989
|
ListFlowGraphsUseCase,
|
|
1539
2990
|
ListMessagesUseCase,
|
|
@@ -1541,6 +2992,10 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1541
2992
|
META_WHATSAPP_MIGRATIONS_TABLE,
|
|
1542
2993
|
MessageRepository,
|
|
1543
2994
|
OptimisticLockError,
|
|
2995
|
+
PREVIEW_MEDIA_ID_PREFIX,
|
|
2996
|
+
PRODUCT_LIST_LIMIT,
|
|
2997
|
+
ProcessInboundDispatchUseCase,
|
|
2998
|
+
PurgeExpiredDocumentsUseCase,
|
|
1544
2999
|
ReceiveWebhookUseCase,
|
|
1545
3000
|
ReleaseConversationUseCase,
|
|
1546
3001
|
SaveFlowGraphUseCase,
|
|
@@ -1548,21 +3003,41 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1548
3003
|
SessionRepository,
|
|
1549
3004
|
SettingsRepository,
|
|
1550
3005
|
SseHub,
|
|
3006
|
+
StorePreviewMediaUseCase,
|
|
3007
|
+
TRANSCRIPTION_MODE,
|
|
3008
|
+
TRANSCRIPTION_STATUS,
|
|
1551
3009
|
TakeoverConversationUseCase,
|
|
3010
|
+
TranscribeAudioUseCase,
|
|
3011
|
+
WEBHOOK_CLAIM_TTL_SECONDS,
|
|
1552
3012
|
WEBHOOK_NONCE_TTL_SECONDS,
|
|
1553
3013
|
WhatsAppChannelAdapter,
|
|
3014
|
+
buildInboundJobId,
|
|
1554
3015
|
claimWebhookDelivery,
|
|
3016
|
+
confirmWebhookDelivery,
|
|
1555
3017
|
createMetaWhatsAppModule,
|
|
3018
|
+
createSendMediaAction,
|
|
3019
|
+
createSendProductListAction,
|
|
3020
|
+
createTranscriptionPolicyResolver,
|
|
3021
|
+
documents,
|
|
1556
3022
|
extractMediaDescriptor,
|
|
1557
3023
|
flowGraphs,
|
|
3024
|
+
flowMedia,
|
|
3025
|
+
isAudioMimeType,
|
|
3026
|
+
isRetriableTranscriptionError,
|
|
3027
|
+
isUnsupportedTranscriptionError,
|
|
1558
3028
|
issueSseTicket,
|
|
1559
3029
|
messages,
|
|
1560
3030
|
metaWhatsAppMigrationsFolder,
|
|
1561
3031
|
metaWhatsAppSchema,
|
|
1562
3032
|
redeemSseTicket,
|
|
3033
|
+
resolveFailureStatus,
|
|
3034
|
+
resolvePreviewUploadId,
|
|
1563
3035
|
runMetaWhatsAppMigrations,
|
|
1564
3036
|
sessions,
|
|
1565
3037
|
settings,
|
|
3038
|
+
toPreviewMediaId,
|
|
3039
|
+
toSessionContract,
|
|
3040
|
+
transcriptionRetryAfterSeconds,
|
|
1566
3041
|
verifyWebhookChallenge,
|
|
1567
3042
|
verifyWebhookSignature
|
|
1568
3043
|
});
|