You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
3.9 KiB
TypeScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
};
}
}
}