@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 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 import_drizzle_orm7 = require("drizzle-orm");
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, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm7.eq)(messages.id, params.messageId))).limit(1);
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, import_drizzle_orm7.and)((0, import_drizzle_orm7.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm7.eq)(messages.id, params.messageId)));
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,