Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 220 additions & 1 deletion src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,8 @@

this.eventHandler();

this.startLidCleanupScheduler();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider making the lid cleanup scheduler opt-in or configurable.

Some deployments may require tighter control over resource usage or side effects, so providing a configuration option to enable or disable the scheduler would improve flexibility.

Suggested implementation:

    this.eventHandler();

    if (this.config?.enableLidCleanupScheduler) {
      this.startLidCleanupScheduler();
    }

    this.client.ws.on('CB:call', (packet) => {
  1. Ensure that the class has access to a config object or property. If not, you will need to add it to the constructor or initialization logic.
  2. Document the new configuration option (enableLidCleanupScheduler) in your configuration schema or documentation.
  3. Update any relevant tests to cover both enabled and disabled scenarios.


this.client.ws.on('CB:call', (packet) => {
console.log('CB:call', packet);
const payload = { event: 'CB:call', packet: packet };
Expand Down Expand Up @@ -1049,8 +1051,10 @@
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (performance): Nested loop over keys may be redundant and impact performance.

The inner loop over 'keys' may cause unnecessary repeated work. Consider refactoring to avoid double iteration.

if (
received?.messageStubParameters?.some?.((param) =>
Expand Down Expand Up @@ -3991,6 +3995,221 @@
}
}

/**
* 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>('DATABASE');
const cache = this.configService.get<CacheConf>('CACHE');

Check failure on line 4008 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `······`
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;

Check failure on line 4045 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··············`
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}`;

Check failure on line 4065 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··········`
// 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);

Check failure on line 4072 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `············`
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;

Check failure on line 4087 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Replace `contacts` with `·contacts·`
if (contacts[lidJid]) {
contacts[realJid] = contacts[lidJid];
delete contacts[lidJid];

Check failure on line 4091 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··············`
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...');

Check failure on line 4113 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `······`
const db = this.configService.get<Database>('DATABASE');
const cache = this.configService.get<CacheConf>('CACHE');

Check failure on line 4116 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `······`
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 }

Check failure on line 4141 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Insert `,`
});
}
} 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`);

Check failure on line 4158 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··········`
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;

Expand Down
49 changes: 34 additions & 15 deletions src/api/routes/business.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<NumberDto>({
request: req,
schema: catalogSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCatalog(instance, data),
});
try {
const response = await this.dataValidate<NumberDto>({
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<NumberDto>({
request: req,
schema: collectionsSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCollections(instance, data),
});
try {
const response = await this.dataValidate<NumberDto>({
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();
}
}
49 changes: 34 additions & 15 deletions src/api/routes/template.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<TemplateDto>({
request: req,
schema: templateSchema,
ClassRef: TemplateDto,
execute: (instance, data) => templateController.createTemplate(instance, data),
});
try {
const response = await this.dataValidate<TemplateDto>({
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<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => templateController.findTemplate(instance),
});
try {
const response = await this.dataValidate<InstanceDto>({
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();
}
}
Loading