@adatechnology/meta-whatsapp-module 0.2.0-rc.30 → 0.2.0-rc.31
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 +181 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -21
- package/dist/index.d.ts +100 -21
- package/dist/index.js +181 -16
- package/dist/index.js.map +1 -1
- package/dist/migrations/0010_flow_media_meta_ids.sql +10 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -251,6 +251,14 @@ var flowMedia = metaWhatsAppSchema.table("flow_media", {
|
|
|
251
251
|
// Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
|
|
252
252
|
// perderia a ordem e a legenda já ajustadas.
|
|
253
253
|
active: boolean("active").notNull().default(true),
|
|
254
|
+
// Id do arquivo já subido para a Meta, por número remetente. Sem isto, o MESMO binário subia de
|
|
255
|
+
// novo para cada cliente que passava pelo nó: a Meta aceita reusar o id por 30 dias.
|
|
256
|
+
//
|
|
257
|
+
// Mapa por `phone_number_id`, e não coluna única: o id é escopado ao número que envia, e uma
|
|
258
|
+
// instalação com dois números mandaria o id de um pelo outro. A validade não é gravada de
|
|
259
|
+
// propósito — confiar em "30 dias" calculados erra nos casos de borda, e quem decide é a
|
|
260
|
+
// recusa da Meta, que devolve ao caminho de subir o binário.
|
|
261
|
+
metaMediaIds: jsonb("meta_media_ids").$type().notNull().default({}),
|
|
254
262
|
createdAt: timestamp("created_at", {
|
|
255
263
|
withTimezone: true
|
|
256
264
|
}).notNull().defaultNow(),
|
|
@@ -1692,6 +1700,43 @@ function mediaTypeFor2(mimeType) {
|
|
|
1692
1700
|
return "document";
|
|
1693
1701
|
}
|
|
1694
1702
|
__name(mediaTypeFor2, "mediaTypeFor");
|
|
1703
|
+
async function sendAttachment(params) {
|
|
1704
|
+
const { attachment, channel, to, cache } = params;
|
|
1705
|
+
const common = {
|
|
1706
|
+
to,
|
|
1707
|
+
mimeType: attachment.mimeType,
|
|
1708
|
+
filename: attachment.filename,
|
|
1709
|
+
caption: attachment.caption ?? void 0
|
|
1710
|
+
};
|
|
1711
|
+
const cacheKey = cache ? {
|
|
1712
|
+
flowMediaId: attachment.id,
|
|
1713
|
+
senderKey: cache.senderKey
|
|
1714
|
+
} : void 0;
|
|
1715
|
+
if (cache && cacheKey) {
|
|
1716
|
+
const knownMediaId = await cache.store.get(cacheKey);
|
|
1717
|
+
if (knownMediaId) {
|
|
1718
|
+
try {
|
|
1719
|
+
return await channel.sendMedia({
|
|
1720
|
+
...common,
|
|
1721
|
+
mediaId: knownMediaId
|
|
1722
|
+
});
|
|
1723
|
+
} catch {
|
|
1724
|
+
await cache.store.clear(cacheKey);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const buffer = await params.objectStorage.getObject(attachment.uploadId);
|
|
1729
|
+
const result = await channel.sendMedia({
|
|
1730
|
+
...common,
|
|
1731
|
+
buffer
|
|
1732
|
+
});
|
|
1733
|
+
if (cache && cacheKey && result.mediaId) await cache.store.set({
|
|
1734
|
+
...cacheKey,
|
|
1735
|
+
mediaId: result.mediaId
|
|
1736
|
+
});
|
|
1737
|
+
return result;
|
|
1738
|
+
}
|
|
1739
|
+
__name(sendAttachment, "sendAttachment");
|
|
1695
1740
|
function createSendMediaAction(params) {
|
|
1696
1741
|
return async ({ node, session, channel }) => {
|
|
1697
1742
|
if (!session.flowKey) return;
|
|
@@ -1703,13 +1748,14 @@ function createSendMediaAction(params) {
|
|
|
1703
1748
|
const attachments = await params.flowMediaRepository.listActive(location);
|
|
1704
1749
|
for (const attachment of attachments) {
|
|
1705
1750
|
try {
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1751
|
+
const { externalMessageId } = await sendAttachment({
|
|
1752
|
+
attachment,
|
|
1753
|
+
channel,
|
|
1708
1754
|
to: session.whatsappNumber,
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1755
|
+
objectStorage: params.objectStorage,
|
|
1756
|
+
...params.mediaIdCache ? {
|
|
1757
|
+
cache: params.mediaIdCache
|
|
1758
|
+
} : {}
|
|
1713
1759
|
});
|
|
1714
1760
|
await params.logMessage.execute({
|
|
1715
1761
|
companyId: session.companyId,
|
|
@@ -1898,6 +1944,42 @@ var FlowMediaRepository = class {
|
|
|
1898
1944
|
}
|
|
1899
1945
|
};
|
|
1900
1946
|
|
|
1947
|
+
// src/repositories/FlowMediaIdRepository.ts
|
|
1948
|
+
import { eq as eq7, sql as sql5 } from "drizzle-orm";
|
|
1949
|
+
var FlowMediaIdRepository = class {
|
|
1950
|
+
static {
|
|
1951
|
+
__name(this, "FlowMediaIdRepository");
|
|
1952
|
+
}
|
|
1953
|
+
db;
|
|
1954
|
+
constructor(db) {
|
|
1955
|
+
this.db = db;
|
|
1956
|
+
}
|
|
1957
|
+
async get(params) {
|
|
1958
|
+
const rows = await this.db.select({
|
|
1959
|
+
ids: flowMedia.metaMediaIds
|
|
1960
|
+
}).from(flowMedia).where(eq7(flowMedia.id, params.flowMediaId)).limit(1);
|
|
1961
|
+
return rows[0]?.ids?.[params.senderKey];
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Grava só a chave deste número.
|
|
1965
|
+
*
|
|
1966
|
+
* `jsonb_set` no banco, e não ler-alterar-escrever na aplicação: dois clientes passando pelo nó
|
|
1967
|
+
* ao mesmo tempo com números diferentes leriam o mesmo mapa e o último gravaria por cima,
|
|
1968
|
+
* apagando o id do outro. A escrita atômica não tem essa janela.
|
|
1969
|
+
*/
|
|
1970
|
+
async set(params) {
|
|
1971
|
+
await this.db.update(flowMedia).set({
|
|
1972
|
+
metaMediaIds: sql5`jsonb_set(${flowMedia.metaMediaIds}, ARRAY[${params.senderKey}::text], to_jsonb(${params.mediaId}::text), true)`
|
|
1973
|
+
}).where(eq7(flowMedia.id, params.flowMediaId));
|
|
1974
|
+
}
|
|
1975
|
+
/** Remove só a chave deste número — o id do outro número continua válido. */
|
|
1976
|
+
async clear(params) {
|
|
1977
|
+
await this.db.update(flowMedia).set({
|
|
1978
|
+
metaMediaIds: sql5`${flowMedia.metaMediaIds} - ${params.senderKey}::text`
|
|
1979
|
+
}).where(eq7(flowMedia.id, params.flowMediaId));
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1901
1983
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1902
1984
|
import { WhatsAppWindowExpiredError as ProviderWindowExpiredError } from "@adatechnology/meta-graph-core";
|
|
1903
1985
|
import { WindowExpiredError as WindowExpiredError2 } from "@adatechnology/meta-whatsapp-contracts";
|
|
@@ -1933,7 +2015,8 @@ var WhatsAppChannelAdapter = class {
|
|
|
1933
2015
|
async sendMedia(params) {
|
|
1934
2016
|
const result = await this.translateErrors(() => this.messages.sendMedia(params));
|
|
1935
2017
|
return {
|
|
1936
|
-
externalMessageId: result.waMessageId
|
|
2018
|
+
externalMessageId: result.waMessageId,
|
|
2019
|
+
mediaId: result.mediaId
|
|
1937
2020
|
};
|
|
1938
2021
|
}
|
|
1939
2022
|
async sendTemplate(params) {
|
|
@@ -2002,7 +2085,7 @@ var WhatsAppChannelAdapter = class {
|
|
|
2002
2085
|
};
|
|
2003
2086
|
|
|
2004
2087
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
2005
|
-
import { whatsAppWebhookPayloadSchema } from "@adatechnology/meta-whatsapp-contracts";
|
|
2088
|
+
import { whatsAppWebhookPayloadSchema, whatsAppTemplateStatusUpdateSchema, whatsAppPhoneNumberQualityUpdateSchema, WHATSAPP_WEBHOOK_FIELDS } from "@adatechnology/meta-whatsapp-contracts";
|
|
2006
2089
|
|
|
2007
2090
|
// src/channel/webhookSecurity.ts
|
|
2008
2091
|
import { WEBHOOK_CLAIM_TTL_SECONDS, WEBHOOK_NONCE_TTL_SECONDS, buildWebhookDeliveryKey, isValidWebhookChallenge, isValidWebhookSignature } from "@adatechnology/meta-graph-core";
|
|
@@ -2034,7 +2117,7 @@ async function confirmWebhookDelivery(params) {
|
|
|
2034
2117
|
__name(confirmWebhookDelivery, "confirmWebhookDelivery");
|
|
2035
2118
|
|
|
2036
2119
|
// src/channel/IngestInboundMedia.use-case.ts
|
|
2037
|
-
import { eq as
|
|
2120
|
+
import { eq as eq8, and as and6 } from "drizzle-orm";
|
|
2038
2121
|
|
|
2039
2122
|
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2040
2123
|
import { AudioNotIngestedError, MessageNotAudioError, TranscriptionDisabledError } from "@adatechnology/meta-whatsapp-contracts";
|
|
@@ -2206,7 +2289,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
2206
2289
|
this.transcription = transcription;
|
|
2207
2290
|
}
|
|
2208
2291
|
async execute(params) {
|
|
2209
|
-
const [message] = await this.db.select().from(messages).where(and6(
|
|
2292
|
+
const [message] = await this.db.select().from(messages).where(and6(eq8(messages.companyId, params.companyId), eq8(messages.id, params.messageId))).limit(1);
|
|
2210
2293
|
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
2211
2294
|
const payload = message.payload ?? {};
|
|
2212
2295
|
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
@@ -2237,7 +2320,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
2237
2320
|
};
|
|
2238
2321
|
await this.db.update(messages).set({
|
|
2239
2322
|
payload: updatedPayload
|
|
2240
|
-
}).where(and6(
|
|
2323
|
+
}).where(and6(eq8(messages.companyId, params.companyId), eq8(messages.id, params.messageId)));
|
|
2241
2324
|
await this.documentRepository?.link({
|
|
2242
2325
|
companyId: params.companyId,
|
|
2243
2326
|
sessionId: message.sessionId,
|
|
@@ -2436,6 +2519,13 @@ var ProcessInboundDispatchUseCase = class {
|
|
|
2436
2519
|
};
|
|
2437
2520
|
|
|
2438
2521
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
2522
|
+
var EMPTY_RESULT = {
|
|
2523
|
+
messagesProcessed: 0,
|
|
2524
|
+
statusesProcessed: 0,
|
|
2525
|
+
ignoredForeignNumber: 0,
|
|
2526
|
+
accountEventsProcessed: 0,
|
|
2527
|
+
unhandledEvents: 0
|
|
2528
|
+
};
|
|
2439
2529
|
function extractContent(message) {
|
|
2440
2530
|
if (message.text?.body) return message.text.body;
|
|
2441
2531
|
const interactive = message.interactive;
|
|
@@ -2461,6 +2551,14 @@ function extractPayload(message) {
|
|
|
2461
2551
|
return Object.keys(payload).length > 0 ? payload : null;
|
|
2462
2552
|
}
|
|
2463
2553
|
__name(extractPayload, "extractPayload");
|
|
2554
|
+
var ACCOUNT_EVENT_FIELDS = [
|
|
2555
|
+
WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE,
|
|
2556
|
+
WHATSAPP_WEBHOOK_FIELDS.PHONE_NUMBER_QUALITY_UPDATE
|
|
2557
|
+
];
|
|
2558
|
+
function isAccountEventField(field) {
|
|
2559
|
+
return field !== void 0 && ACCOUNT_EVENT_FIELDS.includes(field);
|
|
2560
|
+
}
|
|
2561
|
+
__name(isAccountEventField, "isAccountEventField");
|
|
2464
2562
|
var ReceiveWebhookUseCase = class {
|
|
2465
2563
|
static {
|
|
2466
2564
|
__name(this, "ReceiveWebhookUseCase");
|
|
@@ -2491,17 +2589,36 @@ var ReceiveWebhookUseCase = class {
|
|
|
2491
2589
|
});
|
|
2492
2590
|
if (!claimed) return {
|
|
2493
2591
|
duplicate: true,
|
|
2494
|
-
|
|
2495
|
-
statusesProcessed: 0,
|
|
2496
|
-
ignoredForeignNumber: 0
|
|
2592
|
+
...EMPTY_RESULT
|
|
2497
2593
|
};
|
|
2498
2594
|
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
2499
|
-
const
|
|
2595
|
+
const parsed = whatsAppWebhookPayloadSchema.safeParse(JSON.parse(rawText));
|
|
2596
|
+
if (!parsed.success) {
|
|
2597
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2598
|
+
field: void 0,
|
|
2599
|
+
reason: "invalid-shape",
|
|
2600
|
+
value: rawText
|
|
2601
|
+
});
|
|
2602
|
+
return {
|
|
2603
|
+
duplicate: false,
|
|
2604
|
+
...EMPTY_RESULT,
|
|
2605
|
+
unhandledEvents: 1
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
const payload = parsed.data;
|
|
2500
2609
|
let messagesProcessed = 0;
|
|
2501
2610
|
let statusesProcessed = 0;
|
|
2502
2611
|
let ignoredForeignNumber = 0;
|
|
2612
|
+
let accountEventsProcessed = 0;
|
|
2613
|
+
let unhandledEvents = 0;
|
|
2503
2614
|
for (const entry of payload.entry) {
|
|
2504
2615
|
for (const change of entry.changes) {
|
|
2616
|
+
if (isAccountEventField(change.field)) {
|
|
2617
|
+
const handled = await this.handleAccountEvent(change);
|
|
2618
|
+
if (handled) accountEventsProcessed++;
|
|
2619
|
+
else unhandledEvents++;
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2505
2622
|
const targetNumber = change.value.metadata?.phone_number_id;
|
|
2506
2623
|
if (targetNumber && targetNumber !== this.params.phoneNumberId) {
|
|
2507
2624
|
ignoredForeignNumber++;
|
|
@@ -2515,6 +2632,15 @@ var ReceiveWebhookUseCase = class {
|
|
|
2515
2632
|
await this.handleStatus(input.companyId, status);
|
|
2516
2633
|
statusesProcessed++;
|
|
2517
2634
|
}
|
|
2635
|
+
const carriedConversationData = (change.value.messages?.length ?? 0) > 0 || (change.value.message_echoes?.length ?? 0) > 0 || (change.value.statuses?.length ?? 0) > 0;
|
|
2636
|
+
if (!carriedConversationData) {
|
|
2637
|
+
unhandledEvents++;
|
|
2638
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2639
|
+
field: change.field,
|
|
2640
|
+
reason: "unknown-field",
|
|
2641
|
+
value: change.value
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2518
2644
|
}
|
|
2519
2645
|
}
|
|
2520
2646
|
await confirmWebhookDelivery({
|
|
@@ -2525,9 +2651,39 @@ var ReceiveWebhookUseCase = class {
|
|
|
2525
2651
|
duplicate: false,
|
|
2526
2652
|
messagesProcessed,
|
|
2527
2653
|
statusesProcessed,
|
|
2528
|
-
ignoredForeignNumber
|
|
2654
|
+
ignoredForeignNumber,
|
|
2655
|
+
accountEventsProcessed,
|
|
2656
|
+
unhandledEvents
|
|
2529
2657
|
};
|
|
2530
2658
|
}
|
|
2659
|
+
// Devolve `false` quando o corpo não bate com o schema do field — o chamador conta como não
|
|
2660
|
+
// tratado. O host é avisado nos dois casos, mas com `reason` diferente.
|
|
2661
|
+
async handleAccountEvent(change) {
|
|
2662
|
+
if (change.field === WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE) {
|
|
2663
|
+
const update = whatsAppTemplateStatusUpdateSchema.safeParse(change.value);
|
|
2664
|
+
if (!update.success) {
|
|
2665
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2666
|
+
field: change.field,
|
|
2667
|
+
reason: "invalid-shape",
|
|
2668
|
+
value: change.value
|
|
2669
|
+
});
|
|
2670
|
+
return false;
|
|
2671
|
+
}
|
|
2672
|
+
await this.params.hooks?.onTemplateStatusUpdate?.(update.data);
|
|
2673
|
+
return true;
|
|
2674
|
+
}
|
|
2675
|
+
const quality = whatsAppPhoneNumberQualityUpdateSchema.safeParse(change.value);
|
|
2676
|
+
if (!quality.success) {
|
|
2677
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2678
|
+
field: change.field,
|
|
2679
|
+
reason: "invalid-shape",
|
|
2680
|
+
value: change.value
|
|
2681
|
+
});
|
|
2682
|
+
return false;
|
|
2683
|
+
}
|
|
2684
|
+
await this.params.hooks?.onPhoneNumberQualityUpdate?.(quality.data);
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
2531
2687
|
async handleMessage(companyId, message) {
|
|
2532
2688
|
const saved = await this.params.logMessage.execute({
|
|
2533
2689
|
companyId,
|
|
@@ -2672,6 +2828,14 @@ function createMetaWhatsAppModule(params) {
|
|
|
2672
2828
|
objectStorage: providers.objectStorage,
|
|
2673
2829
|
logMessage,
|
|
2674
2830
|
startState,
|
|
2831
|
+
// Cache ligado por padrão, com o store do próprio módulo: a tabela é daqui e a coluna é
|
|
2832
|
+
// daqui, então exigir que cada host escrevesse o repositório faria todos reescreverem o
|
|
2833
|
+
// mesmo código — e, até escreverem, o mesmo binário continuaria subindo por cliente. Quem
|
|
2834
|
+
// quiser outro lugar (Redis) troca o `store`; quem não fizer nada já sai sem ressubir.
|
|
2835
|
+
mediaIdCache: {
|
|
2836
|
+
store: new FlowMediaIdRepository(db),
|
|
2837
|
+
senderKey: config.phoneNumberId
|
|
2838
|
+
},
|
|
2675
2839
|
onError: hooks?.onFlowMediaError
|
|
2676
2840
|
}));
|
|
2677
2841
|
}
|
|
@@ -2888,6 +3052,7 @@ export {
|
|
|
2888
3052
|
FlowGraphCache,
|
|
2889
3053
|
FlowGraphRepository,
|
|
2890
3054
|
FlowInterpreter,
|
|
3055
|
+
FlowMediaIdRepository,
|
|
2891
3056
|
FlowMediaRepository,
|
|
2892
3057
|
GetFlowGraphUseCase,
|
|
2893
3058
|
GetLiveFlowPositionsUseCase,
|