import type { Database } from 'bun:sqlite'; import { db, ensureUserExists } from '../db'; import { normalizeWhatsAppId } from '../utils/whatsapp'; import { TaskService } from '../tasks/service'; import { GroupSyncService } from './group-sync'; type CommandContext = { sender: string; // normalized user id (digits only), but accept raw too groupId: string; // full JID (e.g., xxx@g.us) message: string; // raw message text mentions: string[]; // array of raw JIDs mentioned }; export type CommandResponse = { recipient: string; message: string; mentions?: string[]; // full JIDs to mention in the outgoing message }; export class CommandService { static dbInstance: Database = db; private static parseNueva(message: string, mentionsNormalized: string[]): { action: string; description: string; dueDate: string | null; } { const parts = (message || '').trim().split(/\s+/); const action = (parts[1] || '').toLowerCase(); // Buscar última fecha futura con formato YYYY-MM-DD const dateIndices: { index: number; text: string }[] = []; for (let i = 2; i < parts.length; i++) { const p = parts[i]; if (/^\d{4}-\d{2}-\d{2}$/.test(p)) { const d = new Date(p); const today = new Date(); today.setHours(0, 0, 0, 0); if (!isNaN(d.getTime()) && d >= today) { dateIndices.push({ index: i, text: p }); } } } let dueDate: string | null = null; let descriptionTokens: string[] = []; const isMentionToken = (token: string) => token.startsWith('@'); if (dateIndices.length > 0) { const last = dateIndices[dateIndices.length - 1]; dueDate = last.text; for (let i = 2; i < parts.length; i++) { if (i === last.index) continue; const token = parts[i]; if (isMentionToken(token)) continue; // quitar @menciones del texto descriptionTokens.push(token); } } else { for (let i = 2; i < parts.length; i++) { const token = parts[i]; if (isMentionToken(token)) continue; descriptionTokens.push(token); } } const description = descriptionTokens.join(' ').trim(); return { action, description, dueDate, }; } private static async processTareaCommand( context: CommandContext ): Promise { const trimmed = (context.message || '').trim(); const tokens = trimmed.split(/\s+/); const action = (tokens[1] || '').toLowerCase(); if (action !== 'nueva') { return [{ recipient: context.sender, message: `Acción ${action || '(vacía)'} no implementada aún` }]; } // Parseo específico de "nueva" // Normalizar menciones del contexto para parseo y asignaciones const mentionsNormalizedFromContext = Array.from(new Set( (context.mentions || []) .map(j => normalizeWhatsAppId(j)) .filter((id): id is string => !!id) )); // Detectar también tokens de texto que empiezan por '@' como posibles asignados const atTokenCandidates = tokens.slice(2) .filter(t => t.startsWith('@')) .map(t => t.replace(/^@+/, '')); const normalizedFromAtTokens = Array.from(new Set( atTokenCandidates .map(v => normalizeWhatsAppId(v)) .filter((id): id is string => !!id) )); const combinedAssigneeCandidates = Array.from(new Set([ ...mentionsNormalizedFromContext, ...normalizedFromAtTokens ])); const { description, dueDate } = this.parseNueva(trimmed, mentionsNormalizedFromContext); // Asegurar creador const createdBy = ensureUserExists(context.sender, this.dbInstance); if (!createdBy) { throw new Error('No se pudo asegurar el usuario creador'); } // Normalizar menciones y excluir duplicados y el número del bot const botNumber = process.env.CHATBOT_PHONE_NUMBER || ''; const assigneesNormalized = Array.from(new Set( combinedAssigneeCandidates .filter(id => !botNumber || id !== botNumber) )); // Asegurar usuarios asignados const ensuredAssignees = assigneesNormalized .map(id => ensureUserExists(id, this.dbInstance)) .filter((id): id is string => !!id); // Si no hay asignados, asignar al creador const assignmentUserIds = ensuredAssignees.length > 0 ? ensuredAssignees : [createdBy]; // Definir group_id solo si el grupo está activo const groupIdToUse = (context.groupId && GroupSyncService.isGroupActive(context.groupId)) ? context.groupId : null; // Crear tarea y asignaciones const taskId = TaskService.createTask( { description: description || '', due_date: dueDate ?? null, group_id: groupIdToUse, created_by: createdBy, }, assignmentUserIds.map(uid => ({ user_id: uid, assigned_by: createdBy, })) ); const mentionsForSending = assignmentUserIds.map(uid => `${uid}@s.whatsapp.net`); const assignedList = assignmentUserIds.map(uid => `@${uid}`).join(' '); const resp = `✅ Tarea ${taskId} creada: "${description || '(sin descripción)'}"` + (dueDate ? ` (vence ${dueDate})` : '') + (assignedList ? ` — asignados: ${assignedList}` : ''); // Enviar al grupo si está activo; si no, al creador (DM) const recipient = groupIdToUse || createdBy; return [{ recipient, message: resp, mentions: mentionsForSending.length > 0 ? mentionsForSending : undefined }]; } static async handle(context: CommandContext): Promise { if (!context.message.startsWith('/tarea')) { return []; } try { return await this.processTareaCommand(context); } catch (error) { return [{ recipient: context.sender, message: 'Error processing command' }]; } } }