@adatechnology/meta-whatsapp-module 0.2.0-rc.10 → 0.2.0-rc.12
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 +527 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +467 -3
- package/dist/index.d.ts +467 -3
- package/dist/index.js +507 -78
- package/dist/index.js.map +1 -1
- package/dist/migrations/0006_conversation_documents.sql +19 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -8,11 +8,11 @@ var __export = (target, all) => {
|
|
|
8
8
|
for (var name in all)
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
|
-
var __copyProps = (to, from, except,
|
|
11
|
+
var __copyProps = (to, from, except, desc3) => {
|
|
12
12
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
13
|
for (let key of __getOwnPropNames(from))
|
|
14
14
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc3 = __getOwnPropDesc(from, key)) || desc3.enumerable });
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
@@ -22,7 +22,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
CreateFlowGraphUseCase: () => CreateFlowGraphUseCase,
|
|
25
|
+
DeleteConversationUseCase: () => DeleteConversationUseCase,
|
|
25
26
|
DeleteFlowGraphUseCase: () => DeleteFlowGraphUseCase,
|
|
27
|
+
DocumentRepository: () => DocumentRepository,
|
|
26
28
|
ExportConversationUseCase: () => ExportConversationUseCase,
|
|
27
29
|
FlowGraphRepository: () => FlowGraphRepository,
|
|
28
30
|
FlowInterpreter: () => FlowInterpreter,
|
|
@@ -30,6 +32,8 @@ __export(index_exports, {
|
|
|
30
32
|
GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
|
|
31
33
|
IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
|
|
32
34
|
InvalidFlowGraphError: () => InvalidFlowGraphError,
|
|
35
|
+
ListCompanyDocumentsUseCase: () => ListCompanyDocumentsUseCase,
|
|
36
|
+
ListConversationDocumentsUseCase: () => ListConversationDocumentsUseCase,
|
|
33
37
|
ListConversationsUseCase: () => ListConversationsUseCase,
|
|
34
38
|
ListFlowGraphsUseCase: () => ListFlowGraphsUseCase,
|
|
35
39
|
ListMessagesUseCase: () => ListMessagesUseCase,
|
|
@@ -37,6 +41,7 @@ __export(index_exports, {
|
|
|
37
41
|
META_WHATSAPP_MIGRATIONS_TABLE: () => META_WHATSAPP_MIGRATIONS_TABLE,
|
|
38
42
|
MessageRepository: () => MessageRepository,
|
|
39
43
|
OptimisticLockError: () => OptimisticLockError,
|
|
44
|
+
PurgeExpiredDocumentsUseCase: () => PurgeExpiredDocumentsUseCase,
|
|
40
45
|
ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
|
|
41
46
|
ReleaseConversationUseCase: () => ReleaseConversationUseCase,
|
|
42
47
|
SaveFlowGraphUseCase: () => SaveFlowGraphUseCase,
|
|
@@ -49,6 +54,7 @@ __export(index_exports, {
|
|
|
49
54
|
WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
|
|
50
55
|
claimWebhookDelivery: () => claimWebhookDelivery,
|
|
51
56
|
createMetaWhatsAppModule: () => createMetaWhatsAppModule,
|
|
57
|
+
documents: () => documents,
|
|
52
58
|
extractMediaDescriptor: () => extractMediaDescriptor,
|
|
53
59
|
flowGraphs: () => flowGraphs,
|
|
54
60
|
issueSseTicket: () => issueSseTicket,
|
|
@@ -184,6 +190,46 @@ var messages = metaWhatsAppSchema.table("messages", {
|
|
|
184
190
|
// tamanho do transcript.
|
|
185
191
|
(0, import_pg_core.index)("idx_messages_moderation_flagged").on(table.companyId, table.createdAt).where(import_drizzle_orm.sql`${table.moderationFlagged}`)
|
|
186
192
|
]);
|
|
193
|
+
var documents = metaWhatsAppSchema.table("documents", {
|
|
194
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
195
|
+
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
196
|
+
sessionId: (0, import_pg_core.uuid)("session_id").notNull().references(() => sessions.id, {
|
|
197
|
+
onDelete: "cascade"
|
|
198
|
+
}),
|
|
199
|
+
// Anulável e `set null`: apagar a mensagem não deve apagar o arquivo da biblioteca, e arquivo
|
|
200
|
+
// do atendente nasce sem mensagem.
|
|
201
|
+
messageId: (0, import_pg_core.uuid)("message_id").references(() => messages.id, {
|
|
202
|
+
onDelete: "set null"
|
|
203
|
+
}),
|
|
204
|
+
uploadId: (0, import_pg_core.varchar)("upload_id", {
|
|
205
|
+
length: 256
|
|
206
|
+
}).notNull(),
|
|
207
|
+
filename: (0, import_pg_core.varchar)("filename", {
|
|
208
|
+
length: 512
|
|
209
|
+
}).notNull(),
|
|
210
|
+
mimeType: (0, import_pg_core.varchar)("mime_type", {
|
|
211
|
+
length: 128
|
|
212
|
+
}).notNull(),
|
|
213
|
+
sizeBytes: (0, import_pg_core.integer)("size_bytes").notNull(),
|
|
214
|
+
// Do provider de storage, que é endereçado por conteúdo — serve para reconciliar storage
|
|
215
|
+
// contra tabela e para detectar o mesmo binário chegando duas vezes.
|
|
216
|
+
sha256: (0, import_pg_core.varchar)("sha256", {
|
|
217
|
+
length: 64
|
|
218
|
+
}),
|
|
219
|
+
source: (0, import_pg_core.varchar)("source", {
|
|
220
|
+
length: 12
|
|
221
|
+
}).notNull(),
|
|
222
|
+
linkedAt: (0, import_pg_core.timestamp)("linked_at", {
|
|
223
|
+
withTimezone: true
|
|
224
|
+
}).notNull().defaultNow()
|
|
225
|
+
}, (table) => [
|
|
226
|
+
(0, import_pg_core.index)("idx_documents_session_linked").on(table.sessionId, table.linkedAt),
|
|
227
|
+
// Para varredura de retenção por idade sem passar por sessão.
|
|
228
|
+
(0, import_pg_core.index)("idx_documents_company_linked").on(table.companyId, table.linkedAt),
|
|
229
|
+
// O mesmo objeto não pode ser linkado duas vezes na mesma empresa: o job de ingestão é
|
|
230
|
+
// reentregue por retry, e sem isto a reentrega criaria linha duplicada no painel.
|
|
231
|
+
(0, import_pg_core.uniqueIndex)("idx_documents_company_upload").on(table.companyId, table.uploadId)
|
|
232
|
+
]);
|
|
187
233
|
var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
188
234
|
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
189
235
|
companyId: (0, import_pg_core.uuid)("company_id").notNull(),
|
|
@@ -364,6 +410,16 @@ var SessionRepository = class {
|
|
|
364
410
|
async release(companyId, whatsappNumber) {
|
|
365
411
|
await this.setMode(companyId, whatsappNumber, "bot", null);
|
|
366
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Apaga a sessão; a cascata das FKs leva mensagens e documentos.
|
|
415
|
+
*
|
|
416
|
+
* Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é
|
|
417
|
+
* chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os
|
|
418
|
+
* objetos antes deixa mídia órfã sendo cobrada para sempre.
|
|
419
|
+
*/
|
|
420
|
+
async deleteByNumber(companyId, whatsappNumber) {
|
|
421
|
+
await this.db.delete(sessions).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
|
|
422
|
+
}
|
|
367
423
|
async requestHuman(companyId, whatsappNumber) {
|
|
368
424
|
await this.db.update(sessions).set({
|
|
369
425
|
humanRequestedAt: import_drizzle_orm2.sql`now()`,
|
|
@@ -732,11 +788,13 @@ var SendMessageUseCase = class {
|
|
|
732
788
|
sessionRepository;
|
|
733
789
|
logMessage;
|
|
734
790
|
objectStorage;
|
|
735
|
-
|
|
791
|
+
documentRepository;
|
|
792
|
+
constructor(channel, sessionRepository, logMessage, objectStorage, documentRepository) {
|
|
736
793
|
this.channel = channel;
|
|
737
794
|
this.sessionRepository = sessionRepository;
|
|
738
795
|
this.logMessage = logMessage;
|
|
739
796
|
this.objectStorage = objectStorage;
|
|
797
|
+
this.documentRepository = documentRepository;
|
|
740
798
|
}
|
|
741
799
|
async assertWithinWindow(companyId, whatsappNumber) {
|
|
742
800
|
const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber);
|
|
@@ -773,7 +831,7 @@ var SendMessageUseCase = class {
|
|
|
773
831
|
mimeType: params.mimeType,
|
|
774
832
|
key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`
|
|
775
833
|
})).uploadId : void 0;
|
|
776
|
-
|
|
834
|
+
const saved = await this.logMessage.execute({
|
|
777
835
|
companyId: params.companyId,
|
|
778
836
|
whatsappNumber: params.whatsappNumber,
|
|
779
837
|
direction: "outbound",
|
|
@@ -792,6 +850,19 @@ var SendMessageUseCase = class {
|
|
|
792
850
|
status: "sent",
|
|
793
851
|
startState: params.startState
|
|
794
852
|
});
|
|
853
|
+
if (saved && uploadId) {
|
|
854
|
+
await this.documentRepository?.link({
|
|
855
|
+
companyId: params.companyId,
|
|
856
|
+
sessionId: saved.sessionId,
|
|
857
|
+
messageId: saved.id,
|
|
858
|
+
uploadId,
|
|
859
|
+
filename: params.filename,
|
|
860
|
+
mimeType: params.mimeType,
|
|
861
|
+
sizeBytes: params.buffer.length,
|
|
862
|
+
source: params.sender
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return saved;
|
|
795
866
|
}
|
|
796
867
|
// Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece
|
|
797
868
|
// para reabri-la.
|
|
@@ -909,8 +980,330 @@ var ListMessagesUseCase = class {
|
|
|
909
980
|
}
|
|
910
981
|
};
|
|
911
982
|
|
|
912
|
-
// src/use-cases/
|
|
983
|
+
// src/use-cases/ListConversationDocuments.use-case.ts
|
|
913
984
|
var import_meta_whatsapp_contracts4 = require("@adatechnology/meta-whatsapp-contracts");
|
|
985
|
+
var ListConversationDocumentsUseCase = class {
|
|
986
|
+
static {
|
|
987
|
+
__name(this, "ListConversationDocumentsUseCase");
|
|
988
|
+
}
|
|
989
|
+
sessionRepository;
|
|
990
|
+
documentRepository;
|
|
991
|
+
constructor(sessionRepository, documentRepository) {
|
|
992
|
+
this.sessionRepository = sessionRepository;
|
|
993
|
+
this.documentRepository = documentRepository;
|
|
994
|
+
}
|
|
995
|
+
async execute(params) {
|
|
996
|
+
const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
|
|
997
|
+
if (!session) throw new import_meta_whatsapp_contracts4.SessionNotFoundError(params.whatsappNumber);
|
|
998
|
+
const { rows, total } = await this.documentRepository.listByConversation({
|
|
999
|
+
companyId: params.companyId,
|
|
1000
|
+
sessionId: session.id,
|
|
1001
|
+
...params.search ? {
|
|
1002
|
+
search: params.search
|
|
1003
|
+
} : {},
|
|
1004
|
+
...params.sources && params.sources.length > 0 ? {
|
|
1005
|
+
sources: params.sources
|
|
1006
|
+
} : {},
|
|
1007
|
+
...params.sortDirection ? {
|
|
1008
|
+
sortDirection: params.sortDirection
|
|
1009
|
+
} : {},
|
|
1010
|
+
...params.page ? {
|
|
1011
|
+
page: params.page
|
|
1012
|
+
} : {},
|
|
1013
|
+
...params.limit ? {
|
|
1014
|
+
limit: params.limit
|
|
1015
|
+
} : {}
|
|
1016
|
+
});
|
|
1017
|
+
return {
|
|
1018
|
+
documents: rows.map((row) => ({
|
|
1019
|
+
id: row.uploadId,
|
|
1020
|
+
filename: row.filename,
|
|
1021
|
+
mimeType: row.mimeType,
|
|
1022
|
+
sizeBytes: row.sizeBytes,
|
|
1023
|
+
source: row.source,
|
|
1024
|
+
linkedAt: row.linkedAt.toISOString()
|
|
1025
|
+
})),
|
|
1026
|
+
total
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// src/use-cases/ListCompanyDocuments.use-case.ts
|
|
1032
|
+
var ListCompanyDocumentsUseCase = class {
|
|
1033
|
+
static {
|
|
1034
|
+
__name(this, "ListCompanyDocumentsUseCase");
|
|
1035
|
+
}
|
|
1036
|
+
documentRepository;
|
|
1037
|
+
constructor(documentRepository) {
|
|
1038
|
+
this.documentRepository = documentRepository;
|
|
1039
|
+
}
|
|
1040
|
+
async execute(params) {
|
|
1041
|
+
const { rows, total } = await this.documentRepository.listByCompany({
|
|
1042
|
+
companyId: params.companyId,
|
|
1043
|
+
...params.search ? {
|
|
1044
|
+
search: params.search
|
|
1045
|
+
} : {},
|
|
1046
|
+
...params.sources && params.sources.length > 0 ? {
|
|
1047
|
+
sources: params.sources
|
|
1048
|
+
} : {},
|
|
1049
|
+
...params.sortDirection ? {
|
|
1050
|
+
sortDirection: params.sortDirection
|
|
1051
|
+
} : {},
|
|
1052
|
+
...params.page ? {
|
|
1053
|
+
page: params.page
|
|
1054
|
+
} : {},
|
|
1055
|
+
...params.limit ? {
|
|
1056
|
+
limit: params.limit
|
|
1057
|
+
} : {}
|
|
1058
|
+
});
|
|
1059
|
+
return {
|
|
1060
|
+
documents: rows.map((row) => ({
|
|
1061
|
+
id: row.uploadId,
|
|
1062
|
+
conversationId: row.whatsappNumber,
|
|
1063
|
+
filename: row.filename,
|
|
1064
|
+
mimeType: row.mimeType,
|
|
1065
|
+
sizeBytes: row.sizeBytes,
|
|
1066
|
+
source: row.source,
|
|
1067
|
+
linkedAt: row.linkedAt.toISOString()
|
|
1068
|
+
})),
|
|
1069
|
+
total
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// src/use-cases/DeleteConversation.use-case.ts
|
|
1075
|
+
var import_meta_whatsapp_contracts5 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1076
|
+
var DeleteConversationUseCase = class {
|
|
1077
|
+
static {
|
|
1078
|
+
__name(this, "DeleteConversationUseCase");
|
|
1079
|
+
}
|
|
1080
|
+
sessionRepository;
|
|
1081
|
+
documentRepository;
|
|
1082
|
+
objectStorage;
|
|
1083
|
+
constructor(sessionRepository, documentRepository, objectStorage) {
|
|
1084
|
+
this.sessionRepository = sessionRepository;
|
|
1085
|
+
this.documentRepository = documentRepository;
|
|
1086
|
+
this.objectStorage = objectStorage;
|
|
1087
|
+
}
|
|
1088
|
+
async execute(params) {
|
|
1089
|
+
const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
|
|
1090
|
+
if (!session) throw new import_meta_whatsapp_contracts5.SessionNotFoundError(params.whatsappNumber);
|
|
1091
|
+
const uploadIds = await this.documentRepository.listUploadIdsBySession(params.companyId, session.id);
|
|
1092
|
+
if (uploadIds.length > 0 && !this.objectStorage?.delete) {
|
|
1093
|
+
throw new Error("storage_delete_unsupported: a conversa tem arquivos e o storage injetado n\xE3o implementa delete");
|
|
1094
|
+
}
|
|
1095
|
+
const failedObjects = [];
|
|
1096
|
+
let deletedObjects = 0;
|
|
1097
|
+
for (const uploadId of uploadIds) {
|
|
1098
|
+
try {
|
|
1099
|
+
await this.objectStorage?.delete?.(uploadId);
|
|
1100
|
+
deletedObjects++;
|
|
1101
|
+
} catch {
|
|
1102
|
+
failedObjects.push(uploadId);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (failedObjects.length > 0) return {
|
|
1106
|
+
deletedObjects,
|
|
1107
|
+
failedObjects
|
|
1108
|
+
};
|
|
1109
|
+
await this.sessionRepository.deleteByNumber(params.companyId, params.whatsappNumber);
|
|
1110
|
+
return {
|
|
1111
|
+
deletedObjects,
|
|
1112
|
+
failedObjects: []
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// src/use-cases/PurgeExpiredDocuments.use-case.ts
|
|
1118
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
1119
|
+
var HOURS_IN_DAY = 24;
|
|
1120
|
+
var MILLISECONDS_IN_HOUR = 60 * 60 * 1e3;
|
|
1121
|
+
var PurgeExpiredDocumentsUseCase = class {
|
|
1122
|
+
static {
|
|
1123
|
+
__name(this, "PurgeExpiredDocumentsUseCase");
|
|
1124
|
+
}
|
|
1125
|
+
documentRepository;
|
|
1126
|
+
objectStorage;
|
|
1127
|
+
constructor(documentRepository, objectStorage) {
|
|
1128
|
+
this.documentRepository = documentRepository;
|
|
1129
|
+
this.objectStorage = objectStorage;
|
|
1130
|
+
}
|
|
1131
|
+
async execute(params) {
|
|
1132
|
+
if (!this.objectStorage?.delete) {
|
|
1133
|
+
throw new Error("storage_delete_unsupported: reten\xE7\xE3o exige um storage que implemente delete");
|
|
1134
|
+
}
|
|
1135
|
+
const reference = params.now ?? /* @__PURE__ */ new Date();
|
|
1136
|
+
const olderThan = new Date(reference.getTime() - params.retentionDays * HOURS_IN_DAY * MILLISECONDS_IN_HOUR);
|
|
1137
|
+
const expired = await this.documentRepository.listExpired(params.companyId, olderThan, params.batchSize ?? DEFAULT_BATCH_SIZE);
|
|
1138
|
+
const failed = [];
|
|
1139
|
+
let purged = 0;
|
|
1140
|
+
for (const document of expired) {
|
|
1141
|
+
try {
|
|
1142
|
+
await this.objectStorage.delete(document.uploadId);
|
|
1143
|
+
} catch {
|
|
1144
|
+
failed.push(document.uploadId);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
await this.documentRepository.deleteById(params.companyId, document.id);
|
|
1148
|
+
purged++;
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
purged,
|
|
1152
|
+
failed
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// src/repositories/DocumentRepository.ts
|
|
1158
|
+
var import_drizzle_orm6 = require("drizzle-orm");
|
|
1159
|
+
var DEFAULT_LIMIT3 = 50;
|
|
1160
|
+
function companyDocumentSearch(search) {
|
|
1161
|
+
const term = search?.trim();
|
|
1162
|
+
if (!term) return void 0;
|
|
1163
|
+
const digits = term.replace(/\D/g, "");
|
|
1164
|
+
const byFilename = (0, import_drizzle_orm6.ilike)(documents.filename, `%${term}%`);
|
|
1165
|
+
if (!digits) return byFilename;
|
|
1166
|
+
return (0, import_drizzle_orm6.or)(byFilename, (0, import_drizzle_orm6.ilike)(sessions.whatsappNumber, `%${digits}%`));
|
|
1167
|
+
}
|
|
1168
|
+
__name(companyDocumentSearch, "companyDocumentSearch");
|
|
1169
|
+
var DocumentRepository = class {
|
|
1170
|
+
static {
|
|
1171
|
+
__name(this, "DocumentRepository");
|
|
1172
|
+
}
|
|
1173
|
+
db;
|
|
1174
|
+
constructor(db) {
|
|
1175
|
+
this.db = db;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o
|
|
1179
|
+
* job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma
|
|
1180
|
+
* checagem, duplicando a linha no painel.
|
|
1181
|
+
*
|
|
1182
|
+
* Devolve `undefined` quando o documento já estava linkado.
|
|
1183
|
+
*/
|
|
1184
|
+
async link(params) {
|
|
1185
|
+
const values = {
|
|
1186
|
+
companyId: params.companyId,
|
|
1187
|
+
sessionId: params.sessionId,
|
|
1188
|
+
messageId: params.messageId ?? null,
|
|
1189
|
+
uploadId: params.uploadId,
|
|
1190
|
+
filename: params.filename,
|
|
1191
|
+
mimeType: params.mimeType,
|
|
1192
|
+
sizeBytes: params.sizeBytes,
|
|
1193
|
+
sha256: params.sha256 ?? null,
|
|
1194
|
+
source: params.source
|
|
1195
|
+
};
|
|
1196
|
+
const [created] = await this.db.insert(documents).values(values).onConflictDoNothing().returning();
|
|
1197
|
+
return created;
|
|
1198
|
+
}
|
|
1199
|
+
async listByConversation(params) {
|
|
1200
|
+
const filters = [
|
|
1201
|
+
(0, import_drizzle_orm6.eq)(documents.companyId, params.companyId),
|
|
1202
|
+
(0, import_drizzle_orm6.eq)(documents.sessionId, params.sessionId)
|
|
1203
|
+
];
|
|
1204
|
+
if (params.search) filters.push((0, import_drizzle_orm6.ilike)(documents.filename, `%${params.search}%`));
|
|
1205
|
+
if (params.sources && params.sources.length > 0) {
|
|
1206
|
+
filters.push((0, import_drizzle_orm6.inArray)(documents.source, [
|
|
1207
|
+
...params.sources
|
|
1208
|
+
]));
|
|
1209
|
+
}
|
|
1210
|
+
const where = (0, import_drizzle_orm6.and)(...filters);
|
|
1211
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
1212
|
+
const page = params.page && params.page > 0 ? params.page : 1;
|
|
1213
|
+
const orderBy = params.sortDirection === "asc" ? (0, import_drizzle_orm6.asc)(documents.linkedAt) : (0, import_drizzle_orm6.desc)(documents.linkedAt);
|
|
1214
|
+
const [rows, counted] = await Promise.all([
|
|
1215
|
+
this.db.select().from(documents).where(where).orderBy(orderBy).limit(limit).offset((page - 1) * limit),
|
|
1216
|
+
this.db.select({
|
|
1217
|
+
value: (0, import_drizzle_orm6.count)()
|
|
1218
|
+
}).from(documents).where(where)
|
|
1219
|
+
]);
|
|
1220
|
+
return {
|
|
1221
|
+
rows,
|
|
1222
|
+
total: counted[0]?.value ?? 0
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* A biblioteca da EMPRESA inteira, não de uma conversa.
|
|
1227
|
+
*
|
|
1228
|
+
* A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.
|
|
1229
|
+
*
|
|
1230
|
+
* Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,
|
|
1231
|
+
* arquivo sem essa referência é inútil: o atendente vê "comprovante.pdf" e não sabe de quem.
|
|
1232
|
+
*
|
|
1233
|
+
* Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a
|
|
1234
|
+
* varredura de retenção.
|
|
1235
|
+
*/
|
|
1236
|
+
async listByCompany(params) {
|
|
1237
|
+
const filters = [
|
|
1238
|
+
(0, import_drizzle_orm6.eq)(documents.companyId, params.companyId)
|
|
1239
|
+
];
|
|
1240
|
+
const search = companyDocumentSearch(params.search);
|
|
1241
|
+
if (search) filters.push(search);
|
|
1242
|
+
if (params.sources && params.sources.length > 0) {
|
|
1243
|
+
filters.push((0, import_drizzle_orm6.inArray)(documents.source, [
|
|
1244
|
+
...params.sources
|
|
1245
|
+
]));
|
|
1246
|
+
}
|
|
1247
|
+
const where = (0, import_drizzle_orm6.and)(...filters);
|
|
1248
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
1249
|
+
const page = params.page && params.page > 0 ? params.page : 1;
|
|
1250
|
+
const orderBy = params.sortDirection === "asc" ? (0, import_drizzle_orm6.asc)(documents.linkedAt) : (0, import_drizzle_orm6.desc)(documents.linkedAt);
|
|
1251
|
+
const [rows, counted] = await Promise.all([
|
|
1252
|
+
this.db.select({
|
|
1253
|
+
id: documents.id,
|
|
1254
|
+
uploadId: documents.uploadId,
|
|
1255
|
+
filename: documents.filename,
|
|
1256
|
+
mimeType: documents.mimeType,
|
|
1257
|
+
sizeBytes: documents.sizeBytes,
|
|
1258
|
+
source: documents.source,
|
|
1259
|
+
linkedAt: documents.linkedAt,
|
|
1260
|
+
// Só o número: o NOME do cliente é dado do produto (tabela própria dele), não do
|
|
1261
|
+
// módulo. Quem quiser exibir "Marina Alves" enriquece na borda.
|
|
1262
|
+
whatsappNumber: sessions.whatsappNumber
|
|
1263
|
+
}).from(documents).innerJoin(sessions, (0, import_drizzle_orm6.eq)(documents.sessionId, sessions.id)).where(where).orderBy(orderBy).limit(limit).offset((page - 1) * limit),
|
|
1264
|
+
// O mesmo join da listagem, e não só `from(documents)`: a busca pode citar
|
|
1265
|
+
// `sessions.whatsapp_number`, e uma contagem sem a tabela na cláusula não compila — pior,
|
|
1266
|
+
// se compilasse, o total divergiria das linhas e a paginação prometeria páginas vazias.
|
|
1267
|
+
this.db.select({
|
|
1268
|
+
value: (0, import_drizzle_orm6.count)()
|
|
1269
|
+
}).from(documents).innerJoin(sessions, (0, import_drizzle_orm6.eq)(documents.sessionId, sessions.id)).where(where)
|
|
1270
|
+
]);
|
|
1271
|
+
return {
|
|
1272
|
+
rows,
|
|
1273
|
+
total: counted[0]?.value ?? 0
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o
|
|
1278
|
+
* download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.
|
|
1279
|
+
*/
|
|
1280
|
+
async findByUploadId(companyId, uploadId) {
|
|
1281
|
+
const [row] = await this.db.select().from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.uploadId, uploadId))).limit(1);
|
|
1282
|
+
return row;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Os objetos a apagar no storage antes de a linha sumir.
|
|
1286
|
+
*
|
|
1287
|
+
* Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa
|
|
1288
|
+
* precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.
|
|
1289
|
+
*/
|
|
1290
|
+
async listUploadIdsBySession(companyId, sessionId) {
|
|
1291
|
+
const rows = await this.db.select({
|
|
1292
|
+
uploadId: documents.uploadId
|
|
1293
|
+
}).from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.sessionId, sessionId)));
|
|
1294
|
+
return rows.map((row) => row.uploadId);
|
|
1295
|
+
}
|
|
1296
|
+
/** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */
|
|
1297
|
+
async listExpired(companyId, olderThan, limit = DEFAULT_LIMIT3) {
|
|
1298
|
+
return this.db.select().from(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.lt)(documents.linkedAt, olderThan))).orderBy(documents.linkedAt).limit(limit);
|
|
1299
|
+
}
|
|
1300
|
+
async deleteById(companyId, id) {
|
|
1301
|
+
await this.db.delete(documents).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(documents.companyId, companyId), (0, import_drizzle_orm6.eq)(documents.id, id)));
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
// src/use-cases/ExportConversation.use-case.ts
|
|
1306
|
+
var import_meta_whatsapp_contracts6 = require("@adatechnology/meta-whatsapp-contracts");
|
|
914
1307
|
var ExportConversationUseCase = class {
|
|
915
1308
|
static {
|
|
916
1309
|
__name(this, "ExportConversationUseCase");
|
|
@@ -921,7 +1314,7 @@ var ExportConversationUseCase = class {
|
|
|
921
1314
|
}
|
|
922
1315
|
async execute(params) {
|
|
923
1316
|
const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber);
|
|
924
|
-
if (!result) throw new
|
|
1317
|
+
if (!result) throw new import_meta_whatsapp_contracts6.SessionNotFoundError(params.whatsappNumber);
|
|
925
1318
|
return result;
|
|
926
1319
|
}
|
|
927
1320
|
};
|
|
@@ -1001,7 +1394,7 @@ var GetLiveFlowPositionsUseCase = class {
|
|
|
1001
1394
|
};
|
|
1002
1395
|
|
|
1003
1396
|
// src/flows/FlowInterpreter.ts
|
|
1004
|
-
var
|
|
1397
|
+
var import_meta_whatsapp_contracts7 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1005
1398
|
var DEFAULT_MAX_STEPS = 50;
|
|
1006
1399
|
function evaluateCondition(node, context) {
|
|
1007
1400
|
if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false;
|
|
@@ -1102,9 +1495,9 @@ var FlowInterpreter = class {
|
|
|
1102
1495
|
kind: "terminal",
|
|
1103
1496
|
context: input.context
|
|
1104
1497
|
};
|
|
1105
|
-
if ((0,
|
|
1498
|
+
if ((0, import_meta_whatsapp_contracts7.isCrossFlowTarget)(nextNodeId)) return {
|
|
1106
1499
|
kind: "cross-flow",
|
|
1107
|
-
flowKey: (0,
|
|
1500
|
+
flowKey: (0, import_meta_whatsapp_contracts7.crossFlowKey)(nextNodeId),
|
|
1108
1501
|
context: input.context
|
|
1109
1502
|
};
|
|
1110
1503
|
if (!input.graph.nodes[nextNodeId]) return {
|
|
@@ -1160,7 +1553,7 @@ var FlowInterpreter = class {
|
|
|
1160
1553
|
|
|
1161
1554
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1162
1555
|
var import_meta_graph_core = require("@adatechnology/meta-graph-core");
|
|
1163
|
-
var
|
|
1556
|
+
var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1164
1557
|
var WhatsAppChannelAdapter = class {
|
|
1165
1558
|
static {
|
|
1166
1559
|
__name(this, "WhatsAppChannelAdapter");
|
|
@@ -1173,7 +1566,7 @@ var WhatsAppChannelAdapter = class {
|
|
|
1173
1566
|
try {
|
|
1174
1567
|
return await operation();
|
|
1175
1568
|
} catch (error) {
|
|
1176
|
-
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new
|
|
1569
|
+
if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new import_meta_whatsapp_contracts8.WindowExpiredError();
|
|
1177
1570
|
throw error;
|
|
1178
1571
|
}
|
|
1179
1572
|
}
|
|
@@ -1217,11 +1610,11 @@ var WhatsAppChannelAdapter = class {
|
|
|
1217
1610
|
};
|
|
1218
1611
|
|
|
1219
1612
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
1220
|
-
var
|
|
1613
|
+
var import_meta_whatsapp_contracts10 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1221
1614
|
|
|
1222
1615
|
// src/channel/webhookSecurity.ts
|
|
1223
1616
|
var import_node_crypto = require("crypto");
|
|
1224
|
-
var
|
|
1617
|
+
var import_meta_whatsapp_contracts9 = require("@adatechnology/meta-whatsapp-contracts");
|
|
1225
1618
|
var WEBHOOK_NONCE_TTL_SECONDS = 300;
|
|
1226
1619
|
function safeEqualStrings(left, right) {
|
|
1227
1620
|
const leftDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(left).digest();
|
|
@@ -1231,20 +1624,20 @@ function safeEqualStrings(left, right) {
|
|
|
1231
1624
|
__name(safeEqualStrings, "safeEqualStrings");
|
|
1232
1625
|
function verifyWebhookChallenge(params) {
|
|
1233
1626
|
if (params.mode !== "subscribe" || !params.token || !params.challenge) {
|
|
1234
|
-
throw new
|
|
1627
|
+
throw new import_meta_whatsapp_contracts9.InvalidWebhookSignatureError();
|
|
1235
1628
|
}
|
|
1236
1629
|
if (!safeEqualStrings(params.token, params.expectedToken)) {
|
|
1237
|
-
throw new
|
|
1630
|
+
throw new import_meta_whatsapp_contracts9.InvalidWebhookSignatureError();
|
|
1238
1631
|
}
|
|
1239
1632
|
return params.challenge;
|
|
1240
1633
|
}
|
|
1241
1634
|
__name(verifyWebhookChallenge, "verifyWebhookChallenge");
|
|
1242
1635
|
function verifyWebhookSignature(params) {
|
|
1243
1636
|
const { rawBody, signatureHeader, appSecret } = params;
|
|
1244
|
-
if (!signatureHeader?.startsWith("sha256=")) throw new
|
|
1637
|
+
if (!signatureHeader?.startsWith("sha256=")) throw new import_meta_whatsapp_contracts9.InvalidWebhookSignatureError();
|
|
1245
1638
|
const expected = (0, import_node_crypto.createHmac)("sha256", appSecret).update(rawBody).digest("hex");
|
|
1246
1639
|
const received = signatureHeader.slice("sha256=".length);
|
|
1247
|
-
if (!safeEqualStrings(received, expected)) throw new
|
|
1640
|
+
if (!safeEqualStrings(received, expected)) throw new import_meta_whatsapp_contracts9.InvalidWebhookSignatureError();
|
|
1248
1641
|
}
|
|
1249
1642
|
__name(verifyWebhookSignature, "verifyWebhookSignature");
|
|
1250
1643
|
async function claimWebhookDelivery(params) {
|
|
@@ -1253,6 +1646,90 @@ async function claimWebhookDelivery(params) {
|
|
|
1253
1646
|
}
|
|
1254
1647
|
__name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
1255
1648
|
|
|
1649
|
+
// src/channel/IngestInboundMedia.use-case.ts
|
|
1650
|
+
var import_drizzle_orm7 = require("drizzle-orm");
|
|
1651
|
+
var IngestInboundMediaUseCase = class {
|
|
1652
|
+
static {
|
|
1653
|
+
__name(this, "IngestInboundMediaUseCase");
|
|
1654
|
+
}
|
|
1655
|
+
db;
|
|
1656
|
+
channel;
|
|
1657
|
+
objectStorage;
|
|
1658
|
+
documentRepository;
|
|
1659
|
+
constructor(db, channel, objectStorage, documentRepository) {
|
|
1660
|
+
this.db = db;
|
|
1661
|
+
this.channel = channel;
|
|
1662
|
+
this.objectStorage = objectStorage;
|
|
1663
|
+
this.documentRepository = documentRepository;
|
|
1664
|
+
}
|
|
1665
|
+
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);
|
|
1667
|
+
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1668
|
+
const payload = message.payload ?? {};
|
|
1669
|
+
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
1670
|
+
return {
|
|
1671
|
+
uploadId: String(payload["uploadId"]),
|
|
1672
|
+
alreadyIngested: true
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
|
|
1676
|
+
const buffer = Buffer.from(data, "base64");
|
|
1677
|
+
const { uploadId } = await this.objectStorage.upload({
|
|
1678
|
+
buffer,
|
|
1679
|
+
mimeType: mimeType || params.mimeType,
|
|
1680
|
+
key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`
|
|
1681
|
+
});
|
|
1682
|
+
const updatedPayload = {
|
|
1683
|
+
...payload,
|
|
1684
|
+
uploadId,
|
|
1685
|
+
sourceMediaId: params.sourceMediaId,
|
|
1686
|
+
mimeType: mimeType || params.mimeType,
|
|
1687
|
+
...params.filename ? {
|
|
1688
|
+
filename: params.filename
|
|
1689
|
+
} : {}
|
|
1690
|
+
};
|
|
1691
|
+
await this.db.update(messages).set({
|
|
1692
|
+
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)));
|
|
1694
|
+
await this.documentRepository?.link({
|
|
1695
|
+
companyId: params.companyId,
|
|
1696
|
+
sessionId: message.sessionId,
|
|
1697
|
+
messageId: message.id,
|
|
1698
|
+
uploadId,
|
|
1699
|
+
// Áudio e sticker chegam sem nome; sem um rótulo o painel mostraria linha vazia.
|
|
1700
|
+
filename: params.filename ?? `${params.sourceMediaId}`,
|
|
1701
|
+
mimeType: mimeType || params.mimeType,
|
|
1702
|
+
sizeBytes: buffer.length,
|
|
1703
|
+
source: message.sender
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
uploadId,
|
|
1707
|
+
alreadyIngested: false
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
function extractMediaDescriptor(message) {
|
|
1712
|
+
const payload = message.payload ?? {};
|
|
1713
|
+
for (const key of [
|
|
1714
|
+
"image",
|
|
1715
|
+
"audio",
|
|
1716
|
+
"video",
|
|
1717
|
+
"document",
|
|
1718
|
+
"sticker"
|
|
1719
|
+
]) {
|
|
1720
|
+
const media = payload[key];
|
|
1721
|
+
if (media?.id) {
|
|
1722
|
+
return {
|
|
1723
|
+
sourceMediaId: media.id,
|
|
1724
|
+
mimeType: media.mime_type ?? "application/octet-stream",
|
|
1725
|
+
filename: media.filename
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return void 0;
|
|
1730
|
+
}
|
|
1731
|
+
__name(extractMediaDescriptor, "extractMediaDescriptor");
|
|
1732
|
+
|
|
1256
1733
|
// src/channel/ReceiveWebhook.use-case.ts
|
|
1257
1734
|
function toSessionContract(row) {
|
|
1258
1735
|
return {
|
|
@@ -1330,7 +1807,7 @@ var ReceiveWebhookUseCase = class {
|
|
|
1330
1807
|
statusesProcessed: 0
|
|
1331
1808
|
};
|
|
1332
1809
|
const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
|
|
1333
|
-
const payload =
|
|
1810
|
+
const payload = import_meta_whatsapp_contracts10.whatsAppWebhookPayloadSchema.parse(JSON.parse(rawText));
|
|
1334
1811
|
let messagesProcessed = 0;
|
|
1335
1812
|
let statusesProcessed = 0;
|
|
1336
1813
|
for (const entry of payload.entry) {
|
|
@@ -1365,6 +1842,19 @@ var ReceiveWebhookUseCase = class {
|
|
|
1365
1842
|
startState: this.params.startState
|
|
1366
1843
|
});
|
|
1367
1844
|
if (!saved) return;
|
|
1845
|
+
const media = extractMediaDescriptor(saved);
|
|
1846
|
+
if (media) {
|
|
1847
|
+
await this.params.hooks?.onMediaReceived?.({
|
|
1848
|
+
companyId,
|
|
1849
|
+
messageId: saved.id,
|
|
1850
|
+
whatsappNumber: message.from,
|
|
1851
|
+
sourceMediaId: media.sourceMediaId,
|
|
1852
|
+
mimeType: media.mimeType,
|
|
1853
|
+
...media.filename ? {
|
|
1854
|
+
filename: media.filename
|
|
1855
|
+
} : {}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1368
1858
|
const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from);
|
|
1369
1859
|
if (!sessionRow) return;
|
|
1370
1860
|
if (sessionRow.mode === "human") return;
|
|
@@ -1384,77 +1874,6 @@ var ReceiveWebhookUseCase = class {
|
|
|
1384
1874
|
}
|
|
1385
1875
|
};
|
|
1386
1876
|
|
|
1387
|
-
// src/channel/IngestInboundMedia.use-case.ts
|
|
1388
|
-
var import_drizzle_orm6 = require("drizzle-orm");
|
|
1389
|
-
var IngestInboundMediaUseCase = class {
|
|
1390
|
-
static {
|
|
1391
|
-
__name(this, "IngestInboundMediaUseCase");
|
|
1392
|
-
}
|
|
1393
|
-
db;
|
|
1394
|
-
channel;
|
|
1395
|
-
objectStorage;
|
|
1396
|
-
constructor(db, channel, objectStorage) {
|
|
1397
|
-
this.db = db;
|
|
1398
|
-
this.channel = channel;
|
|
1399
|
-
this.objectStorage = objectStorage;
|
|
1400
|
-
}
|
|
1401
|
-
async execute(params) {
|
|
1402
|
-
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);
|
|
1403
|
-
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1404
|
-
const payload = message.payload ?? {};
|
|
1405
|
-
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
1406
|
-
return {
|
|
1407
|
-
uploadId: String(payload["uploadId"]),
|
|
1408
|
-
alreadyIngested: true
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
|
|
1412
|
-
const buffer = Buffer.from(data, "base64");
|
|
1413
|
-
const { uploadId } = await this.objectStorage.upload({
|
|
1414
|
-
buffer,
|
|
1415
|
-
mimeType: mimeType || params.mimeType,
|
|
1416
|
-
key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`
|
|
1417
|
-
});
|
|
1418
|
-
const updatedPayload = {
|
|
1419
|
-
...payload,
|
|
1420
|
-
uploadId,
|
|
1421
|
-
sourceMediaId: params.sourceMediaId,
|
|
1422
|
-
mimeType: mimeType || params.mimeType,
|
|
1423
|
-
...params.filename ? {
|
|
1424
|
-
filename: params.filename
|
|
1425
|
-
} : {}
|
|
1426
|
-
};
|
|
1427
|
-
await this.db.update(messages).set({
|
|
1428
|
-
payload: updatedPayload
|
|
1429
|
-
}).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId)));
|
|
1430
|
-
return {
|
|
1431
|
-
uploadId,
|
|
1432
|
-
alreadyIngested: false
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
};
|
|
1436
|
-
function extractMediaDescriptor(message) {
|
|
1437
|
-
const payload = message.payload ?? {};
|
|
1438
|
-
for (const key of [
|
|
1439
|
-
"image",
|
|
1440
|
-
"audio",
|
|
1441
|
-
"video",
|
|
1442
|
-
"document",
|
|
1443
|
-
"sticker"
|
|
1444
|
-
]) {
|
|
1445
|
-
const media = payload[key];
|
|
1446
|
-
if (media?.id) {
|
|
1447
|
-
return {
|
|
1448
|
-
sourceMediaId: media.id,
|
|
1449
|
-
mimeType: media.mime_type ?? "application/octet-stream",
|
|
1450
|
-
filename: media.filename
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
return void 0;
|
|
1455
|
-
}
|
|
1456
|
-
__name(extractMediaDescriptor, "extractMediaDescriptor");
|
|
1457
|
-
|
|
1458
1877
|
// src/createMetaWhatsAppModule.ts
|
|
1459
1878
|
function createMetaWhatsAppModule(params) {
|
|
1460
1879
|
const { db, config, nonceStore, providers = {}, hooks } = params;
|
|
@@ -1472,8 +1891,9 @@ function createMetaWhatsAppModule(params) {
|
|
|
1472
1891
|
const messageRepository = new MessageRepository(db);
|
|
1473
1892
|
const settingsRepository = new SettingsRepository(db);
|
|
1474
1893
|
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1894
|
+
const documentRepository = new DocumentRepository(db);
|
|
1475
1895
|
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
1476
|
-
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage);
|
|
1896
|
+
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
|
|
1477
1897
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
1478
1898
|
appSecret: config.appSecret,
|
|
1479
1899
|
nonceStore,
|
|
@@ -1485,7 +1905,8 @@ function createMetaWhatsAppModule(params) {
|
|
|
1485
1905
|
realtime: providers.realtime
|
|
1486
1906
|
});
|
|
1487
1907
|
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
1488
|
-
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage) : void 0;
|
|
1908
|
+
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository) : void 0;
|
|
1909
|
+
const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
|
|
1489
1910
|
return {
|
|
1490
1911
|
channel,
|
|
1491
1912
|
// undefined quando providers.objectStorage não foi injetado.
|
|
@@ -1497,8 +1918,16 @@ function createMetaWhatsAppModule(params) {
|
|
|
1497
1918
|
release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),
|
|
1498
1919
|
list: new ListConversationsUseCase(sessionRepository),
|
|
1499
1920
|
listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),
|
|
1921
|
+
listDocuments,
|
|
1922
|
+
// Biblioteca da empresa inteira, para uma tela de Documentos fora da conversa.
|
|
1923
|
+
listCompanyDocuments: new ListCompanyDocumentsUseCase(documentRepository),
|
|
1924
|
+
// Apaga a mídia no storage antes das linhas — a cascata da FK sozinha deixaria os binários
|
|
1925
|
+
// órfãos, já que a lista de uploadId vive justamente nas linhas que ela derruba.
|
|
1926
|
+
delete: new DeleteConversationUseCase(sessionRepository, documentRepository, providers.objectStorage),
|
|
1927
|
+
purgeExpiredDocuments: new PurgeExpiredDocumentsUseCase(documentRepository, providers.objectStorage),
|
|
1500
1928
|
export: new ExportConversationUseCase(sessionRepository),
|
|
1501
|
-
repository: sessionRepository
|
|
1929
|
+
repository: sessionRepository,
|
|
1930
|
+
documentRepository
|
|
1502
1931
|
},
|
|
1503
1932
|
settings: settingsRepository,
|
|
1504
1933
|
webhook: {
|
|
@@ -1619,7 +2048,9 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1619
2048
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1620
2049
|
0 && (module.exports = {
|
|
1621
2050
|
CreateFlowGraphUseCase,
|
|
2051
|
+
DeleteConversationUseCase,
|
|
1622
2052
|
DeleteFlowGraphUseCase,
|
|
2053
|
+
DocumentRepository,
|
|
1623
2054
|
ExportConversationUseCase,
|
|
1624
2055
|
FlowGraphRepository,
|
|
1625
2056
|
FlowInterpreter,
|
|
@@ -1627,6 +2058,8 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1627
2058
|
GetLiveFlowPositionsUseCase,
|
|
1628
2059
|
IngestInboundMediaUseCase,
|
|
1629
2060
|
InvalidFlowGraphError,
|
|
2061
|
+
ListCompanyDocumentsUseCase,
|
|
2062
|
+
ListConversationDocumentsUseCase,
|
|
1630
2063
|
ListConversationsUseCase,
|
|
1631
2064
|
ListFlowGraphsUseCase,
|
|
1632
2065
|
ListMessagesUseCase,
|
|
@@ -1634,6 +2067,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1634
2067
|
META_WHATSAPP_MIGRATIONS_TABLE,
|
|
1635
2068
|
MessageRepository,
|
|
1636
2069
|
OptimisticLockError,
|
|
2070
|
+
PurgeExpiredDocumentsUseCase,
|
|
1637
2071
|
ReceiveWebhookUseCase,
|
|
1638
2072
|
ReleaseConversationUseCase,
|
|
1639
2073
|
SaveFlowGraphUseCase,
|
|
@@ -1646,6 +2080,7 @@ __name(redeemSseTicket, "redeemSseTicket");
|
|
|
1646
2080
|
WhatsAppChannelAdapter,
|
|
1647
2081
|
claimWebhookDelivery,
|
|
1648
2082
|
createMetaWhatsAppModule,
|
|
2083
|
+
documents,
|
|
1649
2084
|
extractMediaDescriptor,
|
|
1650
2085
|
flowGraphs,
|
|
1651
2086
|
issueSseTicket,
|