@adatechnology/meta-whatsapp-module 0.2.0-rc.13 → 0.2.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +214 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -1
- package/dist/index.d.ts +333 -1
- package/dist/index.js +211 -4
- package/dist/index.js.map +1 -1
- package/dist/migrations/0007_flow_media.sql +18 -0
- package/dist/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1000,6 +1000,257 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
1000
1000
|
};
|
|
1001
1001
|
dialect: "pg";
|
|
1002
1002
|
}>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.
|
|
1005
|
+
*
|
|
1006
|
+
* Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA
|
|
1007
|
+
* CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o
|
|
1008
|
+
* mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é
|
|
1009
|
+
* enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.
|
|
1010
|
+
*
|
|
1011
|
+
* Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o
|
|
1012
|
+
* índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então
|
|
1013
|
+
* apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a
|
|
1014
|
+
* limpeza é passo de aplicação ao salvar o grafo.
|
|
1015
|
+
*/
|
|
1016
|
+
declare const flowMedia: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
1017
|
+
name: "flow_media";
|
|
1018
|
+
schema: "meta_whatsapp";
|
|
1019
|
+
columns: {
|
|
1020
|
+
id: drizzle_orm_pg_core.PgColumn<{
|
|
1021
|
+
name: "id";
|
|
1022
|
+
tableName: "flow_media";
|
|
1023
|
+
dataType: "string";
|
|
1024
|
+
columnType: "PgUUID";
|
|
1025
|
+
data: string;
|
|
1026
|
+
driverParam: string;
|
|
1027
|
+
notNull: true;
|
|
1028
|
+
hasDefault: true;
|
|
1029
|
+
isPrimaryKey: true;
|
|
1030
|
+
isAutoincrement: false;
|
|
1031
|
+
hasRuntimeDefault: false;
|
|
1032
|
+
enumValues: undefined;
|
|
1033
|
+
baseColumn: never;
|
|
1034
|
+
identity: undefined;
|
|
1035
|
+
generated: undefined;
|
|
1036
|
+
}, {}, {}>;
|
|
1037
|
+
companyId: drizzle_orm_pg_core.PgColumn<{
|
|
1038
|
+
name: "company_id";
|
|
1039
|
+
tableName: "flow_media";
|
|
1040
|
+
dataType: "string";
|
|
1041
|
+
columnType: "PgUUID";
|
|
1042
|
+
data: string;
|
|
1043
|
+
driverParam: string;
|
|
1044
|
+
notNull: true;
|
|
1045
|
+
hasDefault: false;
|
|
1046
|
+
isPrimaryKey: false;
|
|
1047
|
+
isAutoincrement: false;
|
|
1048
|
+
hasRuntimeDefault: false;
|
|
1049
|
+
enumValues: undefined;
|
|
1050
|
+
baseColumn: never;
|
|
1051
|
+
identity: undefined;
|
|
1052
|
+
generated: undefined;
|
|
1053
|
+
}, {}, {}>;
|
|
1054
|
+
flowKey: drizzle_orm_pg_core.PgColumn<{
|
|
1055
|
+
name: "flow_key";
|
|
1056
|
+
tableName: "flow_media";
|
|
1057
|
+
dataType: "string";
|
|
1058
|
+
columnType: "PgVarchar";
|
|
1059
|
+
data: string;
|
|
1060
|
+
driverParam: string;
|
|
1061
|
+
notNull: true;
|
|
1062
|
+
hasDefault: false;
|
|
1063
|
+
isPrimaryKey: false;
|
|
1064
|
+
isAutoincrement: false;
|
|
1065
|
+
hasRuntimeDefault: false;
|
|
1066
|
+
enumValues: [string, ...string[]];
|
|
1067
|
+
baseColumn: never;
|
|
1068
|
+
identity: undefined;
|
|
1069
|
+
generated: undefined;
|
|
1070
|
+
}, {}, {
|
|
1071
|
+
length: 64;
|
|
1072
|
+
}>;
|
|
1073
|
+
nodeId: drizzle_orm_pg_core.PgColumn<{
|
|
1074
|
+
name: "node_id";
|
|
1075
|
+
tableName: "flow_media";
|
|
1076
|
+
dataType: "string";
|
|
1077
|
+
columnType: "PgVarchar";
|
|
1078
|
+
data: string;
|
|
1079
|
+
driverParam: string;
|
|
1080
|
+
notNull: true;
|
|
1081
|
+
hasDefault: false;
|
|
1082
|
+
isPrimaryKey: false;
|
|
1083
|
+
isAutoincrement: false;
|
|
1084
|
+
hasRuntimeDefault: false;
|
|
1085
|
+
enumValues: [string, ...string[]];
|
|
1086
|
+
baseColumn: never;
|
|
1087
|
+
identity: undefined;
|
|
1088
|
+
generated: undefined;
|
|
1089
|
+
}, {}, {
|
|
1090
|
+
length: 64;
|
|
1091
|
+
}>;
|
|
1092
|
+
uploadId: drizzle_orm_pg_core.PgColumn<{
|
|
1093
|
+
name: "upload_id";
|
|
1094
|
+
tableName: "flow_media";
|
|
1095
|
+
dataType: "string";
|
|
1096
|
+
columnType: "PgVarchar";
|
|
1097
|
+
data: string;
|
|
1098
|
+
driverParam: string;
|
|
1099
|
+
notNull: true;
|
|
1100
|
+
hasDefault: false;
|
|
1101
|
+
isPrimaryKey: false;
|
|
1102
|
+
isAutoincrement: false;
|
|
1103
|
+
hasRuntimeDefault: false;
|
|
1104
|
+
enumValues: [string, ...string[]];
|
|
1105
|
+
baseColumn: never;
|
|
1106
|
+
identity: undefined;
|
|
1107
|
+
generated: undefined;
|
|
1108
|
+
}, {}, {
|
|
1109
|
+
length: 256;
|
|
1110
|
+
}>;
|
|
1111
|
+
filename: drizzle_orm_pg_core.PgColumn<{
|
|
1112
|
+
name: "filename";
|
|
1113
|
+
tableName: "flow_media";
|
|
1114
|
+
dataType: "string";
|
|
1115
|
+
columnType: "PgVarchar";
|
|
1116
|
+
data: string;
|
|
1117
|
+
driverParam: string;
|
|
1118
|
+
notNull: true;
|
|
1119
|
+
hasDefault: false;
|
|
1120
|
+
isPrimaryKey: false;
|
|
1121
|
+
isAutoincrement: false;
|
|
1122
|
+
hasRuntimeDefault: false;
|
|
1123
|
+
enumValues: [string, ...string[]];
|
|
1124
|
+
baseColumn: never;
|
|
1125
|
+
identity: undefined;
|
|
1126
|
+
generated: undefined;
|
|
1127
|
+
}, {}, {
|
|
1128
|
+
length: 512;
|
|
1129
|
+
}>;
|
|
1130
|
+
mimeType: drizzle_orm_pg_core.PgColumn<{
|
|
1131
|
+
name: "mime_type";
|
|
1132
|
+
tableName: "flow_media";
|
|
1133
|
+
dataType: "string";
|
|
1134
|
+
columnType: "PgVarchar";
|
|
1135
|
+
data: string;
|
|
1136
|
+
driverParam: string;
|
|
1137
|
+
notNull: true;
|
|
1138
|
+
hasDefault: false;
|
|
1139
|
+
isPrimaryKey: false;
|
|
1140
|
+
isAutoincrement: false;
|
|
1141
|
+
hasRuntimeDefault: false;
|
|
1142
|
+
enumValues: [string, ...string[]];
|
|
1143
|
+
baseColumn: never;
|
|
1144
|
+
identity: undefined;
|
|
1145
|
+
generated: undefined;
|
|
1146
|
+
}, {}, {
|
|
1147
|
+
length: 128;
|
|
1148
|
+
}>;
|
|
1149
|
+
sizeBytes: drizzle_orm_pg_core.PgColumn<{
|
|
1150
|
+
name: "size_bytes";
|
|
1151
|
+
tableName: "flow_media";
|
|
1152
|
+
dataType: "number";
|
|
1153
|
+
columnType: "PgInteger";
|
|
1154
|
+
data: number;
|
|
1155
|
+
driverParam: string | number;
|
|
1156
|
+
notNull: true;
|
|
1157
|
+
hasDefault: false;
|
|
1158
|
+
isPrimaryKey: false;
|
|
1159
|
+
isAutoincrement: false;
|
|
1160
|
+
hasRuntimeDefault: false;
|
|
1161
|
+
enumValues: undefined;
|
|
1162
|
+
baseColumn: never;
|
|
1163
|
+
identity: undefined;
|
|
1164
|
+
generated: undefined;
|
|
1165
|
+
}, {}, {}>;
|
|
1166
|
+
caption: drizzle_orm_pg_core.PgColumn<{
|
|
1167
|
+
name: "caption";
|
|
1168
|
+
tableName: "flow_media";
|
|
1169
|
+
dataType: "string";
|
|
1170
|
+
columnType: "PgText";
|
|
1171
|
+
data: string;
|
|
1172
|
+
driverParam: string;
|
|
1173
|
+
notNull: false;
|
|
1174
|
+
hasDefault: false;
|
|
1175
|
+
isPrimaryKey: false;
|
|
1176
|
+
isAutoincrement: false;
|
|
1177
|
+
hasRuntimeDefault: false;
|
|
1178
|
+
enumValues: [string, ...string[]];
|
|
1179
|
+
baseColumn: never;
|
|
1180
|
+
identity: undefined;
|
|
1181
|
+
generated: undefined;
|
|
1182
|
+
}, {}, {}>;
|
|
1183
|
+
sortOrder: drizzle_orm_pg_core.PgColumn<{
|
|
1184
|
+
name: "sort_order";
|
|
1185
|
+
tableName: "flow_media";
|
|
1186
|
+
dataType: "number";
|
|
1187
|
+
columnType: "PgInteger";
|
|
1188
|
+
data: number;
|
|
1189
|
+
driverParam: string | number;
|
|
1190
|
+
notNull: true;
|
|
1191
|
+
hasDefault: true;
|
|
1192
|
+
isPrimaryKey: false;
|
|
1193
|
+
isAutoincrement: false;
|
|
1194
|
+
hasRuntimeDefault: false;
|
|
1195
|
+
enumValues: undefined;
|
|
1196
|
+
baseColumn: never;
|
|
1197
|
+
identity: undefined;
|
|
1198
|
+
generated: undefined;
|
|
1199
|
+
}, {}, {}>;
|
|
1200
|
+
active: drizzle_orm_pg_core.PgColumn<{
|
|
1201
|
+
name: "active";
|
|
1202
|
+
tableName: "flow_media";
|
|
1203
|
+
dataType: "boolean";
|
|
1204
|
+
columnType: "PgBoolean";
|
|
1205
|
+
data: boolean;
|
|
1206
|
+
driverParam: boolean;
|
|
1207
|
+
notNull: true;
|
|
1208
|
+
hasDefault: true;
|
|
1209
|
+
isPrimaryKey: false;
|
|
1210
|
+
isAutoincrement: false;
|
|
1211
|
+
hasRuntimeDefault: false;
|
|
1212
|
+
enumValues: undefined;
|
|
1213
|
+
baseColumn: never;
|
|
1214
|
+
identity: undefined;
|
|
1215
|
+
generated: undefined;
|
|
1216
|
+
}, {}, {}>;
|
|
1217
|
+
createdAt: drizzle_orm_pg_core.PgColumn<{
|
|
1218
|
+
name: "created_at";
|
|
1219
|
+
tableName: "flow_media";
|
|
1220
|
+
dataType: "date";
|
|
1221
|
+
columnType: "PgTimestamp";
|
|
1222
|
+
data: Date;
|
|
1223
|
+
driverParam: string;
|
|
1224
|
+
notNull: true;
|
|
1225
|
+
hasDefault: true;
|
|
1226
|
+
isPrimaryKey: false;
|
|
1227
|
+
isAutoincrement: false;
|
|
1228
|
+
hasRuntimeDefault: false;
|
|
1229
|
+
enumValues: undefined;
|
|
1230
|
+
baseColumn: never;
|
|
1231
|
+
identity: undefined;
|
|
1232
|
+
generated: undefined;
|
|
1233
|
+
}, {}, {}>;
|
|
1234
|
+
updatedAt: drizzle_orm_pg_core.PgColumn<{
|
|
1235
|
+
name: "updated_at";
|
|
1236
|
+
tableName: "flow_media";
|
|
1237
|
+
dataType: "date";
|
|
1238
|
+
columnType: "PgTimestamp";
|
|
1239
|
+
data: Date;
|
|
1240
|
+
driverParam: string;
|
|
1241
|
+
notNull: true;
|
|
1242
|
+
hasDefault: true;
|
|
1243
|
+
isPrimaryKey: false;
|
|
1244
|
+
isAutoincrement: false;
|
|
1245
|
+
hasRuntimeDefault: false;
|
|
1246
|
+
enumValues: undefined;
|
|
1247
|
+
baseColumn: never;
|
|
1248
|
+
identity: undefined;
|
|
1249
|
+
generated: undefined;
|
|
1250
|
+
}, {}, {}>;
|
|
1251
|
+
};
|
|
1252
|
+
dialect: "pg";
|
|
1253
|
+
}>;
|
|
1003
1254
|
declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
1004
1255
|
name: "settings";
|
|
1005
1256
|
schema: "meta_whatsapp";
|
|
@@ -1159,6 +1410,8 @@ type FlowGraphRow = typeof flowGraphs.$inferSelect;
|
|
|
1159
1410
|
type NewFlowGraphRow = typeof flowGraphs.$inferInsert;
|
|
1160
1411
|
type DocumentRow = typeof documents.$inferSelect;
|
|
1161
1412
|
type NewDocumentRow = typeof documents.$inferInsert;
|
|
1413
|
+
type FlowMediaRow = typeof flowMedia.$inferSelect;
|
|
1414
|
+
type NewFlowMediaRow = typeof flowMedia.$inferInsert;
|
|
1162
1415
|
|
|
1163
1416
|
interface ListConversationsFilters {
|
|
1164
1417
|
page?: number;
|
|
@@ -1757,6 +2010,57 @@ declare class FlowInterpreter {
|
|
|
1757
2010
|
}): Promise<FlowRunResult>;
|
|
1758
2011
|
}
|
|
1759
2012
|
|
|
2013
|
+
type FlowMediaLocation = {
|
|
2014
|
+
companyId: string;
|
|
2015
|
+
flowKey: string;
|
|
2016
|
+
nodeId: string;
|
|
2017
|
+
};
|
|
2018
|
+
type AttachFlowMediaParams = FlowMediaLocation & {
|
|
2019
|
+
uploadId: string;
|
|
2020
|
+
filename: string;
|
|
2021
|
+
mimeType: string;
|
|
2022
|
+
sizeBytes: number;
|
|
2023
|
+
caption?: string;
|
|
2024
|
+
sortOrder?: number;
|
|
2025
|
+
};
|
|
2026
|
+
type UpdateFlowMediaParams = {
|
|
2027
|
+
companyId: string;
|
|
2028
|
+
id: string;
|
|
2029
|
+
caption?: string | null;
|
|
2030
|
+
sortOrder?: number;
|
|
2031
|
+
active?: boolean;
|
|
2032
|
+
};
|
|
2033
|
+
declare class FlowMediaRepository {
|
|
2034
|
+
private readonly db;
|
|
2035
|
+
constructor(db: MetaWhatsAppDatabase);
|
|
2036
|
+
private locationFilter;
|
|
2037
|
+
listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
|
|
2038
|
+
listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
|
|
2039
|
+
/**
|
|
2040
|
+
* Anexa um arquivo já existente no storage ao nó.
|
|
2041
|
+
*
|
|
2042
|
+
* `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
|
|
2043
|
+
* editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
|
|
2044
|
+
* quem está montando o fluxo.
|
|
2045
|
+
*/
|
|
2046
|
+
attach(params: AttachFlowMediaParams): Promise<FlowMediaRow>;
|
|
2047
|
+
update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined>;
|
|
2048
|
+
/**
|
|
2049
|
+
* Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
|
|
2050
|
+
* outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
|
|
2051
|
+
* host, que é dono da biblioteca de arquivos.
|
|
2052
|
+
*/
|
|
2053
|
+
detach(params: {
|
|
2054
|
+
companyId: string;
|
|
2055
|
+
id: string;
|
|
2056
|
+
}): Promise<void>;
|
|
2057
|
+
detachRemovedNodes(params: {
|
|
2058
|
+
companyId: string;
|
|
2059
|
+
flowKey: string;
|
|
2060
|
+
existingNodeIds: string[];
|
|
2061
|
+
}): Promise<void>;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1760
2064
|
interface NonceStoreInterface {
|
|
1761
2065
|
setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>;
|
|
1762
2066
|
}
|
|
@@ -1895,6 +2199,7 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
|
|
|
1895
2199
|
delete: DeleteFlowGraphUseCase;
|
|
1896
2200
|
livePositions: GetLiveFlowPositionsUseCase;
|
|
1897
2201
|
repository: FlowGraphRepository;
|
|
2202
|
+
mediaRepository: FlowMediaRepository;
|
|
1898
2203
|
} | undefined;
|
|
1899
2204
|
catalog: CatalogPort | undefined;
|
|
1900
2205
|
};
|
|
@@ -1978,4 +2283,31 @@ declare function redeemSseTicket(store: TicketStoreInterface, ticket: string): P
|
|
|
1978
2283
|
whatsappNumber: string;
|
|
1979
2284
|
} | null>;
|
|
1980
2285
|
|
|
1981
|
-
|
|
2286
|
+
type CreateSendMediaActionParams = {
|
|
2287
|
+
flowMediaRepository: FlowMediaRepository;
|
|
2288
|
+
objectStorage: ObjectStorageInterface & {
|
|
2289
|
+
getObject: NonNullable<ObjectStorageInterface['getObject']>;
|
|
2290
|
+
};
|
|
2291
|
+
logMessage: LogMessageUseCase;
|
|
2292
|
+
startState: SessionState;
|
|
2293
|
+
onError?: (error: unknown, details: {
|
|
2294
|
+
flowKey: string;
|
|
2295
|
+
nodeId: string;
|
|
2296
|
+
uploadId: string;
|
|
2297
|
+
}) => void;
|
|
2298
|
+
};
|
|
2299
|
+
/**
|
|
2300
|
+
* Action `send_media`: ao passar pelo nó, envia os arquivos que a biblioteca tem anexados a ele.
|
|
2301
|
+
*
|
|
2302
|
+
* É a única action built-in que o módulo implementa de ponta a ponta — as outras
|
|
2303
|
+
* (`handoff`, `send_product_list`) dependem de regra de negócio do produto. Esta não: "mandar
|
|
2304
|
+
* estes arquivos ao chegar aqui" é comportamento de canal, e replicá-la em cada host seria
|
|
2305
|
+
* copiar o mesmo código com os mesmos bugs.
|
|
2306
|
+
*
|
|
2307
|
+
* O nó não guarda os arquivos em `actionParams` de propósito. Se guardasse, trocar o material
|
|
2308
|
+
* exigiria editar e republicar o grafo — e o ponto todo é o contrário: quem cuida do conteúdo
|
|
2309
|
+
* troca o arquivo na biblioteca e o fluxo continua igual.
|
|
2310
|
+
*/
|
|
2311
|
+
declare function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler;
|
|
2312
|
+
|
|
2313
|
+
export { type AttachFlowMediaParams, type CompanyDocumentView, type CompanyDocumentsPage, type ConversationDocumentView, type ConversationDocumentsPage, type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, type CreateSendMediaActionParams, type DeleteConversationParams, type DeleteConversationResult, DeleteConversationUseCase, DeleteFlowGraphUseCase, DocumentRepository, type DocumentRow, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowMediaLocation, FlowMediaRepository, type FlowMediaRow, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type InsertMessageParams, InvalidFlowGraphError, type LinkDocumentParams, type ListCompanyDocumentsParams, ListCompanyDocumentsUseCase, type ListConversationDocumentsParams, ListConversationDocumentsUseCase, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, type ListDocumentsParams, type ListDocumentsResult, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type NewDocumentRow, type NewFlowGraphRow, type NewFlowMediaRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, type PurgeExpiredDocumentsParams, type PurgeExpiredDocumentsResult, PurgeExpiredDocumentsUseCase, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, type UpdateFlowMediaParams, WEBHOOK_NONCE_TTL_SECONDS, WhatsAppChannelAdapter, claimWebhookDelivery, createMetaWhatsAppModule, createSendMediaAction, documents, extractMediaDescriptor, flowGraphs, flowMedia, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, runMetaWhatsAppMigrations, sessions, settings, verifyWebhookChallenge, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ var __dirname = /* @__PURE__ */ getDirname();
|
|
|
10
10
|
|
|
11
11
|
// src/createMetaWhatsAppModule.ts
|
|
12
12
|
import { WhatsAppMessageProvider } from "@adatechnology/meta-whatsapp-provider";
|
|
13
|
+
import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
|
|
13
14
|
|
|
14
15
|
// src/repositories/SessionRepository.ts
|
|
15
16
|
import { and, desc, eq, sql as sql2 } from "drizzle-orm";
|
|
@@ -197,6 +198,46 @@ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
|
|
|
197
198
|
}, (table) => [
|
|
198
199
|
uniqueIndex("idx_flow_graphs_company_key").on(table.companyId, table.key)
|
|
199
200
|
]);
|
|
201
|
+
var flowMedia = metaWhatsAppSchema.table("flow_media", {
|
|
202
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
203
|
+
companyId: uuid("company_id").notNull(),
|
|
204
|
+
flowKey: varchar("flow_key", {
|
|
205
|
+
length: 64
|
|
206
|
+
}).notNull(),
|
|
207
|
+
nodeId: varchar("node_id", {
|
|
208
|
+
length: 64
|
|
209
|
+
}).notNull(),
|
|
210
|
+
uploadId: varchar("upload_id", {
|
|
211
|
+
length: 256
|
|
212
|
+
}).notNull(),
|
|
213
|
+
filename: varchar("filename", {
|
|
214
|
+
length: 512
|
|
215
|
+
}).notNull(),
|
|
216
|
+
mimeType: varchar("mime_type", {
|
|
217
|
+
length: 128
|
|
218
|
+
}).notNull(),
|
|
219
|
+
sizeBytes: integer("size_bytes").notNull(),
|
|
220
|
+
// Legenda da mídia no WhatsApp. Por arquivo, não por nó: um nó que manda tabela de preços e
|
|
221
|
+
// um folder precisa de textos diferentes para cada um.
|
|
222
|
+
caption: text("caption"),
|
|
223
|
+
// Ordem de envio dentro do nó — o cliente recebe as mensagens em sequência, e "tabela antes
|
|
224
|
+
// do folder" é decisão de quem edita, não do banco.
|
|
225
|
+
sortOrder: integer("sort_order").notNull().default(0),
|
|
226
|
+
// Desligar sem desanexar: trocar o material da campanha é o caso comum, e apagar a linha
|
|
227
|
+
// perderia a ordem e a legenda já ajustadas.
|
|
228
|
+
active: boolean("active").notNull().default(true),
|
|
229
|
+
createdAt: timestamp("created_at", {
|
|
230
|
+
withTimezone: true
|
|
231
|
+
}).notNull().defaultNow(),
|
|
232
|
+
updatedAt: timestamp("updated_at", {
|
|
233
|
+
withTimezone: true
|
|
234
|
+
}).notNull().defaultNow()
|
|
235
|
+
}, (table) => [
|
|
236
|
+
index("idx_flow_media_node").on(table.companyId, table.flowKey, table.nodeId, table.sortOrder),
|
|
237
|
+
// O mesmo arquivo anexado duas vezes ao MESMO nó é erro de clique no editor, e o cliente
|
|
238
|
+
// receberia o documento repetido.
|
|
239
|
+
uniqueIndex("idx_flow_media_node_upload").on(table.companyId, table.flowKey, table.nodeId, table.uploadId)
|
|
240
|
+
]);
|
|
200
241
|
var settings = metaWhatsAppSchema.table("settings", {
|
|
201
242
|
companyId: uuid("company_id").primaryKey(),
|
|
202
243
|
templateName: varchar("template_name", {
|
|
@@ -1489,6 +1530,151 @@ var FlowInterpreter = class {
|
|
|
1489
1530
|
}
|
|
1490
1531
|
};
|
|
1491
1532
|
|
|
1533
|
+
// src/flows/createSendMediaAction.ts
|
|
1534
|
+
function mediaTypeFor2(mimeType) {
|
|
1535
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
1536
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
1537
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
1538
|
+
return "document";
|
|
1539
|
+
}
|
|
1540
|
+
__name(mediaTypeFor2, "mediaTypeFor");
|
|
1541
|
+
function createSendMediaAction(params) {
|
|
1542
|
+
return async ({ node, session, channel }) => {
|
|
1543
|
+
if (!session.flowKey) return;
|
|
1544
|
+
const location = {
|
|
1545
|
+
companyId: session.companyId,
|
|
1546
|
+
flowKey: session.flowKey,
|
|
1547
|
+
nodeId: node.id
|
|
1548
|
+
};
|
|
1549
|
+
const attachments = await params.flowMediaRepository.listActive(location);
|
|
1550
|
+
for (const attachment of attachments) {
|
|
1551
|
+
try {
|
|
1552
|
+
const buffer = await params.objectStorage.getObject(attachment.uploadId);
|
|
1553
|
+
const { externalMessageId } = await channel.sendMedia({
|
|
1554
|
+
to: session.whatsappNumber,
|
|
1555
|
+
buffer,
|
|
1556
|
+
mimeType: attachment.mimeType,
|
|
1557
|
+
filename: attachment.filename,
|
|
1558
|
+
caption: attachment.caption ?? void 0
|
|
1559
|
+
});
|
|
1560
|
+
await params.logMessage.execute({
|
|
1561
|
+
companyId: session.companyId,
|
|
1562
|
+
whatsappNumber: session.whatsappNumber,
|
|
1563
|
+
direction: "outbound",
|
|
1564
|
+
sender: "bot",
|
|
1565
|
+
agentUserId: null,
|
|
1566
|
+
type: mediaTypeFor2(attachment.mimeType),
|
|
1567
|
+
content: attachment.caption ?? attachment.filename,
|
|
1568
|
+
payload: {
|
|
1569
|
+
filename: attachment.filename,
|
|
1570
|
+
mimeType: attachment.mimeType,
|
|
1571
|
+
uploadId: attachment.uploadId,
|
|
1572
|
+
flowMediaId: attachment.id
|
|
1573
|
+
},
|
|
1574
|
+
waMessageId: externalMessageId,
|
|
1575
|
+
status: "sent",
|
|
1576
|
+
startState: params.startState
|
|
1577
|
+
});
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
params.onError?.(error, {
|
|
1580
|
+
flowKey: location.flowKey,
|
|
1581
|
+
nodeId: node.id,
|
|
1582
|
+
uploadId: attachment.uploadId
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
__name(createSendMediaAction, "createSendMediaAction");
|
|
1589
|
+
|
|
1590
|
+
// src/repositories/FlowMediaRepository.ts
|
|
1591
|
+
import { and as and5, asc as asc2, eq as eq6, sql as sql4 } from "drizzle-orm";
|
|
1592
|
+
var FlowMediaRepository = class {
|
|
1593
|
+
static {
|
|
1594
|
+
__name(this, "FlowMediaRepository");
|
|
1595
|
+
}
|
|
1596
|
+
db;
|
|
1597
|
+
constructor(db) {
|
|
1598
|
+
this.db = db;
|
|
1599
|
+
}
|
|
1600
|
+
locationFilter(location) {
|
|
1601
|
+
return and5(eq6(flowMedia.companyId, location.companyId), eq6(flowMedia.flowKey, location.flowKey), eq6(flowMedia.nodeId, location.nodeId));
|
|
1602
|
+
}
|
|
1603
|
+
// O que o nó realmente envia, na ordem de envio. `active` filtrado aqui e não no chamador:
|
|
1604
|
+
// é a razão de a coluna existir, e um chamador que esquecesse do filtro mandaria ao cliente
|
|
1605
|
+
// justamente o material que alguém desligou.
|
|
1606
|
+
async listActive(location) {
|
|
1607
|
+
return this.db.select().from(flowMedia).where(and5(this.locationFilter(location), eq6(flowMedia.active, true))).orderBy(asc2(flowMedia.sortOrder), asc2(flowMedia.createdAt));
|
|
1608
|
+
}
|
|
1609
|
+
// Inclui os desligados — é a visão do editor, onde desligar precisa continuar visível para
|
|
1610
|
+
// poder ser religado.
|
|
1611
|
+
async listAll(location) {
|
|
1612
|
+
return this.db.select().from(flowMedia).where(this.locationFilter(location)).orderBy(asc2(flowMedia.sortOrder), asc2(flowMedia.createdAt));
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Anexa um arquivo já existente no storage ao nó.
|
|
1616
|
+
*
|
|
1617
|
+
* `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
|
|
1618
|
+
* editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
|
|
1619
|
+
* quem está montando o fluxo.
|
|
1620
|
+
*/
|
|
1621
|
+
async attach(params) {
|
|
1622
|
+
const [row] = await this.db.insert(flowMedia).values({
|
|
1623
|
+
companyId: params.companyId,
|
|
1624
|
+
flowKey: params.flowKey,
|
|
1625
|
+
nodeId: params.nodeId,
|
|
1626
|
+
uploadId: params.uploadId,
|
|
1627
|
+
filename: params.filename,
|
|
1628
|
+
mimeType: params.mimeType,
|
|
1629
|
+
sizeBytes: params.sizeBytes,
|
|
1630
|
+
caption: params.caption ?? null,
|
|
1631
|
+
sortOrder: params.sortOrder ?? 0
|
|
1632
|
+
}).onConflictDoUpdate({
|
|
1633
|
+
target: [
|
|
1634
|
+
flowMedia.companyId,
|
|
1635
|
+
flowMedia.flowKey,
|
|
1636
|
+
flowMedia.nodeId,
|
|
1637
|
+
flowMedia.uploadId
|
|
1638
|
+
],
|
|
1639
|
+
set: {
|
|
1640
|
+
caption: params.caption ?? null,
|
|
1641
|
+
sortOrder: params.sortOrder ?? 0,
|
|
1642
|
+
active: true,
|
|
1643
|
+
updatedAt: sql4`now()`
|
|
1644
|
+
}
|
|
1645
|
+
}).returning();
|
|
1646
|
+
return row;
|
|
1647
|
+
}
|
|
1648
|
+
async update(params) {
|
|
1649
|
+
const [row] = await this.db.update(flowMedia).set({
|
|
1650
|
+
...params.caption !== void 0 ? {
|
|
1651
|
+
caption: params.caption
|
|
1652
|
+
} : {},
|
|
1653
|
+
...params.sortOrder !== void 0 ? {
|
|
1654
|
+
sortOrder: params.sortOrder
|
|
1655
|
+
} : {},
|
|
1656
|
+
...params.active !== void 0 ? {
|
|
1657
|
+
active: params.active
|
|
1658
|
+
} : {},
|
|
1659
|
+
updatedAt: sql4`now()`
|
|
1660
|
+
}).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.id, params.id))).returning();
|
|
1661
|
+
return row;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
|
|
1665
|
+
* outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
|
|
1666
|
+
* host, que é dono da biblioteca de arquivos.
|
|
1667
|
+
*/
|
|
1668
|
+
async detach(params) {
|
|
1669
|
+
await this.db.delete(flowMedia).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.id, params.id)));
|
|
1670
|
+
}
|
|
1671
|
+
// Chamado ao salvar o grafo: nós apagados no editor deixam linhas que nada mais alcança.
|
|
1672
|
+
async detachRemovedNodes(params) {
|
|
1673
|
+
const condition = params.existingNodeIds.length === 0 ? void 0 : sql4`${flowMedia.nodeId} NOT IN ${params.existingNodeIds}`;
|
|
1674
|
+
await this.db.delete(flowMedia).where(and5(eq6(flowMedia.companyId, params.companyId), eq6(flowMedia.flowKey, params.flowKey), condition));
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1492
1678
|
// src/channel/WhatsAppChannelAdapter.ts
|
|
1493
1679
|
import { WhatsAppWindowExpiredError as ProviderWindowExpiredError } from "@adatechnology/meta-graph-core";
|
|
1494
1680
|
import { WindowExpiredError as WindowExpiredError2 } from "@adatechnology/meta-whatsapp-contracts";
|
|
@@ -1585,7 +1771,7 @@ async function claimWebhookDelivery(params) {
|
|
|
1585
1771
|
__name(claimWebhookDelivery, "claimWebhookDelivery");
|
|
1586
1772
|
|
|
1587
1773
|
// src/channel/IngestInboundMedia.use-case.ts
|
|
1588
|
-
import { eq as
|
|
1774
|
+
import { eq as eq7, and as and6 } from "drizzle-orm";
|
|
1589
1775
|
var IngestInboundMediaUseCase = class {
|
|
1590
1776
|
static {
|
|
1591
1777
|
__name(this, "IngestInboundMediaUseCase");
|
|
@@ -1601,7 +1787,7 @@ var IngestInboundMediaUseCase = class {
|
|
|
1601
1787
|
this.documentRepository = documentRepository;
|
|
1602
1788
|
}
|
|
1603
1789
|
async execute(params) {
|
|
1604
|
-
const [message] = await this.db.select().from(messages).where(
|
|
1790
|
+
const [message] = await this.db.select().from(messages).where(and6(eq7(messages.companyId, params.companyId), eq7(messages.id, params.messageId))).limit(1);
|
|
1605
1791
|
if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
|
|
1606
1792
|
const payload = message.payload ?? {};
|
|
1607
1793
|
if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
|
|
@@ -1622,13 +1808,17 @@ var IngestInboundMediaUseCase = class {
|
|
|
1622
1808
|
uploadId,
|
|
1623
1809
|
sourceMediaId: params.sourceMediaId,
|
|
1624
1810
|
mimeType: mimeType || params.mimeType,
|
|
1811
|
+
// O tamanho já está na mão (é o buffer que acabou de ser copiado) e a bolha de documento o
|
|
1812
|
+
// exibe ao lado do tipo. Sem gravar aqui, a UI mostraria "PDF" sem o "· 180 KB", e buscá-lo
|
|
1813
|
+
// depois custaria uma consulta à tabela de documentos por mensagem renderizada.
|
|
1814
|
+
sizeBytes: buffer.length,
|
|
1625
1815
|
...params.filename ? {
|
|
1626
1816
|
filename: params.filename
|
|
1627
1817
|
} : {}
|
|
1628
1818
|
};
|
|
1629
1819
|
await this.db.update(messages).set({
|
|
1630
1820
|
payload: updatedPayload
|
|
1631
|
-
}).where(
|
|
1821
|
+
}).where(and6(eq7(messages.companyId, params.companyId), eq7(messages.id, params.messageId)));
|
|
1632
1822
|
await this.documentRepository?.link({
|
|
1633
1823
|
companyId: params.companyId,
|
|
1634
1824
|
sessionId: message.sessionId,
|
|
@@ -1830,6 +2020,7 @@ function createMetaWhatsAppModule(params) {
|
|
|
1830
2020
|
const settingsRepository = new SettingsRepository(db);
|
|
1831
2021
|
const flowGraphRepository = new FlowGraphRepository(db);
|
|
1832
2022
|
const documentRepository = new DocumentRepository(db);
|
|
2023
|
+
const flowMediaRepository = new FlowMediaRepository(db);
|
|
1833
2024
|
const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime, providers.moderator);
|
|
1834
2025
|
const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage, documentRepository);
|
|
1835
2026
|
const receiveWebhook = new ReceiveWebhookUseCase({
|
|
@@ -1843,6 +2034,15 @@ function createMetaWhatsAppModule(params) {
|
|
|
1843
2034
|
realtime: providers.realtime
|
|
1844
2035
|
});
|
|
1845
2036
|
const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
|
|
2037
|
+
if (flowInterpreter && providers.objectStorage?.getObject) {
|
|
2038
|
+
flowInterpreter.registerFlowAction(FLOW_ACTION_KIND.SEND_MEDIA, createSendMediaAction({
|
|
2039
|
+
flowMediaRepository,
|
|
2040
|
+
objectStorage: providers.objectStorage,
|
|
2041
|
+
logMessage,
|
|
2042
|
+
startState,
|
|
2043
|
+
onError: hooks?.onFlowMediaError
|
|
2044
|
+
}));
|
|
2045
|
+
}
|
|
1846
2046
|
const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage, documentRepository) : void 0;
|
|
1847
2047
|
const listDocuments = new ListConversationDocumentsUseCase(sessionRepository, documentRepository);
|
|
1848
2048
|
return {
|
|
@@ -1886,7 +2086,11 @@ function createMetaWhatsAppModule(params) {
|
|
|
1886
2086
|
save: new SaveFlowGraphUseCase(flowGraphRepository),
|
|
1887
2087
|
delete: new DeleteFlowGraphUseCase(flowGraphRepository),
|
|
1888
2088
|
livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
|
|
1889
|
-
repository: flowGraphRepository
|
|
2089
|
+
repository: flowGraphRepository,
|
|
2090
|
+
// Biblioteca de mídia dos nós `send_media` — o host liga nas rotas do editor
|
|
2091
|
+
// (anexar/reordenar/desligar). Existe mesmo sem storage injetado: gerenciar anexos é
|
|
2092
|
+
// consultar a tabela, e só o ENVIO precisa dos bytes.
|
|
2093
|
+
mediaRepository: flowMediaRepository
|
|
1890
2094
|
} : void 0,
|
|
1891
2095
|
catalog: providers.catalog
|
|
1892
2096
|
};
|
|
@@ -1991,6 +2195,7 @@ export {
|
|
|
1991
2195
|
ExportConversationUseCase,
|
|
1992
2196
|
FlowGraphRepository,
|
|
1993
2197
|
FlowInterpreter,
|
|
2198
|
+
FlowMediaRepository,
|
|
1994
2199
|
GetFlowGraphUseCase,
|
|
1995
2200
|
GetLiveFlowPositionsUseCase,
|
|
1996
2201
|
IngestInboundMediaUseCase,
|
|
@@ -2017,9 +2222,11 @@ export {
|
|
|
2017
2222
|
WhatsAppChannelAdapter,
|
|
2018
2223
|
claimWebhookDelivery,
|
|
2019
2224
|
createMetaWhatsAppModule,
|
|
2225
|
+
createSendMediaAction,
|
|
2020
2226
|
documents,
|
|
2021
2227
|
extractMediaDescriptor,
|
|
2022
2228
|
flowGraphs,
|
|
2229
|
+
flowMedia,
|
|
2023
2230
|
issueSseTicket,
|
|
2024
2231
|
messages,
|
|
2025
2232
|
metaWhatsAppMigrationsFolder,
|