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.

208 lines
6.8 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 { db } from '../db';
// Environment variables will be mocked in tests
const env = process.env;
// In-memory cache for active groups
const activeGroupsCache = new Map<string, string>(); // groupId -> groupName
type EvolutionGroup = {
id: string;
subject: string;
linkedParent?: string;
// Other fields from API response
};
export class GroupSyncService {
/**
* Gets the sync interval duration in milliseconds.
*
* Priority:
* 1. GROUP_SYNC_INTERVAL_MS environment variable if set
* 2. Default 24 hour interval
*
* In development mode, enforces minimum 10 second interval
* to prevent accidental excessive API calls.
*
* @returns {number} Sync interval in milliseconds
*/
private static get SYNC_INTERVAL_MS(): number {
const interval = process.env.GROUP_SYNC_INTERVAL_MS
? Number(process.env.GROUP_SYNC_INTERVAL_MS)
: 24 * 60 * 60 * 1000; // Default 24 hours
// Ensure minimum 10 second interval in development
if (process.env.NODE_ENV === 'development' && interval < 10000) {
console.warn(`Sync interval too low (${interval}ms), using 10s minimum`);
return 10000;
}
return interval;
}
private static lastSyncAttempt = 0;
static async syncGroups(): Promise<{ added: number; updated: number }> {
if (!this.shouldSync()) {
return { added: 0, updated: 0 };
}
try {
const communityId = env.WHATSAPP_COMMUNITY_ID;
if (!communityId) {
throw new Error('WHATSAPP_COMMUNITY_ID is not set');
}
const groups = await this.fetchGroupsFromAPI();
const communityGroups = groups.filter(
(group) => group.linkedParent === communityId
);
return await this.upsertGroups(communityGroups);
} catch (error) {
console.error('Group sync failed:', error);
throw error;
} finally {
this.lastSyncAttempt = Date.now();
}
}
private static shouldSync(): boolean {
const timeSinceLastSync = Date.now() - this.lastSyncAttempt;
const shouldSync = timeSinceLastSync > this.SYNC_INTERVAL_MS;
if (!shouldSync && process.env.NODE_ENV !== 'test') {
const nextSyncIn = this.SYNC_INTERVAL_MS - timeSinceLastSync;
console.debug(`Next sync available in ${Math.round(nextSyncIn/1000)} seconds`);
}
return shouldSync;
}
private static async fetchGroupsFromAPI(): Promise<EvolutionGroup[]> {
const url = `${env.EVOLUTION_API_URL}/group/fetchAllGroups/${env.EVOLUTION_API_INSTANCE}?getParticipants=false`;
console.log(' Fetching groups from API:', {
url: `${url.substring(0, 50)}...`, // Log partial URL for security
communityId: env.WHATSAPP_COMMUNITY_ID,
time: new Date().toISOString()
});
try {
const response = await fetch(url, {
headers: {
apikey: env.EVOLUTION_API_KEY,
},
timeout: 120000 // 120 second timeout
});
if (!response.ok) {
const errorBody = await response.text().catch(() => 'Unable to read error body');
console.error('❌ API request failed:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: errorBody
});
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log(' API response:', {
status: data.status,
message: data.message,
responseLength: data.response?.length || 0
});
if (data.status !== 'success') {
throw new Error(`API error: ${data.message || 'Unknown error'}`);
}
return data.response;
} catch (error) {
console.error('❌ Failed to fetch groups:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
throw error;
}
}
private static cacheActiveGroups(): void {
const groups = db.prepare('SELECT id, name FROM groups WHERE active = TRUE').all();
activeGroupsCache.clear();
for (const group of groups) {
activeGroupsCache.set(group.id, group.name);
}
console.log(`Cached ${activeGroupsCache.size} active groups`);
}
private static getActiveGroupsCount(): number {
const result = db.prepare('SELECT COUNT(*) as count FROM groups WHERE active = TRUE').get();
return result?.count || 0;
}
static async checkInitialGroups(): Promise<void> {
const count = this.getActiveGroupsCount();
if (count > 0) {
this.cacheActiveGroups();
console.log(`✅ Using ${count} existing groups from database`);
return;
}
console.log('⚠️ No groups found in database - performing initial sync');
try {
const { added } = await this.syncGroups();
if (added === 0) {
throw new Error('Initial group sync completed but no groups were added');
}
this.cacheActiveGroups();
console.log(`✅ Initial group sync completed - added ${added} groups`);
} catch (error) {
console.error('❌ Critical: Initial group sync failed - no groups available');
console.error(error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
}
}
private static async upsertGroups(groups: EvolutionGroup[]): Promise<{ added: number; updated: number }> {
let added = 0;
let updated = 0;
const transactionResult = db.transaction(() => {
// First mark all groups as inactive and update verification timestamp
const inactiveResult = db.prepare(`
UPDATE groups
SET active = FALSE,
last_verified = CURRENT_TIMESTAMP
`).run();
console.log('Marked groups inactive:', inactiveResult);
for (const group of groups) {
const existing = db.prepare('SELECT 1 FROM groups WHERE id = ?').get(group.id);
console.log('Checking group:', group.id, 'exists:', !!existing);
if (existing) {
const updateResult = db.prepare(
'UPDATE groups SET name = ?, active = TRUE, last_verified = CURRENT_TIMESTAMP WHERE id = ?'
).run(group.subject, group.id);
console.log('Updated group:', group.id, 'result:', updateResult);
updated++;
} else {
const insertResult = db.prepare(
'INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, TRUE)'
).run(group.id, env.WHATSAPP_COMMUNITY_ID, group.subject);
console.log('Added group:', group.id, 'result:', insertResult);
added++;
}
}
return { added, updated };
});
try {
const result = transactionResult();
console.log(`Group sync completed: ${result.added} added, ${result.updated} updated`);
return result;
} catch (error) {
console.error('Error in upsertGroups:', error);
throw error;
}
}
}