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