feat: add support for relative time parsing in reminder command

This commit is contained in:
2025-09-28 01:17:25 -05:00
parent 1cd11e8f9a
commit 809128e2cc

View File

@@ -13,6 +13,66 @@ function humanizeDate(d: Date) {
return d.toLocaleString('es-ES', { timeZone: 'UTC', hour12: false }); return d.toLocaleString('es-ES', { timeZone: 'UTC', hour12: false });
} }
function parseRelativeDelay(text: string): { when: Date, reminderText: string } | null {
const lower = text.toLowerCase();
// 1) "en menos de 1h" -> 59 minutos
let m = lower.match(/en\s+menos\s+de\s+1\s*h(ora(s)?)?/i);
if (m) {
const minutes = 59;
const when = new Date(Date.now() + minutes * 60 * 1000);
const reminderText = text.replace(m[0], '').trim() || text;
return { when, reminderText };
}
// 2) "en menos de X min" -> (X-1) minutos
m = lower.match(/en\s+menos\s+de\s+(\d+)\s*m(in(utos)?)?/i);
if (m) {
const minutes = Math.max(1, parseInt(m[1], 10) - 1);
const when = new Date(Date.now() + minutes * 60 * 1000);
const reminderText = text.replace(m[0], '').trim() || text;
return { when, reminderText };
}
// 3) "en X minutos" / "en Xm"
m = lower.match(/en\s+(\d+)\s*m(in(utos)?)?/i);
if (m) {
const minutes = Math.max(1, parseInt(m[1], 10));
const when = new Date(Date.now() + minutes * 60 * 1000);
const reminderText = text.replace(m[0], '').trim() || text;
return { when, reminderText };
}
// 4) "en X horas" / "en Xh"
m = lower.match(/en\s+(\d+)\s*h(ora(s)?)?/i);
if (m) {
const hours = Math.max(1, parseInt(m[1], 10));
const when = new Date(Date.now() + hours * 60 * 60 * 1000);
const reminderText = text.replace(m[0], '').trim() || text;
return { when, reminderText };
}
// 5) Post-fijo corto: "15m" o "45 min" al final
m = lower.match(/(\d+)\s*m(in(utos)?)?$/i);
if (m) {
const minutes = Math.max(1, parseInt(m[1], 10));
const when = new Date(Date.now() + minutes * 60 * 1000);
const reminderText = text.slice(0, m.index).trim() || text;
return { when, reminderText };
}
// 6) Post-fijo corto: "1h" o "2 horas" al final
m = lower.match(/(\d+)\s*h(ora(s)?)?$/i);
if (m) {
const hours = Math.max(1, parseInt(m[1], 10));
const when = new Date(Date.now() + hours * 60 * 60 * 1000);
const reminderText = text.slice(0, m.index).trim() || text;
return { when, reminderText };
}
return null;
}
export const command: CommandMessage = { export const command: CommandMessage = {
name: 'recordar', name: 'recordar',
type: 'message', type: 'message',
@@ -32,39 +92,46 @@ export const command: CommandMessage = {
return; return;
} }
// Parsear en español, forzando fechas futuras cuando sea ambiguo // 1) Soporte rápido para tiempos relativos: "en 10 minutos", "15m", "1h", "en menos de 1h"
const rel = parseRelativeDelay(text);
let when: Date | null = null;
let reminderText = text;
if (rel) {
when = rel.when;
reminderText = rel.reminderText;
}
// 2) Si no hubo match relativo, usar parser natural (chrono) en español
if (!when) {
const ref = new Date(); const ref = new Date();
const results = chrono.es.parse(text, ref, { forwardDate: true }); const results = chrono.es.parse(text, ref, { forwardDate: true });
if (!results.length) { if (!results.length) {
await message.reply('❌ No pude entender cuándo. Intenta algo como: "mañana 9am", "el miércoles 17:00", "en 2 horas".'); await message.reply('❌ No pude entender cuándo. Intenta algo como: "en 10 minutos", "mañana 9am", "el miércoles 17:00".');
return; return;
} }
const r = results[0]; const r = results[0];
const when = r.date(); when = r.date();
if (!when || isNaN(when.getTime())) { if (!when || isNaN(when.getTime())) {
await message.reply('❌ La fecha/hora no es válida.'); await message.reply('❌ La fecha/hora no es válida.');
return; return;
} }
// Evitar fechas pasadas
if (when.getTime() <= Date.now()) {
await message.reply('❌ La fecha/hora ya pasó. Especifica una fecha futura.');
return;
}
// Extraer el texto del recordatorio eliminando el fragmento reconocido de fecha // Extraer el texto del recordatorio eliminando el fragmento reconocido de fecha
const matched = r.text || ''; const matched = r.text || '';
let reminderText = text;
if (matched) { if (matched) {
const idx = text.toLowerCase().indexOf(matched.toLowerCase()); const idx = text.toLowerCase().indexOf(matched.toLowerCase());
if (idx >= 0) { if (idx >= 0) {
reminderText = (text.slice(0, idx) + text.slice(idx + matched.length)).trim(); reminderText = (text.slice(0, idx) + text.slice(idx + matched.length)).trim() || text;
} }
} }
// Si quedó vacío, usar el texto completo }
if (!reminderText) reminderText = text;
// Validaciones de futuro y límites mínimos
if (when.getTime() <= Date.now() + 20 * 1000) { // al menos 20s al futuro
await message.reply('❌ Especifica un tiempo al futuro (al menos ~20 segundos).');
return;
}
// Guardar en Appwrite // Guardar en Appwrite
const iso = new Date(when.getTime()).toISOString(); const iso = new Date(when.getTime()).toISOString();
@@ -86,4 +153,3 @@ export const command: CommandMessage = {
await message.reply(`✅ Recordatorio guardado para: ${whenHuman} UTC\nMensaje: ${reminderText}`); await message.reply(`✅ Recordatorio guardado para: ${whenHuman} UTC\nMensaje: ${reminderText}`);
} }
}; };