Skip to content
Closed
Changes from 2 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
313 changes: 274 additions & 39 deletions 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
Copy Markdown
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
Copy Markdown
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 @@ -1446,16 +1450,7 @@
}
}

const findMessage = await this.prismaRepository.message.findFirst({
where: { instanceId: this.instanceId, key: { path: ['id'], equals: key.id } },
});

if (!findMessage) {
continue;
}

const message: any = {
messageId: findMessage.id,
keyId: key.id,
remoteJid: key?.remoteJid,
fromMe: key.fromMe,
Expand All @@ -1465,6 +1460,16 @@
instanceId: this.instanceId,
};

let findMessage: any;
const configDatabaseData = this.configService.get<Database>('DATABASE').SAVE_DATA;
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
findMessage = await this.prismaRepository.message.findFirst({
where: { instanceId: this.instanceId, key: { path: ['id'], equals: key.id } },
});

if (findMessage) message.messageId = findMessage.id;
}

if (update.message === null && update.status === undefined) {
this.sendDataWebhook(Events.MESSAGES_DELETE, key);

Expand All @@ -1480,7 +1485,9 @@
}

continue;
} else if (update.status !== undefined && status[update.status] !== findMessage.status) {
}

if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) {
if (!key.fromMe && key.remoteJid) {
readChatToUpdate[key.remoteJid] = true;

Expand Down Expand Up @@ -3438,17 +3445,20 @@
where: { id: message.id },
data: { key: { ...existingKey, deleted: true }, status: 'DELETED' },
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: response.key.remoteJid,
fromMe: response.key.fromMe,
participant: response.key?.remoteJid,
status: 'DELETED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({ data: messageUpdate });
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: response.key.remoteJid,
fromMe: response.key.fromMe,
participant: response.key?.remoteJid,
status: 'DELETED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({ data: messageUpdate });
}
} else {
if (!message) return response;
await this.prismaRepository.message.deleteMany({ where: { id: message.id } });
}
this.sendDataWebhook(Events.MESSAGES_DELETE, {
Expand Down Expand Up @@ -3780,6 +3790,10 @@

private async formatUpdateMessage(data: UpdateMessageDto) {
try {
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
return data;
}

const msg: any = await this.getMessage(data.key, true);

if (msg?.messageType === 'conversation' || msg?.messageType === 'extendedTextMessage') {
Expand Down Expand Up @@ -3813,13 +3827,15 @@

try {
const oldMessage: any = await this.getMessage(data.key, true);
if (!oldMessage) throw new NotFoundException('Message not found');
if (oldMessage?.key?.remoteJid !== jid) {
throw new BadRequestException('RemoteJid does not match');
}
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
// 15 minutes in milliseconds
throw new BadRequestException('Message is older than 15 minutes');
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
if (!oldMessage) throw new NotFoundException('Message not found');
if (oldMessage?.key?.remoteJid !== jid) {
throw new BadRequestException('RemoteJid does not match');
}
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
// 15 minutes in milliseconds
throw new BadRequestException('Message is older than 15 minutes');
}
}

const messageSent = await this.client.sendMessage(jid, { ...(options as any), edit: data.key });
Expand All @@ -3837,7 +3853,7 @@
);

const messageId = messageSent.message?.protocolMessage?.key?.id;
if (messageId) {
if (messageId && this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
let message = await this.prismaRepository.message.findFirst({
where: { key: { path: ['id'], equals: messageId } },
});
Expand All @@ -3849,6 +3865,7 @@
if ((message.key.valueOf() as any)?.deleted) {
new BadRequestException('You cannot edit deleted messages');
}

if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') {
oldMessage.message.conversation = data.text;
} else {
Expand All @@ -3862,16 +3879,19 @@
messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds
},
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: messageSent.key.remoteJid,
fromMe: messageSent.key.fromMe,
participant: messageSent.key?.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({ data: messageUpdate });

if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: messageSent.key.remoteJid,
fromMe: messageSent.key.fromMe,
participant: messageSent.key?.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({ data: messageUpdate });
}
}
}
}
Expand Down Expand Up @@ -3941,6 +3961,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 3974 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 4011 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 4031 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 4038 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.contacts;
Comment thread
ricaelchiquetti marked this conversation as resolved.
Outdated
if (contacts[lidJid]) {
contacts[realJid] = contacts[lidJid];
delete contacts[lidJid];

Check failure on line 4057 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 4079 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 4082 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 4107 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 4124 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);

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

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