@adatechnology/meta-whatsapp-module 0.2.0-rc.16 → 0.2.0-rc.18
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 +495 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -34
- package/dist/index.d.ts +514 -34
- package/dist/index.js +472 -9
- package/dist/index.js.map +1 -1
- 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 +14 -0
- package/dist/testing/index.cjs +150 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +42 -2
- package/dist/testing/index.d.ts +42 -2
- package/dist/testing/index.js +147 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,7 @@ __export(index_exports, {
|
|
|
44
44
|
META_WHATSAPP_MIGRATIONS_TABLE: () => META_WHATSAPP_MIGRATIONS_TABLE,
|
|
45
45
|
MessageRepository: () => MessageRepository,
|
|
46
46
|
OptimisticLockError: () => OptimisticLockError,
|
|
47
|
+
PREVIEW_MEDIA_ID_PREFIX: () => import_meta_whatsapp_contracts8.PREVIEW_MEDIA_ID_PREFIX,
|
|
47
48
|
PurgeExpiredDocumentsUseCase: () => PurgeExpiredDocumentsUseCase,
|
|
48
49
|
ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
|
|
49
50
|
ReleaseConversationUseCase: () => ReleaseConversationUseCase,
|
|
@@ -52,24 +53,36 @@ __export(index_exports, {
|
|
|
52
53
|
SessionRepository: () => SessionRepository,
|
|
53
54
|
SettingsRepository: () => SettingsRepository,
|
|
54
55
|
SseHub: () => SseHub,
|
|
56
|
+
StorePreviewMediaUseCase: () => StorePreviewMediaUseCase,
|
|
57
|
+
TRANSCRIPTION_MODE: () => TRANSCRIPTION_MODE,
|
|
58
|
+
TRANSCRIPTION_STATUS: () => TRANSCRIPTION_STATUS,
|
|
55
59
|
TakeoverConversationUseCase: () => TakeoverConversationUseCase,
|
|
60
|
+
TranscribeAudioUseCase: () => TranscribeAudioUseCase,
|
|
56
61
|
WEBHOOK_NONCE_TTL_SECONDS: () => WEBHOOK_NONCE_TTL_SECONDS,
|
|
57
62
|
WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
|
|
58
63
|
claimWebhookDelivery: () => claimWebhookDelivery,
|
|
59
64
|
createMetaWhatsAppModule: () => createMetaWhatsAppModule,
|
|
60
65
|
createSendMediaAction: () => createSendMediaAction,
|
|
66
|
+
createTranscriptionPolicyResolver: () => createTranscriptionPolicyResolver,
|
|
61
67
|
documents: () => documents,
|
|
62
68
|
extractMediaDescriptor: () => extractMediaDescriptor,
|
|
63
69
|
flowGraphs: () => flowGraphs,
|
|
64
70
|
flowMedia: () => flowMedia,
|
|
71
|
+
isAudioMimeType: () => isAudioMimeType,
|
|
72
|
+
isRetriableTranscriptionError: () => isRetriableTranscriptionError,
|
|
73
|
+
isUnsupportedTranscriptionError: () => isUnsupportedTranscriptionError,
|
|
65
74
|
issueSseTicket: () => issueSseTicket,
|
|
66
75
|
messages: () => messages,
|
|
67
76
|
metaWhatsAppMigrationsFolder: () => metaWhatsAppMigrationsFolder,
|
|
68
77
|
metaWhatsAppSchema: () => metaWhatsAppSchema,
|
|
69
78
|
redeemSseTicket: () => redeemSseTicket,
|
|
79
|
+
resolveFailureStatus: () => resolveFailureStatus,
|
|
80
|
+
resolvePreviewUploadId: () => import_meta_whatsapp_contracts8.resolvePreviewUploadId,
|
|
70
81
|
runMetaWhatsAppMigrations: () => runMetaWhatsAppMigrations,
|
|
71
82
|
sessions: () => sessions,
|
|
72
83
|
settings: () => settings,
|
|
84
|
+
toPreviewMediaId: () => import_meta_whatsapp_contracts8.toPreviewMediaId,
|
|
85
|
+
transcriptionRetryAfterSeconds: () => transcriptionRetryAfterSeconds,
|
|
73
86
|
verifyWebhookChallenge: () => verifyWebhookChallenge,
|
|
74
87
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
75
88
|
});
|
|
@@ -77,7 +90,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
77
90
|
|
|
78
91
|
// src/createMetaWhatsAppModule.ts
|
|
79
92
|
var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
|
|
80
|
-
var
|
|
93
|
+
var import_meta_whatsapp_contracts13 = require("@adatechnology/meta-whatsapp-contracts");
|
|
81
94
|
|
|
82
95
|
// src/repositories/SessionRepository.ts
|
|
83
96
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
@@ -180,6 +193,27 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
180
193
|
// parcial sobre boolean resolve isso sem cavar jsonb.
|
|
181
194
|
moderationFlagged: (0, import_pg_core.boolean)("moderation_flagged"),
|
|
182
195
|
moderationTerms: (0, import_pg_core.jsonb)("moderation_terms").$type(),
|
|
196
|
+
/**
|
|
197
|
+
* Transcrição de áudio. `null` em `transcription_status` significa NÃO AVALIADO — áudio nunca
|
|
198
|
+
* pedido (modo sob demanda), transcrição desligada, mensagem anterior ao recurso, ou mensagem
|
|
199
|
+
* que não é áudio. Diferente de `'done'` com texto vazio, que é áudio em silêncio já processado
|
|
200
|
+
* e que NÃO deve ser reprocessado.
|
|
201
|
+
*
|
|
202
|
+
* Colunas em vez de chave em `payload` pelo mesmo motivo da moderação: "quais áudios ficaram
|
|
203
|
+
* pendentes" e "quais falharam" são consultas de operação, e índice parcial resolve sem cavar
|
|
204
|
+
* jsonb. Buscar texto de áudio também deixa de exigir varredura de payload.
|
|
205
|
+
*/
|
|
206
|
+
transcriptionStatus: (0, import_pg_core.varchar)("transcription_status", {
|
|
207
|
+
length: 16
|
|
208
|
+
}).$type(),
|
|
209
|
+
transcriptionText: (0, import_pg_core.text)("transcription_text"),
|
|
210
|
+
transcriptionLanguage: (0, import_pg_core.varchar)("transcription_language", {
|
|
211
|
+
length: 32
|
|
212
|
+
}),
|
|
213
|
+
/** Qual engine produziu. Com uma cadeia de engines, é o que responde "por que esta saiu ruim". */
|
|
214
|
+
transcriptionEngine: (0, import_pg_core.varchar)("transcription_engine", {
|
|
215
|
+
length: 32
|
|
216
|
+
}),
|
|
183
217
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
184
218
|
withTimezone: true
|
|
185
219
|
}).notNull().defaultNow()
|
|
@@ -194,7 +228,11 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
194
228
|
(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`),
|
|
195
229
|
// Parcial: só as sinalizadas entram, então o índice fica do tamanho do problema e não do
|
|
196
230
|
// tamanho do transcript.
|
|
197
|
-
(0, import_pg_core.index)("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.moderationFlagged}`)
|
|
231
|
+
(0, import_pg_core.index)("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.moderationFlagged}`),
|
|
232
|
+
// Alimenta a varredura de retomada: "quais áudios ficaram pendentes de transcrição". Parcial
|
|
233
|
+
// porque pendente é estado transitório e raro — o índice fica do tamanho da fila atrasada, não
|
|
234
|
+
// do transcript inteiro.
|
|
235
|
+
(0, import_pg_core.index)("idx_messages_transcription_pending").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.transcriptionStatus} = 'pending'`)
|
|
198
236
|
]);
|
|
199
237
|
var documents = metaWhatsAppSchema.table("documents", {
|
|
200
238
|
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
@@ -317,6 +355,15 @@ var settings = metaWhatsAppSchema.table("settings", {
|
|
|
317
355
|
templateVariables: (0, import_pg_core.jsonb)("template_variables").$type().notNull().default([]),
|
|
318
356
|
welcomeMessage: (0, import_pg_core.text)("welcome_message"),
|
|
319
357
|
farewellMessage: (0, import_pg_core.text)("farewell_message"),
|
|
358
|
+
/**
|
|
359
|
+
* Política de transcrição desta empresa. Nulo é significativo: "o painel não decidiu", e aí vale
|
|
360
|
+
* o padrão que o host injetou. Sem a distinção, atualizar o módulo desligaria a transcrição de
|
|
361
|
+
* quem já a tinha ligada por ambiente.
|
|
362
|
+
*/
|
|
363
|
+
transcriptionEnabled: (0, import_pg_core.boolean)("transcription_enabled"),
|
|
364
|
+
transcriptionMode: (0, import_pg_core.varchar)("transcription_mode", {
|
|
365
|
+
length: 16
|
|
366
|
+
}).$type(),
|
|
320
367
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
321
368
|
withTimezone: true
|
|
322
369
|
}).notNull().defaultNow(),
|
|
@@ -580,6 +627,58 @@ var MessageRepository = class {
|
|
|
580
627
|
}).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, companyId), (0, import_drizzle_orm3.eq)(messages.waMessageId, waMessageId))).returning();
|
|
581
628
|
return updated;
|
|
582
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.
|
|
632
|
+
*
|
|
633
|
+
* Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para
|
|
634
|
+
* responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever
|
|
635
|
+
* o mesmo áudio uma segunda vez só para o painel ver.
|
|
636
|
+
*
|
|
637
|
+
* Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são
|
|
638
|
+
* corridas normais, não erro.
|
|
639
|
+
*/
|
|
640
|
+
async saveTranscriptionByWaMessageId(params) {
|
|
641
|
+
const [updated] = await this.db.update(messages).set({
|
|
642
|
+
transcriptionStatus: params.status,
|
|
643
|
+
...params.text !== void 0 ? {
|
|
644
|
+
transcriptionText: params.text
|
|
645
|
+
} : {},
|
|
646
|
+
...params.language !== void 0 ? {
|
|
647
|
+
transcriptionLanguage: params.language
|
|
648
|
+
} : {},
|
|
649
|
+
...params.engine !== void 0 ? {
|
|
650
|
+
transcriptionEngine: params.engine
|
|
651
|
+
} : {}
|
|
652
|
+
}).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();
|
|
653
|
+
return updated;
|
|
654
|
+
}
|
|
655
|
+
async findById(companyId, messageId) {
|
|
656
|
+
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);
|
|
657
|
+
return found;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada
|
|
661
|
+
* entre o enfileiramento e a execução do job) — não é erro, é corrida normal.
|
|
662
|
+
*
|
|
663
|
+
* `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar
|
|
664
|
+
* atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de
|
|
665
|
+
* outro engine da cadeia.
|
|
666
|
+
*/
|
|
667
|
+
async saveTranscription(params) {
|
|
668
|
+
const [updated] = await this.db.update(messages).set({
|
|
669
|
+
transcriptionStatus: params.status,
|
|
670
|
+
...params.text !== void 0 ? {
|
|
671
|
+
transcriptionText: params.text
|
|
672
|
+
} : {},
|
|
673
|
+
...params.language !== void 0 ? {
|
|
674
|
+
transcriptionLanguage: params.language
|
|
675
|
+
} : {},
|
|
676
|
+
...params.engine !== void 0 ? {
|
|
677
|
+
transcriptionEngine: params.engine
|
|
678
|
+
} : {}
|
|
679
|
+
}).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();
|
|
680
|
+
return updated;
|
|
681
|
+
}
|
|
583
682
|
async listByConversation(params) {
|
|
584
683
|
const conditions = [
|
|
585
684
|
(0, import_drizzle_orm3.eq)(messages.companyId, params.companyId),
|
|
@@ -764,7 +863,11 @@ function toContractSettings(row) {
|
|
|
764
863
|
templateLanguage: row.templateLanguage,
|
|
765
864
|
templateVariables: row.templateVariables,
|
|
766
865
|
welcomeMessage: row.welcomeMessage ?? "",
|
|
767
|
-
farewellMessage: row.farewellMessage ?? ""
|
|
866
|
+
farewellMessage: row.farewellMessage ?? "",
|
|
867
|
+
// `?? null` e não `?? false`: nulo é "o painel não decidiu", e é o que faz valer o padrão do
|
|
868
|
+
// host. Colapsar para `false` desligaria quem ligou por ambiente.
|
|
869
|
+
transcriptionEnabled: row.transcriptionEnabled ?? null,
|
|
870
|
+
transcriptionMode: row.transcriptionMode ?? null
|
|
768
871
|
};
|
|
769
872
|
}
|
|
770
873
|
__name(toContractSettings, "toContractSettings");
|
|
@@ -773,7 +876,9 @@ var EMPTY_SETTINGS = {
|
|
|
773
876
|
templateLanguage: "pt_BR",
|
|
774
877
|
templateVariables: [],
|
|
775
878
|
welcomeMessage: "",
|
|
776
|
-
farewellMessage: ""
|
|
879
|
+
farewellMessage: "",
|
|
880
|
+
transcriptionEnabled: null,
|
|
881
|
+
transcriptionMode: null
|
|
777
882
|
};
|
|
778
883
|
var SettingsRepository = class {
|
|
779
884
|
static {
|
|
@@ -802,7 +907,12 @@ var SettingsRepository = class {
|
|
|
802
907
|
templateLanguage: merged.templateLanguage,
|
|
803
908
|
templateVariables: merged.templateVariables,
|
|
804
909
|
welcomeMessage: merged.welcomeMessage || null,
|
|
805
|
-
farewellMessage: merged.farewellMessage || null
|
|
910
|
+
farewellMessage: merged.farewellMessage || null,
|
|
911
|
+
// Sem `|| null`: `false` aqui é decisão explícita do painel ("desligado para esta empresa"),
|
|
912
|
+
// e colapsá-lo para nulo faria a empresa voltar a herdar o padrão do host — exatamente o
|
|
913
|
+
// oposto do que o operador acabou de pedir.
|
|
914
|
+
transcriptionEnabled: merged.transcriptionEnabled,
|
|
915
|
+
transcriptionMode: merged.transcriptionMode
|
|
806
916
|
}).onConflictDoUpdate({
|
|
807
917
|
target: settings.companyId,
|
|
808
918
|
set: {
|
|
@@ -811,6 +921,8 @@ var SettingsRepository = class {
|
|
|
811
921
|
templateVariables: merged.templateVariables,
|
|
812
922
|
welcomeMessage: merged.welcomeMessage || null,
|
|
813
923
|
farewellMessage: merged.farewellMessage || null,
|
|
924
|
+
transcriptionEnabled: merged.transcriptionEnabled,
|
|
925
|
+
transcriptionMode: merged.transcriptionMode,
|
|
814
926
|
updatedAt: /* @__PURE__ */ new Date()
|
|
815
927
|
}
|
|
816
928
|
}).returning();
|
|
@@ -1799,20 +1911,27 @@ var FlowMediaRepository = class {
|
|
|
1799
1911
|
|
|
1800
1912
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1801
1913
|
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1914
|
+
var import_meta_whatsapp_contracts9 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1915
|
+
|
|
1916
|
+
// src/channel/previewMedia.ts
|
|
1802
1917
|
var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1918
|
+
|
|
1919
|
+
// src/channel/WhatsAppChannelAdapter.ts
|
|
1803
1920
|
var WhatsAppChannelAdapter = class {
|
|
1804
1921
|
static {
|
|
1805
1922
|
__name(this, "WhatsAppChannelAdapter");
|
|
1806
1923
|
}
|
|
1807
1924
|
messages;
|
|
1808
|
-
|
|
1925
|
+
previewMedia;
|
|
1926
|
+
constructor(messages2, previewMedia) {
|
|
1809
1927
|
this.messages = messages2;
|
|
1928
|
+
this.previewMedia = previewMedia;
|
|
1810
1929
|
}
|
|
1811
1930
|
async translateErrors(operation) {
|
|
1812
1931
|
try {
|
|
1813
1932
|
return await operation();
|
|
1814
1933
|
} catch (error) {
|
|
1815
|
-
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new
|
|
1934
|
+
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new import_meta_whatsapp_contracts9.WindowExpiredError();
|
|
1816
1935
|
throw error;
|
|
1817
1936
|
}
|
|
1818
1937
|
}
|
|
@@ -1850,17 +1969,31 @@ var WhatsAppChannelAdapter = class {
|
|
|
1850
1969
|
externalMessageId: result.waMessageId
|
|
1851
1970
|
};
|
|
1852
1971
|
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.
|
|
1974
|
+
*
|
|
1975
|
+
* O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e
|
|
1976
|
+
* tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.
|
|
1977
|
+
*/
|
|
1853
1978
|
async fetchMediaAsBase64(mediaId) {
|
|
1979
|
+
const uploadId = this.previewMedia?.isEnabled ? (0, import_meta_whatsapp_contracts8.resolvePreviewUploadId)(mediaId) : void 0;
|
|
1980
|
+
if (uploadId) {
|
|
1981
|
+
const buffer = await this.previewMedia.objectStorage.getObject(uploadId);
|
|
1982
|
+
return {
|
|
1983
|
+
data: buffer.toString("base64"),
|
|
1984
|
+
mimeType: this.previewMedia.defaultMimeType ?? "audio/ogg"
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1854
1987
|
return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId));
|
|
1855
1988
|
}
|
|
1856
1989
|
};
|
|
1857
1990
|
|
|
1858
1991
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
1859
|
-
var
|
|
1992
|
+
var import_meta_whatsapp_contracts12 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1860
1993
|
|
|
1861
1994
|
// src/channel/webhookSecurity.ts
|
|
1862
1995
|
var import_node_crypto = require("crypto");
|
|
1863
|
-
var
|
|
1996
|
+
var import_meta_whatsapp_contracts10 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1864
1997
|
var WEBHOOK_NONCE_TTL_SECONDS = 300;
|
|
1865
1998
|
function safeEqualStrings(left, right) {
|
|
1866
1999
|
const leftDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(left).digest();
|
|
@@ -1870,20 +2003,20 @@ function safeEqualStrings(left, right) {
|
|
|
1870
2003
|
__name(safeEqualStrings, "safeEqualStrings");
|
|
1871
2004
|
function verifyWebhookChallenge(params) {
|
|
1872
2005
|
if (params.mode !== "subscribe" || !params.token || !params.challenge) {
|
|
1873
|
-
throw new
|
|
2006
|
+
throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1874
2007
|
}
|
|
1875
2008
|
if (!safeEqualStrings(params.token, params.expectedToken)) {
|
|
1876
|
-
throw new
|
|
2009
|
+
throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1877
2010
|
}
|
|
1878
2011
|
return params.challenge;
|
|
1879
2012
|
}
|
|
1880
2013
|
__name(verifyWebhookChallenge, "verifyWebhookChallenge");
|
|
1881
2014
|
function verifyWebhookSignature(params) {
|
|
1882
2015
|
const { rawBody, signatureHeader, appSecret } = params;
|
|
1883
|
-
if (!signatureHeader?.startsWith("sha256=")) throw new
|
|
2016
|
+
if (!signatureHeader?.startsWith("sha256=")) throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1884
2017
|
const expected = (0, import_node_crypto.createHmac)("sha256", appSecret).update(rawBody).digest("hex");
|
|
1885
2018
|
const received = signatureHeader.slice("sha256=".length);
|
|
1886
|
-
if (!safeEqualStrings(received, expected)) throw new
|
|
2019
|
+
if (!safeEqualStrings(received, expected)) throw new import_meta_whatsapp_contracts10.InvalidWebhookSignatureError();
|
|
1887
2020
|
}
|
|
1888
2021
|
__name(verifyWebhookSignature, "verifyWebhookSignature");
|
|
1889
2022
|
async function claimWebhookDelivery(params) {
|
|
@@ -1894,6 +2027,160 @@ __name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
|
1894
2027
|
|
|
1895
2028
|
// src/channel/IngestInboundMedia.use-case.ts
|
|
1896
2029
|
var import_drizzle_orm8 = require("drizzle-orm");
|
|
2030
|
+
|
|
2031
|
+
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2032
|
+
var import_meta_whatsapp_contracts11 = require("@adatechnology/meta-whatsapp-contracts");
|
|
2033
|
+
|
|
2034
|
+
// src/transcription.types.ts
|
|
2035
|
+
var TRANSCRIPTION_STATUS = {
|
|
2036
|
+
/** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */
|
|
2037
|
+
PENDING: "pending",
|
|
2038
|
+
/** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */
|
|
2039
|
+
DONE: "done",
|
|
2040
|
+
/** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */
|
|
2041
|
+
FAILED: "failed",
|
|
2042
|
+
/** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */
|
|
2043
|
+
UNSUPPORTED: "unsupported"
|
|
2044
|
+
};
|
|
2045
|
+
var TRANSCRIPTION_MODE = {
|
|
2046
|
+
AUTO: "auto",
|
|
2047
|
+
ON_DEMAND: "onDemand"
|
|
2048
|
+
};
|
|
2049
|
+
function isRetriableTranscriptionError(error) {
|
|
2050
|
+
if (typeof error !== "object" || error === null) return true;
|
|
2051
|
+
const isRetriable = error.isRetriable;
|
|
2052
|
+
return typeof isRetriable === "boolean" ? isRetriable : true;
|
|
2053
|
+
}
|
|
2054
|
+
__name(isRetriableTranscriptionError, "isRetriableTranscriptionError");
|
|
2055
|
+
function isUnsupportedTranscriptionError(error) {
|
|
2056
|
+
return typeof error === "object" && error !== null && error.name === "TranscriptionUnsupportedError";
|
|
2057
|
+
}
|
|
2058
|
+
__name(isUnsupportedTranscriptionError, "isUnsupportedTranscriptionError");
|
|
2059
|
+
function transcriptionRetryAfterSeconds(error) {
|
|
2060
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
2061
|
+
const retryAfter = error.retryAfterSeconds;
|
|
2062
|
+
return typeof retryAfter === "number" ? retryAfter : void 0;
|
|
2063
|
+
}
|
|
2064
|
+
__name(transcriptionRetryAfterSeconds, "transcriptionRetryAfterSeconds");
|
|
2065
|
+
function isAudioMimeType(mimeType) {
|
|
2066
|
+
return typeof mimeType === "string" && mimeType.trim().toLowerCase().startsWith("audio/");
|
|
2067
|
+
}
|
|
2068
|
+
__name(isAudioMimeType, "isAudioMimeType");
|
|
2069
|
+
|
|
2070
|
+
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2071
|
+
var TranscribeAudioUseCase = class {
|
|
2072
|
+
static {
|
|
2073
|
+
__name(this, "TranscribeAudioUseCase");
|
|
2074
|
+
}
|
|
2075
|
+
dependencies;
|
|
2076
|
+
constructor(dependencies) {
|
|
2077
|
+
this.dependencies = dependencies;
|
|
2078
|
+
}
|
|
2079
|
+
async execute(params) {
|
|
2080
|
+
const message = await this.dependencies.messageRepository.findById(params.companyId, params.messageId);
|
|
2081
|
+
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para transcri\xE7\xE3o`);
|
|
2082
|
+
if (message.transcriptionStatus === TRANSCRIPTION_STATUS.DONE && !params.force) {
|
|
2083
|
+
return {
|
|
2084
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2085
|
+
text: message.transcriptionText,
|
|
2086
|
+
language: message.transcriptionLanguage,
|
|
2087
|
+
engine: message.transcriptionEngine,
|
|
2088
|
+
alreadyTranscribed: true
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
if (this.dependencies.resolvePolicy) {
|
|
2092
|
+
const policy = await this.dependencies.resolvePolicy(params.companyId);
|
|
2093
|
+
if (!policy.isEnabled) throw new import_meta_whatsapp_contracts11.TranscriptionDisabledError();
|
|
2094
|
+
}
|
|
2095
|
+
const audio = extractAudioReference(message);
|
|
2096
|
+
const buffer = await this.dependencies.objectStorage.getObject(audio.uploadId);
|
|
2097
|
+
return this.transcribeBuffer({
|
|
2098
|
+
...params,
|
|
2099
|
+
buffer,
|
|
2100
|
+
mimeType: audio.mimeType,
|
|
2101
|
+
uploadId: audio.uploadId,
|
|
2102
|
+
message
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
async transcribeBuffer(context) {
|
|
2106
|
+
try {
|
|
2107
|
+
const result = await this.dependencies.transcriber.transcribe({
|
|
2108
|
+
buffer: context.buffer,
|
|
2109
|
+
mimeType: context.mimeType,
|
|
2110
|
+
...this.dependencies.languageHint ? {
|
|
2111
|
+
languageHint: this.dependencies.languageHint
|
|
2112
|
+
} : {}
|
|
2113
|
+
});
|
|
2114
|
+
await this.dependencies.messageRepository.saveTranscription({
|
|
2115
|
+
companyId: context.companyId,
|
|
2116
|
+
messageId: context.messageId,
|
|
2117
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2118
|
+
text: result.text,
|
|
2119
|
+
language: result.language ?? null,
|
|
2120
|
+
engine: result.engine
|
|
2121
|
+
});
|
|
2122
|
+
return {
|
|
2123
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2124
|
+
text: result.text,
|
|
2125
|
+
language: result.language ?? null,
|
|
2126
|
+
engine: result.engine,
|
|
2127
|
+
alreadyTranscribed: false
|
|
2128
|
+
};
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
await this.persistFailure(context, error);
|
|
2131
|
+
throw error;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:
|
|
2136
|
+
* reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.
|
|
2137
|
+
*/
|
|
2138
|
+
async persistFailure(context, error) {
|
|
2139
|
+
const status = resolveFailureStatus(error);
|
|
2140
|
+
await this.dependencies.messageRepository.saveTranscription({
|
|
2141
|
+
companyId: context.companyId,
|
|
2142
|
+
messageId: context.messageId,
|
|
2143
|
+
status
|
|
2144
|
+
});
|
|
2145
|
+
if (status !== TRANSCRIPTION_STATUS.PENDING) return;
|
|
2146
|
+
const retryAfterSeconds = transcriptionRetryAfterSeconds(error);
|
|
2147
|
+
await this.dependencies.hooks?.onTranscriptionDeferred?.({
|
|
2148
|
+
companyId: context.companyId,
|
|
2149
|
+
messageId: context.messageId,
|
|
2150
|
+
whatsappNumber: context.message.whatsappNumber,
|
|
2151
|
+
uploadId: context.uploadId,
|
|
2152
|
+
...retryAfterSeconds !== void 0 ? {
|
|
2153
|
+
retryAfterSeconds
|
|
2154
|
+
} : {},
|
|
2155
|
+
reason: retryAfterSeconds !== void 0 ? "rate-limited" : "transient-failure",
|
|
2156
|
+
error
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
function resolveFailureStatus(error) {
|
|
2161
|
+
if (isUnsupportedTranscriptionError(error)) return TRANSCRIPTION_STATUS.UNSUPPORTED;
|
|
2162
|
+
return isRetriableTranscriptionError(error) ? TRANSCRIPTION_STATUS.PENDING : TRANSCRIPTION_STATUS.FAILED;
|
|
2163
|
+
}
|
|
2164
|
+
__name(resolveFailureStatus, "resolveFailureStatus");
|
|
2165
|
+
function extractAudioReference(message) {
|
|
2166
|
+
const payload = message.payload ?? {};
|
|
2167
|
+
const audio = payload["audio"];
|
|
2168
|
+
const mimeType = typeof payload["mimeType"] === "string" ? payload["mimeType"] : audio?.mime_type;
|
|
2169
|
+
if (!isAudioMimeType(mimeType) && !audio) {
|
|
2170
|
+
throw new import_meta_whatsapp_contracts11.MessageNotAudioError(message.id, message.type);
|
|
2171
|
+
}
|
|
2172
|
+
const uploadId = payload["uploadId"];
|
|
2173
|
+
if (typeof uploadId !== "string" || uploadId.length === 0) {
|
|
2174
|
+
throw new import_meta_whatsapp_contracts11.AudioNotIngestedError(message.id);
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
uploadId,
|
|
2178
|
+
mimeType: mimeType ?? "audio/ogg"
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
__name(extractAudioReference, "extractAudioReference");
|
|
2182
|
+
|
|
2183
|
+
// src/channel/IngestInboundMedia.use-case.ts
|
|
1897
2184
|
var IngestInboundMediaUseCase = class {
|
|
1898
2185
|
static {
|
|
1899
2186
|
__name(this, "IngestInboundMediaUseCase");
|
|
@@ -1902,11 +2189,13 @@ var IngestInboundMediaUseCase = class {
|
|
|
1902
2189
|
channel;
|
|
1903
2190
|
objectStorage;
|
|
1904
2191
|
documentRepository;
|
|
1905
|
-
|
|
2192
|
+
transcription;
|
|
2193
|
+
constructor(db, channel, objectStorage, documentRepository, transcription) {
|
|
1906
2194
|
this.db = db;
|
|
1907
2195
|
this.channel = channel;
|
|
1908
2196
|
this.objectStorage = objectStorage;
|
|
1909
2197
|
this.documentRepository = documentRepository;
|
|
2198
|
+
this.transcription = transcription;
|
|
1910
2199
|
}
|
|
1911
2200
|
async execute(params) {
|
|
1912
2201
|
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);
|
|
@@ -1952,9 +2241,84 @@ var IngestInboundMediaUseCase = class {
|
|
|
1952
2241
|
sizeBytes: buffer.length,
|
|
1953
2242
|
source: message.sender
|
|
1954
2243
|
});
|
|
2244
|
+
const transcription = await this.transcribeIfAuto({
|
|
2245
|
+
companyId: params.companyId,
|
|
2246
|
+
message,
|
|
2247
|
+
uploadId,
|
|
2248
|
+
buffer,
|
|
2249
|
+
mimeType: mimeType || params.mimeType
|
|
2250
|
+
});
|
|
1955
2251
|
return {
|
|
1956
2252
|
uploadId,
|
|
1957
|
-
alreadyIngested: false
|
|
2253
|
+
alreadyIngested: false,
|
|
2254
|
+
...transcription ? {
|
|
2255
|
+
transcription
|
|
2256
|
+
} : {}
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Transcreve o áudio recém-baixado, quando o modo é `auto`.
|
|
2261
|
+
*
|
|
2262
|
+
* **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da
|
|
2263
|
+
* conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o
|
|
2264
|
+
* retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir
|
|
2265
|
+
* um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`
|
|
2266
|
+
* avisa quem sabe reenfileirar.
|
|
2267
|
+
*/
|
|
2268
|
+
async transcribeIfAuto(context) {
|
|
2269
|
+
const transcription = this.transcription;
|
|
2270
|
+
if (!transcription) return void 0;
|
|
2271
|
+
if (!isAudioMimeType(context.mimeType)) return void 0;
|
|
2272
|
+
const policy = await transcription.resolvePolicy(context.companyId);
|
|
2273
|
+
if (!policy.isEnabled || policy.mode !== TRANSCRIPTION_MODE.AUTO) return void 0;
|
|
2274
|
+
const current = await transcription.messageRepository.findById(context.companyId, context.message.id);
|
|
2275
|
+
if (current?.transcriptionStatus === TRANSCRIPTION_STATUS.DONE) return void 0;
|
|
2276
|
+
try {
|
|
2277
|
+
const result = await transcription.transcriber.transcribe({
|
|
2278
|
+
buffer: context.buffer,
|
|
2279
|
+
mimeType: context.mimeType,
|
|
2280
|
+
...transcription.languageHint ? {
|
|
2281
|
+
languageHint: transcription.languageHint
|
|
2282
|
+
} : {}
|
|
2283
|
+
});
|
|
2284
|
+
await transcription.messageRepository.saveTranscription({
|
|
2285
|
+
companyId: context.companyId,
|
|
2286
|
+
messageId: context.message.id,
|
|
2287
|
+
status: TRANSCRIPTION_STATUS.DONE,
|
|
2288
|
+
text: result.text,
|
|
2289
|
+
language: result.language ?? null,
|
|
2290
|
+
engine: result.engine
|
|
2291
|
+
});
|
|
2292
|
+
return {
|
|
2293
|
+
status: TRANSCRIPTION_STATUS.DONE
|
|
2294
|
+
};
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
return this.recordTranscriptionFailure(context, error);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async recordTranscriptionFailure(context, error) {
|
|
2300
|
+
const status = resolveFailureStatus(error);
|
|
2301
|
+
await this.transcription?.messageRepository.saveTranscription({
|
|
2302
|
+
companyId: context.companyId,
|
|
2303
|
+
messageId: context.message.id,
|
|
2304
|
+
status
|
|
2305
|
+
});
|
|
2306
|
+
if (status === TRANSCRIPTION_STATUS.PENDING) {
|
|
2307
|
+
const retryAfterSeconds = transcriptionRetryAfterSeconds(error);
|
|
2308
|
+
await this.transcription?.hooks?.onTranscriptionDeferred?.({
|
|
2309
|
+
companyId: context.companyId,
|
|
2310
|
+
messageId: context.message.id,
|
|
2311
|
+
whatsappNumber: context.message.whatsappNumber,
|
|
2312
|
+
uploadId: context.uploadId,
|
|
2313
|
+
...retryAfterSeconds !== void 0 ? {
|
|
2314
|
+
retryAfterSeconds
|
|
2315
|
+
} : {},
|
|
2316
|
+
reason: retryAfterSeconds !== void 0 ? "rate-limited" : "transient-failure",
|
|
2317
|
+
error
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
return {
|
|
2321
|
+
status
|
|
1958
2322
|
};
|
|
1959
2323
|
}
|
|
1960
2324
|
};
|
|
@@ -2057,7 +2421,7 @@ var ReceiveWebhookUseCase = class {
|
|
|
2057
2421
|
statusesProcessed: 0
|
|
2058
2422
|
};
|
|
2059
2423
|
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
2060
|
-
const payload =
|
|
2424
|
+
const payload = import_meta_whatsapp_contracts12.whatsAppWebhookPayloadSchema.parse(JSON.parse(rawText));
|
|
2061
2425
|
let messagesProcessed = 0;
|
|
2062
2426
|
let statusesProcessed = 0;
|
|
2063
2427
|
for (const entry of payload.entry) {
|
|
@@ -2124,6 +2488,51 @@ var ReceiveWebhookUseCase = class {
|
|
|
2124
2488
|
}
|
|
2125
2489
|
};
|
|
2126
2490
|
|
|
2491
|
+
// src/use-cases/resolveTranscriptionPolicy.ts
|
|
2492
|
+
function createTranscriptionPolicyResolver(dependencies) {
|
|
2493
|
+
return /* @__PURE__ */ __name(async function resolveTranscriptionPolicy(companyId) {
|
|
2494
|
+
const settings2 = await dependencies.settingsRepository.get(companyId);
|
|
2495
|
+
return {
|
|
2496
|
+
// `??` e não `||`: `false` gravado é decisão explícita de desligar, e `||` a trocaria pelo
|
|
2497
|
+
// padrão do host — desligar no painel não faria nada num deploy com transcrição ligada.
|
|
2498
|
+
isEnabled: settings2.transcriptionEnabled ?? dependencies.defaults.isEnabled,
|
|
2499
|
+
mode: normalizeMode(settings2.transcriptionMode) ?? dependencies.defaults.mode
|
|
2500
|
+
};
|
|
2501
|
+
}, "resolveTranscriptionPolicy");
|
|
2502
|
+
}
|
|
2503
|
+
__name(createTranscriptionPolicyResolver, "createTranscriptionPolicyResolver");
|
|
2504
|
+
function normalizeMode(value) {
|
|
2505
|
+
if (value === TRANSCRIPTION_MODE.AUTO) return TRANSCRIPTION_MODE.AUTO;
|
|
2506
|
+
if (value === TRANSCRIPTION_MODE.ON_DEMAND) return TRANSCRIPTION_MODE.ON_DEMAND;
|
|
2507
|
+
return void 0;
|
|
2508
|
+
}
|
|
2509
|
+
__name(normalizeMode, "normalizeMode");
|
|
2510
|
+
|
|
2511
|
+
// src/use-cases/StorePreviewMedia.use-case.ts
|
|
2512
|
+
var StorePreviewMediaUseCase = class {
|
|
2513
|
+
static {
|
|
2514
|
+
__name(this, "StorePreviewMediaUseCase");
|
|
2515
|
+
}
|
|
2516
|
+
objectStorage;
|
|
2517
|
+
generateKeySuffix;
|
|
2518
|
+
constructor(objectStorage, generateKeySuffix) {
|
|
2519
|
+
this.objectStorage = objectStorage;
|
|
2520
|
+
this.generateKeySuffix = generateKeySuffix;
|
|
2521
|
+
}
|
|
2522
|
+
async execute(params) {
|
|
2523
|
+
const key = `meta-whatsapp/${params.companyId}/preview/${this.generateKeySuffix()}`;
|
|
2524
|
+
const { uploadId } = await this.objectStorage.upload({
|
|
2525
|
+
buffer: params.buffer,
|
|
2526
|
+
mimeType: params.mimeType,
|
|
2527
|
+
key
|
|
2528
|
+
});
|
|
2529
|
+
return {
|
|
2530
|
+
mediaId: (0, import_meta_whatsapp_contracts8.toPreviewMediaId)(uploadId),
|
|
2531
|
+
uploadId
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
|
|
2127
2536
|
// src/createMetaWhatsAppModule.ts
|
|
2128
2537
|
function createMetaWhatsAppModule(params) {
|
|
2129
2538
|
const { db, config, nonceStore, providers = {}, hooks } = params;
|
|
@@ -2136,7 +2545,11 @@ function createMetaWhatsAppModule(params) {
|
|
|
2136
2545
|
apiVersion: config.apiVersion,
|
|
2137
2546
|
baseUrl: config.baseUrl
|
|
2138
2547
|
});
|
|
2139
|
-
const
|
|
2548
|
+
const previewMediaSupport = params.features?.previewMedia && providers.objectStorage?.getObject ? {
|
|
2549
|
+
isEnabled: true,
|
|
2550
|
+
objectStorage: providers.objectStorage
|
|
2551
|
+
} : void 0;
|
|
2552
|
+
const channel = new WhatsAppChannelAdapter(messageProvider, previewMediaSupport);
|
|
2140
2553
|
const sessionRepository = new SessionRepository(db);
|
|
2141
2554
|
const messageRepository = new MessageRepository(db);
|
|
2142
2555
|
const settingsRepository = new SettingsRepository(db);
|
|
@@ -2159,7 +2572,7 @@ function createMetaWhatsAppModule(params) {
|
|
|
2159
2572
|
});
|
|
2160
2573
|
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
2161
2574
|
if (flowInterpreter && providers.objectStorage?.getObject) {
|
|
2162
|
-
flowInterpreter.registerFlowAction(
|
|
2575
|
+
flowInterpreter.registerFlowAction(import_meta_whatsapp_contracts13.FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
|
|
2163
2576
|
flowMediaRepository,
|
|
2164
2577
|
objectStorage: providers.objectStorage,
|
|
2165
2578
|
logMessage,
|
|
@@ -2167,7 +2580,39 @@ function createMetaWhatsAppModule(params) {
|
|
|
2167
2580
|
onError: hooks?.onFlowMediaError
|
|
2168
2581
|
}));
|
|
2169
2582
|
}
|
|
2170
|
-
const
|
|
2583
|
+
const transcriptionMode = providers.transcription?.mode ?? TRANSCRIPTION_MODE.ON_DEMAND;
|
|
2584
|
+
const resolveTranscriptionPolicy = providers.transcription ? createTranscriptionPolicyResolver({
|
|
2585
|
+
settingsRepository,
|
|
2586
|
+
defaults: {
|
|
2587
|
+
isEnabled: providers.transcription.isEnabledByDefault ?? true,
|
|
2588
|
+
mode: transcriptionMode
|
|
2589
|
+
}
|
|
2590
|
+
}) : void 0;
|
|
2591
|
+
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository, providers.transcription && resolveTranscriptionPolicy ? {
|
|
2592
|
+
transcriber: providers.transcription.transcriber,
|
|
2593
|
+
resolvePolicy: resolveTranscriptionPolicy,
|
|
2594
|
+
messageRepository,
|
|
2595
|
+
...providers.transcription.languageHint ? {
|
|
2596
|
+
languageHint: providers.transcription.languageHint
|
|
2597
|
+
} : {},
|
|
2598
|
+
...hooks ? {
|
|
2599
|
+
hooks
|
|
2600
|
+
} : {}
|
|
2601
|
+
} : void 0) : void 0;
|
|
2602
|
+
const transcribeAudio = providers.transcription && providers.objectStorage?.getObject ? new TranscribeAudioUseCase({
|
|
2603
|
+
messageRepository,
|
|
2604
|
+
objectStorage: providers.objectStorage,
|
|
2605
|
+
transcriber: providers.transcription.transcriber,
|
|
2606
|
+
...resolveTranscriptionPolicy ? {
|
|
2607
|
+
resolvePolicy: resolveTranscriptionPolicy
|
|
2608
|
+
} : {},
|
|
2609
|
+
...providers.transcription.languageHint ? {
|
|
2610
|
+
languageHint: providers.transcription.languageHint
|
|
2611
|
+
} : {},
|
|
2612
|
+
...hooks ? {
|
|
2613
|
+
hooks
|
|
2614
|
+
} : {}
|
|
2615
|
+
}) : void 0;
|
|
2171
2616
|
const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
|
|
2172
2617
|
return {
|
|
2173
2618
|
channel,
|
|
@@ -2188,10 +2633,28 @@ function createMetaWhatsAppModule(params) {
|
|
|
2188
2633
|
delete: new DeleteConversationUseCase(sessionRepository, documentRepository, providers.objectStorage),
|
|
2189
2634
|
purgeExpiredDocuments: new PurgeExpiredDocumentsUseCase(documentRepository, providers.objectStorage),
|
|
2190
2635
|
export: new ExportConversationUseCase(sessionRepository),
|
|
2636
|
+
// undefined quando transcrição não foi injetada, ou quando o storage não sabe ler de volta.
|
|
2637
|
+
// O painel consulta a ausência para decidir se desenha o botão "transcrever".
|
|
2638
|
+
transcribeAudio,
|
|
2191
2639
|
repository: sessionRepository,
|
|
2640
|
+
messageRepository,
|
|
2192
2641
|
documentRepository
|
|
2193
2642
|
},
|
|
2643
|
+
/**
|
|
2644
|
+
* `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a
|
|
2645
|
+
* capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —
|
|
2646
|
+
* é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.
|
|
2647
|
+
*/
|
|
2648
|
+
transcription: providers.transcription && resolveTranscriptionPolicy ? {
|
|
2649
|
+
defaultMode: transcriptionMode,
|
|
2650
|
+
resolvePolicy: resolveTranscriptionPolicy
|
|
2651
|
+
} : void 0,
|
|
2194
2652
|
settings: settingsRepository,
|
|
2653
|
+
/**
|
|
2654
|
+
* `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a
|
|
2655
|
+
* ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.
|
|
2656
|
+
*/
|
|
2657
|
+
previewMedia: previewMediaSupport ? new StorePreviewMediaUseCase(providers.objectStorage, () => `${Date.now()}-${Math.random().toString(36).slice(2)}`) : void 0,
|
|
2195
2658
|
webhook: {
|
|
2196
2659
|
receive: receiveWebhook,
|
|
2197
2660
|
// GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.
|
|
@@ -2336,6 +2799,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
2336
2799
|
META_WHATSAPP_MIGRATIONS_TABLE,
|
|
2337
2800
|
MessageRepository,
|
|
2338
2801
|
OptimisticLockError,
|
|
2802
|
+
PREVIEW_MEDIA_ID_PREFIX,
|
|
2339
2803
|
PurgeExpiredDocumentsUseCase,
|
|
2340
2804
|
ReceiveWebhookUseCase,
|
|
2341
2805
|
ReleaseConversationUseCase,
|
|
@@ -2344,24 +2808,36 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
2344
2808
|
SessionRepository,
|
|
2345
2809
|
SettingsRepository,
|
|
2346
2810
|
SseHub,
|
|
2811
|
+
StorePreviewMediaUseCase,
|
|
2812
|
+
TRANSCRIPTION_MODE,
|
|
2813
|
+
TRANSCRIPTION_STATUS,
|
|
2347
2814
|
TakeoverConversationUseCase,
|
|
2815
|
+
TranscribeAudioUseCase,
|
|
2348
2816
|
WEBHOOK_NONCE_TTL_SECONDS,
|
|
2349
2817
|
WhatsAppChannelAdapter,
|
|
2350
2818
|
claimWebhookDelivery,
|
|
2351
2819
|
createMetaWhatsAppModule,
|
|
2352
2820
|
createSendMediaAction,
|
|
2821
|
+
createTranscriptionPolicyResolver,
|
|
2353
2822
|
documents,
|
|
2354
2823
|
extractMediaDescriptor,
|
|
2355
2824
|
flowGraphs,
|
|
2356
2825
|
flowMedia,
|
|
2826
|
+
isAudioMimeType,
|
|
2827
|
+
isRetriableTranscriptionError,
|
|
2828
|
+
isUnsupportedTranscriptionError,
|
|
2357
2829
|
issueSseTicket,
|
|
2358
2830
|
messages,
|
|
2359
2831
|
metaWhatsAppMigrationsFolder,
|
|
2360
2832
|
metaWhatsAppSchema,
|
|
2361
2833
|
redeemSseTicket,
|
|
2834
|
+
resolveFailureStatus,
|
|
2835
|
+
resolvePreviewUploadId,
|
|
2362
2836
|
runMetaWhatsAppMigrations,
|
|
2363
2837
|
sessions,
|
|
2364
2838
|
settings,
|
|
2839
|
+
toPreviewMediaId,
|
|
2840
|
+
transcriptionRetryAfterSeconds,
|
|
2365
2841
|
verifyWebhookChallenge,
|
|
2366
2842
|
verifyWebhookSignature
|
|
2367
2843
|
});
|