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.

218 lines
8.3 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 } 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() }];
}
}