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