import type { Database } from 'bun:sqlite'; import { db, ensureUserExists } from '../db'; import { isGroupId } from '../utils/whatsapp'; import { AllowedGroups } from './allowed-groups'; import { Metrics } from './metrics'; import { route as routeCommand } from './commands'; import { ACTION_ALIASES } from './commands/shared'; 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 messageId?: string; // id del mensaje origen (para task_origins y reacciones) participant?: string; // JID del autor del mensaje origen (en grupos) fromMe?: boolean; // si el mensaje origen fue enviado por la instancia }; export type CommandResponse = { recipient: string; message: string; mentions?: string[]; // full JIDs to mention in the outgoing message }; export type CommandOutcome = { responses: CommandResponse[]; ok: boolean; createdTaskIds?: number[]; }; export class CommandService { static dbInstance: Database = db; static async handle(context: CommandContext): Promise { const outcome = await this.handleWithOutcome(context); return outcome.responses; } static async handleWithOutcome(context: CommandContext): Promise { const msg = (context.message || '').trim(); if (!/^\/(tarea|t)\b/i.test(msg)) { return { responses: [], ok: true }; } // Registrar interacción del usuario (last_command_at) para cualquier comando /t … try { let usersTableExists = false; try { const row = this.dbInstance.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`).get() as { name?: string } | undefined; usersTableExists = !!row; } catch {} if (usersTableExists) { const ensured = ensureUserExists(context.sender, this.dbInstance); if (ensured) { try { this.dbInstance.prepare(`UPDATE users SET last_command_at = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ?`).run(ensured); } catch {} } } } catch {} // Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente) if (isGroupId(context.groupId)) { try { AllowedGroups.dbInstance = this.dbInstance; } catch { } const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { try { if (!AllowedGroups.isAllowed(context.groupId)) { try { Metrics.inc('commands_blocked_total'); } catch { } return { responses: [], ok: true }; } } catch { // Si falla el check, ser permisivos } } } try { const routed = await routeCommand(context, { db: this.dbInstance }); const responses = routed ?? []; // Clasificación explícita del outcome (evita lógica en server) const tokens = msg.split(/\s+/); const rawAction = (tokens[1] || '').toLowerCase(); const action = ACTION_ALIASES[rawAction] || rawAction; // Casos explícitos considerados éxito if (!action || action === 'ayuda' || action === 'web') { return { responses, ok: true }; } const lowerMsgs = (responses || []).map(r => String(r?.message || '').toLowerCase()); const isOkException = (m: string) => m.includes('ya estaba completada') || m.includes('ya la tenías') || m.includes('no la tenías'); const isErrorMsg = (m: string) => m.startsWith('ℹ️ uso:'.toLowerCase()) || m.includes('uso:') || m.includes('no puedes') || m.includes('no permitido') || m.includes('no encontrada') || m.includes('comando no reconocido'); let hasError = false; for (const m of lowerMsgs) { if (isErrorMsg(m) && !isOkException(m)) { hasError = true; break; } } return { responses, ok: !hasError }; } catch (error) { return { responses: [{ recipient: context.sender, message: 'Error processing command' }], ok: false }; } } }