diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index c70ab6f6e..79d86a411 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -670,6 +670,8 @@ export class BaileysStartupService extends ChannelStartupService { this.eventHandler(); + this.startLidCleanupScheduler(); + this.client.ws.on('CB:call', (packet) => { console.log('CB:call', packet); const payload = { event: 'CB:call', packet: packet }; @@ -1049,8 +1051,10 @@ export class BaileysStartupService extends ChannelStartupService { try { for (const received of messages) { if (received.key.remoteJid?.includes('@lid') && received.key.senderPn) { - (received.key as { previousRemoteJid?: string | null }).previousRemoteJid = received.key.remoteJid; + this.logger.verbose(`Processing @lid message: ${received.key.remoteJid} -> ${received.key.senderPn}`); + const previousRemoteJid = received.key.remoteJid; received.key.remoteJid = received.key.senderPn; + await this.updateContactFromLid(previousRemoteJid, received.key.remoteJid); } if ( received?.messageStubParameters?.some?.((param) => @@ -3991,6 +3995,221 @@ export class BaileysStartupService extends ChannelStartupService { } } + /** + * Atualiza contatos que foram criados com @lid para o JID real + * Isso resolve problemas de mensagens não chegando no iPhone + * Funciona com ou sem banco de dados + */ + private async updateContactFromLid(lidJid: string, realJid: string) { + try { + // Verificar se o banco de dados está habilitado + const db = this.configService.get('DATABASE'); + const cache = this.configService.get('CACHE'); + + if (db.SAVE_DATA.CONTACTS) { + // Com banco de dados - usar Prisma + try { + // Buscar contato com @lid + const lidContact = await this.prismaRepository.contact.findFirst({ + where: { + remoteJid: lidJid, + instanceId: this.instanceId, + }, + }); + + if (lidContact) { + // Atualizar para o JID real + await this.prismaRepository.contact.update({ + where: { id: lidContact.id }, + data: { remoteJid: realJid }, + }); + + this.logger.verbose(`Updated contact from @lid: ${lidJid} -> ${realJid}`); + } + + // Também atualizar mensagens com @lid + const lidMessages = await this.prismaRepository.message.findMany({ + where: { + instanceId: this.instanceId, + key: { + path: ['remoteJid'], + equals: lidJid, + }, + }, + }); + + if (lidMessages.length > 0) { + for (const message of lidMessages) { + const key = message.key as any; + key.remoteJid = realJid; + + await this.prismaRepository.message.update({ + where: { id: message.id }, + data: { key: key }, + }); + } + + this.logger.verbose(`Updated ${lidMessages.length} messages from @lid: ${lidJid} -> ${realJid}`); + } + } catch (dbError) { + this.logger.warn(`Database operation failed, falling back to cache: ${dbError.message}`); + } + } + + // Sem banco de dados - usar cache e arquivos locais + if (cache?.REDIS?.ENABLED) { + // Atualizar no cache Redis + try { + const cacheKey = `contact:${this.instanceId}:${lidJid}`; + const realContactKey = `contact:${this.instanceId}:${realJid}`; + + // Buscar dados do contato @lid no cache + const lidContactData = await this.cache.hGet(this.instanceId, cacheKey); + if (lidContactData) { + // Atualizar para o JID real no cache + await this.cache.hSet(this.instanceId, realContactKey, lidContactData); + await this.cache.hDelete(this.instanceId, cacheKey); + + this.logger.verbose(`Updated Redis cache contact from @lid: ${lidJid} -> ${realJid}`); + } + } catch (cacheError) { + this.logger.warn(`Redis cache operation failed: ${cacheError.message}`); + } + } + + // Atualizar arquivos locais se necessário + if (this.instance.authState) { + try { + // Atualizar o estado de autenticação local + const authState = this.instance.authState as any; + if (authState.store && authState.store.contacts) { + // Atualizar contatos no store local + const {contacts} = authState.store; + if (contacts[lidJid]) { + contacts[realJid] = contacts[lidJid]; + delete contacts[lidJid]; + + this.logger.verbose(`Updated local auth state contact from @lid: ${lidJid} -> ${realJid}`); + } + } + } catch (localError) { + this.logger.warn(`Local auth state update failed: ${localError.message}`); + } + } + + this.logger.info(`Successfully processed @lid update: ${lidJid} -> ${realJid}`); + } catch (error) { + this.logger.error(`Error updating contact from @lid: ${lidJid}`); + } + } + + /** + * Limpa contatos @lid órfãos e faz manutenção periódica + * Executa automaticamente para resolver problemas de mensagens não chegando + */ + private async cleanupOrphanedLidContacts() { + try { + this.logger.verbose('Starting cleanup of orphaned @lid contacts...'); + + const db = this.configService.get('DATABASE'); + const cache = this.configService.get('CACHE'); + + if (db.SAVE_DATA.CONTACTS) { + // Com banco: buscar todos os contatos @lid + try { + const lidContacts = await this.prismaRepository.contact.findMany({ + where: { + remoteJid: { contains: '@lid' }, + instanceId: this.instanceId, + }, + }); + + this.logger.verbose(`Found ${lidContacts.length} @lid contacts to cleanup`); + + for (const contact of lidContacts) { + // Tentar resolver o JID real através do WhatsApp + try { + // Usar o cliente WhatsApp para verificar se o contato existe + const contactInfo = await this.client.onWhatsApp(contact.remoteJid); + if (contactInfo && contactInfo.length > 0 && contactInfo[0].jid && !contactInfo[0].jid.includes('@lid')) { + // Contato foi resolvido, atualizar + await this.updateContactFromLid(contact.remoteJid, contactInfo[0].jid); + } else { + // Contato não pode ser resolvido, remover + this.logger.warn(`Removing orphaned @lid contact: ${contact.remoteJid}`); + await this.prismaRepository.contact.delete({ + where: { id: contact.id } + }); + } + } catch (contactError) { + this.logger.warn(`Could not resolve contact ${contact.remoteJid}: ${contactError.message}`); + } + } + } catch (dbError) { + this.logger.warn(`Database cleanup failed: ${dbError.message}`); + } + } + + // Limpeza de cache Redis + if (cache?.REDIS?.ENABLED) { + try { + const keys = await this.cache.keys('*@lid*'); + this.logger.verbose(`Found ${keys.length} @lid keys in Redis cache`); + + for (const key of keys) { + // Tentar resolver e atualizar + const contactData = await this.cache.hGet(this.instanceId, key); + if (contactData) { + this.logger.verbose(`Processing Redis cache key: ${key}`); + for (const key of keys) { + const contactData = await this.cache.hGet(this.instanceId, key); + if (contactData) { + try { + // Extrai o JID @lid da chave do cache + const lidJid = key.split(':').pop(); + // Usa o Baileys para tentar resolver o JID real + const contactInfo = await this.client.onWhatsApp(lidJid); + if (contactInfo && contactInfo.length > 0 && contactInfo[0].jid && !contactInfo[0].jid.includes('@lid')) { + // Atualiza o cache para o JID real + const realContactKey = `contact:${this.instanceId}:${contactInfo[0].jid}`; + await this.cache.hSet(this.instanceId, realContactKey, contactData); + await this.cache.hDelete(this.instanceId, key); + this.logger.verbose(`Updated Redis cache contact from @lid: ${key} -> ${realContactKey}`); + } + } catch (resolveError) { + this.logger.warn(`Could not resolve contact in cache: ${key} - ${resolveError.message}`); + } + } + } + } + } + } catch (cacheError) { + this.logger.warn(`Redis cleanup failed: ${cacheError.message}`); + } + } + + this.logger.info('Completed cleanup of orphaned @lid contacts'); + } catch (error) { + this.logger.error(`Error during @lid cleanup`); + } + } + + /** + * Inicia o processo de limpeza periódica de @lid + * Executa a cada 5 minutos para manter o sistema limpo + */ + private startLidCleanupScheduler() { + // Limpeza inicial + setTimeout(() => this.cleanupOrphanedLidContacts(), 30000); // 30 segundos após inicialização + + // Limpeza periódica a cada 5 minutos + setInterval(() => { + this.cleanupOrphanedLidContacts(); + }, 5 * 60 * 1000); // 5 minutos + + this.logger.info('Started periodic @lid cleanup scheduler (every 5 minutes)'); + } + private getGroupMetadataCache = async (groupJid: string) => { if (!isJidGroup(groupJid)) return null; diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index 1e510a4ff..8a56bba4d 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -3,6 +3,7 @@ import { NumberDto } from '@api/dto/chat.dto'; import { businessController } from '@api/server.module'; import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; import { RequestHandler, Router } from 'express'; +import { createMetaErrorResponse } from '@utils/errorResponse'; import { HttpStatus } from './index.router'; @@ -11,27 +12,45 @@ export class BusinessRouter extends RouterBroker { super(); this.router .post(this.routerPath('getCatalog'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: catalogSchema, - ClassRef: NumberDto, - execute: (instance, data) => businessController.fetchCatalog(instance, data), - }); + try { + const response = await this.dataValidate({ + request: req, + schema: catalogSchema, + ClassRef: NumberDto, + execute: (instance, data) => businessController.fetchCatalog(instance, data), + }); - return res.status(HttpStatus.OK).json(response); + return res.status(HttpStatus.OK).json(response); + } catch (error) { + // Log error for debugging + console.error('Business catalog error:', error); + + // Use utility function to create standardized error response + const errorResponse = createMetaErrorResponse(error, 'business_catalog'); + return res.status(errorResponse.status).json(errorResponse); + } }) .post(this.routerPath('getCollections'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: collectionsSchema, - ClassRef: NumberDto, - execute: (instance, data) => businessController.fetchCollections(instance, data), - }); + try { + const response = await this.dataValidate({ + request: req, + schema: collectionsSchema, + ClassRef: NumberDto, + execute: (instance, data) => businessController.fetchCollections(instance, data), + }); - return res.status(HttpStatus.OK).json(response); + return res.status(HttpStatus.OK).json(response); + } catch (error) { + // Log error for debugging + console.error('Business collections error:', error); + + // Use utility function to create standardized error response + const errorResponse = createMetaErrorResponse(error, 'business_collections'); + return res.status(errorResponse.status).json(errorResponse); + } }); } public readonly router: Router = Router(); -} +} \ No newline at end of file diff --git a/src/api/routes/template.router.ts b/src/api/routes/template.router.ts index b77b7d834..9a956e949 100644 --- a/src/api/routes/template.router.ts +++ b/src/api/routes/template.router.ts @@ -5,6 +5,7 @@ import { templateController } from '@api/server.module'; import { ConfigService } from '@config/env.config'; import { instanceSchema, templateSchema } from '@validate/validate.schema'; import { RequestHandler, Router } from 'express'; +import { createMetaErrorResponse } from '@utils/errorResponse'; import { HttpStatus } from './index.router'; @@ -16,26 +17,44 @@ export class TemplateRouter extends RouterBroker { super(); this.router .post(this.routerPath('create'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: templateSchema, - ClassRef: TemplateDto, - execute: (instance, data) => templateController.createTemplate(instance, data), - }); + try { + const response = await this.dataValidate({ + request: req, + schema: templateSchema, + ClassRef: TemplateDto, + execute: (instance, data) => templateController.createTemplate(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); + res.status(HttpStatus.CREATED).json(response); + } catch (error) { + // Log error for debugging + console.error('Template creation error:', error); + + // Use utility function to create standardized error response + const errorResponse = createMetaErrorResponse(error, 'template_creation'); + res.status(errorResponse.status).json(errorResponse); + } }) .get(this.routerPath('find'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: instanceSchema, - ClassRef: InstanceDto, - execute: (instance) => templateController.findTemplate(instance), - }); + try { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => templateController.findTemplate(instance), + }); - res.status(HttpStatus.OK).json(response); + res.status(HttpStatus.OK).json(response); + } catch (error) { + // Log error for debugging + console.error('Template find error:', error); + + // Use utility function to create standardized error response + const errorResponse = createMetaErrorResponse(error, 'template_find'); + res.status(errorResponse.status).json(errorResponse); + } }); } public readonly router: Router = Router(); -} +} \ No newline at end of file diff --git a/src/api/services/template.service.ts b/src/api/services/template.service.ts index 949f71c78..8cbdc486b 100644 --- a/src/api/services/template.service.ts +++ b/src/api/services/template.service.ts @@ -60,6 +60,13 @@ export class TemplateService { const response = await this.requestTemplate(postData, 'POST'); if (!response || response.error) { + // If there's an error from WhatsApp API, throw it with the real error data + if (response && response.error) { + // Create an error object that includes the template field for Meta errors + const metaError = new Error(response.error.message || 'WhatsApp API Error'); + (metaError as any).template = response.error; + throw metaError; + } throw new Error('Error to create template'); } @@ -75,8 +82,9 @@ export class TemplateService { return template; } catch (error) { - this.logger.error(error); - throw new Error('Error to create template'); + this.logger.error('Error in create template: ' + error); + // Propagate the real error instead of "engolindo" it + throw error; } } @@ -86,6 +94,7 @@ export class TemplateService { const version = this.configService.get('WA_BUSINESS').VERSION; urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; + if (method === 'GET') { const result = await axios.get(urlServer, { headers }); return result.data; @@ -94,8 +103,15 @@ export class TemplateService { return result.data; } } catch (e) { - this.logger.error(e.response.data); - return e.response.data.error; + this.logger.error('WhatsApp API request error: ' + (e.response?.data || e.message)); + + // Return the complete error response from WhatsApp API + if (e.response?.data) { + return e.response.data; + } + + // If no response data, throw connection error + throw new Error(`Connection error: ${e.message}`); } } -} +} \ No newline at end of file diff --git a/src/utils/errorResponse.ts b/src/utils/errorResponse.ts new file mode 100644 index 000000000..66b61e40b --- /dev/null +++ b/src/utils/errorResponse.ts @@ -0,0 +1,47 @@ +import { HttpStatus } from '@api/routes/index.router'; + +export interface MetaErrorResponse { + status: number; + error: string; + message: string; + details: { + whatsapp_error: string; + whatsapp_code: string | number; + error_user_title: string; + error_user_msg: string; + error_type: string; + error_subcode: number | null; + fbtrace_id: string | null; + context: string; + type: string; + }; + timestamp: string; +} + +/** + * Creates standardized error response for Meta/WhatsApp API errors + */ +export function createMetaErrorResponse(error: any, context: string): MetaErrorResponse { + // Extract Meta/WhatsApp specific error fields + const metaError = error.template || error; + const errorUserTitle = metaError.error_user_title || metaError.message || 'Unknown error'; + const errorUserMsg = metaError.error_user_msg || metaError.message || 'Unknown error'; + + return { + status: HttpStatus.BAD_REQUEST, + error: 'Bad Request', + message: errorUserTitle, + details: { + whatsapp_error: errorUserMsg, + whatsapp_code: metaError.code || 'UNKNOWN_ERROR', + error_user_title: errorUserTitle, + error_user_msg: errorUserMsg, + error_type: metaError.type || 'UNKNOWN', + error_subcode: metaError.error_subcode || null, + fbtrace_id: metaError.fbtrace_id || null, + context, + type: 'whatsapp_api_error' + }, + timestamp: new Date().toISOString() + }; +} \ No newline at end of file