From c51cb3f124adb2a49c3f68b382197e9a82ba9cb5 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 29 Sep 2025 11:48:44 +0200 Subject: [PATCH] feat: notifica a ADMIN_USERS al descubrir grupos (modo discover) Co-authored-by: aider (openrouter/openai/gpt-5) --- src/server.ts | 22 +++++ src/services/admin.ts | 4 + .../server/discovery-notify-admins.test.ts | 91 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/unit/server/discovery-notify-admins.test.ts diff --git a/src/server.ts b/src/server.ts index e1357d5..e0f4c40 100644 --- a/src/server.ts +++ b/src/server.ts @@ -315,12 +315,34 @@ export class WebhookServer { if (!exists) { try { AllowedGroups.upsertPending(remoteJid, null, normalizedSenderId); } catch {} try { Metrics.inc('unknown_groups_discovered_total'); } catch {} + try { + const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true'; + if (notify && !isAdminCmd) { + const admins = AdminService.getAdmins(); + if (admins.length > 0) { + const info = remoteJid; + const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`; + await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg }))); + } + } + } catch {} if (!isAdminCmd) return; } } catch { // Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente try { AllowedGroups.upsertPending(remoteJid, null, normalizedSenderId); } catch {} try { Metrics.inc('unknown_groups_discovered_total'); } catch {} + try { + const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true'; + if (notify && !isAdminCmd) { + const admins = AdminService.getAdmins(); + if (admins.length > 0) { + const info = remoteJid; + const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`; + await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg }))); + } + } + } catch {} if (!isAdminCmd) return; } } diff --git a/src/services/admin.ts b/src/services/admin.ts index 3a26e57..69584b4 100644 --- a/src/services/admin.ts +++ b/src/services/admin.ts @@ -24,6 +24,10 @@ export class AdminService { 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; diff --git a/tests/unit/server/discovery-notify-admins.test.ts b/tests/unit/server/discovery-notify-admins.test.ts new file mode 100644 index 0000000..bbba8df --- /dev/null +++ b/tests/unit/server/discovery-notify-admins.test.ts @@ -0,0 +1,91 @@ +import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { WebhookServer } from '../../../src/server'; +import { initializeDatabase } from '../../../src/db'; +import { ResponseQueue } from '../../../src/services/response-queue'; + +let testDb: Database; +let originalAdd: any; + +let simulatedQueue: any[] = []; +const SimulatedResponseQueue = { + async add(responses: any[]) { + simulatedQueue.push(...responses); + }, + clear() { simulatedQueue = []; }, + get() { return simulatedQueue; } +}; + +const createTestRequest = (payload: any) => + new Request('http://localhost:3007', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + +describe('WebhookServer - notifica a ADMIN_USERS en descubrimiento (modo discover)', () => { + const envBackup = process.env; + + beforeAll(() => { + testDb = new Database(':memory:'); + initializeDatabase(testDb); + originalAdd = (ResponseQueue as any).add; + }); + + afterAll(() => { + (ResponseQueue as any).add = originalAdd; + testDb.close(); + }); + + beforeEach(() => { + process.env = { + ...envBackup, + NODE_ENV: 'test', + GROUP_GATING_MODE: 'discover', + NOTIFY_ADMINS_ON_DISCOVERY: 'true', + ADMIN_USERS: '1234567890, 5555555555' + }; + SimulatedResponseQueue.clear(); + (ResponseQueue as any).add = SimulatedResponseQueue.add; + WebhookServer.dbInstance = testDb; + + // Limpiar tablas relevantes + testDb.exec('DELETE FROM response_queue'); + testDb.exec('DELETE FROM allowed_groups'); + testDb.exec('DELETE FROM users'); + }); + + afterEach(() => { + process.env = envBackup; + }); + + test('registra pending y envía DMs a todos los admins', async () => { + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'notify-group@g.us', + participant: '9999999999@s.whatsapp.net' + }, + message: { conversation: '/t n hola' } + } + }; + + const res = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(res.status).toBe(200); + + // allowed_groups debe tener el grupo en pending + const row = testDb + .query(`SELECT status FROM allowed_groups WHERE group_id = 'notify-group@g.us'`) + .get() as any; + expect(row).toBeDefined(); + expect(String(row.status)).toBe('pending'); + + // Debe haberse encolado una notificación por cada admin + const out = SimulatedResponseQueue.get(); + const recipients = out.map((r: any) => r.recipient).sort(); + expect(out.length).toBe(2); + expect(recipients).toEqual(['1234567890', '5555555555']); + }); +});