@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.cjs
CHANGED
|
@@ -30,6 +30,7 @@ __export(index_exports, {
|
|
|
30
30
|
FlowGraphCache: () => FlowGraphCache,
|
|
31
31
|
FlowGraphRepository: () => FlowGraphRepository,
|
|
32
32
|
FlowInterpreter: () => FlowInterpreter,
|
|
33
|
+
FlowMediaIdRepository: () => FlowMediaIdRepository,
|
|
33
34
|
FlowMediaRepository: () => FlowMediaRepository,
|
|
34
35
|
GetFlowGraphUseCase: () => GetFlowGraphUseCase,
|
|
35
36
|
GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
|
|
@@ -339,6 +340,14 @@ var flowMedia = metaWhatsAppSchema.table("flow_media", {
|
|
|
339
340
|
// Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
|
|
340
341
|
// perderia a ordem e a legenda já ajustadas.
|
|
341
342
|
active: (0, import_pg_core.boolean)("active").notNull().default(true),
|
|
343
|
+
// Id do arquivo já subido para a Meta, por número remetente. Sem isto, o MESMO binário subia de
|
|
344
|
+
// novo para cada cliente que passava pelo nó: a Meta aceita reusar o id por 30 dias.
|
|
345
|
+
//
|
|
346
|
+
// Mapa por `phone_number_id`, e não coluna única: o id é escopado ao número que envia, e uma
|
|
347
|
+
// instalação com dois números mandaria o id de um pelo outro. A validade não é gravada de
|
|
348
|
+
// propósito — confiar em "30 dias" calculados erra nos casos de borda, e quem decide é a
|
|
349
|
+
// recusa da Meta, que devolve ao caminho de subir o binário.
|
|
350
|
+
metaMediaIds: (0, import_pg_core.jsonb)("meta_media_ids").$type().notNull().default({}),
|
|
342
351
|
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
343
352
|
withTimezone: true
|
|
344
353
|
}).notNull().defaultNow(),
|
|
@@ -1780,6 +1789,43 @@ function mediaTypeFor2(mimeType) {
|
|
|
1780
1789
|
return "document";
|
|
1781
1790
|
}
|
|
1782
1791
|
__name(mediaTypeFor2, "mediaTypeFor");
|
|
1792
|
+
async function sendAttachment(params) {
|
|
1793
|
+
const { attachment, channel, to, cache } = params;
|
|
1794
|
+
const common = {
|
|
1795
|
+
to,
|
|
1796
|
+
mimeType: attachment.mimeType,
|
|
1797
|
+
filename: attachment.filename,
|
|
1798
|
+
caption: attachment.caption ?? void 0
|
|
1799
|
+
};
|
|
1800
|
+
const cacheKey = cache ? {
|
|
1801
|
+
flowMediaId: attachment.id,
|
|
1802
|
+
senderKey: cache.senderKey
|
|
1803
|
+
} : void 0;
|
|
1804
|
+
if (cache && cacheKey) {
|
|
1805
|
+
const knownMediaId = await cache.store.get(cacheKey);
|
|
1806
|
+
if (knownMediaId) {
|
|
1807
|
+
try {
|
|
1808
|
+
return await channel.sendMedia({
|
|
1809
|
+
...common,
|
|
1810
|
+
mediaId: knownMediaId
|
|
1811
|
+
});
|
|
1812
|
+
} catch {
|
|
1813
|
+
await cache.store.clear(cacheKey);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
const buffer = await params.objectStorage.getObject(attachment.uploadId);
|
|
1818
|
+
const result = await channel.sendMedia({
|
|
1819
|
+
...common,
|
|
1820
|
+
buffer
|
|
1821
|
+
});
|
|
1822
|
+
if (cache && cacheKey && result.mediaId) await cache.store.set({
|
|
1823
|
+
...cacheKey,
|
|
1824
|
+
mediaId: result.mediaId
|
|
1825
|
+
});
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
__name(sendAttachment, "sendAttachment");
|
|
1783
1829
|
function createSendMediaAction(params) {
|
|
1784
1830
|
return async ({ node, session, channel }) => {
|
|
1785
1831
|
if (!session.flowKey) return;
|
|
@@ -1791,13 +1837,14 @@ function createSendMediaAction(params) {
|
|
|
1791
1837
|
const attachments = await params.flowMediaRepository.listActive(location);
|
|
1792
1838
|
for (const attachment of attachments) {
|
|
1793
1839
|
try {
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1840
|
+
const { externalMessageId } = await sendAttachment({
|
|
1841
|
+
attachment,
|
|
1842
|
+
channel,
|
|
1796
1843
|
to: session.whatsappNumber,
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1844
|
+
objectStorage: params.objectStorage,
|
|
1845
|
+
...params.mediaIdCache ? {
|
|
1846
|
+
cache: params.mediaIdCache
|
|
1847
|
+
} : {}
|
|
1801
1848
|
});
|
|
1802
1849
|
await params.logMessage.execute({
|
|
1803
1850
|
companyId: session.companyId,
|
|
@@ -1986,6 +2033,42 @@ var FlowMediaRepository = class {
|
|
|
1986
2033
|
}
|
|
1987
2034
|
};
|
|
1988
2035
|
|
|
2036
|
+
// src/repositories/FlowMediaIdRepository.ts
|
|
2037
|
+
var import_drizzle_orm8 = require("drizzle-orm");
|
|
2038
|
+
var FlowMediaIdRepository = class {
|
|
2039
|
+
static {
|
|
2040
|
+
__name(this, "FlowMediaIdRepository");
|
|
2041
|
+
}
|
|
2042
|
+
db;
|
|
2043
|
+
constructor(db) {
|
|
2044
|
+
this.db = db;
|
|
2045
|
+
}
|
|
2046
|
+
async get(params) {
|
|
2047
|
+
const rows = await this.db.select({
|
|
2048
|
+
ids: flowMedia.metaMediaIds
|
|
2049
|
+
}).from(flowMedia).where((0, import_drizzle_orm8.eq)(flowMedia.id, params.flowMediaId)).limit(1);
|
|
2050
|
+
return rows[0]?.ids?.[params.senderKey];
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Grava só a chave deste número.
|
|
2054
|
+
*
|
|
2055
|
+
* `jsonb_set` no banco, e não ler-alterar-escrever na aplicação: dois clientes passando pelo nó
|
|
2056
|
+
* ao mesmo tempo com números diferentes leriam o mesmo mapa e o último gravaria por cima,
|
|
2057
|
+
* apagando o id do outro. A escrita atômica não tem essa janela.
|
|
2058
|
+
*/
|
|
2059
|
+
async set(params) {
|
|
2060
|
+
await this.db.update(flowMedia).set({
|
|
2061
|
+
metaMediaIds: import_drizzle_orm8.sql`jsonb_set(${flowMedia.metaMediaIds}, ARRAY[${params.senderKey}::text], to_jsonb(${params.mediaId}::text), true)`
|
|
2062
|
+
}).where((0, import_drizzle_orm8.eq)(flowMedia.id, params.flowMediaId));
|
|
2063
|
+
}
|
|
2064
|
+
/** Remove só a chave deste número — o id do outro número continua válido. */
|
|
2065
|
+
async clear(params) {
|
|
2066
|
+
await this.db.update(flowMedia).set({
|
|
2067
|
+
metaMediaIds: import_drizzle_orm8.sql`${flowMedia.metaMediaIds} - ${params.senderKey}::text`
|
|
2068
|
+
}).where((0, import_drizzle_orm8.eq)(flowMedia.id, params.flowMediaId));
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
|
|
1989
2072
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1990
2073
|
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1991
2074
|
var import_meta_whatsapp_contracts9 = require("@adatechnology/meta-whatsapp-contracts");
|
|
@@ -2021,7 +2104,8 @@ var WhatsAppChannelAdapter = class {
|
|
|
2021
2104
|
async sendMedia(params) {
|
|
2022
2105
|
const result = await this.translateErrors(() => this.messages.sendMedia(params));
|
|
2023
2106
|
return {
|
|
2024
|
-
externalMessageId: result.waMessageId
|
|
2107
|
+
externalMessageId: result.waMessageId,
|
|
2108
|
+
mediaId: result.mediaId
|
|
2025
2109
|
};
|
|
2026
2110
|
}
|
|
2027
2111
|
async sendTemplate(params) {
|
|
@@ -2122,7 +2206,7 @@ async function confirmWebhookDelivery(params) {
|
|
|
2122
2206
|
__name(confirmWebhookDelivery, "confirmWebhookDelivery");
|
|
2123
2207
|
|
|
2124
2208
|
// src/channel/IngestInboundMedia.use-case.ts
|
|
2125
|
-
var
|
|
2209
|
+
var import_drizzle_orm9 = require("drizzle-orm");
|
|
2126
2210
|
|
|
2127
2211
|
// src/use-cases/TranscribeAudio.use-case.ts
|
|
2128
2212
|
var import_meta_whatsapp_contracts11 = require("@adatechnology/meta-whatsapp-contracts");
|
|
@@ -2294,7 +2378,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
2294
2378
|
this.transcription = transcription;
|
|
2295
2379
|
}
|
|
2296
2380
|
async execute(params) {
|
|
2297
|
-
const [message] = await this.db.select().from(messages).where((0,
|
|
2381
|
+
const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm9.and)((0, import_drizzle_orm9.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm9.eq)(messages.id, params.messageId))).limit(1);
|
|
2298
2382
|
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
2299
2383
|
const payload = message.payload ?? {};
|
|
2300
2384
|
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
@@ -2325,7 +2409,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
2325
2409
|
};
|
|
2326
2410
|
await this.db.update(messages).set({
|
|
2327
2411
|
payload: updatedPayload
|
|
2328
|
-
}).where((0,
|
|
2412
|
+
}).where((0, import_drizzle_orm9.and)((0, import_drizzle_orm9.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm9.eq)(messages.id, params.messageId)));
|
|
2329
2413
|
await this.documentRepository?.link({
|
|
2330
2414
|
companyId: params.companyId,
|
|
2331
2415
|
sessionId: message.sessionId,
|
|
@@ -2524,6 +2608,13 @@ var ProcessInboundDispatchUseCase = class {
|
|
|
2524
2608
|
};
|
|
2525
2609
|
|
|
2526
2610
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
2611
|
+
var EMPTY_RESULT = {
|
|
2612
|
+
messagesProcessed: 0,
|
|
2613
|
+
statusesProcessed: 0,
|
|
2614
|
+
ignoredForeignNumber: 0,
|
|
2615
|
+
accountEventsProcessed: 0,
|
|
2616
|
+
unhandledEvents: 0
|
|
2617
|
+
};
|
|
2527
2618
|
function extractContent(message) {
|
|
2528
2619
|
if (message.text?.body) return message.text.body;
|
|
2529
2620
|
const interactive = message.interactive;
|
|
@@ -2549,6 +2640,14 @@ function extractPayload(message) {
|
|
|
2549
2640
|
return Object.keys(payload).length > 0 ? payload : null;
|
|
2550
2641
|
}
|
|
2551
2642
|
__name(extractPayload, "extractPayload");
|
|
2643
|
+
var ACCOUNT_EVENT_FIELDS = [
|
|
2644
|
+
import_meta_whatsapp_contracts12.WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE,
|
|
2645
|
+
import_meta_whatsapp_contracts12.WHATSAPP_WEBHOOK_FIELDS.PHONE_NUMBER_QUALITY_UPDATE
|
|
2646
|
+
];
|
|
2647
|
+
function isAccountEventField(field) {
|
|
2648
|
+
return field !== void 0 && ACCOUNT_EVENT_FIELDS.includes(field);
|
|
2649
|
+
}
|
|
2650
|
+
__name(isAccountEventField, "isAccountEventField");
|
|
2552
2651
|
var ReceiveWebhookUseCase = class {
|
|
2553
2652
|
static {
|
|
2554
2653
|
__name(this, "ReceiveWebhookUseCase");
|
|
@@ -2579,17 +2678,36 @@ var ReceiveWebhookUseCase = class {
|
|
|
2579
2678
|
});
|
|
2580
2679
|
if (!claimed) return {
|
|
2581
2680
|
duplicate: true,
|
|
2582
|
-
|
|
2583
|
-
statusesProcessed: 0,
|
|
2584
|
-
ignoredForeignNumber: 0
|
|
2681
|
+
...EMPTY_RESULT
|
|
2585
2682
|
};
|
|
2586
2683
|
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
2587
|
-
const
|
|
2684
|
+
const parsed = import_meta_whatsapp_contracts12.whatsAppWebhookPayloadSchema.safeParse(JSON.parse(rawText));
|
|
2685
|
+
if (!parsed.success) {
|
|
2686
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2687
|
+
field: void 0,
|
|
2688
|
+
reason: "invalid-shape",
|
|
2689
|
+
value: rawText
|
|
2690
|
+
});
|
|
2691
|
+
return {
|
|
2692
|
+
duplicate: false,
|
|
2693
|
+
...EMPTY_RESULT,
|
|
2694
|
+
unhandledEvents: 1
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
const payload = parsed.data;
|
|
2588
2698
|
let messagesProcessed = 0;
|
|
2589
2699
|
let statusesProcessed = 0;
|
|
2590
2700
|
let ignoredForeignNumber = 0;
|
|
2701
|
+
let accountEventsProcessed = 0;
|
|
2702
|
+
let unhandledEvents = 0;
|
|
2591
2703
|
for (const entry of payload.entry) {
|
|
2592
2704
|
for (const change of entry.changes) {
|
|
2705
|
+
if (isAccountEventField(change.field)) {
|
|
2706
|
+
const handled = await this.handleAccountEvent(change);
|
|
2707
|
+
if (handled) accountEventsProcessed++;
|
|
2708
|
+
else unhandledEvents++;
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2593
2711
|
const targetNumber = change.value.metadata?.phone_number_id;
|
|
2594
2712
|
if (targetNumber && targetNumber !== this.params.phoneNumberId) {
|
|
2595
2713
|
ignoredForeignNumber++;
|
|
@@ -2603,6 +2721,15 @@ var ReceiveWebhookUseCase = class {
|
|
|
2603
2721
|
await this.handleStatus(input.companyId, status);
|
|
2604
2722
|
statusesProcessed++;
|
|
2605
2723
|
}
|
|
2724
|
+
const carriedConversationData = (change.value.messages?.length ?? 0) > 0 || (change.value.message_echoes?.length ?? 0) > 0 || (change.value.statuses?.length ?? 0) > 0;
|
|
2725
|
+
if (!carriedConversationData) {
|
|
2726
|
+
unhandledEvents++;
|
|
2727
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2728
|
+
field: change.field,
|
|
2729
|
+
reason: "unknown-field",
|
|
2730
|
+
value: change.value
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2606
2733
|
}
|
|
2607
2734
|
}
|
|
2608
2735
|
await confirmWebhookDelivery({
|
|
@@ -2613,9 +2740,39 @@ var ReceiveWebhookUseCase = class {
|
|
|
2613
2740
|
duplicate: false,
|
|
2614
2741
|
messagesProcessed,
|
|
2615
2742
|
statusesProcessed,
|
|
2616
|
-
ignoredForeignNumber
|
|
2743
|
+
ignoredForeignNumber,
|
|
2744
|
+
accountEventsProcessed,
|
|
2745
|
+
unhandledEvents
|
|
2617
2746
|
};
|
|
2618
2747
|
}
|
|
2748
|
+
// Devolve `false` quando o corpo não bate com o schema do field — o chamador conta como não
|
|
2749
|
+
// tratado. O host é avisado nos dois casos, mas com `reason` diferente.
|
|
2750
|
+
async handleAccountEvent(change) {
|
|
2751
|
+
if (change.field === import_meta_whatsapp_contracts12.WHATSAPP_WEBHOOK_FIELDS.TEMPLATE_STATUS_UPDATE) {
|
|
2752
|
+
const update = import_meta_whatsapp_contracts12.whatsAppTemplateStatusUpdateSchema.safeParse(change.value);
|
|
2753
|
+
if (!update.success) {
|
|
2754
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2755
|
+
field: change.field,
|
|
2756
|
+
reason: "invalid-shape",
|
|
2757
|
+
value: change.value
|
|
2758
|
+
});
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
await this.params.hooks?.onTemplateStatusUpdate?.(update.data);
|
|
2762
|
+
return true;
|
|
2763
|
+
}
|
|
2764
|
+
const quality = import_meta_whatsapp_contracts12.whatsAppPhoneNumberQualityUpdateSchema.safeParse(change.value);
|
|
2765
|
+
if (!quality.success) {
|
|
2766
|
+
await this.params.hooks?.onUnhandledWebhookEvent?.({
|
|
2767
|
+
field: change.field,
|
|
2768
|
+
reason: "invalid-shape",
|
|
2769
|
+
value: change.value
|
|
2770
|
+
});
|
|
2771
|
+
return false;
|
|
2772
|
+
}
|
|
2773
|
+
await this.params.hooks?.onPhoneNumberQualityUpdate?.(quality.data);
|
|
2774
|
+
return true;
|
|
2775
|
+
}
|
|
2619
2776
|
async handleMessage(companyId, message) {
|
|
2620
2777
|
const saved = await this.params.logMessage.execute({
|
|
2621
2778
|
companyId,
|
|
@@ -2760,6 +2917,14 @@ function createMetaWhatsAppModule(params) {
|
|
|
2760
2917
|
objectStorage: providers.objectStorage,
|
|
2761
2918
|
logMessage,
|
|
2762
2919
|
startState,
|
|
2920
|
+
// Cache ligado por padrão, com o store do próprio módulo: a tabela é daqui e a coluna é
|
|
2921
|
+
// daqui, então exigir que cada host escrevesse o repositório faria todos reescreverem o
|
|
2922
|
+
// mesmo código — e, até escreverem, o mesmo binário continuaria subindo por cliente. Quem
|
|
2923
|
+
// quiser outro lugar (Redis) troca o `store`; quem não fizer nada já sai sem ressubir.
|
|
2924
|
+
mediaIdCache: {
|
|
2925
|
+
store: new FlowMediaIdRepository(db),
|
|
2926
|
+
senderKey: config.phoneNumberId
|
|
2927
|
+
},
|
|
2763
2928
|
onError: hooks?.onFlowMediaError
|
|
2764
2929
|
}));
|
|
2765
2930
|
}
|
|
@@ -2977,6 +3142,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
2977
3142
|
FlowGraphCache,
|
|
2978
3143
|
FlowGraphRepository,
|
|
2979
3144
|
FlowInterpreter,
|
|
3145
|
+
FlowMediaIdRepository,
|
|
2980
3146
|
FlowMediaRepository,
|
|
2981
3147
|
GetFlowGraphUseCase,
|
|
2982
3148
|
GetLiveFlowPositionsUseCase,
|