|
|
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<CommandResponse[]> {
|
|
|
const outcome = await this.handleWithOutcome(context);
|
|
|
return outcome.responses;
|
|
|
}
|
|
|
|
|
|
static async handleWithOutcome(context: CommandContext): Promise<CommandOutcome> {
|
|
|
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
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
}
|