@hed-hog/core 0.0.401 → 0.0.402
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/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +121 -12
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +245 -3
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +7 -2
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.css.js +88 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.js +73 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -1
- package/dist/pdf/document/pdf-document.types.d.ts +56 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -1
- package/dist/pdf/pdf.service.d.ts +6 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +3 -0
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +83 -21
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +46 -20
- package/hedhog/frontend/messages/pt.json +46 -20
- package/hedhog/table/file.yaml +3 -1
- package/package.json +4 -4
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.ts +363 -2
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/integration-profile/integration-profile.secrets.ts +7 -2
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/tester-coverage.spec.ts +2 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/pdf/document/pdf-document.css.ts +88 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +43 -0
- package/src/pdf/document/pdf-document.renderer.ts +102 -0
- package/src/pdf/document/pdf-document.types.ts +52 -0
- package/src/pdf/pdf.service.ts +11 -0
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +191 -1
- package/src/whatsapp-sender/whatsapp-sender.service.ts +122 -14
|
@@ -32,8 +32,17 @@ export type WhatsappSendInput = {
|
|
|
32
32
|
phoneNumberId?: string;
|
|
33
33
|
/** Template aprovado pela Meta — sem ele o envio só chega na janela de 24h. */
|
|
34
34
|
template?: { name: string; language?: string; params?: string[] };
|
|
35
|
+
/**
|
|
36
|
+
* Providers aceitos para este envio. Vazio aceita qualquer um. Quem manda em
|
|
37
|
+
* lote para alunos restringe ao oficial: disparo em massa pela Evolution
|
|
38
|
+
* (WhatsApp Web) derruba e bane o número.
|
|
39
|
+
*/
|
|
40
|
+
providers?: readonly string[];
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
/** Só a Cloud API da Meta — para envio em volume a quem não é da equipe. */
|
|
44
|
+
export const WHATSAPP_OFFICIAL_ONLY: readonly string[] = ['whatsapp-official'];
|
|
45
|
+
|
|
37
46
|
type ResolvedProfile = {
|
|
38
47
|
id: number;
|
|
39
48
|
slug: string;
|
|
@@ -46,13 +55,27 @@ type ResolvedProfile = {
|
|
|
46
55
|
* resultado diferente. Um HTTP 4xx da Graph API (id inexistente, sem
|
|
47
56
|
* permissão, token ruim) não muda em cinco minutos; um 5xx ou falha de rede
|
|
48
57
|
* pode ser uma instabilidade passageira do provedor.
|
|
58
|
+
*
|
|
59
|
+
* `isTransient` repete o `retryable` no formato que o `TransientError.is` da
|
|
60
|
+
* fila reconhece por duck-typing (o core não depende do queue): com ele as
|
|
61
|
+
* tentativas intermediárias vão ao Sentry como warning, e só a última como erro.
|
|
62
|
+
*
|
|
63
|
+
* `reason` marca as falhas que valem para a instância inteira, não para a
|
|
64
|
+
* mensagem: com a sessão fechada ou a credencial recusada, a próxima mensagem
|
|
65
|
+
* do mesmo lote tem o mesmo destino, e quem envia em série pode parar ali.
|
|
49
66
|
*/
|
|
67
|
+
export type WhatsappSendErrorReason = 'session_closed' | 'unauthorized';
|
|
68
|
+
|
|
50
69
|
export class WhatsappSendError extends BadRequestException {
|
|
70
|
+
readonly isTransient: boolean;
|
|
71
|
+
|
|
51
72
|
constructor(
|
|
52
73
|
message: string,
|
|
53
74
|
public readonly retryable: boolean,
|
|
75
|
+
public readonly reason?: WhatsappSendErrorReason,
|
|
54
76
|
) {
|
|
55
77
|
super(message);
|
|
78
|
+
this.isTransient = retryable;
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -73,7 +96,7 @@ export class WhatsappSenderService {
|
|
|
73
96
|
) {}
|
|
74
97
|
|
|
75
98
|
async send(input: WhatsappSendInput): Promise<void> {
|
|
76
|
-
const profile = await this.resolveProfile(input.profileSlug);
|
|
99
|
+
const profile = await this.resolveProfile(input.profileSlug, input.providers);
|
|
77
100
|
|
|
78
101
|
switch (profile.providerSlug) {
|
|
79
102
|
case 'evolution-api':
|
|
@@ -88,9 +111,12 @@ export class WhatsappSenderService {
|
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
/** Diz se há perfil utilizável, para o chamador decidir por um fallback sem tentar o envio. */
|
|
91
|
-
async isConfigured(
|
|
114
|
+
async isConfigured(
|
|
115
|
+
profileSlug?: string,
|
|
116
|
+
providers?: readonly string[],
|
|
117
|
+
): Promise<boolean> {
|
|
92
118
|
try {
|
|
93
|
-
const profile = await this.resolveProfile(profileSlug);
|
|
119
|
+
const profile = await this.resolveProfile(profileSlug, providers);
|
|
94
120
|
|
|
95
121
|
if (profile.providerSlug === 'evolution-api') {
|
|
96
122
|
const { baseUrl, token, instance } = this.readEvolutionConfig(profile);
|
|
@@ -150,8 +176,12 @@ export class WhatsappSenderService {
|
|
|
150
176
|
return normalized;
|
|
151
177
|
}
|
|
152
178
|
|
|
153
|
-
private async resolveProfile(
|
|
179
|
+
private async resolveProfile(
|
|
180
|
+
profileSlug?: string,
|
|
181
|
+
providers?: readonly string[],
|
|
182
|
+
): Promise<ResolvedProfile> {
|
|
154
183
|
const slug = String(profileSlug ?? '').trim();
|
|
184
|
+
const allowed = providers?.length ? [...providers] : null;
|
|
155
185
|
|
|
156
186
|
const profile = slug
|
|
157
187
|
? await this.prisma.integration_profile.findUnique({
|
|
@@ -162,7 +192,11 @@ export class WhatsappSenderService {
|
|
|
162
192
|
},
|
|
163
193
|
})
|
|
164
194
|
: await this.prisma.integration_profile.findFirst({
|
|
165
|
-
where: {
|
|
195
|
+
where: {
|
|
196
|
+
is_active: true,
|
|
197
|
+
integration_type: { slug: 'whatsapp' },
|
|
198
|
+
...(allowed ? { integration_provider: { slug: { in: allowed } } } : {}),
|
|
199
|
+
},
|
|
166
200
|
orderBy: { updated_at: 'desc' },
|
|
167
201
|
include: {
|
|
168
202
|
integration_provider: { select: { slug: true } },
|
|
@@ -174,7 +208,9 @@ export class WhatsappSenderService {
|
|
|
174
208
|
throw new NotFoundException(
|
|
175
209
|
slug
|
|
176
210
|
? `Perfil de integração "${slug}" não encontrado.`
|
|
177
|
-
:
|
|
211
|
+
: allowed
|
|
212
|
+
? `Nenhum perfil de WhatsApp ativo do provider ${allowed.join(' ou ')} encontrado.`
|
|
213
|
+
: 'Nenhum perfil de integração de WhatsApp ativo encontrado.',
|
|
178
214
|
);
|
|
179
215
|
}
|
|
180
216
|
|
|
@@ -184,10 +220,21 @@ export class WhatsappSenderService {
|
|
|
184
220
|
);
|
|
185
221
|
}
|
|
186
222
|
|
|
223
|
+
const providerSlug = String((profile as any).integration_provider?.slug ?? '');
|
|
224
|
+
|
|
225
|
+
// Perfil escolhido à mão que não é do provider exigido: é configuração, não
|
|
226
|
+
// instabilidade — reenviar daria no mesmo.
|
|
227
|
+
if (allowed && !allowed.includes(providerSlug)) {
|
|
228
|
+
throw new WhatsappSendError(
|
|
229
|
+
`Perfil de integração "${profile.slug}" usa o provider "${providerSlug}", e este envio só aceita ${allowed.join(' ou ')}.`,
|
|
230
|
+
false,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
187
234
|
return {
|
|
188
235
|
id: profile.id,
|
|
189
236
|
slug: profile.slug,
|
|
190
|
-
providerSlug
|
|
237
|
+
providerSlug,
|
|
191
238
|
config: ((profile.config ?? {}) as Record<string, unknown>),
|
|
192
239
|
};
|
|
193
240
|
}
|
|
@@ -245,6 +292,7 @@ export class WhatsappSenderService {
|
|
|
245
292
|
throw new WhatsappSendError(
|
|
246
293
|
`Credenciais do WhatsApp inválidas (401). Verifique o token do perfil "${profile.slug}".`,
|
|
247
294
|
false,
|
|
295
|
+
'unauthorized',
|
|
248
296
|
);
|
|
249
297
|
}
|
|
250
298
|
|
|
@@ -258,6 +306,18 @@ export class WhatsappSenderService {
|
|
|
258
306
|
);
|
|
259
307
|
}
|
|
260
308
|
|
|
309
|
+
// Sessão da instância caída (celular deslogado ou Baileys reconectando):
|
|
310
|
+
// a Evolution responde 500 "Connection Closed". Segue retentável porque a
|
|
311
|
+
// reconexão costuma ser sozinha; se persistir, quem ler a falha precisa
|
|
312
|
+
// saber que a ação é reconectar a instância, não mexer no envio.
|
|
313
|
+
if (isEvolutionSessionClosed(detail)) {
|
|
314
|
+
throw new WhatsappSendError(
|
|
315
|
+
`A instância "${instance}" da Evolution está desconectada do WhatsApp (Connection Closed). Reconecte a instância (QR code) do perfil "${profile.slug}".`,
|
|
316
|
+
true,
|
|
317
|
+
'session_closed',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
261
321
|
// Mesmo critério do provider oficial: 4xx é recusa estrutural, 5xx e
|
|
262
322
|
// 429 podem passar numa próxima tentativa.
|
|
263
323
|
throw new WhatsappSendError(
|
|
@@ -267,6 +327,24 @@ export class WhatsappSenderService {
|
|
|
267
327
|
}
|
|
268
328
|
}
|
|
269
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Número padrão do perfil, mantido pela library `whatsapp` (sync com o WABA
|
|
332
|
+
* ou registro na primeira mensagem recebida).
|
|
333
|
+
*
|
|
334
|
+
* A tabela não é do core: num projeto sem a library `whatsapp` o delegate não
|
|
335
|
+
* existe, e o envio segue sem padrão — cai no erro de phone_number_id ausente.
|
|
336
|
+
*/
|
|
337
|
+
private async resolveDefaultPhoneNumberId(profileId: number): Promise<string> {
|
|
338
|
+
const delegate = (this.prisma as any).whatsapp_phone_number;
|
|
339
|
+
if (!delegate?.findFirst) return '';
|
|
340
|
+
|
|
341
|
+
const found = await delegate.findFirst({
|
|
342
|
+
where: { integration_profile_id: profileId, is_default: true, status: 'active' },
|
|
343
|
+
select: { phone_number_id: true },
|
|
344
|
+
});
|
|
345
|
+
return String(found?.phone_number_id ?? '').trim();
|
|
346
|
+
}
|
|
347
|
+
|
|
270
348
|
private async sendWithOfficial(
|
|
271
349
|
profile: ResolvedProfile,
|
|
272
350
|
input: WhatsappSendInput,
|
|
@@ -292,15 +370,16 @@ export class WhatsappSenderService {
|
|
|
292
370
|
);
|
|
293
371
|
}
|
|
294
372
|
|
|
295
|
-
// O phone_number_id não vive no perfil: um WABA pode ter vários números
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
373
|
+
// O phone_number_id não vive no perfil: um WABA pode ter vários números.
|
|
374
|
+
// Quem envia pode dizer por qual deles a mensagem sai; sem isso, vale o
|
|
375
|
+
// número padrão do perfil.
|
|
376
|
+
const phoneNumberId =
|
|
377
|
+
String(input.phoneNumberId ?? profile.config.phone_number_id ?? '').trim() ||
|
|
378
|
+
(await this.resolveDefaultPhoneNumberId(profile.id));
|
|
300
379
|
|
|
301
380
|
if (!phoneNumberId) {
|
|
302
381
|
throw new BadRequestException(
|
|
303
|
-
|
|
382
|
+
`Perfil de WhatsApp "${profile.slug}" não tem número padrão: informe o phone_number_id do número remetente ou mande uma mensagem para o número, que ele é registrado.`,
|
|
304
383
|
);
|
|
305
384
|
}
|
|
306
385
|
|
|
@@ -322,7 +401,7 @@ export class WhatsappSenderService {
|
|
|
322
401
|
type: 'body',
|
|
323
402
|
parameters: (input.template.params ?? [input.text]).map((text) => ({
|
|
324
403
|
type: 'text',
|
|
325
|
-
text,
|
|
404
|
+
text: toTemplateParam(text),
|
|
326
405
|
})),
|
|
327
406
|
},
|
|
328
407
|
],
|
|
@@ -389,6 +468,35 @@ function isEvolutionNumberMissing(detail: string): boolean {
|
|
|
389
468
|
}
|
|
390
469
|
}
|
|
391
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Reconhece o 500 da Evolution com a sessão do WhatsApp fechada:
|
|
473
|
+
* `{"response":{"message":"Connection Closed"}}` (às vezes com a string num array).
|
|
474
|
+
*/
|
|
475
|
+
function isEvolutionSessionClosed(detail: string): boolean {
|
|
476
|
+
try {
|
|
477
|
+
const message = JSON.parse(detail)?.response?.message;
|
|
478
|
+
const texts = Array.isArray(message) ? message : [message];
|
|
479
|
+
return texts.some(
|
|
480
|
+
(item) => typeof item === 'string' && /connection closed/i.test(item),
|
|
481
|
+
);
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* A Meta recusa parâmetro de template com quebra de linha, tab ou mais de
|
|
489
|
+
* quatro espaços seguidos (erro 132018) — e o texto que os módulos montam para
|
|
490
|
+
* o WhatsApp vem em parágrafos. Vazio também é recusado.
|
|
491
|
+
*/
|
|
492
|
+
export function toTemplateParam(text: unknown): string {
|
|
493
|
+
const flat = String(text ?? '')
|
|
494
|
+
.replace(/\s*[\r\n\t]+\s*/g, ' · ')
|
|
495
|
+
.replace(/ {2,}/g, ' ')
|
|
496
|
+
.trim();
|
|
497
|
+
return flat || '-';
|
|
498
|
+
}
|
|
499
|
+
|
|
392
500
|
/** Evolution espera base64 puro; um data URL vindo do chamador quebraria o envio. */
|
|
393
501
|
function stripDataUrlPrefix(image: string): string {
|
|
394
502
|
const match = /^data:[^;]+;base64,(.*)$/i.exec(image);
|