feat: integrar EvolutionClient, limpieza de cola y parseo de metadata
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>main
parent
a25fd4ee3b
commit
1b7420e123
@ -0,0 +1,63 @@
|
|||||||
|
export type EvolutionResult = { ok: boolean; status?: number; error?: string };
|
||||||
|
|
||||||
|
export function buildHeaders(): HeadersInit {
|
||||||
|
return {
|
||||||
|
apikey: process.env.EVOLUTION_API_KEY || '',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendText(payload: { number: string; text: string; mentioned?: string[] }): Promise<EvolutionResult> {
|
||||||
|
const baseUrl = process.env.EVOLUTION_API_URL;
|
||||||
|
const instance = process.env.EVOLUTION_API_INSTANCE;
|
||||||
|
if (!baseUrl || !instance) {
|
||||||
|
const msg = 'Missing EVOLUTION_API_URL or EVOLUTION_API_INSTANCE';
|
||||||
|
return { ok: false, error: msg };
|
||||||
|
}
|
||||||
|
const url = `${baseUrl}/message/sendText/${instance}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: buildHeaders(),
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
const errTxt = body?.slice(0, 200) || `HTTP ${res.status}`;
|
||||||
|
return { ok: false, status: res.status, error: errTxt };
|
||||||
|
}
|
||||||
|
return { ok: true, status: res.status };
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
return { ok: false, error: errMsg };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendReaction(payload: {
|
||||||
|
key: { remoteJid: string; id: string; fromMe: boolean; participant?: string };
|
||||||
|
reaction: string;
|
||||||
|
}): Promise<EvolutionResult> {
|
||||||
|
const baseUrl = process.env.EVOLUTION_API_URL;
|
||||||
|
const instance = process.env.EVOLUTION_API_INSTANCE;
|
||||||
|
if (!baseUrl || !instance) {
|
||||||
|
const msg = 'Missing EVOLUTION_API_URL or EVOLUTION_API_INSTANCE';
|
||||||
|
return { ok: false, error: msg };
|
||||||
|
}
|
||||||
|
const url = `${baseUrl}/message/sendReaction/${instance}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: buildHeaders(),
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
const errTxt = body?.slice(0, 200) || `HTTP ${res.status}`;
|
||||||
|
return { ok: false, status: res.status, error: errTxt };
|
||||||
|
}
|
||||||
|
return { ok: true, status: res.status };
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
return { ok: false, error: errMsg };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import type { Database } from 'bun:sqlite';
|
||||||
|
import { toIsoSqlUTC } from '../../utils/datetime';
|
||||||
|
|
||||||
|
export type CleanupOptions = {
|
||||||
|
retentionDaysSent: number;
|
||||||
|
retentionDaysFailed: number;
|
||||||
|
batchSize: number;
|
||||||
|
optimize: boolean;
|
||||||
|
vacuum: boolean;
|
||||||
|
vacuumEveryNRuns: number;
|
||||||
|
cleanupRunCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runCleanupOnce(
|
||||||
|
db: Database,
|
||||||
|
opts: CleanupOptions,
|
||||||
|
now: Date = new Date()
|
||||||
|
): Promise<{ deletedSent: number; deletedFailed: number; totalDeleted: number; nextCleanupRunCount: number }> {
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
const sentThresholdIso = toIsoSqlUTC(new Date(now.getTime() - opts.retentionDaysSent * msPerDay));
|
||||||
|
const failedThresholdIso = toIsoSqlUTC(new Date(now.getTime() - opts.retentionDaysFailed * msPerDay));
|
||||||
|
|
||||||
|
const cleanStatus = (status: 'sent' | 'failed', thresholdIso: string, batch: number): number => {
|
||||||
|
let deleted = 0;
|
||||||
|
const selectStmt = db.prepare(`
|
||||||
|
SELECT id
|
||||||
|
FROM response_queue
|
||||||
|
WHERE status = ? AND updated_at < ?
|
||||||
|
ORDER BY updated_at
|
||||||
|
LIMIT ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const rows = selectStmt.all(status, thresholdIso, batch) as Array<{ id: number }>;
|
||||||
|
if (!rows || rows.length === 0) break;
|
||||||
|
|
||||||
|
const ids = rows.map((r) => r.id);
|
||||||
|
const placeholders = ids.map(() => '?').join(',');
|
||||||
|
db.prepare(`DELETE FROM response_queue WHERE id IN (${placeholders})`).run(...ids);
|
||||||
|
deleted += ids.length;
|
||||||
|
|
||||||
|
if (rows.length < batch) break;
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletedSent = cleanStatus('sent', sentThresholdIso, opts.batchSize);
|
||||||
|
const deletedFailed = cleanStatus('failed', failedThresholdIso, opts.batchSize);
|
||||||
|
const totalDeleted = deletedSent + deletedFailed;
|
||||||
|
|
||||||
|
// Mantenimiento ligero tras limpieza
|
||||||
|
if (opts.optimize && totalDeleted > 0) {
|
||||||
|
try {
|
||||||
|
db.exec('PRAGMA optimize;');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('PRAGMA optimize failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VACUUM opcional
|
||||||
|
let nextCleanupRunCount = opts.cleanupRunCount;
|
||||||
|
if (opts.vacuum && totalDeleted > 0) {
|
||||||
|
nextCleanupRunCount++;
|
||||||
|
if (nextCleanupRunCount % Math.max(1, opts.vacuumEveryNRuns) === 0) {
|
||||||
|
try {
|
||||||
|
db.exec('VACUUM;');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('VACUUM failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deletedSent, deletedFailed, totalDeleted, nextCleanupRunCount };
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
export type OnboardingMeta = {
|
||||||
|
kind: 'onboarding';
|
||||||
|
variant: 'initial' | 'reminder';
|
||||||
|
part: 1 | 2;
|
||||||
|
bundle_id: string;
|
||||||
|
group_id?: string | null;
|
||||||
|
task_id?: number | null;
|
||||||
|
display_code?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReactionMeta = {
|
||||||
|
kind: 'reaction';
|
||||||
|
emoji: string;
|
||||||
|
chatId: string;
|
||||||
|
messageId: string;
|
||||||
|
participant?: string;
|
||||||
|
fromMe?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueueMetadata = OnboardingMeta | ReactionMeta | Record<string, any>;
|
||||||
|
|
||||||
|
export function parseQueueMetadata(raw: string | null | undefined): QueueMetadata | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(String(raw));
|
||||||
|
if (!obj || typeof obj !== 'object') return null;
|
||||||
|
const kind = String((obj as any).kind || '');
|
||||||
|
if (kind === 'reaction') {
|
||||||
|
// Validación mínima
|
||||||
|
return {
|
||||||
|
kind: 'reaction',
|
||||||
|
emoji: String((obj as any).emoji || ''),
|
||||||
|
chatId: String((obj as any).chatId || ''),
|
||||||
|
messageId: String((obj as any).messageId || ''),
|
||||||
|
participant: typeof (obj as any).participant === 'string' ? String((obj as any).participant) : undefined,
|
||||||
|
fromMe: typeof (obj as any).fromMe === 'boolean' ? Boolean((obj as any).fromMe) : undefined
|
||||||
|
} as ReactionMeta;
|
||||||
|
}
|
||||||
|
if (kind === 'onboarding') {
|
||||||
|
return obj as OnboardingMeta;
|
||||||
|
}
|
||||||
|
return obj as Record<string, any>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue