|
|
|
|
@ -66,6 +66,8 @@ export class GroupSyncService {
|
|
|
|
|
return interval;
|
|
|
|
|
}
|
|
|
|
|
private static lastSyncAttempt = 0;
|
|
|
|
|
private static _membersTimer: any = null;
|
|
|
|
|
private static _membersSchedulerRunning = false;
|
|
|
|
|
|
|
|
|
|
static async syncGroups(): Promise<{ added: number; updated: number }> {
|
|
|
|
|
if (!this.shouldSync()) {
|
|
|
|
|
@ -340,6 +342,70 @@ export class GroupSyncService {
|
|
|
|
|
|
|
|
|
|
// Fetch members for a single group from Evolution API. Uses a robust parser to accept multiple payload shapes.
|
|
|
|
|
private static async fetchGroupMembersFromAPI(groupId: string): Promise<Array<{ userId: string; isAdmin: boolean }>> {
|
|
|
|
|
// Evitar llamadas de red en tests
|
|
|
|
|
if (process.env.NODE_ENV === 'test') return [];
|
|
|
|
|
|
|
|
|
|
// 1) Intento preferente: endpoint de Evolution "Find Group Members"
|
|
|
|
|
// Documentación provista: GET /group/participants/{instance}
|
|
|
|
|
// Suponemos soporte de query param groupJid
|
|
|
|
|
try {
|
|
|
|
|
const url1 = `${env.EVOLUTION_API_URL}/group/participants/${env.EVOLUTION_API_INSTANCE}?groupJid=${encodeURIComponent(groupId)}`;
|
|
|
|
|
console.log('ℹ️ Fetching members via /group/participants:', { groupId });
|
|
|
|
|
|
|
|
|
|
const r1 = await fetch(url1, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: { apikey: env.EVOLUTION_API_KEY },
|
|
|
|
|
httpVersion: '2',
|
|
|
|
|
timeout: 320000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (r1.ok) {
|
|
|
|
|
const raw1 = await r1.text();
|
|
|
|
|
let parsed1: any;
|
|
|
|
|
try {
|
|
|
|
|
parsed1 = JSON.parse(raw1);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('❌ Failed to parse /group/participants JSON:', String(e));
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const participantsArr = Array.isArray(parsed1?.participants) ? parsed1.participants : null;
|
|
|
|
|
if (participantsArr) {
|
|
|
|
|
const result: Array<{ userId: string; isAdmin: boolean }> = [];
|
|
|
|
|
for (const p of participantsArr) {
|
|
|
|
|
let jid: string | null = null;
|
|
|
|
|
let isAdmin = false;
|
|
|
|
|
|
|
|
|
|
if (typeof p === 'string') {
|
|
|
|
|
jid = p;
|
|
|
|
|
} else if (p && typeof p === 'object') {
|
|
|
|
|
jid = p.id || p.jid || p.user || p?.user?.id || null;
|
|
|
|
|
if (typeof p.isAdmin === 'boolean') {
|
|
|
|
|
isAdmin = p.isAdmin;
|
|
|
|
|
} else if (typeof p.admin === 'string') {
|
|
|
|
|
isAdmin = p.admin === 'admin' || p.admin === 'superadmin';
|
|
|
|
|
} else if (typeof p.role === 'string') {
|
|
|
|
|
isAdmin = p.role.toLowerCase().includes('admin');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const norm = normalizeWhatsAppId(jid);
|
|
|
|
|
if (!norm) continue;
|
|
|
|
|
result.push({ userId: norm, isAdmin });
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
// Si no viene en el formato esperado, caemos al plan B
|
|
|
|
|
console.warn('⚠️ /group/participants responded without participants array, falling back to fetchAllGroups');
|
|
|
|
|
} else {
|
|
|
|
|
const body = await r1.text().catch(() => '');
|
|
|
|
|
console.warn(`⚠️ /group/participants failed: ${r1.status} ${r1.statusText} - ${body.slice(0, 200)}. Falling back to fetchAllGroups`);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('⚠️ Error calling /group/participants, falling back to fetchAllGroups:', e instanceof Error ? e.message : String(e));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2) Fallback robusto: fetchAllGroups(getParticipants=true) y filtrar por groupId
|
|
|
|
|
const url = `${env.EVOLUTION_API_URL}/group/fetchAllGroups/${env.EVOLUTION_API_INSTANCE}?getParticipants=true`;
|
|
|
|
|
console.log('ℹ️ Fetching members via fetchAllGroups (participants=true):', { groupId });
|
|
|
|
|
|
|
|
|
|
@ -491,6 +557,9 @@ export class GroupSyncService {
|
|
|
|
|
* Devuelve contadores agregados.
|
|
|
|
|
*/
|
|
|
|
|
static async syncMembersForActiveGroups(): Promise<{ groups: number; added: number; updated: number; deactivated: number }> {
|
|
|
|
|
if (process.env.NODE_ENV === 'test') {
|
|
|
|
|
return { groups: 0, added: 0, updated: 0, deactivated: 0 };
|
|
|
|
|
}
|
|
|
|
|
// ensure cache is populated
|
|
|
|
|
if (this.activeGroupsCache.size === 0) {
|
|
|
|
|
this.cacheActiveGroups();
|
|
|
|
|
@ -511,4 +580,35 @@ export class GroupSyncService {
|
|
|
|
|
console.log('ℹ️ Members sync summary:', { groups, added, updated, deactivated });
|
|
|
|
|
return { groups, added, updated, deactivated };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static refreshActiveGroupsCache(): void {
|
|
|
|
|
this.cacheActiveGroups();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static startMembersScheduler(): void {
|
|
|
|
|
if (process.env.NODE_ENV === 'test') return;
|
|
|
|
|
if (this._membersSchedulerRunning) return;
|
|
|
|
|
this._membersSchedulerRunning = true;
|
|
|
|
|
|
|
|
|
|
// Intervalo por defecto 6h; configurable por env; mínimo 10s en desarrollo
|
|
|
|
|
const raw = process.env.GROUP_MEMBERS_SYNC_INTERVAL_MS;
|
|
|
|
|
let interval = Number.isFinite(Number(raw)) && Number(raw) > 0 ? Number(raw) : 6 * 60 * 60 * 1000;
|
|
|
|
|
if (process.env.NODE_ENV === 'development' && interval < 10000) {
|
|
|
|
|
interval = 10000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._membersTimer = setInterval(() => {
|
|
|
|
|
this.syncMembersForActiveGroups().catch(err => {
|
|
|
|
|
console.error('❌ Members scheduler run error:', err);
|
|
|
|
|
});
|
|
|
|
|
}, interval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static stopMembersScheduler(): void {
|
|
|
|
|
this._membersSchedulerRunning = false;
|
|
|
|
|
if (this._membersTimer) {
|
|
|
|
|
clearInterval(this._membersTimer);
|
|
|
|
|
this._membersTimer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|