From 46d172fc1213769c6f35b7d5a8a033856bc841ad Mon Sep 17 00:00:00 2001 From: brobert Date: Mon, 29 Sep 2025 23:40:33 +0200 Subject: [PATCH] feat: permitir forzar sync de grupos y completar labels faltantes Co-authored-by: aider (openrouter/openai/gpt-5) --- src/services/admin.ts | 14 ++++++++++ src/services/group-sync.ts | 53 +++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/services/admin.ts b/src/services/admin.ts index cffd537..1809e3f 100644 --- a/src/services/admin.ts +++ b/src/services/admin.ts @@ -1,6 +1,7 @@ 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'; @@ -43,6 +44,7 @@ export class AdminService { '- /admin deshabilitar-aquí (alias: disable)', '- /admin allow-group (alias: allow)', '- /admin block-group (alias: block)', + '- /admin sync-grupos (alias: group-sync, syncgroups)', ].join('\n'); } @@ -120,6 +122,18 @@ export class AdminService { 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}` }]; + } + } + // Ayuda por defecto return [{ recipient: sender, message: this.help() }]; } diff --git a/src/services/group-sync.ts b/src/services/group-sync.ts index b90383d..327b9f7 100644 --- a/src/services/group-sync.ts +++ b/src/services/group-sync.ts @@ -72,8 +72,8 @@ export class GroupSyncService { private static _membersTimer: any = null; private static _membersSchedulerRunning = false; - static async syncGroups(): Promise<{ added: number; updated: number }> { - if (!this.shouldSync()) { + static async syncGroups(force: boolean = false): Promise<{ added: number; updated: number }> { + if (!this.shouldSync(force)) { return { added: 0, updated: 0 }; } const startedAt = Date.now(); @@ -85,6 +85,8 @@ export class GroupSyncService { console.log('ℹ️ WHATSAPP_COMMUNITY_ID no definido - mostrando todas las comunidades'); const groups = await this.fetchGroupsFromAPI(); const communities = groups.filter(g => g.linkedParent); + // Intento best-effort de rellenar labels faltantes en allowed_groups usando la lista completa + try { (AllowedGroups as any).dbInstance = this.dbInstance; this.fillMissingAllowedGroupLabels(groups); } catch {} return { added: 0, updated: 0 }; // No sync when just listing } @@ -115,6 +117,9 @@ export class GroupSyncService { const dbGroupsAfter = this.dbInstance.prepare('SELECT id, active FROM groups').all(); console.log('ℹ️ Grupos en DB después de upsert:', dbGroupsAfter); + // Completar labels faltantes en allowed_groups usando todos los grupos devueltos por la API + try { (AllowedGroups as any).dbInstance = this.dbInstance; this.fillMissingAllowedGroupLabels(groups); } catch {} + // Actualizar métricas this.cacheActiveGroups(); Metrics.set('active_groups', this.activeGroupsCache.size); @@ -136,7 +141,8 @@ export class GroupSyncService { } } - private static shouldSync(): boolean { + private static shouldSync(force: boolean = false): boolean { + if (force) return true; const timeSinceLastSync = Date.now() - this.lastSyncAttempt; const shouldSync = timeSinceLastSync > this.SYNC_INTERVAL_MS; @@ -230,6 +236,47 @@ export class GroupSyncService { console.log(`Cached ${this.activeGroupsCache.size} active groups`); } + // Rellena labels faltantes en allowed_groups a partir de los grupos devueltos por la API. + private static fillMissingAllowedGroupLabels(allGroups: EvolutionGroup[]): number { + try { + if (!Array.isArray(allGroups) || allGroups.length === 0) return 0; + const nameById = new Map(); + for (const g of allGroups) { + if (!g?.id) continue; + const name = String(g.subject || '').trim(); + if (!name) continue; + nameById.set(String(g.id), name); + } + if (nameById.size === 0) return 0; + + const rows = this.dbInstance.prepare(` + SELECT group_id AS id + FROM allowed_groups + WHERE label IS NULL OR TRIM(label) = '' + `).all() as any[]; + if (!rows || rows.length === 0) return 0; + + let filled = 0; + for (const r of rows) { + const id = r?.id ? String(r.id) : null; + if (!id) continue; + const label = nameById.get(id); + if (label) { + try { (AllowedGroups as any).dbInstance = this.dbInstance; AllowedGroups.upsertPending(id, label, null); } catch {} + filled++; + } + } + if (filled > 0) { + try { Metrics.inc('allowed_groups_labels_filled_total', filled); } catch {} + console.log(`ℹ️ Rellenadas ${filled} labels faltantes en allowed_groups`); + } + return filled; + } catch (e) { + console.warn('⚠️ No se pudieron rellenar labels faltantes en allowed_groups:', e); + return 0; + } + } + private static getActiveGroupsCount(): number { const result = this.dbInstance.prepare('SELECT COUNT(*) as count FROM groups WHERE active = TRUE').get(); return result?.count || 0;