|  |  | 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<string> {
 | 
						
						
						
							|  |  |     const raw = String(process.env.ADMIN_USERS || '');
 | 
						
						
						
							|  |  |     const set = new Set<string>();
 | 
						
						
						
							|  |  |     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 <group_id@g.us> (alias: allow)',
 | 
						
						
						
							|  |  |       '- /admin block-group <group_id@g.us> (alias: block)',
 | 
						
						
						
							|  |  |       '- /admin sync-grupos (alias: group-sync, syncgroups)',
 | 
						
						
						
							|  |  |       '- /admin ver todos (alias: listar, list all)',
 | 
						
						
						
							|  |  |     ].join('\n');
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   static async handle(ctx: AdminContext): Promise<AdminResponse[]> {
 | 
						
						
						
							|  |  |     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 <jid>
 | 
						
						
						
							|  |  |     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 <jid>
 | 
						
						
						
							|  |  |     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 [<limite>]
 | 
						
						
						
							|  |  |     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() }];
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | }
 |