@adatechnology/meta-whatsapp-module 0.1.0
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 +1560 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1306 -0
- package/dist/index.d.ts +1306 -0
- package/dist/index.js +1506 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/20260725195853_freezing_switch/migration.sql +41 -0
- package/dist/migrations/20260725195853_freezing_switch/snapshot.json +555 -0
- package/dist/migrations/20260725210958_military_leper_queen/migration.sql +15 -0
- package/dist/migrations/20260725210958_military_leper_queen/snapshot.json +742 -0
- package/dist/migrations/20260725214800_illegal_black_tarantula/migration.sql +5 -0
- package/dist/migrations/20260725214800_illegal_black_tarantula/snapshot.json +810 -0
- package/dist/migrations/20260725234507_normal_viper/migration.sql +10 -0
- package/dist/migrations/20260725234507_normal_viper/snapshot.json +930 -0
- package/package.json +29 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1560 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc2) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CreateFlowGraphUseCase: () => CreateFlowGraphUseCase,
|
|
24
|
+
DeleteFlowGraphUseCase: () => DeleteFlowGraphUseCase,
|
|
25
|
+
ExportConversationUseCase: () => ExportConversationUseCase,
|
|
26
|
+
FlowGraphRepository: () => FlowGraphRepository,
|
|
27
|
+
FlowInterpreter: () => FlowInterpreter,
|
|
28
|
+
GetFlowGraphUseCase: () => GetFlowGraphUseCase,
|
|
29
|
+
GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
|
|
30
|
+
IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
|
|
31
|
+
InvalidFlowGraphError: () => InvalidFlowGraphError,
|
|
32
|
+
ListConversationsUseCase: () => ListConversationsUseCase,
|
|
33
|
+
ListFlowGraphsUseCase: () => ListFlowGraphsUseCase,
|
|
34
|
+
ListMessagesUseCase: () => ListMessagesUseCase,
|
|
35
|
+
LogMessageUseCase: () => LogMessageUseCase,
|
|
36
|
+
MessageRepository: () => MessageRepository,
|
|
37
|
+
OptimisticLockError: () => OptimisticLockError,
|
|
38
|
+
ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
|
|
39
|
+
ReleaseConversationUseCase: () => ReleaseConversationUseCase,
|
|
40
|
+
SaveFlowGraphUseCase: () => SaveFlowGraphUseCase,
|
|
41
|
+
SendMessageUseCase: () => SendMessageUseCase,
|
|
42
|
+
SessionRepository: () => SessionRepository,
|
|
43
|
+
SettingsRepository: () => SettingsRepository,
|
|
44
|
+
SseHub: () => SseHub,
|
|
45
|
+
TakeoverConversationUseCase: () => TakeoverConversationUseCase,
|
|
46
|
+
WEBHOOK_NONCE_TTL_SECONDS: () => WEBHOOK_NONCE_TTL_SECONDS,
|
|
47
|
+
WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
|
|
48
|
+
claimWebhookDelivery: () => claimWebhookDelivery,
|
|
49
|
+
createMetaWhatsAppModule: () => createMetaWhatsAppModule,
|
|
50
|
+
extractMediaDescriptor: () => extractMediaDescriptor,
|
|
51
|
+
flowGraphs: () => flowGraphs,
|
|
52
|
+
issueSseTicket: () => issueSseTicket,
|
|
53
|
+
messages: () => messages,
|
|
54
|
+
metaWhatsAppSchema: () => metaWhatsAppSchema,
|
|
55
|
+
redeemSseTicket: () => redeemSseTicket,
|
|
56
|
+
runMetaWhatsAppMigrations: () => runMetaWhatsAppMigrations,
|
|
57
|
+
sessions: () => sessions,
|
|
58
|
+
settings: () => settings,
|
|
59
|
+
verifyWebhookChallenge: () => verifyWebhookChallenge,
|
|
60
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
61
|
+
});
|
|
62
|
+
module.exports = __toCommonJS(index_exports);
|
|
63
|
+
|
|
64
|
+
// src/createMetaWhatsAppModule.ts
|
|
65
|
+
var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
|
|
66
|
+
|
|
67
|
+
// src/repositories/SessionRepository.ts
|
|
68
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
69
|
+
|
|
70
|
+
// src/schema/schema.ts
|
|
71
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
72
|
+
var import_pg_core = require("drizzle-orm/pg-core");
|
|
73
|
+
var metaWhatsAppSchema = (0, import_pg_core.pgSchema)("meta_whatsapp");
|
|
74
|
+
var sessions = metaWhatsAppSchema.table("sessions", {
|
|
75
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
76
|
+
// Multiempresa: toda entidade carrega companyId, vindo do contexto autenticado do host —
|
|
77
|
+
// nunca de um campo livre do cliente (ver database.md "Consistência e multiempresa").
|
|
78
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
79
|
+
whatsappNumber: (0, import_pg_core.varchar)("whatsapp_number", {
|
|
80
|
+
length: 20
|
|
81
|
+
}).notNull(),
|
|
82
|
+
// varchar (não enum): a máquina de estados de cada produto evolui de forma independente do módulo.
|
|
83
|
+
currentState: (0, import_pg_core.varchar)("current_state", {
|
|
84
|
+
length: 64
|
|
85
|
+
}).notNull().default("start"),
|
|
86
|
+
// Posição no grafo, quando o motor de fluxo está em uso — colunas separadas em vez de
|
|
87
|
+
// convencionar "flowKey:nodeId" dentro de currentState: além de ser um contrato implícito que
|
|
88
|
+
// nada garantia, a concatenação estourava o varchar(64) (flow key já é varchar(64) sozinha).
|
|
89
|
+
flowKey: (0, import_pg_core.varchar)("flow_key", {
|
|
90
|
+
length: 64
|
|
91
|
+
}),
|
|
92
|
+
currentNodeId: (0, import_pg_core.varchar)("current_node_id", {
|
|
93
|
+
length: 64
|
|
94
|
+
}),
|
|
95
|
+
context: (0, import_pg_core.jsonb)("context").$type().notNull().default({}),
|
|
96
|
+
mode: (0, import_pg_core.varchar)("mode", {
|
|
97
|
+
length: 12
|
|
98
|
+
}).notNull().default("bot"),
|
|
99
|
+
assignedUserId: (0, import_pg_core.uuid)("assigned_user_id"),
|
|
100
|
+
humanRequestedAt: (0, import_pg_core.timestamp)("human_requested_at", {
|
|
101
|
+
withTimezone: true
|
|
102
|
+
}),
|
|
103
|
+
lastInboundAt: (0, import_pg_core.timestamp)("last_inbound_at", {
|
|
104
|
+
withTimezone: true
|
|
105
|
+
}),
|
|
106
|
+
lastAgentReadAt: (0, import_pg_core.timestamp)("last_agent_read_at", {
|
|
107
|
+
withTimezone: true
|
|
108
|
+
}),
|
|
109
|
+
lastActivity: (0, import_pg_core.timestamp)("last_activity", {
|
|
110
|
+
withTimezone: true
|
|
111
|
+
}).notNull().defaultNow(),
|
|
112
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
113
|
+
withTimezone: true
|
|
114
|
+
}).notNull().defaultNow(),
|
|
115
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", {
|
|
116
|
+
withTimezone: true
|
|
117
|
+
}).notNull().defaultNow()
|
|
118
|
+
}, (table) => [
|
|
119
|
+
// Único por empresa (não globalmente) — dois hosts diferentes podem atender o mesmo número.
|
|
120
|
+
(0, import_pg_core.uniqueIndex)("idx_sessions_company_number").on(table.companyId, table.whatsappNumber),
|
|
121
|
+
(0, import_pg_core.index)("idx_sessions_company_mode").on(table.companyId, table.mode),
|
|
122
|
+
// Alimenta getLiveFlowPositions (agregação por nó do grafo).
|
|
123
|
+
(0, import_pg_core.index)("idx_sessions_company_flow_node").on(table.companyId, table.flowKey, table.currentNodeId)
|
|
124
|
+
]);
|
|
125
|
+
var messages = metaWhatsAppSchema.table("messages", {
|
|
126
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
127
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
128
|
+
sessionId: (0, import_pg_core.uuid)("session_id").notNull().references(() => sessions.id, {
|
|
129
|
+
onDelete: "cascade"
|
|
130
|
+
}),
|
|
131
|
+
whatsappNumber: (0, import_pg_core.varchar)("whatsapp_number", {
|
|
132
|
+
length: 20
|
|
133
|
+
}).notNull(),
|
|
134
|
+
direction: (0, import_pg_core.varchar)("direction", {
|
|
135
|
+
length: 12
|
|
136
|
+
}).notNull(),
|
|
137
|
+
sender: (0, import_pg_core.varchar)("sender", {
|
|
138
|
+
length: 12
|
|
139
|
+
}).notNull(),
|
|
140
|
+
agentUserId: (0, import_pg_core.uuid)("agent_user_id"),
|
|
141
|
+
type: (0, import_pg_core.varchar)("type", {
|
|
142
|
+
length: 16
|
|
143
|
+
}).notNull().default("text"),
|
|
144
|
+
content: (0, import_pg_core.text)("content"),
|
|
145
|
+
// T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por
|
|
146
|
+
// uploadId dentro deste jsonb.
|
|
147
|
+
payload: (0, import_pg_core.jsonb)("payload").$type(),
|
|
148
|
+
waMessageId: (0, import_pg_core.varchar)("wa_message_id", {
|
|
149
|
+
length: 128
|
|
150
|
+
}),
|
|
151
|
+
status: (0, import_pg_core.varchar)("status", {
|
|
152
|
+
length: 16
|
|
153
|
+
}),
|
|
154
|
+
readAt: (0, import_pg_core.timestamp)("read_at", {
|
|
155
|
+
withTimezone: true
|
|
156
|
+
}),
|
|
157
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
158
|
+
withTimezone: true
|
|
159
|
+
}).notNull().defaultNow()
|
|
160
|
+
}, (table) => [
|
|
161
|
+
(0, import_pg_core.index)("idx_messages_session_created").on(table.sessionId, table.createdAt),
|
|
162
|
+
(0, import_pg_core.index)("idx_messages_company_number_created").on(table.companyId, table.whatsappNumber, table.createdAt),
|
|
163
|
+
// Parcial e ÚNICO: é o que de fato garante idempotência de entrega. Um SELECT-antes-de-INSERT
|
|
164
|
+
// não basta — a Meta reenvia a mesma entrega e várias instâncias do host processam em
|
|
165
|
+
// paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque
|
|
166
|
+
// mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs
|
|
167
|
+
// não podem competir entre si por unicidade.
|
|
168
|
+
(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`)
|
|
169
|
+
]);
|
|
170
|
+
var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
171
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
172
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
173
|
+
key: (0, import_pg_core.varchar)("key", {
|
|
174
|
+
length: 64
|
|
175
|
+
}).notNull(),
|
|
176
|
+
label: (0, import_pg_core.varchar)("label", {
|
|
177
|
+
length: 120
|
|
178
|
+
}).notNull(),
|
|
179
|
+
startNodeId: (0, import_pg_core.varchar)("start_node_id", {
|
|
180
|
+
length: 64
|
|
181
|
+
}).notNull(),
|
|
182
|
+
nodes: (0, import_pg_core.jsonb)("nodes").$type().notNull().default({}),
|
|
183
|
+
// Otimista: o editor manda a versão que carregou; salvar com versão desatualizada é
|
|
184
|
+
// rejeitado pelo host (evita duas abas sobrescreverem uma a outra silenciosamente).
|
|
185
|
+
version: (0, import_pg_core.integer)("version").notNull().default(1),
|
|
186
|
+
showInMenu: (0, import_pg_core.boolean)("show_in_menu").notNull().default(false),
|
|
187
|
+
menuOptionLabel: (0, import_pg_core.varchar)("menu_option_label", {
|
|
188
|
+
length: 64
|
|
189
|
+
}),
|
|
190
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
191
|
+
withTimezone: true
|
|
192
|
+
}).notNull().defaultNow(),
|
|
193
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", {
|
|
194
|
+
withTimezone: true
|
|
195
|
+
}).notNull().defaultNow()
|
|
196
|
+
}, (table) => [
|
|
197
|
+
(0, import_pg_core.uniqueIndex)("idx_flow_graphs_company_key").on(table.companyId, table.key)
|
|
198
|
+
]);
|
|
199
|
+
var settings = metaWhatsAppSchema.table("settings", {
|
|
200
|
+
companyId: (0, import_pg_core.uuid)("company_id").primaryKey(),
|
|
201
|
+
templateName: (0, import_pg_core.varchar)("template_name", {
|
|
202
|
+
length: 128
|
|
203
|
+
}),
|
|
204
|
+
templateLanguage: (0, import_pg_core.varchar)("template_language", {
|
|
205
|
+
length: 16
|
|
206
|
+
}).notNull().default("pt_BR"),
|
|
207
|
+
// Mapa posicional {{1}}, {{2}}... → token resolvido no envio (ex.: '{clientName}').
|
|
208
|
+
templateVariables: (0, import_pg_core.jsonb)("template_variables").$type().notNull().default([]),
|
|
209
|
+
welcomeMessage: (0, import_pg_core.text)("welcome_message"),
|
|
210
|
+
farewellMessage: (0, import_pg_core.text)("farewell_message"),
|
|
211
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", {
|
|
212
|
+
withTimezone: true
|
|
213
|
+
}).notNull().defaultNow(),
|
|
214
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", {
|
|
215
|
+
withTimezone: true
|
|
216
|
+
}).notNull().defaultNow()
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/repositories/SessionRepository.ts
|
|
220
|
+
var DEFAULT_LIMIT = 20;
|
|
221
|
+
var SessionRepository = class {
|
|
222
|
+
static {
|
|
223
|
+
__name(this, "SessionRepository");
|
|
224
|
+
}
|
|
225
|
+
db;
|
|
226
|
+
constructor(db) {
|
|
227
|
+
this.db = db;
|
|
228
|
+
}
|
|
229
|
+
async getContext(companyId, whatsappNumber) {
|
|
230
|
+
const [row] = await this.db.select().from(sessions).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber))).limit(1);
|
|
231
|
+
return row;
|
|
232
|
+
}
|
|
233
|
+
async getOrCreate(companyId, whatsappNumber, startState) {
|
|
234
|
+
const existing = await this.getContext(companyId, whatsappNumber);
|
|
235
|
+
if (existing) return existing;
|
|
236
|
+
const [created] = await this.db.insert(sessions).values({
|
|
237
|
+
companyId,
|
|
238
|
+
whatsappNumber,
|
|
239
|
+
currentState: startState
|
|
240
|
+
}).onConflictDoUpdate({
|
|
241
|
+
target: [
|
|
242
|
+
sessions.companyId,
|
|
243
|
+
sessions.whatsappNumber
|
|
244
|
+
],
|
|
245
|
+
set: {
|
|
246
|
+
lastActivity: import_drizzle_orm2.sql`now()`
|
|
247
|
+
}
|
|
248
|
+
}).returning();
|
|
249
|
+
return created;
|
|
250
|
+
}
|
|
251
|
+
async setState(companyId, whatsappNumber, state, context) {
|
|
252
|
+
await this.db.update(sessions).set({
|
|
253
|
+
currentState: state,
|
|
254
|
+
...context !== void 0 ? {
|
|
255
|
+
context
|
|
256
|
+
} : {},
|
|
257
|
+
lastActivity: import_drizzle_orm2.sql`now()`,
|
|
258
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
259
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
260
|
+
}
|
|
261
|
+
// Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.
|
|
262
|
+
// Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).
|
|
263
|
+
async setFlowPosition(companyId, whatsappNumber, flowKey, currentNodeId) {
|
|
264
|
+
await this.db.update(sessions).set({
|
|
265
|
+
flowKey,
|
|
266
|
+
currentNodeId,
|
|
267
|
+
lastActivity: import_drizzle_orm2.sql`now()`,
|
|
268
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
269
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
270
|
+
}
|
|
271
|
+
// Marca a chegada de uma mensagem DO CLIENTE. É este carimbo — e não lastActivity, que também
|
|
272
|
+
// se move em ações do atendente/bot — que define a janela de 24h do WhatsApp: fora dela, só
|
|
273
|
+
// template aprovado reabre a conversa (ver WindowExpiredError nos contracts).
|
|
274
|
+
async touchInbound(companyId, whatsappNumber) {
|
|
275
|
+
await this.db.update(sessions).set({
|
|
276
|
+
lastInboundAt: import_drizzle_orm2.sql`now()`,
|
|
277
|
+
lastActivity: import_drizzle_orm2.sql`now()`,
|
|
278
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
279
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
280
|
+
}
|
|
281
|
+
// Horas desde a última mensagem do cliente — undefined se ele nunca escreveu. O canal (Fase 5)
|
|
282
|
+
// usa isto para decidir entre mensagem livre e template antes de chamar a Graph API.
|
|
283
|
+
async hoursSinceLastInbound(companyId, whatsappNumber) {
|
|
284
|
+
const session = await this.getContext(companyId, whatsappNumber);
|
|
285
|
+
if (!session?.lastInboundAt) return void 0;
|
|
286
|
+
return (Date.now() - session.lastInboundAt.getTime()) / (1e3 * 60 * 60);
|
|
287
|
+
}
|
|
288
|
+
async setMode(companyId, whatsappNumber, mode, assignedUserId) {
|
|
289
|
+
await this.db.update(sessions).set({
|
|
290
|
+
mode,
|
|
291
|
+
assignedUserId: assignedUserId ?? null,
|
|
292
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
293
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
294
|
+
}
|
|
295
|
+
// T3.3 — takeover: atendente assume a conversa, tirando-a do modo bot.
|
|
296
|
+
async takeover(companyId, whatsappNumber, agentUserId) {
|
|
297
|
+
await this.setMode(companyId, whatsappNumber, "human", agentUserId);
|
|
298
|
+
}
|
|
299
|
+
// T3.3 — release: devolve a conversa ao bot.
|
|
300
|
+
async release(companyId, whatsappNumber) {
|
|
301
|
+
await this.setMode(companyId, whatsappNumber, "bot", null);
|
|
302
|
+
}
|
|
303
|
+
async requestHuman(companyId, whatsappNumber) {
|
|
304
|
+
await this.db.update(sessions).set({
|
|
305
|
+
humanRequestedAt: import_drizzle_orm2.sql`now()`,
|
|
306
|
+
updatedAt: import_drizzle_orm2.sql`now()`
|
|
307
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
308
|
+
}
|
|
309
|
+
async markRead(companyId, whatsappNumber) {
|
|
310
|
+
await this.db.update(sessions).set({
|
|
311
|
+
lastAgentReadAt: import_drizzle_orm2.sql`now()`
|
|
312
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
313
|
+
}
|
|
314
|
+
async markAllRead(companyId, userId) {
|
|
315
|
+
const result = await this.db.update(sessions).set({
|
|
316
|
+
lastAgentReadAt: import_drizzle_orm2.sql`now()`
|
|
317
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.assignedUserId, userId)));
|
|
318
|
+
return Number(result.rowCount ?? 0);
|
|
319
|
+
}
|
|
320
|
+
async listByContextFilters(companyId, filters) {
|
|
321
|
+
const limit = filters.limit ?? DEFAULT_LIMIT;
|
|
322
|
+
const offset = ((filters.page ?? 1) - 1) * limit;
|
|
323
|
+
const conditions = [
|
|
324
|
+
(0, import_drizzle_orm2.eq)(sessions.companyId, companyId)
|
|
325
|
+
];
|
|
326
|
+
if (filters.waitingHuman) conditions.push(import_drizzle_orm2.sql`${sessions.humanRequestedAt} is not null`);
|
|
327
|
+
if (filters.search) conditions.push(import_drizzle_orm2.sql`${sessions.whatsappNumber} ilike ${"%" + filters.search + "%"}`);
|
|
328
|
+
const rows = await this.db.select({
|
|
329
|
+
id: sessions.id,
|
|
330
|
+
whatsappNumber: sessions.whatsappNumber,
|
|
331
|
+
mode: sessions.mode,
|
|
332
|
+
assignedUserId: sessions.assignedUserId,
|
|
333
|
+
currentState: sessions.currentState,
|
|
334
|
+
lastActivity: sessions.lastActivity,
|
|
335
|
+
lastInboundAt: sessions.lastInboundAt,
|
|
336
|
+
humanRequestedAt: sessions.humanRequestedAt
|
|
337
|
+
}).from(sessions).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(sessions.lastActivity)).limit(limit).offset(offset);
|
|
338
|
+
return rows.map((row) => ({
|
|
339
|
+
id: row.id,
|
|
340
|
+
whatsappNumber: row.whatsappNumber,
|
|
341
|
+
lastAt: row.lastActivity.toISOString(),
|
|
342
|
+
lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
|
|
343
|
+
mode: row.mode,
|
|
344
|
+
assignedUserId: row.assignedUserId,
|
|
345
|
+
waitingHuman: row.humanRequestedAt !== null,
|
|
346
|
+
unread: 0,
|
|
347
|
+
currentState: row.currentState
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
// T3.3 — export: transcript completo de uma conversa, mensagens em ordem cronológica.
|
|
351
|
+
async exportConversation(companyId, whatsappNumber) {
|
|
352
|
+
const session = await this.getContext(companyId, whatsappNumber);
|
|
353
|
+
if (!session) return null;
|
|
354
|
+
const rows = await this.db.select().from(messages).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(messages.companyId, companyId), (0, import_drizzle_orm2.eq)(messages.sessionId, session.id))).orderBy(messages.createdAt);
|
|
355
|
+
return {
|
|
356
|
+
session,
|
|
357
|
+
messages: rows
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/repositories/MessageRepository.ts
|
|
363
|
+
var import_drizzle_orm3 = require("drizzle-orm");
|
|
364
|
+
var DEFAULT_LIMIT2 = 50;
|
|
365
|
+
var MessageRepository = class {
|
|
366
|
+
static {
|
|
367
|
+
__name(this, "MessageRepository");
|
|
368
|
+
}
|
|
369
|
+
db;
|
|
370
|
+
constructor(db) {
|
|
371
|
+
this.db = db;
|
|
372
|
+
}
|
|
373
|
+
// Idempotente por (companyId, waMessageId) — o mesmo webhook chega mais de uma vez (a Meta
|
|
374
|
+
// reenvia em instabilidade) e várias instâncias do host processam em paralelo. A garantia é o
|
|
375
|
+
// índice único parcial no banco + onConflictDoNothing, NÃO um SELECT prévio: duas entregas
|
|
376
|
+
// concorrentes passariam as duas pela checagem e inseririam duplicado.
|
|
377
|
+
// Devolve undefined quando a mensagem já existia (nada foi inserido).
|
|
378
|
+
async insertMessage(params) {
|
|
379
|
+
const values = {
|
|
380
|
+
companyId: params.companyId,
|
|
381
|
+
sessionId: params.sessionId,
|
|
382
|
+
whatsappNumber: params.whatsappNumber,
|
|
383
|
+
direction: params.direction,
|
|
384
|
+
sender: params.sender,
|
|
385
|
+
agentUserId: params.agentUserId ?? null,
|
|
386
|
+
type: params.type,
|
|
387
|
+
content: params.content ?? null,
|
|
388
|
+
payload: params.payload ?? null,
|
|
389
|
+
waMessageId: params.waMessageId ?? null,
|
|
390
|
+
status: params.status ?? null
|
|
391
|
+
};
|
|
392
|
+
const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning();
|
|
393
|
+
return created;
|
|
394
|
+
}
|
|
395
|
+
async updateMessageStatus(companyId, waMessageId, status) {
|
|
396
|
+
const [updated] = await this.db.update(messages).set({
|
|
397
|
+
status,
|
|
398
|
+
...status === "read" ? {
|
|
399
|
+
readAt: /* @__PURE__ */ new Date()
|
|
400
|
+
} : {}
|
|
401
|
+
}).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, companyId), (0, import_drizzle_orm3.eq)(messages.waMessageId, waMessageId))).returning();
|
|
402
|
+
return updated;
|
|
403
|
+
}
|
|
404
|
+
async listByConversation(params) {
|
|
405
|
+
const conditions = [
|
|
406
|
+
(0, import_drizzle_orm3.eq)(messages.companyId, params.companyId),
|
|
407
|
+
(0, import_drizzle_orm3.eq)(messages.sessionId, params.sessionId)
|
|
408
|
+
];
|
|
409
|
+
if (params.before) conditions.push((0, import_drizzle_orm3.lt)(messages.createdAt, new Date(params.before)));
|
|
410
|
+
return this.db.select().from(messages).where((0, import_drizzle_orm3.and)(...conditions)).orderBy(messages.createdAt).limit(params.limit ?? DEFAULT_LIMIT2);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/repositories/FlowGraphRepository.ts
|
|
415
|
+
var import_drizzle_orm4 = require("drizzle-orm");
|
|
416
|
+
var import_meta_whatsapp_contracts = require("@adatechnology/meta-whatsapp-contracts");
|
|
417
|
+
var InvalidFlowGraphError = class extends Error {
|
|
418
|
+
static {
|
|
419
|
+
__name(this, "InvalidFlowGraphError");
|
|
420
|
+
}
|
|
421
|
+
validationMessage;
|
|
422
|
+
constructor(key, validationMessage) {
|
|
423
|
+
super(`Grafo do fluxo "${key}" est\xE1 malformado no banco: ${validationMessage}`), this.validationMessage = validationMessage;
|
|
424
|
+
this.name = "InvalidFlowGraphError";
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
function toContractGraph(row) {
|
|
428
|
+
const parsed = import_meta_whatsapp_contracts.flowGraphNodesSchema.safeParse(row.nodes);
|
|
429
|
+
if (!parsed.success) throw new InvalidFlowGraphError(row.key, parsed.error.message);
|
|
430
|
+
return {
|
|
431
|
+
key: row.key,
|
|
432
|
+
label: row.label,
|
|
433
|
+
startNodeId: row.startNodeId,
|
|
434
|
+
version: row.version,
|
|
435
|
+
nodes: parsed.data
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
__name(toContractGraph, "toContractGraph");
|
|
439
|
+
var OptimisticLockError = class extends Error {
|
|
440
|
+
static {
|
|
441
|
+
__name(this, "OptimisticLockError");
|
|
442
|
+
}
|
|
443
|
+
constructor(key) {
|
|
444
|
+
super(`Fluxo "${key}" foi alterado por outra sess\xE3o \u2014 recarregue antes de salvar.`);
|
|
445
|
+
this.name = "OptimisticLockError";
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var FlowGraphRepository = class {
|
|
449
|
+
static {
|
|
450
|
+
__name(this, "FlowGraphRepository");
|
|
451
|
+
}
|
|
452
|
+
db;
|
|
453
|
+
constructor(db) {
|
|
454
|
+
this.db = db;
|
|
455
|
+
}
|
|
456
|
+
async get(companyId, key) {
|
|
457
|
+
const [row] = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key))).limit(1);
|
|
458
|
+
return row ? toContractGraph(row) : void 0;
|
|
459
|
+
}
|
|
460
|
+
async list(companyId) {
|
|
461
|
+
const rows = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId));
|
|
462
|
+
return rows.map((row) => ({
|
|
463
|
+
key: row.key,
|
|
464
|
+
label: row.label,
|
|
465
|
+
nodeCount: Object.keys(row.nodes).length,
|
|
466
|
+
showInMenu: row.showInMenu,
|
|
467
|
+
menuOptionLabel: row.menuOptionLabel ?? void 0,
|
|
468
|
+
updatedAt: row.updatedAt.toISOString()
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
// Valida antes de gravar: aceitar um grafo malformado e só reclamar na leitura empurraria a
|
|
472
|
+
// falha para o meio de uma conversa real, em vez de barrar quem salvou.
|
|
473
|
+
assertValidNodes(key, nodes) {
|
|
474
|
+
const parsed = import_meta_whatsapp_contracts.flowGraphNodesSchema.safeParse(nodes);
|
|
475
|
+
if (!parsed.success) throw new InvalidFlowGraphError(key, parsed.error.message);
|
|
476
|
+
}
|
|
477
|
+
async create(companyId, graph) {
|
|
478
|
+
this.assertValidNodes(graph.key, graph.nodes);
|
|
479
|
+
const [created] = await this.db.insert(flowGraphs).values({
|
|
480
|
+
companyId,
|
|
481
|
+
key: graph.key,
|
|
482
|
+
label: graph.label,
|
|
483
|
+
startNodeId: graph.startNodeId,
|
|
484
|
+
nodes: graph.nodes,
|
|
485
|
+
showInMenu: graph.showInMenu ?? false,
|
|
486
|
+
menuOptionLabel: graph.menuOptionLabel
|
|
487
|
+
}).returning();
|
|
488
|
+
return toContractGraph(created);
|
|
489
|
+
}
|
|
490
|
+
// Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —
|
|
491
|
+
// senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).
|
|
492
|
+
async save(companyId, graph, expectedVersion) {
|
|
493
|
+
this.assertValidNodes(graph.key, graph.nodes);
|
|
494
|
+
const rows = await this.db.update(flowGraphs).set({
|
|
495
|
+
label: graph.label,
|
|
496
|
+
startNodeId: graph.startNodeId,
|
|
497
|
+
nodes: graph.nodes,
|
|
498
|
+
version: expectedVersion + 1,
|
|
499
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
500
|
+
}).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, graph.key), (0, import_drizzle_orm4.eq)(flowGraphs.version, expectedVersion))).returning();
|
|
501
|
+
if (rows.length === 0) throw new OptimisticLockError(graph.key);
|
|
502
|
+
return toContractGraph(rows[0]);
|
|
503
|
+
}
|
|
504
|
+
async delete(companyId, key) {
|
|
505
|
+
await this.db.delete(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key)));
|
|
506
|
+
}
|
|
507
|
+
// T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as
|
|
508
|
+
// colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,
|
|
509
|
+
// coberto por idx_sessions_company_flow_node) em vez de trazer toda sessão da empresa para
|
|
510
|
+
// contar em memória.
|
|
511
|
+
async getLiveFlowPositions(companyId) {
|
|
512
|
+
const rows = await this.db.select({
|
|
513
|
+
flowKey: sessions.flowKey,
|
|
514
|
+
nodeId: sessions.currentNodeId,
|
|
515
|
+
count: import_drizzle_orm4.sql`count(*)::int`
|
|
516
|
+
}).from(sessions).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(sessions.companyId, companyId), (0, import_drizzle_orm4.eq)(sessions.mode, "bot"), (0, import_drizzle_orm4.isNotNull)(sessions.flowKey), (0, import_drizzle_orm4.isNotNull)(sessions.currentNodeId))).groupBy(sessions.flowKey, sessions.currentNodeId);
|
|
517
|
+
return rows.map((row) => ({
|
|
518
|
+
flowKey: row.flowKey,
|
|
519
|
+
nodeId: row.nodeId,
|
|
520
|
+
count: row.count
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// src/repositories/SettingsRepository.ts
|
|
526
|
+
var import_drizzle_orm5 = require("drizzle-orm");
|
|
527
|
+
function toContractSettings(row) {
|
|
528
|
+
return {
|
|
529
|
+
templateName: row.templateName ?? "",
|
|
530
|
+
templateLanguage: row.templateLanguage,
|
|
531
|
+
templateVariables: row.templateVariables,
|
|
532
|
+
welcomeMessage: row.welcomeMessage ?? "",
|
|
533
|
+
farewellMessage: row.farewellMessage ?? ""
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
__name(toContractSettings, "toContractSettings");
|
|
537
|
+
var EMPTY_SETTINGS = {
|
|
538
|
+
templateName: "",
|
|
539
|
+
templateLanguage: "pt_BR",
|
|
540
|
+
templateVariables: [],
|
|
541
|
+
welcomeMessage: "",
|
|
542
|
+
farewellMessage: ""
|
|
543
|
+
};
|
|
544
|
+
var SettingsRepository = class {
|
|
545
|
+
static {
|
|
546
|
+
__name(this, "SettingsRepository");
|
|
547
|
+
}
|
|
548
|
+
db;
|
|
549
|
+
constructor(db) {
|
|
550
|
+
this.db = db;
|
|
551
|
+
}
|
|
552
|
+
// Empresa sem linha de configuração é o estado normal logo após instalar o módulo — devolve
|
|
553
|
+
// os defaults em vez de undefined, para o chamador não precisar tratar "ainda não configurado"
|
|
554
|
+
// como caso especial em todo lugar.
|
|
555
|
+
async get(companyId) {
|
|
556
|
+
const [row] = await this.db.select().from(settings).where((0, import_drizzle_orm5.eq)(settings.companyId, companyId)).limit(1);
|
|
557
|
+
return row ? toContractSettings(row) : EMPTY_SETTINGS;
|
|
558
|
+
}
|
|
559
|
+
async save(companyId, update) {
|
|
560
|
+
const current = await this.get(companyId);
|
|
561
|
+
const merged = {
|
|
562
|
+
...current,
|
|
563
|
+
...update
|
|
564
|
+
};
|
|
565
|
+
const [row] = await this.db.insert(settings).values({
|
|
566
|
+
companyId,
|
|
567
|
+
templateName: merged.templateName || null,
|
|
568
|
+
templateLanguage: merged.templateLanguage,
|
|
569
|
+
templateVariables: merged.templateVariables,
|
|
570
|
+
welcomeMessage: merged.welcomeMessage || null,
|
|
571
|
+
farewellMessage: merged.farewellMessage || null
|
|
572
|
+
}).onConflictDoUpdate({
|
|
573
|
+
target: settings.companyId,
|
|
574
|
+
set: {
|
|
575
|
+
templateName: merged.templateName || null,
|
|
576
|
+
templateLanguage: merged.templateLanguage,
|
|
577
|
+
templateVariables: merged.templateVariables,
|
|
578
|
+
welcomeMessage: merged.welcomeMessage || null,
|
|
579
|
+
farewellMessage: merged.farewellMessage || null,
|
|
580
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
581
|
+
}
|
|
582
|
+
}).returning();
|
|
583
|
+
return toContractSettings(row);
|
|
584
|
+
}
|
|
585
|
+
// Resolve {{1}}, {{2}}... a partir do mapa configurado + o contexto da conversa. Ex.: se
|
|
586
|
+
// templateVariables = ['{clientName}', '{city}'], devolve [nome, cidade] na ordem posicional
|
|
587
|
+
// que a Graph API espera nos bodyParameters.
|
|
588
|
+
async resolveTemplateVariables(companyId, context) {
|
|
589
|
+
const { templateVariables } = await this.get(companyId);
|
|
590
|
+
return templateVariables.map((token) => {
|
|
591
|
+
const key = token.replace(/^\{|\}$/g, "");
|
|
592
|
+
const value = context[key];
|
|
593
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/use-cases/LogMessage.use-case.ts
|
|
599
|
+
var LogMessageUseCase = class {
|
|
600
|
+
static {
|
|
601
|
+
__name(this, "LogMessageUseCase");
|
|
602
|
+
}
|
|
603
|
+
sessionRepository;
|
|
604
|
+
messageRepository;
|
|
605
|
+
realtime;
|
|
606
|
+
constructor(sessionRepository, messageRepository, realtime) {
|
|
607
|
+
this.sessionRepository = sessionRepository;
|
|
608
|
+
this.messageRepository = messageRepository;
|
|
609
|
+
this.realtime = realtime;
|
|
610
|
+
}
|
|
611
|
+
async execute(params) {
|
|
612
|
+
const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState);
|
|
613
|
+
const saved = await this.messageRepository.insertMessage({
|
|
614
|
+
...params,
|
|
615
|
+
sessionId: session.id
|
|
616
|
+
});
|
|
617
|
+
if (!saved) return void 0;
|
|
618
|
+
if (params.direction === "inbound") {
|
|
619
|
+
await this.sessionRepository.touchInbound(params.companyId, params.whatsappNumber);
|
|
620
|
+
}
|
|
621
|
+
this.realtime?.emit(`conv:${params.whatsappNumber}`, "message", {
|
|
622
|
+
direction: params.direction,
|
|
623
|
+
sender: params.sender
|
|
624
|
+
});
|
|
625
|
+
this.realtime?.emit("global", "data-changed", {});
|
|
626
|
+
return saved;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/use-cases/SendMessage.use-case.ts
|
|
631
|
+
var import_meta_whatsapp_contracts2 = require("@adatechnology/meta-whatsapp-contracts");
|
|
632
|
+
var WHATSAPP_WINDOW_HOURS = 24;
|
|
633
|
+
var SendMessageUseCase = class {
|
|
634
|
+
static {
|
|
635
|
+
__name(this, "SendMessageUseCase");
|
|
636
|
+
}
|
|
637
|
+
channel;
|
|
638
|
+
sessionRepository;
|
|
639
|
+
logMessage;
|
|
640
|
+
objectStorage;
|
|
641
|
+
constructor(channel, sessionRepository, logMessage, objectStorage) {
|
|
642
|
+
this.channel = channel;
|
|
643
|
+
this.sessionRepository = sessionRepository;
|
|
644
|
+
this.logMessage = logMessage;
|
|
645
|
+
this.objectStorage = objectStorage;
|
|
646
|
+
}
|
|
647
|
+
async assertWithinWindow(companyId, whatsappNumber) {
|
|
648
|
+
const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber);
|
|
649
|
+
if (hours === void 0) throw new import_meta_whatsapp_contracts2.WindowExpiredError();
|
|
650
|
+
if (hours >= WHATSAPP_WINDOW_HOURS) throw new import_meta_whatsapp_contracts2.WindowExpiredError(hours);
|
|
651
|
+
}
|
|
652
|
+
async sendText(params) {
|
|
653
|
+
await this.assertWithinWindow(params.companyId, params.whatsappNumber);
|
|
654
|
+
const { externalMessageId } = await this.channel.sendText(params.whatsappNumber, params.body);
|
|
655
|
+
return this.logMessage.execute({
|
|
656
|
+
companyId: params.companyId,
|
|
657
|
+
whatsappNumber: params.whatsappNumber,
|
|
658
|
+
direction: "outbound",
|
|
659
|
+
sender: params.sender,
|
|
660
|
+
agentUserId: params.agentUserId ?? null,
|
|
661
|
+
type: "text",
|
|
662
|
+
content: params.body,
|
|
663
|
+
waMessageId: externalMessageId,
|
|
664
|
+
status: "sent",
|
|
665
|
+
startState: params.startState
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async sendMedia(params) {
|
|
669
|
+
await this.assertWithinWindow(params.companyId, params.whatsappNumber);
|
|
670
|
+
const { externalMessageId } = await this.channel.sendMedia({
|
|
671
|
+
to: params.whatsappNumber,
|
|
672
|
+
buffer: params.buffer,
|
|
673
|
+
mimeType: params.mimeType,
|
|
674
|
+
filename: params.filename,
|
|
675
|
+
caption: params.caption
|
|
676
|
+
});
|
|
677
|
+
const uploadId = this.objectStorage ? (await this.objectStorage.upload({
|
|
678
|
+
buffer: params.buffer,
|
|
679
|
+
mimeType: params.mimeType,
|
|
680
|
+
key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`
|
|
681
|
+
})).uploadId : void 0;
|
|
682
|
+
return this.logMessage.execute({
|
|
683
|
+
companyId: params.companyId,
|
|
684
|
+
whatsappNumber: params.whatsappNumber,
|
|
685
|
+
direction: "outbound",
|
|
686
|
+
sender: params.sender,
|
|
687
|
+
agentUserId: params.agentUserId ?? null,
|
|
688
|
+
type: mediaTypeFor(params.mimeType),
|
|
689
|
+
content: params.caption ?? params.filename,
|
|
690
|
+
payload: {
|
|
691
|
+
filename: params.filename,
|
|
692
|
+
mimeType: params.mimeType,
|
|
693
|
+
...uploadId ? {
|
|
694
|
+
uploadId
|
|
695
|
+
} : {}
|
|
696
|
+
},
|
|
697
|
+
waMessageId: externalMessageId,
|
|
698
|
+
status: "sent",
|
|
699
|
+
startState: params.startState
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
// Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece
|
|
703
|
+
// para reabri-la.
|
|
704
|
+
async sendTemplate(params) {
|
|
705
|
+
const { externalMessageId } = await this.channel.sendTemplate({
|
|
706
|
+
to: params.whatsappNumber,
|
|
707
|
+
templateName: params.templateName,
|
|
708
|
+
languageCode: params.languageCode ?? "pt_BR",
|
|
709
|
+
bodyParameters: params.bodyParameters
|
|
710
|
+
});
|
|
711
|
+
return this.logMessage.execute({
|
|
712
|
+
companyId: params.companyId,
|
|
713
|
+
whatsappNumber: params.whatsappNumber,
|
|
714
|
+
direction: "outbound",
|
|
715
|
+
sender: params.sender,
|
|
716
|
+
agentUserId: params.agentUserId ?? null,
|
|
717
|
+
type: "template",
|
|
718
|
+
content: params.templateName,
|
|
719
|
+
payload: {
|
|
720
|
+
templateName: params.templateName,
|
|
721
|
+
bodyParameters: params.bodyParameters ?? []
|
|
722
|
+
},
|
|
723
|
+
waMessageId: externalMessageId,
|
|
724
|
+
status: "sent",
|
|
725
|
+
startState: params.startState
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
function mediaTypeFor(mimeType) {
|
|
730
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
731
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
732
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
733
|
+
return "document";
|
|
734
|
+
}
|
|
735
|
+
__name(mediaTypeFor, "mediaTypeFor");
|
|
736
|
+
|
|
737
|
+
// src/use-cases/TakeoverConversation.use-case.ts
|
|
738
|
+
var TakeoverConversationUseCase = class {
|
|
739
|
+
static {
|
|
740
|
+
__name(this, "TakeoverConversationUseCase");
|
|
741
|
+
}
|
|
742
|
+
sessionRepository;
|
|
743
|
+
realtime;
|
|
744
|
+
constructor(sessionRepository, realtime) {
|
|
745
|
+
this.sessionRepository = sessionRepository;
|
|
746
|
+
this.realtime = realtime;
|
|
747
|
+
}
|
|
748
|
+
async execute(params) {
|
|
749
|
+
await this.sessionRepository.takeover(params.companyId, params.whatsappNumber, params.agentUserId);
|
|
750
|
+
this.realtime?.emit(`conv:${params.whatsappNumber}`, "mode-changed", {
|
|
751
|
+
mode: "human",
|
|
752
|
+
assignedUserId: params.agentUserId
|
|
753
|
+
});
|
|
754
|
+
this.realtime?.emit("global", "data-changed", {});
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/use-cases/ReleaseConversation.use-case.ts
|
|
759
|
+
var ReleaseConversationUseCase = class {
|
|
760
|
+
static {
|
|
761
|
+
__name(this, "ReleaseConversationUseCase");
|
|
762
|
+
}
|
|
763
|
+
sessionRepository;
|
|
764
|
+
realtime;
|
|
765
|
+
constructor(sessionRepository, realtime) {
|
|
766
|
+
this.sessionRepository = sessionRepository;
|
|
767
|
+
this.realtime = realtime;
|
|
768
|
+
}
|
|
769
|
+
async execute(params) {
|
|
770
|
+
await this.sessionRepository.release(params.companyId, params.whatsappNumber);
|
|
771
|
+
this.realtime?.emit(`conv:${params.whatsappNumber}`, "mode-changed", {
|
|
772
|
+
mode: "bot",
|
|
773
|
+
assignedUserId: null
|
|
774
|
+
});
|
|
775
|
+
this.realtime?.emit("global", "data-changed", {});
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/use-cases/ListConversations.use-case.ts
|
|
780
|
+
var ListConversationsUseCase = class {
|
|
781
|
+
static {
|
|
782
|
+
__name(this, "ListConversationsUseCase");
|
|
783
|
+
}
|
|
784
|
+
sessionRepository;
|
|
785
|
+
constructor(sessionRepository) {
|
|
786
|
+
this.sessionRepository = sessionRepository;
|
|
787
|
+
}
|
|
788
|
+
async execute(params) {
|
|
789
|
+
return this.sessionRepository.listByContextFilters(params.companyId, params.filters ?? {});
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/use-cases/ListMessages.use-case.ts
|
|
794
|
+
var import_meta_whatsapp_contracts3 = require("@adatechnology/meta-whatsapp-contracts");
|
|
795
|
+
var ListMessagesUseCase = class {
|
|
796
|
+
static {
|
|
797
|
+
__name(this, "ListMessagesUseCase");
|
|
798
|
+
}
|
|
799
|
+
sessionRepository;
|
|
800
|
+
messageRepository;
|
|
801
|
+
constructor(sessionRepository, messageRepository) {
|
|
802
|
+
this.sessionRepository = sessionRepository;
|
|
803
|
+
this.messageRepository = messageRepository;
|
|
804
|
+
}
|
|
805
|
+
async execute(params) {
|
|
806
|
+
const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
|
|
807
|
+
if (!session) throw new import_meta_whatsapp_contracts3.SessionNotFoundError(params.whatsappNumber);
|
|
808
|
+
const repoParams = {
|
|
809
|
+
companyId: params.companyId,
|
|
810
|
+
sessionId: session.id,
|
|
811
|
+
limit: params.limit,
|
|
812
|
+
before: params.before
|
|
813
|
+
};
|
|
814
|
+
return this.messageRepository.listByConversation(repoParams);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/use-cases/ExportConversation.use-case.ts
|
|
819
|
+
var import_meta_whatsapp_contracts4 = require("@adatechnology/meta-whatsapp-contracts");
|
|
820
|
+
var ExportConversationUseCase = class {
|
|
821
|
+
static {
|
|
822
|
+
__name(this, "ExportConversationUseCase");
|
|
823
|
+
}
|
|
824
|
+
sessionRepository;
|
|
825
|
+
constructor(sessionRepository) {
|
|
826
|
+
this.sessionRepository = sessionRepository;
|
|
827
|
+
}
|
|
828
|
+
async execute(params) {
|
|
829
|
+
const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber);
|
|
830
|
+
if (!result) throw new import_meta_whatsapp_contracts4.SessionNotFoundError(params.whatsappNumber);
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/use-cases/FlowGraph.use-cases.ts
|
|
836
|
+
var GetFlowGraphUseCase = class {
|
|
837
|
+
static {
|
|
838
|
+
__name(this, "GetFlowGraphUseCase");
|
|
839
|
+
}
|
|
840
|
+
flowGraphRepository;
|
|
841
|
+
constructor(flowGraphRepository) {
|
|
842
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
843
|
+
}
|
|
844
|
+
async execute(params) {
|
|
845
|
+
return this.flowGraphRepository.get(params.companyId, params.key);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
var ListFlowGraphsUseCase = class {
|
|
849
|
+
static {
|
|
850
|
+
__name(this, "ListFlowGraphsUseCase");
|
|
851
|
+
}
|
|
852
|
+
flowGraphRepository;
|
|
853
|
+
constructor(flowGraphRepository) {
|
|
854
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
855
|
+
}
|
|
856
|
+
async execute(params) {
|
|
857
|
+
return this.flowGraphRepository.list(params.companyId);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var CreateFlowGraphUseCase = class {
|
|
861
|
+
static {
|
|
862
|
+
__name(this, "CreateFlowGraphUseCase");
|
|
863
|
+
}
|
|
864
|
+
flowGraphRepository;
|
|
865
|
+
constructor(flowGraphRepository) {
|
|
866
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
867
|
+
}
|
|
868
|
+
async execute(params) {
|
|
869
|
+
return this.flowGraphRepository.create(params.companyId, params);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
var SaveFlowGraphUseCase = class {
|
|
873
|
+
static {
|
|
874
|
+
__name(this, "SaveFlowGraphUseCase");
|
|
875
|
+
}
|
|
876
|
+
flowGraphRepository;
|
|
877
|
+
constructor(flowGraphRepository) {
|
|
878
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
879
|
+
}
|
|
880
|
+
async execute(params) {
|
|
881
|
+
return this.flowGraphRepository.save(params.companyId, params.graph, params.expectedVersion);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
var DeleteFlowGraphUseCase = class {
|
|
885
|
+
static {
|
|
886
|
+
__name(this, "DeleteFlowGraphUseCase");
|
|
887
|
+
}
|
|
888
|
+
flowGraphRepository;
|
|
889
|
+
constructor(flowGraphRepository) {
|
|
890
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
891
|
+
}
|
|
892
|
+
async execute(params) {
|
|
893
|
+
return this.flowGraphRepository.delete(params.companyId, params.key);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
var GetLiveFlowPositionsUseCase = class {
|
|
897
|
+
static {
|
|
898
|
+
__name(this, "GetLiveFlowPositionsUseCase");
|
|
899
|
+
}
|
|
900
|
+
flowGraphRepository;
|
|
901
|
+
constructor(flowGraphRepository) {
|
|
902
|
+
this.flowGraphRepository = flowGraphRepository;
|
|
903
|
+
}
|
|
904
|
+
async execute(params) {
|
|
905
|
+
return this.flowGraphRepository.getLiveFlowPositions(params.companyId);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// src/flows/FlowInterpreter.ts
|
|
910
|
+
var import_meta_whatsapp_contracts5 = require("@adatechnology/meta-whatsapp-contracts");
|
|
911
|
+
var DEFAULT_MAX_STEPS = 50;
|
|
912
|
+
function evaluateCondition(node, context) {
|
|
913
|
+
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false;
|
|
914
|
+
const actual = context[node.conditionContextKey];
|
|
915
|
+
const expected = node.conditionValue;
|
|
916
|
+
switch (node.conditionOperator) {
|
|
917
|
+
case "==":
|
|
918
|
+
return String(actual) === expected;
|
|
919
|
+
case "!=":
|
|
920
|
+
return String(actual) !== expected;
|
|
921
|
+
case "contains":
|
|
922
|
+
return typeof actual === "string" && actual.includes(expected);
|
|
923
|
+
case ">":
|
|
924
|
+
case ">=":
|
|
925
|
+
case "<":
|
|
926
|
+
case "<=": {
|
|
927
|
+
const actualNum = Number(actual);
|
|
928
|
+
const expectedNum = Number(expected);
|
|
929
|
+
if (Number.isNaN(actualNum) || Number.isNaN(expectedNum)) return false;
|
|
930
|
+
if (node.conditionOperator === ">") return actualNum > expectedNum;
|
|
931
|
+
if (node.conditionOperator === ">=") return actualNum >= expectedNum;
|
|
932
|
+
if (node.conditionOperator === "<") return actualNum < expectedNum;
|
|
933
|
+
return actualNum <= expectedNum;
|
|
934
|
+
}
|
|
935
|
+
default:
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
__name(evaluateCondition, "evaluateCondition");
|
|
940
|
+
function resolveNext(node, answerId) {
|
|
941
|
+
if (!node.next) return void 0;
|
|
942
|
+
if (typeof node.next === "string") return node.next;
|
|
943
|
+
if (answerId && node.next.byAnswer[answerId]) return node.next.byAnswer[answerId];
|
|
944
|
+
return node.next.default;
|
|
945
|
+
}
|
|
946
|
+
__name(resolveNext, "resolveNext");
|
|
947
|
+
var FlowInterpreter = class {
|
|
948
|
+
static {
|
|
949
|
+
__name(this, "FlowInterpreter");
|
|
950
|
+
}
|
|
951
|
+
actionHandlers = /* @__PURE__ */ new Map();
|
|
952
|
+
registerFlowAction(kind, handler) {
|
|
953
|
+
this.actionHandlers.set(kind, handler);
|
|
954
|
+
}
|
|
955
|
+
async step(input) {
|
|
956
|
+
const node = input.graph.nodes[input.currentNodeId];
|
|
957
|
+
if (!node) return {
|
|
958
|
+
kind: "terminal",
|
|
959
|
+
context: input.context
|
|
960
|
+
};
|
|
961
|
+
if (node.type === "condition") {
|
|
962
|
+
const branch = evaluateCondition(node, input.context) ? "true" : "false";
|
|
963
|
+
return this.moveTo(input, resolveNext(node, branch));
|
|
964
|
+
}
|
|
965
|
+
if (node.type === "action") {
|
|
966
|
+
const handler = node.actionKind ? this.actionHandlers.get(node.actionKind) : void 0;
|
|
967
|
+
if (!handler) return {
|
|
968
|
+
kind: "terminal",
|
|
969
|
+
context: input.context
|
|
970
|
+
};
|
|
971
|
+
const result = await handler({
|
|
972
|
+
node,
|
|
973
|
+
session: input.session,
|
|
974
|
+
channel: input.channel,
|
|
975
|
+
context: input.context
|
|
976
|
+
});
|
|
977
|
+
const contextAfterAction = result?.context ? {
|
|
978
|
+
...input.context,
|
|
979
|
+
...result.context
|
|
980
|
+
} : input.context;
|
|
981
|
+
const nextNodeId2 = result?.next ?? resolveNext(node, void 0);
|
|
982
|
+
return this.moveTo({
|
|
983
|
+
...input,
|
|
984
|
+
context: contextAfterAction
|
|
985
|
+
}, nextNodeId2);
|
|
986
|
+
}
|
|
987
|
+
const isChoice = node.type === "menu" || node.questionType === "choice";
|
|
988
|
+
if (input.userAnswer === void 0) {
|
|
989
|
+
return {
|
|
990
|
+
kind: "awaiting-answer",
|
|
991
|
+
nodeId: input.currentNodeId,
|
|
992
|
+
context: input.context
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const updatedContext = node.contextKey ? {
|
|
996
|
+
...input.context,
|
|
997
|
+
[node.contextKey]: input.userAnswer
|
|
998
|
+
} : input.context;
|
|
999
|
+
const answerId = isChoice ? input.userAnswer : void 0;
|
|
1000
|
+
const nextNodeId = resolveNext(node, answerId);
|
|
1001
|
+
return this.moveTo({
|
|
1002
|
+
...input,
|
|
1003
|
+
context: updatedContext
|
|
1004
|
+
}, nextNodeId);
|
|
1005
|
+
}
|
|
1006
|
+
moveTo(input, nextNodeId) {
|
|
1007
|
+
if (!nextNodeId) return {
|
|
1008
|
+
kind: "terminal",
|
|
1009
|
+
context: input.context
|
|
1010
|
+
};
|
|
1011
|
+
if ((0, import_meta_whatsapp_contracts5.isCrossFlowTarget)(nextNodeId)) return {
|
|
1012
|
+
kind: "cross-flow",
|
|
1013
|
+
flowKey: (0, import_meta_whatsapp_contracts5.crossFlowKey)(nextNodeId),
|
|
1014
|
+
context: input.context
|
|
1015
|
+
};
|
|
1016
|
+
if (!input.graph.nodes[nextNodeId]) return {
|
|
1017
|
+
kind: "terminal",
|
|
1018
|
+
context: input.context
|
|
1019
|
+
};
|
|
1020
|
+
return {
|
|
1021
|
+
kind: "advanced",
|
|
1022
|
+
nodeId: nextNodeId,
|
|
1023
|
+
context: input.context
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
// Avança pelo grafo até parar num ponto que exige algo de fora: uma pergunta esperando resposta
|
|
1027
|
+
// do cliente, o fim do fluxo, ou um salto para outro fluxo. Nós de condição e ação não pedem
|
|
1028
|
+
// input nenhum, então parar neles obrigaria o chamador a reimplementar este laço — e sem
|
|
1029
|
+
// proteção, um grafo com ciclo de condições (A→B→A) o prenderia para sempre.
|
|
1030
|
+
//
|
|
1031
|
+
// O corte por `maxSteps` é uma rede de segurança de runtime, não a defesa principal: um ciclo é
|
|
1032
|
+
// erro de autoria e deveria ser barrado na publicação. Como validateGraph (no editor) hoje
|
|
1033
|
+
// detecta nó inalcançável mas NÃO detecta ciclo, um grafo cíclico é publicável — então o
|
|
1034
|
+
// runtime precisa sobreviver a ele em vez de travar o processo que atende o webhook.
|
|
1035
|
+
async run(input, options) {
|
|
1036
|
+
const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
1037
|
+
const visited = [
|
|
1038
|
+
input.currentNodeId
|
|
1039
|
+
];
|
|
1040
|
+
let current = input;
|
|
1041
|
+
let steps = 0;
|
|
1042
|
+
while (steps < maxSteps) {
|
|
1043
|
+
const result = await this.step(current);
|
|
1044
|
+
steps++;
|
|
1045
|
+
if (result.kind !== "advanced") return {
|
|
1046
|
+
...result,
|
|
1047
|
+
visited,
|
|
1048
|
+
steps
|
|
1049
|
+
};
|
|
1050
|
+
visited.push(result.nodeId);
|
|
1051
|
+
current = {
|
|
1052
|
+
...current,
|
|
1053
|
+
currentNodeId: result.nodeId,
|
|
1054
|
+
context: result.context,
|
|
1055
|
+
userAnswer: void 0
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
kind: "max-steps-exceeded",
|
|
1060
|
+
context: current.context,
|
|
1061
|
+
visited,
|
|
1062
|
+
steps
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// src/channel/WhatsAppChannelAdapter.ts
|
|
1068
|
+
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1069
|
+
var import_meta_whatsapp_contracts6 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1070
|
+
var WhatsAppChannelAdapter = class {
|
|
1071
|
+
static {
|
|
1072
|
+
__name(this, "WhatsAppChannelAdapter");
|
|
1073
|
+
}
|
|
1074
|
+
messages;
|
|
1075
|
+
constructor(messages2) {
|
|
1076
|
+
this.messages = messages2;
|
|
1077
|
+
}
|
|
1078
|
+
async translateErrors(operation) {
|
|
1079
|
+
try {
|
|
1080
|
+
return await operation();
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new import_meta_whatsapp_contracts6.WindowExpiredError();
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async sendText(to, body) {
|
|
1087
|
+
const result = await this.translateErrors(() => this.messages.sendText(to, body));
|
|
1088
|
+
return {
|
|
1089
|
+
externalMessageId: result.waMessageId
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async sendMedia(params) {
|
|
1093
|
+
const result = await this.translateErrors(() => this.messages.sendMedia(params));
|
|
1094
|
+
return {
|
|
1095
|
+
externalMessageId: result.waMessageId
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
async sendTemplate(params) {
|
|
1099
|
+
const result = await this.translateErrors(() => this.messages.sendTemplate(params));
|
|
1100
|
+
return {
|
|
1101
|
+
externalMessageId: result.waMessageId
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async sendInteractiveList(params) {
|
|
1105
|
+
const result = await this.translateErrors(() => this.messages.sendInteractiveList({
|
|
1106
|
+
to: params.to,
|
|
1107
|
+
bodyText: params.body,
|
|
1108
|
+
buttonText: params.buttonLabel,
|
|
1109
|
+
sections: [
|
|
1110
|
+
{
|
|
1111
|
+
title: params.buttonLabel,
|
|
1112
|
+
rows: params.rows
|
|
1113
|
+
}
|
|
1114
|
+
]
|
|
1115
|
+
}));
|
|
1116
|
+
return {
|
|
1117
|
+
externalMessageId: result.waMessageId
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
async fetchMediaAsBase64(mediaId) {
|
|
1121
|
+
return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId));
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// src/channel/ReceiveWebhook.use-case.ts
|
|
1126
|
+
var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1127
|
+
|
|
1128
|
+
// src/channel/webhookSecurity.ts
|
|
1129
|
+
var import_node_crypto = require("crypto");
|
|
1130
|
+
var import_meta_whatsapp_contracts7 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1131
|
+
var WEBHOOK_NONCE_TTL_SECONDS = 300;
|
|
1132
|
+
function safeEqualStrings(left, right) {
|
|
1133
|
+
const leftDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(left).digest();
|
|
1134
|
+
const rightDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(right).digest();
|
|
1135
|
+
return (0, import_node_crypto.timingSafeEqual)(leftDigest, rightDigest);
|
|
1136
|
+
}
|
|
1137
|
+
__name(safeEqualStrings, "safeEqualStrings");
|
|
1138
|
+
function verifyWebhookChallenge(params) {
|
|
1139
|
+
if (params.mode !== "subscribe" || !params.token || !params.challenge) {
|
|
1140
|
+
throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1141
|
+
}
|
|
1142
|
+
if (!safeEqualStrings(params.token, params.expectedToken)) {
|
|
1143
|
+
throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1144
|
+
}
|
|
1145
|
+
return params.challenge;
|
|
1146
|
+
}
|
|
1147
|
+
__name(verifyWebhookChallenge, "verifyWebhookChallenge");
|
|
1148
|
+
function verifyWebhookSignature(params) {
|
|
1149
|
+
const { rawBody, signatureHeader, appSecret } = params;
|
|
1150
|
+
if (!signatureHeader?.startsWith("sha256=")) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1151
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", appSecret).update(rawBody).digest("hex");
|
|
1152
|
+
const received = signatureHeader.slice("sha256=".length);
|
|
1153
|
+
if (!safeEqualStrings(received, expected)) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
|
|
1154
|
+
}
|
|
1155
|
+
__name(verifyWebhookSignature, "verifyWebhookSignature");
|
|
1156
|
+
async function claimWebhookDelivery(params) {
|
|
1157
|
+
const key = `meta-whatsapp:webhook:${params.signatureHeader}`;
|
|
1158
|
+
return params.nonceStore.setIfAbsent(key, params.ttlSeconds ?? WEBHOOK_NONCE_TTL_SECONDS);
|
|
1159
|
+
}
|
|
1160
|
+
__name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
1161
|
+
|
|
1162
|
+
// src/channel/ReceiveWebhook.use-case.ts
|
|
1163
|
+
function toSessionContract(row) {
|
|
1164
|
+
return {
|
|
1165
|
+
id: row.id,
|
|
1166
|
+
companyId: row.companyId,
|
|
1167
|
+
whatsappNumber: row.whatsappNumber,
|
|
1168
|
+
currentState: row.currentState,
|
|
1169
|
+
context: row.context,
|
|
1170
|
+
mode: row.mode,
|
|
1171
|
+
assignedUserId: row.assignedUserId,
|
|
1172
|
+
humanRequestedAt: row.humanRequestedAt?.toISOString() ?? null,
|
|
1173
|
+
lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
|
|
1174
|
+
lastAgentReadAt: row.lastAgentReadAt?.toISOString() ?? null,
|
|
1175
|
+
lastActivity: row.lastActivity.toISOString(),
|
|
1176
|
+
createdAt: row.createdAt.toISOString(),
|
|
1177
|
+
updatedAt: row.updatedAt.toISOString()
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
__name(toSessionContract, "toSessionContract");
|
|
1181
|
+
function extractContent(message) {
|
|
1182
|
+
if (message.text?.body) return message.text.body;
|
|
1183
|
+
const interactive = message.interactive;
|
|
1184
|
+
if (interactive?.button_reply) return interactive.button_reply.title || interactive.button_reply.id;
|
|
1185
|
+
if (interactive?.list_reply) return interactive.list_reply.title || interactive.list_reply.id;
|
|
1186
|
+
if (message.order) {
|
|
1187
|
+
const items = message.order.product_items.reduce((sum, item) => sum + item.quantity, 0);
|
|
1188
|
+
return `Pedido: ${items} item(ns)`;
|
|
1189
|
+
}
|
|
1190
|
+
return message.image?.caption ?? message.document?.caption ?? null;
|
|
1191
|
+
}
|
|
1192
|
+
__name(extractContent, "extractContent");
|
|
1193
|
+
function extractAnswer(message) {
|
|
1194
|
+
const interactive = message.interactive;
|
|
1195
|
+
if (interactive?.button_reply) return interactive.button_reply.id;
|
|
1196
|
+
if (interactive?.list_reply) return interactive.list_reply.id;
|
|
1197
|
+
return message.text?.body;
|
|
1198
|
+
}
|
|
1199
|
+
__name(extractAnswer, "extractAnswer");
|
|
1200
|
+
function extractPayload(message) {
|
|
1201
|
+
const payload = {};
|
|
1202
|
+
if (message.interactive) payload["interactive"] = message.interactive;
|
|
1203
|
+
if (message.order) payload["order"] = message.order;
|
|
1204
|
+
if (message.image) payload["image"] = message.image;
|
|
1205
|
+
if (message.audio) payload["audio"] = message.audio;
|
|
1206
|
+
if (message.video) payload["video"] = message.video;
|
|
1207
|
+
if (message.document) payload["document"] = message.document;
|
|
1208
|
+
if (message.sticker) payload["sticker"] = message.sticker;
|
|
1209
|
+
if (message.context?.referred_product) payload["referredProduct"] = message.context.referred_product;
|
|
1210
|
+
return Object.keys(payload).length > 0 ? payload : null;
|
|
1211
|
+
}
|
|
1212
|
+
__name(extractPayload, "extractPayload");
|
|
1213
|
+
var ReceiveWebhookUseCase = class {
|
|
1214
|
+
static {
|
|
1215
|
+
__name(this, "ReceiveWebhookUseCase");
|
|
1216
|
+
}
|
|
1217
|
+
params;
|
|
1218
|
+
constructor(params) {
|
|
1219
|
+
this.params = params;
|
|
1220
|
+
}
|
|
1221
|
+
async execute(input) {
|
|
1222
|
+
verifyWebhookSignature({
|
|
1223
|
+
rawBody: input.rawBody,
|
|
1224
|
+
signatureHeader: input.signatureHeader,
|
|
1225
|
+
appSecret: this.params.appSecret
|
|
1226
|
+
});
|
|
1227
|
+
const claimed = await claimWebhookDelivery({
|
|
1228
|
+
nonceStore: this.params.nonceStore,
|
|
1229
|
+
signatureHeader: input.signatureHeader
|
|
1230
|
+
});
|
|
1231
|
+
if (!claimed) return {
|
|
1232
|
+
duplicate: true,
|
|
1233
|
+
messagesProcessed: 0,
|
|
1234
|
+
statusesProcessed: 0
|
|
1235
|
+
};
|
|
1236
|
+
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
1237
|
+
const payload = import_meta_whatsapp_contracts8.whatsAppWebhookPayloadSchema.parse(JSON.parse(rawText));
|
|
1238
|
+
let messagesProcessed = 0;
|
|
1239
|
+
let statusesProcessed = 0;
|
|
1240
|
+
for (const entry of payload.entry) {
|
|
1241
|
+
for (const change of entry.changes) {
|
|
1242
|
+
for (const message of change.value.messages ?? []) {
|
|
1243
|
+
await this.handleMessage(input.companyId, message);
|
|
1244
|
+
messagesProcessed++;
|
|
1245
|
+
}
|
|
1246
|
+
for (const status of change.value.statuses ?? []) {
|
|
1247
|
+
await this.handleStatus(input.companyId, status);
|
|
1248
|
+
statusesProcessed++;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
duplicate: false,
|
|
1254
|
+
messagesProcessed,
|
|
1255
|
+
statusesProcessed
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
async handleMessage(companyId, message) {
|
|
1259
|
+
const saved = await this.params.logMessage.execute({
|
|
1260
|
+
companyId,
|
|
1261
|
+
whatsappNumber: message.from,
|
|
1262
|
+
direction: "inbound",
|
|
1263
|
+
sender: "customer",
|
|
1264
|
+
type: message.type,
|
|
1265
|
+
content: extractContent(message),
|
|
1266
|
+
payload: extractPayload(message),
|
|
1267
|
+
waMessageId: message.id,
|
|
1268
|
+
status: "received",
|
|
1269
|
+
startState: this.params.startState
|
|
1270
|
+
});
|
|
1271
|
+
if (!saved) return;
|
|
1272
|
+
const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from);
|
|
1273
|
+
if (!sessionRow) return;
|
|
1274
|
+
if (sessionRow.mode === "human") return;
|
|
1275
|
+
const outcome = await this.params.hooks?.onMessageReceived?.(message, toSessionContract(sessionRow));
|
|
1276
|
+
if (outcome?.outcome === "handled") return;
|
|
1277
|
+
void extractAnswer(message);
|
|
1278
|
+
}
|
|
1279
|
+
async handleStatus(companyId, status) {
|
|
1280
|
+
const updated = await this.params.messageRepository.updateMessageStatus(companyId, status.id, status.status);
|
|
1281
|
+
if (!updated) return;
|
|
1282
|
+
this.params.realtime?.emit(`conv:${updated.whatsappNumber}`, "message-status", {
|
|
1283
|
+
waMessageId: status.id,
|
|
1284
|
+
status: status.status
|
|
1285
|
+
});
|
|
1286
|
+
const sessionRow = await this.params.sessionRepository.getContext(companyId, updated.whatsappNumber);
|
|
1287
|
+
await this.params.hooks?.onStatusUpdate?.(status, sessionRow ? toSessionContract(sessionRow) : null);
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/channel/IngestInboundMedia.use-case.ts
|
|
1292
|
+
var import_drizzle_orm6 = require("drizzle-orm");
|
|
1293
|
+
var IngestInboundMediaUseCase = class {
|
|
1294
|
+
static {
|
|
1295
|
+
__name(this, "IngestInboundMediaUseCase");
|
|
1296
|
+
}
|
|
1297
|
+
db;
|
|
1298
|
+
channel;
|
|
1299
|
+
objectStorage;
|
|
1300
|
+
constructor(db, channel, objectStorage) {
|
|
1301
|
+
this.db = db;
|
|
1302
|
+
this.channel = channel;
|
|
1303
|
+
this.objectStorage = objectStorage;
|
|
1304
|
+
}
|
|
1305
|
+
async execute(params) {
|
|
1306
|
+
const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId))).limit(1);
|
|
1307
|
+
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1308
|
+
const payload = message.payload ?? {};
|
|
1309
|
+
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
1310
|
+
return {
|
|
1311
|
+
uploadId: String(payload["uploadId"]),
|
|
1312
|
+
alreadyIngested: true
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
|
|
1316
|
+
const buffer = Buffer.from(data, "base64");
|
|
1317
|
+
const { uploadId } = await this.objectStorage.upload({
|
|
1318
|
+
buffer,
|
|
1319
|
+
mimeType: mimeType || params.mimeType,
|
|
1320
|
+
key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`
|
|
1321
|
+
});
|
|
1322
|
+
const updatedPayload = {
|
|
1323
|
+
...payload,
|
|
1324
|
+
uploadId,
|
|
1325
|
+
sourceMediaId: params.sourceMediaId,
|
|
1326
|
+
mimeType: mimeType || params.mimeType,
|
|
1327
|
+
...params.filename ? {
|
|
1328
|
+
filename: params.filename
|
|
1329
|
+
} : {}
|
|
1330
|
+
};
|
|
1331
|
+
await this.db.update(messages).set({
|
|
1332
|
+
payload: updatedPayload
|
|
1333
|
+
}).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId)));
|
|
1334
|
+
return {
|
|
1335
|
+
uploadId,
|
|
1336
|
+
alreadyIngested: false
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
function extractMediaDescriptor(message) {
|
|
1341
|
+
const payload = message.payload ?? {};
|
|
1342
|
+
for (const key of [
|
|
1343
|
+
"image",
|
|
1344
|
+
"audio",
|
|
1345
|
+
"video",
|
|
1346
|
+
"document",
|
|
1347
|
+
"sticker"
|
|
1348
|
+
]) {
|
|
1349
|
+
const media = payload[key];
|
|
1350
|
+
if (media?.id) {
|
|
1351
|
+
return {
|
|
1352
|
+
sourceMediaId: media.id,
|
|
1353
|
+
mimeType: media.mime_type ?? "application/octet-stream",
|
|
1354
|
+
filename: media.filename
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return void 0;
|
|
1359
|
+
}
|
|
1360
|
+
__name(extractMediaDescriptor, "extractMediaDescriptor");
|
|
1361
|
+
|
|
1362
|
+
// src/createMetaWhatsAppModule.ts
|
|
1363
|
+
function createMetaWhatsAppModule(params) {
|
|
1364
|
+
const { db, config, nonceStore, providers = {}, hooks } = params;
|
|
1365
|
+
const startState = params.startState ?? "start";
|
|
1366
|
+
const flowEngineEnabled = params.features?.flowEngine ?? true;
|
|
1367
|
+
const messageProvider = new import_meta_whatsapp_provider.WhatsAppMessageProvider({
|
|
1368
|
+
accessToken: config.accessToken,
|
|
1369
|
+
phoneNumberId: config.phoneNumberId,
|
|
1370
|
+
wabaId: config.wabaId,
|
|
1371
|
+
apiVersion: config.apiVersion,
|
|
1372
|
+
baseUrl: config.baseUrl
|
|
1373
|
+
});
|
|
1374
|
+
const channel = new WhatsAppChannelAdapter(messageProvider);
|
|
1375
|
+
const sessionRepository = new SessionRepository(db);
|
|
1376
|
+
const messageRepository = new MessageRepository(db);
|
|
1377
|
+
const settingsRepository = new SettingsRepository(db);
|
|
1378
|
+
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1379
|
+
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime);
|
|
1380
|
+
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage);
|
|
1381
|
+
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
1382
|
+
appSecret: config.appSecret,
|
|
1383
|
+
nonceStore,
|
|
1384
|
+
sessionRepository,
|
|
1385
|
+
messageRepository,
|
|
1386
|
+
logMessage,
|
|
1387
|
+
startState,
|
|
1388
|
+
hooks,
|
|
1389
|
+
realtime: providers.realtime
|
|
1390
|
+
});
|
|
1391
|
+
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
1392
|
+
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage) : void 0;
|
|
1393
|
+
return {
|
|
1394
|
+
channel,
|
|
1395
|
+
// undefined quando providers.objectStorage não foi injetado.
|
|
1396
|
+
ingestInboundMedia,
|
|
1397
|
+
conversations: {
|
|
1398
|
+
log: logMessage,
|
|
1399
|
+
send: sendMessage,
|
|
1400
|
+
takeover: new TakeoverConversationUseCase(sessionRepository, providers.realtime),
|
|
1401
|
+
release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),
|
|
1402
|
+
list: new ListConversationsUseCase(sessionRepository),
|
|
1403
|
+
listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),
|
|
1404
|
+
export: new ExportConversationUseCase(sessionRepository),
|
|
1405
|
+
repository: sessionRepository
|
|
1406
|
+
},
|
|
1407
|
+
settings: settingsRepository,
|
|
1408
|
+
webhook: {
|
|
1409
|
+
receive: receiveWebhook,
|
|
1410
|
+
// GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.
|
|
1411
|
+
verifyChallenge: /* @__PURE__ */ __name((query) => verifyWebhookChallenge({
|
|
1412
|
+
...query,
|
|
1413
|
+
expectedToken: config.webhookVerifyToken
|
|
1414
|
+
}), "verifyChallenge")
|
|
1415
|
+
},
|
|
1416
|
+
// undefined quando features.flowEngine === false.
|
|
1417
|
+
flows: flowInterpreter ? {
|
|
1418
|
+
interpreter: flowInterpreter,
|
|
1419
|
+
registerFlowAction: /* @__PURE__ */ __name((kind, handler) => flowInterpreter.registerFlowAction(kind, handler), "registerFlowAction"),
|
|
1420
|
+
get: new GetFlowGraphUseCase(flowGraphRepository),
|
|
1421
|
+
list: new ListFlowGraphsUseCase(flowGraphRepository),
|
|
1422
|
+
create: new CreateFlowGraphUseCase(flowGraphRepository),
|
|
1423
|
+
save: new SaveFlowGraphUseCase(flowGraphRepository),
|
|
1424
|
+
delete: new DeleteFlowGraphUseCase(flowGraphRepository),
|
|
1425
|
+
livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
|
|
1426
|
+
repository: flowGraphRepository
|
|
1427
|
+
} : void 0,
|
|
1428
|
+
catalog: providers.catalog
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
__name(createMetaWhatsAppModule, "createMetaWhatsAppModule");
|
|
1432
|
+
|
|
1433
|
+
// src/runMigrations.ts
|
|
1434
|
+
var import_node_path = require("path");
|
|
1435
|
+
var import_migrator = require("drizzle-orm/bun-sql/migrator");
|
|
1436
|
+
async function runMetaWhatsAppMigrations(db) {
|
|
1437
|
+
await (0, import_migrator.migrate)(db, {
|
|
1438
|
+
migrationsFolder: (0, import_node_path.join)(__dirname, "migrations"),
|
|
1439
|
+
migrationsTable: "meta_whatsapp_migrations"
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
__name(runMetaWhatsAppMigrations, "runMetaWhatsAppMigrations");
|
|
1443
|
+
|
|
1444
|
+
// src/realtime/SseHub.ts
|
|
1445
|
+
var import_node_crypto2 = require("crypto");
|
|
1446
|
+
var TICKET_TTL_SECONDS = 60;
|
|
1447
|
+
var SseHub = class {
|
|
1448
|
+
static {
|
|
1449
|
+
__name(this, "SseHub");
|
|
1450
|
+
}
|
|
1451
|
+
relay;
|
|
1452
|
+
localListeners = /* @__PURE__ */ new Map();
|
|
1453
|
+
// Uma assinatura de relay por CANAL, compartilhada por todos os listeners locais dele — uma
|
|
1454
|
+
// por listener faria o relay entregar N cópias num canal com N conexões abertas.
|
|
1455
|
+
relaySubscriptions = /* @__PURE__ */ new Map();
|
|
1456
|
+
// Identifica esta instância do processo para descartar o próprio eco (pub/sub entrega a
|
|
1457
|
+
// mensagem a todos os inscritos, inclusive quem publicou).
|
|
1458
|
+
instanceId = (0, import_node_crypto2.randomUUID)();
|
|
1459
|
+
constructor(relay) {
|
|
1460
|
+
this.relay = relay;
|
|
1461
|
+
}
|
|
1462
|
+
emit(channel, event, payload) {
|
|
1463
|
+
this.dispatchLocal(channel, event, payload);
|
|
1464
|
+
if (this.relay) {
|
|
1465
|
+
const envelope = {
|
|
1466
|
+
originId: this.instanceId,
|
|
1467
|
+
event,
|
|
1468
|
+
payload
|
|
1469
|
+
};
|
|
1470
|
+
this.relay.publish(channel, JSON.stringify(envelope)).catch(() => void 0);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
dispatchLocal(channel, event, payload) {
|
|
1474
|
+
const listeners = this.localListeners.get(channel);
|
|
1475
|
+
if (!listeners) return;
|
|
1476
|
+
for (const listener of listeners) listener(event, payload);
|
|
1477
|
+
}
|
|
1478
|
+
async subscribe(channel, listener) {
|
|
1479
|
+
if (!this.localListeners.has(channel)) this.localListeners.set(channel, /* @__PURE__ */ new Set());
|
|
1480
|
+
this.localListeners.get(channel).add(listener);
|
|
1481
|
+
if (this.relay && !this.relaySubscriptions.has(channel)) {
|
|
1482
|
+
this.relaySubscriptions.set(channel, this.relay.subscribe(channel, (message) => {
|
|
1483
|
+
try {
|
|
1484
|
+
const envelope = JSON.parse(message);
|
|
1485
|
+
if (envelope.originId === this.instanceId) return;
|
|
1486
|
+
this.dispatchLocal(channel, envelope.event, envelope.payload);
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
return () => {
|
|
1492
|
+
const listeners = this.localListeners.get(channel);
|
|
1493
|
+
listeners?.delete(listener);
|
|
1494
|
+
if (listeners && listeners.size === 0) {
|
|
1495
|
+
this.localListeners.delete(channel);
|
|
1496
|
+
const pendingUnsubscribe = this.relaySubscriptions.get(channel);
|
|
1497
|
+
this.relaySubscriptions.delete(channel);
|
|
1498
|
+
pendingUnsubscribe?.then((unsubscribe) => unsubscribe()).catch(() => void 0);
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
async function issueSseTicket(store, companyId, whatsappNumber) {
|
|
1504
|
+
const ticket = (0, import_node_crypto2.randomUUID)();
|
|
1505
|
+
await store.set(`sse:ticket:${ticket}`, JSON.stringify({
|
|
1506
|
+
companyId,
|
|
1507
|
+
whatsappNumber
|
|
1508
|
+
}), TICKET_TTL_SECONDS);
|
|
1509
|
+
return ticket;
|
|
1510
|
+
}
|
|
1511
|
+
__name(issueSseTicket, "issueSseTicket");
|
|
1512
|
+
async function redeemSseTicket(store, ticket) {
|
|
1513
|
+
const raw = await store.get(`sse:ticket:${ticket}`);
|
|
1514
|
+
if (!raw) return null;
|
|
1515
|
+
await store.delete(`sse:ticket:${ticket}`);
|
|
1516
|
+
return JSON.parse(raw);
|
|
1517
|
+
}
|
|
1518
|
+
__name(redeemSseTicket, "redeemSseTicket");
|
|
1519
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1520
|
+
0 && (module.exports = {
|
|
1521
|
+
CreateFlowGraphUseCase,
|
|
1522
|
+
DeleteFlowGraphUseCase,
|
|
1523
|
+
ExportConversationUseCase,
|
|
1524
|
+
FlowGraphRepository,
|
|
1525
|
+
FlowInterpreter,
|
|
1526
|
+
GetFlowGraphUseCase,
|
|
1527
|
+
GetLiveFlowPositionsUseCase,
|
|
1528
|
+
IngestInboundMediaUseCase,
|
|
1529
|
+
InvalidFlowGraphError,
|
|
1530
|
+
ListConversationsUseCase,
|
|
1531
|
+
ListFlowGraphsUseCase,
|
|
1532
|
+
ListMessagesUseCase,
|
|
1533
|
+
LogMessageUseCase,
|
|
1534
|
+
MessageRepository,
|
|
1535
|
+
OptimisticLockError,
|
|
1536
|
+
ReceiveWebhookUseCase,
|
|
1537
|
+
ReleaseConversationUseCase,
|
|
1538
|
+
SaveFlowGraphUseCase,
|
|
1539
|
+
SendMessageUseCase,
|
|
1540
|
+
SessionRepository,
|
|
1541
|
+
SettingsRepository,
|
|
1542
|
+
SseHub,
|
|
1543
|
+
TakeoverConversationUseCase,
|
|
1544
|
+
WEBHOOK_NONCE_TTL_SECONDS,
|
|
1545
|
+
WhatsAppChannelAdapter,
|
|
1546
|
+
claimWebhookDelivery,
|
|
1547
|
+
createMetaWhatsAppModule,
|
|
1548
|
+
extractMediaDescriptor,
|
|
1549
|
+
flowGraphs,
|
|
1550
|
+
issueSseTicket,
|
|
1551
|
+
messages,
|
|
1552
|
+
metaWhatsAppSchema,
|
|
1553
|
+
redeemSseTicket,
|
|
1554
|
+
runMetaWhatsAppMigrations,
|
|
1555
|
+
sessions,
|
|
1556
|
+
settings,
|
|
1557
|
+
verifyWebhookChallenge,
|
|
1558
|
+
verifyWebhookSignature
|
|
1559
|
+
});
|
|
1560
|
+
//# sourceMappingURL=index.cjs.map
|