|
|
|
|
@ -2,6 +2,15 @@ import type { Database } from 'bun:sqlite';
|
|
|
|
|
import { db } from '../db';
|
|
|
|
|
import { IdentityService } from './identity';
|
|
|
|
|
import { normalizeWhatsAppId } from '../utils/whatsapp';
|
|
|
|
|
import { Metrics } from './metrics';
|
|
|
|
|
|
|
|
|
|
const MAX_FALLBACK_DIGITS = (() => {
|
|
|
|
|
const raw = (process.env.ONBOARDING_FALLBACK_MAX_DIGITS || '').trim();
|
|
|
|
|
const n = parseInt(raw || '15', 10);
|
|
|
|
|
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const isDigits = (s: string) => /^\d+$/.test(s);
|
|
|
|
|
|
|
|
|
|
type QueuedResponse = {
|
|
|
|
|
recipient: string;
|
|
|
|
|
@ -117,9 +126,47 @@ export const ResponseQueue = {
|
|
|
|
|
const url = `${baseUrl}/message/sendText/${instance}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Resolver destinatario efectivo (alias → número) y validar antes de construir el payload
|
|
|
|
|
const rawRecipient = String(item.recipient || '');
|
|
|
|
|
let numberOrJid = rawRecipient;
|
|
|
|
|
|
|
|
|
|
if (rawRecipient.includes('@')) {
|
|
|
|
|
if (rawRecipient.endsWith('@g.us')) {
|
|
|
|
|
// Envío a grupo: usar el JID completo tal cual
|
|
|
|
|
numberOrJid = rawRecipient;
|
|
|
|
|
} else if (rawRecipient.endsWith('@s.whatsapp.net')) {
|
|
|
|
|
// JID de usuario: normalizar a dígitos
|
|
|
|
|
const n = normalizeWhatsAppId(rawRecipient);
|
|
|
|
|
if (!n || !isDigits(n)) {
|
|
|
|
|
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'non_numeric' }); } catch {}
|
|
|
|
|
return { ok: false, status: 422, error: 'unresolvable_recipient_non_numeric' };
|
|
|
|
|
}
|
|
|
|
|
if (n.length >= MAX_FALLBACK_DIGITS) {
|
|
|
|
|
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'too_long' }); } catch {}
|
|
|
|
|
return { ok: false, status: 422, error: 'unresolvable_recipient_too_long' };
|
|
|
|
|
}
|
|
|
|
|
numberOrJid = n;
|
|
|
|
|
} else {
|
|
|
|
|
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'bad_domain' }); } catch {}
|
|
|
|
|
return { ok: false, status: 422, error: 'unresolvable_recipient_bad_domain' };
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Sin dominio: resolver alias si existe y validar
|
|
|
|
|
const resolved = IdentityService.resolveAliasOrNull(rawRecipient) || rawRecipient;
|
|
|
|
|
if (!isDigits(resolved)) {
|
|
|
|
|
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'non_numeric' }); } catch {}
|
|
|
|
|
return { ok: false, status: 422, error: 'unresolvable_recipient_non_numeric' };
|
|
|
|
|
}
|
|
|
|
|
if (resolved.length >= MAX_FALLBACK_DIGITS) {
|
|
|
|
|
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'too_long' }); } catch {}
|
|
|
|
|
return { ok: false, status: 422, error: 'unresolvable_recipient_too_long' };
|
|
|
|
|
}
|
|
|
|
|
numberOrJid = resolved;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build payload, adding mentioned JIDs if present in metadata
|
|
|
|
|
const payload: any = {
|
|
|
|
|
number: item.recipient,
|
|
|
|
|
number: numberOrJid,
|
|
|
|
|
text: item.message,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|