|
|
|
@ -42,6 +42,7 @@ export const ResponseQueue = {
|
|
|
|
MAX_ATTEMPTS: process.env.RQ_MAX_ATTEMPTS ? Number(process.env.RQ_MAX_ATTEMPTS) : 6,
|
|
|
|
MAX_ATTEMPTS: process.env.RQ_MAX_ATTEMPTS ? Number(process.env.RQ_MAX_ATTEMPTS) : 6,
|
|
|
|
BASE_BACKOFF_MS: process.env.RQ_BASE_BACKOFF_MS ? Number(process.env.RQ_BASE_BACKOFF_MS) : 5000,
|
|
|
|
BASE_BACKOFF_MS: process.env.RQ_BASE_BACKOFF_MS ? Number(process.env.RQ_BASE_BACKOFF_MS) : 5000,
|
|
|
|
MAX_BACKOFF_MS: process.env.RQ_MAX_BACKOFF_MS ? Number(process.env.RQ_MAX_BACKOFF_MS) : 3600000,
|
|
|
|
MAX_BACKOFF_MS: process.env.RQ_MAX_BACKOFF_MS ? Number(process.env.RQ_MAX_BACKOFF_MS) : 3600000,
|
|
|
|
|
|
|
|
REACTIONS_MAX_ATTEMPTS: process.env.RQ_REACTIONS_MAX_ATTEMPTS ? Number(process.env.RQ_REACTIONS_MAX_ATTEMPTS) : null,
|
|
|
|
|
|
|
|
|
|
|
|
// Limpieza/retención (configurable por entorno)
|
|
|
|
// Limpieza/retención (configurable por entorno)
|
|
|
|
CLEANUP_ENABLED: process.env.RQ_CLEANUP_ENABLED !== 'false',
|
|
|
|
CLEANUP_ENABLED: process.env.RQ_CLEANUP_ENABLED !== 'false',
|
|
|
|
@ -106,6 +107,42 @@ export const ResponseQueue = {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Encolar una reacción con idempotencia (24h) usando metadata canónica
|
|
|
|
|
|
|
|
async enqueueReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (!chatId || !messageId || !emoji) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Construir JSON canónico
|
|
|
|
|
|
|
|
const metaObj = { kind: 'reaction', emoji, chatId, messageId };
|
|
|
|
|
|
|
|
const metadata = JSON.stringify(metaObj);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Ventana de 24h
|
|
|
|
|
|
|
|
const cutoff = this.futureIso(-24 * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Idempotencia: existe job igual reciente en estados activos?
|
|
|
|
|
|
|
|
const exists = this.dbInstance.prepare(`
|
|
|
|
|
|
|
|
SELECT 1
|
|
|
|
|
|
|
|
FROM response_queue
|
|
|
|
|
|
|
|
WHERE metadata = ?
|
|
|
|
|
|
|
|
AND status IN ('queued','processing','sent')
|
|
|
|
|
|
|
|
AND (updated_at > ? OR created_at > ?)
|
|
|
|
|
|
|
|
LIMIT 1
|
|
|
|
|
|
|
|
`).get(metadata, cutoff, cutoff) as any;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (exists) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.dbInstance.prepare(`
|
|
|
|
|
|
|
|
INSERT INTO response_queue (recipient, message, metadata, next_attempt_at)
|
|
|
|
|
|
|
|
VALUES (?, ?, ?, ?)
|
|
|
|
|
|
|
|
`).run(chatId, '', metadata, this.nowIso());
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
|
|
console.error('Failed to enqueue reaction:', err);
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getHeaders(): HeadersInit {
|
|
|
|
getHeaders(): HeadersInit {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
apikey: process.env.EVOLUTION_API_KEY || '',
|
|
|
|
apikey: process.env.EVOLUTION_API_KEY || '',
|
|
|
|
@ -122,6 +159,42 @@ export const ResponseQueue = {
|
|
|
|
return { ok: false, error: msg };
|
|
|
|
return { ok: false, error: msg };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Detectar jobs de reacción
|
|
|
|
|
|
|
|
let meta: any = null;
|
|
|
|
|
|
|
|
try { meta = item.metadata ? JSON.parse(item.metadata) : null; } catch {}
|
|
|
|
|
|
|
|
if (meta && meta.kind === 'reaction') {
|
|
|
|
|
|
|
|
const reactionUrl = `${baseUrl}/message/sendReaction/${instance}`;
|
|
|
|
|
|
|
|
const chatId = String(meta.chatId || '');
|
|
|
|
|
|
|
|
const messageId = String(meta.messageId || '');
|
|
|
|
|
|
|
|
const emoji = String(meta.emoji || '');
|
|
|
|
|
|
|
|
if (!chatId || !messageId || !emoji) {
|
|
|
|
|
|
|
|
return { ok: false, error: 'invalid_reaction_metadata' };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
|
|
key: { remoteJid: chatId, fromMe: true, id: messageId },
|
|
|
|
|
|
|
|
reaction: emoji
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await fetch(reactionUrl, {
|
|
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
|
|
headers: this.getHeaders(),
|
|
|
|
|
|
|
|
body: JSON.stringify(payload),
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
const body = await response.text().catch(() => '');
|
|
|
|
|
|
|
|
const errTxt = body?.slice(0, 200) || `HTTP ${response.status}`;
|
|
|
|
|
|
|
|
console.warn('Send reaction failed:', { status: response.status, body: errTxt });
|
|
|
|
|
|
|
|
return { ok: false, status: response.status, error: errTxt };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`✅ Sent reaction with payload: ${JSON.stringify(payload)}`);
|
|
|
|
|
|
|
|
return { ok: true, status: response.status };
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
|
|
const errMsg = (err instanceof Error ? err.message : String(err));
|
|
|
|
|
|
|
|
console.error('Network error sending reaction:', errMsg);
|
|
|
|
|
|
|
|
return { ok: false, error: errMsg };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Endpoint típico de Evolution API para texto simple
|
|
|
|
// Endpoint típico de Evolution API para texto simple
|
|
|
|
const url = `${baseUrl}/message/sendText/${instance}`;
|
|
|
|
const url = `${baseUrl}/message/sendText/${instance}`;
|
|
|
|
|
|
|
|
|
|
|
|
@ -294,8 +367,13 @@ export const ResponseQueue = {
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5xx o error de red: reintento con backoff si no superó el máximo
|
|
|
|
// 5xx o error de red: reintento con backoff si no superó el máximo (ajustado para reacciones)
|
|
|
|
if (attemptsNow >= this.MAX_ATTEMPTS) {
|
|
|
|
let metaForMax: any = null;
|
|
|
|
|
|
|
|
try { metaForMax = item.metadata ? JSON.parse(String(item.metadata)) : null; } catch {}
|
|
|
|
|
|
|
|
const isReactionJob = !!(metaForMax && metaForMax.kind === 'reaction');
|
|
|
|
|
|
|
|
const effectiveMax = isReactionJob && this.REACTIONS_MAX_ATTEMPTS ? this.REACTIONS_MAX_ATTEMPTS : this.MAX_ATTEMPTS;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (attemptsNow >= effectiveMax) {
|
|
|
|
this.markFailed(item.id, errMsg, status, attemptsNow);
|
|
|
|
this.markFailed(item.id, errMsg, status, attemptsNow);
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|