@adatechnology/meta-whatsapp-module 0.2.0-rc.13 → 0.2.0-rc.14
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 +214 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -1
- package/dist/index.d.ts +333 -1
- package/dist/index.js +211 -4
- package/dist/index.js.map +1 -1
- package/dist/migrations/0007_flow_media.sql +18 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
ExportConversationUseCase: () => ExportConversationUseCase,
|
|
29
29
|
FlowGraphRepository: () => FlowGraphRepository,
|
|
30
30
|
FlowInterpreter: () => FlowInterpreter,
|
|
31
|
+
FlowMediaRepository: () => FlowMediaRepository,
|
|
31
32
|
GetFlowGraphUseCase: () => GetFlowGraphUseCase,
|
|
32
33
|
GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
|
|
33
34
|
IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
|
|
@@ -54,9 +55,11 @@ __export(index_exports, {
|
|
|
54
55
|
WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
|
|
55
56
|
claimWebhookDelivery: () => claimWebhookDelivery,
|
|
56
57
|
createMetaWhatsAppModule: () => createMetaWhatsAppModule,
|
|
58
|
+
createSendMediaAction: () => createSendMediaAction,
|
|
57
59
|
documents: () => documents,
|
|
58
60
|
extractMediaDescriptor: () => extractMediaDescriptor,
|
|
59
61
|
flowGraphs: () => flowGraphs,
|
|
62
|
+
flowMedia: () => flowMedia,
|
|
60
63
|
issueSseTicket: () => issueSseTicket,
|
|
61
64
|
messages: () => messages,
|
|
62
65
|
metaWhatsAppMigrationsFolder: () => metaWhatsAppMigrationsFolder,
|
|
@@ -72,6 +75,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
72
75
|
|
|
73
76
|
// src/createMetaWhatsAppModule.ts
|
|
74
77
|
var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
|
|
78
|
+
var import_meta_whatsapp_contracts11 = require("@adatechnology/meta-whatsapp-contracts");
|
|
75
79
|
|
|
76
80
|
// src/repositories/SessionRepository.ts
|
|
77
81
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
@@ -259,6 +263,46 @@ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
|
259
263
|
}, (table) => [
|
|
260
264
|
(0, import_pg_core.uniqueIndex)("idx_flow_graphs_company_key").on(table.companyId, table.key)
|
|
261
265
|
]);
|
|
266
|
+
var flowMedia = metaWhatsAppSchema.table("flow_media", {
|
|
267
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
268
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
269
|
+
flowKey: (0, import_pg_core.varchar)("flow_key", {
|
|
270
|
+
length: 64
|
|
271
|
+
}).notNull(),
|
|
272
|
+
nodeId: (0, import_pg_core.varchar)("node_id", {
|
|
273
|
+
length: 64
|
|
274
|
+
}).notNull(),
|
|
275
|
+
uploadId: (0, import_pg_core.varchar)("upload_id", {
|
|
276
|
+
length: 256
|
|
277
|
+
}).notNull(),
|
|
278
|
+
filename: (0, import_pg_core.varchar)("filename", {
|
|
279
|
+
length: 512
|
|
280
|
+
}).notNull(),
|
|
281
|
+
mimeType: (0, import_pg_core.varchar)("mime_type", {
|
|
282
|
+
length: 128
|
|
283
|
+
}).notNull(),
|
|
284
|
+
sizeBytes: (0, import_pg_core.integer)("size_bytes").notNull(),
|
|
285
|
+
// Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e
|
|
286
|
+
// um folder precisa de textos diferentes para cada um.
|
|
287
|
+
caption: (0, import_pg_core.text)("caption"),
|
|
288
|
+
// Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e "tabela antes
|
|
289
|
+
// do folder" é decisão de quem edita, não do banco.
|
|
290
|
+
sortOrder: (0, import_pg_core.integer)("sort_order").notNull().default(0),
|
|
291
|
+
// Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
|
|
292
|
+
// perderia a ordem e a legenda já ajustadas.
|
|
293
|
+
active: (0, import_pg_core.boolean)("active").notNull().default(true),
|
|
294
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
295
|
+
withTimezone: true
|
|
296
|
+
}).notNull().defaultNow(),
|
|
297
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", {
|
|
298
|
+
withTimezone: true
|
|
299
|
+
}).notNull().defaultNow()
|
|
300
|
+
}, (table) => [
|
|
301
|
+
(0, import_pg_core.index)("idx_flow_media_node").on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),
|
|
302
|
+
// O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente
|
|
303
|
+
// receberia o documento repetido.
|
|
304
|
+
(0, import_pg_core.uniqueIndex)("idx_flow_media_node_upload").on(table.companyId, table.flowKey, table.nodeId, table.uploadId)
|
|
305
|
+
]);
|
|
262
306
|
var settings = metaWhatsAppSchema.table("settings", {
|
|
263
307
|
companyId: (0, import_pg_core.uuid)("company_id").primaryKey(),
|
|
264
308
|
templateName: (0, import_pg_core.varchar)("template_name", {
|
|
@@ -1551,6 +1595,151 @@ var FlowInterpreter = class {
|
|
|
1551
1595
|
}
|
|
1552
1596
|
};
|
|
1553
1597
|
|
|
1598
|
+
// src/flows/createSendMediaAction.ts
|
|
1599
|
+
function mediaTypeFor2(mimeType) {
|
|
1600
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
1601
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
1602
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
1603
|
+
return "document";
|
|
1604
|
+
}
|
|
1605
|
+
__name(mediaTypeFor2, "mediaTypeFor");
|
|
1606
|
+
function createSendMediaAction(params) {
|
|
1607
|
+
return async ({ node, session, channel }) => {
|
|
1608
|
+
if (!session.flowKey) return;
|
|
1609
|
+
const location = {
|
|
1610
|
+
companyId: session.companyId,
|
|
1611
|
+
flowKey: session.flowKey,
|
|
1612
|
+
nodeId: node.id
|
|
1613
|
+
};
|
|
1614
|
+
const attachments = await params.flowMediaRepository.listActive(location);
|
|
1615
|
+
for (const attachment of attachments) {
|
|
1616
|
+
try {
|
|
1617
|
+
const buffer = await params.objectStorage.getObject(attachment.uploadId);
|
|
1618
|
+
const { externalMessageId } = await channel.sendMedia({
|
|
1619
|
+
to: session.whatsappNumber,
|
|
1620
|
+
buffer,
|
|
1621
|
+
mimeType: attachment.mimeType,
|
|
1622
|
+
filename: attachment.filename,
|
|
1623
|
+
caption: attachment.caption ?? void 0
|
|
1624
|
+
});
|
|
1625
|
+
await params.logMessage.execute({
|
|
1626
|
+
companyId: session.companyId,
|
|
1627
|
+
whatsappNumber: session.whatsappNumber,
|
|
1628
|
+
direction: "outbound",
|
|
1629
|
+
sender: "bot",
|
|
1630
|
+
agentUserId: null,
|
|
1631
|
+
type: mediaTypeFor2(attachment.mimeType),
|
|
1632
|
+
content: attachment.caption ?? attachment.filename,
|
|
1633
|
+
payload: {
|
|
1634
|
+
filename: attachment.filename,
|
|
1635
|
+
mimeType: attachment.mimeType,
|
|
1636
|
+
uploadId: attachment.uploadId,
|
|
1637
|
+
flowMediaId: attachment.id
|
|
1638
|
+
},
|
|
1639
|
+
waMessageId: externalMessageId,
|
|
1640
|
+
status: "sent",
|
|
1641
|
+
startState: params.startState
|
|
1642
|
+
});
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
params.onError?.(error, {
|
|
1645
|
+
flowKey: location.flowKey,
|
|
1646
|
+
nodeId: node.id,
|
|
1647
|
+
uploadId: attachment.uploadId
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
__name(createSendMediaAction, "createSendMediaAction");
|
|
1654
|
+
|
|
1655
|
+
// src/repositories/FlowMediaRepository.ts
|
|
1656
|
+
var import_drizzle_orm7 = require("drizzle-orm");
|
|
1657
|
+
var FlowMediaRepository = class {
|
|
1658
|
+
static {
|
|
1659
|
+
__name(this, "FlowMediaRepository");
|
|
1660
|
+
}
|
|
1661
|
+
db;
|
|
1662
|
+
constructor(db) {
|
|
1663
|
+
this.db = db;
|
|
1664
|
+
}
|
|
1665
|
+
locationFilter(location) {
|
|
1666
|
+
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));
|
|
1667
|
+
}
|
|
1668
|
+
// O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:
|
|
1669
|
+
// é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente
|
|
1670
|
+
// justamente o material que alguém desligou.
|
|
1671
|
+
async listActive(location) {
|
|
1672
|
+
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));
|
|
1673
|
+
}
|
|
1674
|
+
// Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para
|
|
1675
|
+
// poder ser religado.
|
|
1676
|
+
async listAll(location) {
|
|
1677
|
+
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));
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Anexa um arquivo já existente no storage ao nó.
|
|
1681
|
+
*
|
|
1682
|
+
* `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
|
|
1683
|
+
* editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
|
|
1684
|
+
* quem está montando o fluxo.
|
|
1685
|
+
*/
|
|
1686
|
+
async attach(params) {
|
|
1687
|
+
const [row] = await this.db.insert(flowMedia).values({
|
|
1688
|
+
companyId: params.companyId,
|
|
1689
|
+
flowKey: params.flowKey,
|
|
1690
|
+
nodeId: params.nodeId,
|
|
1691
|
+
uploadId: params.uploadId,
|
|
1692
|
+
filename: params.filename,
|
|
1693
|
+
mimeType: params.mimeType,
|
|
1694
|
+
sizeBytes: params.sizeBytes,
|
|
1695
|
+
caption: params.caption ?? null,
|
|
1696
|
+
sortOrder: params.sortOrder ?? 0
|
|
1697
|
+
}).onConflictDoUpdate({
|
|
1698
|
+
target: [
|
|
1699
|
+
flowMedia.companyId,
|
|
1700
|
+
flowMedia.flowKey,
|
|
1701
|
+
flowMedia.nodeId,
|
|
1702
|
+
flowMedia.uploadId
|
|
1703
|
+
],
|
|
1704
|
+
set: {
|
|
1705
|
+
caption: params.caption ?? null,
|
|
1706
|
+
sortOrder: params.sortOrder ?? 0,
|
|
1707
|
+
active: true,
|
|
1708
|
+
updatedAt: import_drizzle_orm7.sql`now()`
|
|
1709
|
+
}
|
|
1710
|
+
}).returning();
|
|
1711
|
+
return row;
|
|
1712
|
+
}
|
|
1713
|
+
async update(params) {
|
|
1714
|
+
const [row] = await this.db.update(flowMedia).set({
|
|
1715
|
+
...params.caption !== void 0 ? {
|
|
1716
|
+
caption: params.caption
|
|
1717
|
+
} : {},
|
|
1718
|
+
...params.sortOrder !== void 0 ? {
|
|
1719
|
+
sortOrder: params.sortOrder
|
|
1720
|
+
} : {},
|
|
1721
|
+
...params.active !== void 0 ? {
|
|
1722
|
+
active: params.active
|
|
1723
|
+
} : {},
|
|
1724
|
+
updatedAt: import_drizzle_orm7.sql`now()`
|
|
1725
|
+
}).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();
|
|
1726
|
+
return row;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
|
|
1730
|
+
* outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
|
|
1731
|
+
* host, que é dono da biblioteca de arquivos.
|
|
1732
|
+
*/
|
|
1733
|
+
async detach(params) {
|
|
1734
|
+
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)));
|
|
1735
|
+
}
|
|
1736
|
+
// Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.
|
|
1737
|
+
async detachRemovedNodes(params) {
|
|
1738
|
+
const condition = params.existingNodeIds.length === 0 ? void 0 : import_drizzle_orm7.sql`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`;
|
|
1739
|
+
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));
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1554
1743
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1555
1744
|
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1556
1745
|
var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
|
|
@@ -1647,7 +1836,7 @@ async function claimWebhookDelivery(params) {
|
|
|
1647
1836
|
__name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
1648
1837
|
|
|
1649
1838
|
// src/channel/IngestInboundMedia.use-case.ts
|
|
1650
|
-
var
|
|
1839
|
+
var import_drizzle_orm8 = require("drizzle-orm");
|
|
1651
1840
|
var IngestInboundMediaUseCase = class {
|
|
1652
1841
|
static {
|
|
1653
1842
|
__name(this, "IngestInboundMediaUseCase");
|
|
@@ -1663,7 +1852,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
1663
1852
|
this.documentRepository = documentRepository;
|
|
1664
1853
|
}
|
|
1665
1854
|
async execute(params) {
|
|
1666
|
-
const [message] = await this.db.select().from(messages).where((0,
|
|
1855
|
+
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);
|
|
1667
1856
|
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1668
1857
|
const payload = message.payload ?? {};
|
|
1669
1858
|
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
@@ -1684,13 +1873,17 @@ var IngestInboundMediaUseCase = class {
|
|
|
1684
1873
|
uploadId,
|
|
1685
1874
|
sourceMediaId: params.sourceMediaId,
|
|
1686
1875
|
mimeType: mimeType || params.mimeType,
|
|
1876
|
+
// O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o
|
|
1877
|
+
// exibe ao lado do tipo. Sem gravar aqui, a UI mostraria "PDF" sem o "· 180 KB", e buscá-lo
|
|
1878
|
+
// depois custaria uma consulta à tabela de documentos por mensagem renderizada.
|
|
1879
|
+
sizeBytes: buffer.length,
|
|
1687
1880
|
...params.filename ? {
|
|
1688
1881
|
filename: params.filename
|
|
1689
1882
|
} : {}
|
|
1690
1883
|
};
|
|
1691
1884
|
await this.db.update(messages).set({
|
|
1692
1885
|
payload: updatedPayload
|
|
1693
|
-
}).where((0,
|
|
1886
|
+
}).where((0, import_drizzle_orm8.and)((0, import_drizzle_orm8.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm8.eq)(messages.id, params.messageId)));
|
|
1694
1887
|
await this.documentRepository?.link({
|
|
1695
1888
|
companyId: params.companyId,
|
|
1696
1889
|
sessionId: message.sessionId,
|
|
@@ -1892,6 +2085,7 @@ function createMetaWhatsAppModule(params) {
|
|
|
1892
2085
|
const settingsRepository = new SettingsRepository(db);
|
|
1893
2086
|
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1894
2087
|
const documentRepository = new DocumentRepository(db);
|
|
2088
|
+
const flowMediaRepository = new FlowMediaRepository(db);
|
|
1895
2089
|
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
1896
2090
|
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
|
|
1897
2091
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
@@ -1905,6 +2099,15 @@ function createMetaWhatsAppModule(params) {
|
|
|
1905
2099
|
realtime: providers.realtime
|
|
1906
2100
|
});
|
|
1907
2101
|
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
2102
|
+
if (flowInterpreter && providers.objectStorage?.getObject) {
|
|
2103
|
+
flowInterpreter.registerFlowAction(import_meta_whatsapp_contracts11.FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
|
|
2104
|
+
flowMediaRepository,
|
|
2105
|
+
objectStorage: providers.objectStorage,
|
|
2106
|
+
logMessage,
|
|
2107
|
+
startState,
|
|
2108
|
+
onError: hooks?.onFlowMediaError
|
|
2109
|
+
}));
|
|
2110
|
+
}
|
|
1908
2111
|
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository) : void 0;
|
|
1909
2112
|
const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
|
|
1910
2113
|
return {
|
|
@@ -1948,7 +2151,11 @@ function createMetaWhatsAppModule(params) {
|
|
|
1948
2151
|
save: new SaveFlowGraphUseCase(flowGraphRepository),
|
|
1949
2152
|
delete: new DeleteFlowGraphUseCase(flowGraphRepository),
|
|
1950
2153
|
livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
|
|
1951
|
-
repository: flowGraphRepository
|
|
2154
|
+
repository: flowGraphRepository,
|
|
2155
|
+
// Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor
|
|
2156
|
+
// (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é
|
|
2157
|
+
// consultar a tabela, e só o ENVIO precisa dos bytes.
|
|
2158
|
+
mediaRepository: flowMediaRepository
|
|
1952
2159
|
} : void 0,
|
|
1953
2160
|
catalog: providers.catalog
|
|
1954
2161
|
};
|
|
@@ -2054,6 +2261,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
2054
2261
|
ExportConversationUseCase,
|
|
2055
2262
|
FlowGraphRepository,
|
|
2056
2263
|
FlowInterpreter,
|
|
2264
|
+
FlowMediaRepository,
|
|
2057
2265
|
GetFlowGraphUseCase,
|
|
2058
2266
|
GetLiveFlowPositionsUseCase,
|
|
2059
2267
|
IngestInboundMediaUseCase,
|
|
@@ -2080,9 +2288,11 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
2080
2288
|
WhatsAppChannelAdapter,
|
|
2081
2289
|
claimWebhookDelivery,
|
|
2082
2290
|
createMetaWhatsAppModule,
|
|
2291
|
+
createSendMediaAction,
|
|
2083
2292
|
documents,
|
|
2084
2293
|
extractMediaDescriptor,
|
|
2085
2294
|
flowGraphs,
|
|
2295
|
+
flowMedia,
|
|
2086
2296
|
issueSseTicket,
|
|
2087
2297
|
messages,
|
|
2088
2298
|
metaWhatsAppMigrationsFolder,
|