import type { Database } from 'bun:sqlite'; import { db } from '../db'; import { AllowedGroups } from './allowed-groups'; import { GroupSyncService } from './group-sync'; import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp'; import { Metrics } from './metrics'; import { TaskService } from '../tasks/service'; import { codeId, formatDDMM } from '../utils/formatting'; type AdminContext = { sender: string; // normalized user id (digits only) groupId: string; // raw JID (group or DM) message: string; // raw message text }; type AdminResponse = { recipient: string; message: string }; export class AdminService { static dbInstance: Database = db; private static admins(): Set { const raw = String(process.env.ADMIN_USERS || ''); const set = new Set(); for (const token of raw.split(',').map(s => s.trim()).filter(Boolean)) { const n = normalizeWhatsAppId(token); if (n) set.add(n); } return set; } static getAdmins(): string[] { return Array.from(this.admins()); } private static isAdmin(userId: string | null | undefined): boolean { const n = normalizeWhatsAppId(userId || ''); if (!n) return false; return this.admins().has(n); } private static help(): string { return [ 'Comandos de administración:', '- /admin pendientes (alias: pending, pend)', '- /admin habilitar-aquí (alias: enable)', '- /admin deshabilitar-aquí (alias: disable)', '- /admin allow all (alias: habilitar-todos, enable all)', '- /admin allow-group (alias: allow)', '- /admin block-group (alias: block)', '- /admin sync-grupos (alias: group-sync, syncgroups)', '- /admin ver todos (alias: listar, list all)', ].join('\n'); } static async handle(ctx: AdminContext): Promise { const sender = normalizeWhatsAppId(ctx.sender); if (!sender) return []; if (!this.isAdmin(sender)) { return [{ recipient: sender, message: '🚫 No estás autorizado para usar /admin.' }]; } // Asegurar acceso a la misma DB para AllowedGroups try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {} const raw = String(ctx.message || '').trim(); const lower = raw.toLowerCase(); if (!lower.startsWith('/admin')) { return []; } const rest = lower.slice('/admin'.length).trim(); // /admin pendientes if (rest === 'pendientes' || rest === 'pending' || rest === 'pend') { const rows = AllowedGroups.listByStatus('pending'); if (!rows || rows.length === 0) { return [{ recipient: sender, message: '✅ No hay grupos pendientes.' }]; } const list = rows.map(r => `- ${r.group_id}${r.label ? ` (${r.label})` : ''}`).join('\n'); return [{ recipient: sender, message: `Grupos pendientes (${rows.length}):\n${list}` }]; } // /admin habilitar-aquí if (rest === 'habilitar-aquí' || rest === 'habilitar-aqui' || rest === 'enable') { if (!isGroupId(ctx.groupId)) { return [{ recipient: sender, message: 'ℹ️ Este comando se debe usar dentro de un grupo.' }]; } const changed = AllowedGroups.setStatus(ctx.groupId, 'allowed'); try { if (changed) Metrics.inc('admin_actions_total_allow'); } catch {} return [{ recipient: sender, message: `✅ Grupo habilitado: ${ctx.groupId}` }]; } // /admin deshabilitar-aquí if (rest === 'deshabilitar-aquí' || rest === 'deshabilitar-aqui' || rest === 'disable') { if (!isGroupId(ctx.groupId)) { return [{ recipient: sender, message: 'ℹ️ Este comando se debe usar dentro de un grupo.' }]; } const changed = AllowedGroups.setStatus(ctx.groupId, 'blocked'); try { if (changed) Metrics.inc('admin_actions_total_block'); } catch {} return [{ recipient: sender, message: `✅ Grupo deshabilitado: ${ctx.groupId}` }]; } // /admin allow all if ( rest === 'allow all' || rest === 'allow-all' || rest === 'habilitar-todos' || rest === 'permitir todos' || rest === 'enable all' ) { const pendings = AllowedGroups.listByStatus('pending'); if (!pendings || pendings.length === 0) { return [{ recipient: sender, message: '✅ No hay grupos pendientes.' }]; } let changed = 0; for (const r of pendings) { const didChange = AllowedGroups.setStatus(r.group_id, 'allowed', r.label ?? null); if (didChange) changed++; try { Metrics.inc('admin_actions_total_allow'); } catch {} } return [{ recipient: sender, message: `✅ Grupos habilitados: ${changed}` }]; } // /admin allow-group if (rest.startsWith('allow-group ') || (rest.startsWith('allow ') && rest !== 'allow all' && rest !== 'allow-all')) { const arg = (rest.startsWith('allow-group ') ? rest.slice('allow-group '.length) : rest.slice('allow '.length)).trim(); if (!isGroupId(arg)) { return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; } const changed = AllowedGroups.setStatus(arg, 'allowed'); try { if (changed) Metrics.inc('admin_actions_total_allow'); } catch {} return [{ recipient: sender, message: `✅ Grupo habilitado: ${arg}` }]; } // /admin block-group if (rest.startsWith('block-group ') || rest.startsWith('block ')) { const arg = (rest.startsWith('block-group ') ? rest.slice('block-group '.length) : rest.slice('block '.length)).trim(); if (!isGroupId(arg)) { return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; } const changed = AllowedGroups.setStatus(arg, 'blocked'); try { if (changed) Metrics.inc('admin_actions_total_block'); } catch {} return [{ recipient: sender, message: `✅ Grupo bloqueado: ${arg}` }]; } // /admin sync-grupos if (rest === 'sync-grupos' || rest === 'group-sync' || rest === 'syncgroups') { try { (GroupSyncService as any).dbInstance = this.dbInstance; } catch {} try { const r = await GroupSyncService.syncGroups(true); return [{ recipient: sender, message: `✅ Sync de grupos ejecutado: ${r.added} añadidos, ${r.updated} actualizados.` }]; } catch (e) { const msg = e instanceof Error ? e.message : String(e); return [{ recipient: sender, message: `❌ Error al ejecutar sync de grupos: ${msg}` }]; } } // /admin ver todos [] if ( rest === 'ver todos' || rest.startsWith('ver todos ') || rest === 'listar' || rest.startsWith('listar ') || rest === 'list all' || rest.startsWith('list all ') || rest === 'list-all' || rest.startsWith('list-all ') ) { // Asegurar acceso a la misma DB para TaskService try { (TaskService as any).dbInstance = this.dbInstance; } catch {} const DEFAULT_LIMIT = 50; let limit = DEFAULT_LIMIT; const maybeNum = parseInt(rest.split(/\s+/).pop() || '', 10); if (Number.isFinite(maybeNum) && maybeNum > 0) { limit = Math.min(maybeNum, 500); // tope razonable } const tasks = TaskService.listAllActive(limit); const total = TaskService.countAllActive(); if (!tasks || tasks.length === 0) { return [{ recipient: sender, message: '✅ No hay tareas activas.' }]; } const lines = tasks.map(t => { const ddmm = formatDDMM(t.due_date); const groupLabel = t.group_name || t.group_id || 'DM'; const parts: string[] = [ `${codeId(t.id, t.display_code)}`, String(t.description || '').trim() ]; if (ddmm) parts.push(`vence ${ddmm}`); if (groupLabel) parts.push(`[${groupLabel}]`); return `- ${parts.join(' · ')}`; }); const header = total > limit ? `Tus tareas — Tareas activas (${total}) — mostrando ${tasks.length} primeras:` : `Tus tareas — Tareas activas (${total}):`; const footer = `ℹ️ Para ver tareas sin responsable de un grupo, pide el listado desde ese grupo.`; try { Metrics.inc('admin_actions_total_list'); } catch {} return [{ recipient: sender, message: `${header}\n${lines.join('\n')}\n\n${footer}` }]; } // Ayuda por defecto return [{ recipient: sender, message: this.help() }]; } }