diff --git a/Dockerfile b/Dockerfile index 02130c434..d889b25c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ COPY ./package*.json ./ COPY ./tsconfig.json ./ COPY ./tsup.config.ts ./ -RUN npm ci --silent +RUN npm install --force --legacy-peer-deps COPY ./src ./src COPY ./public ./public diff --git a/prisma/mysql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql b/prisma/mysql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql new file mode 100644 index 000000000..2dbc595ff --- /dev/null +++ b/prisma/mysql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE `OpenaiSetting` MODIFY COLUMN `speechToText` BOOLEAN NULL DEFAULT true; + +-- Update existing records to use the new default +UPDATE `OpenaiSetting` SET `speechToText` = true WHERE `speechToText` IS NULL OR `speechToText` = false; diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 33d09d2b9..799328575 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -469,7 +469,7 @@ model OpenaiSetting { ignoreJids Json? splitMessages Boolean? @default(false) timePerChar Int? @default(50) @db.Int - speechToText Boolean? @default(false) + speechToText Boolean? @default(true) createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id]) diff --git a/prisma/postgresql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql b/prisma/postgresql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql new file mode 100644 index 000000000..7fb18428b --- /dev/null +++ b/prisma/postgresql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "OpenaiSetting" ALTER COLUMN "speechToText" SET DEFAULT true; + +-- Update existing records to use the new default +UPDATE "OpenaiSetting" SET "speechToText" = true WHERE "speechToText" IS NULL OR "speechToText" = false; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 221d69d9d..2357cbf75 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -476,7 +476,7 @@ model OpenaiSetting { ignoreJids Json? splitMessages Boolean? @default(false) @db.Boolean timePerChar Int? @default(50) @db.Integer - speechToText Boolean? @default(false) @db.Boolean + speechToText Boolean? @default(true) @db.Boolean createdAt DateTime? @default(now()) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id]) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 758a5bf96..d5a25186d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -139,6 +139,7 @@ import Long from 'long'; import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; import cron from 'node-cron'; +import dayjs from 'dayjs'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; @@ -153,6 +154,52 @@ import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); +// Function to normalize JID and handle LID/JID conversion +function normalizeJid(jid: string): string { + if (!jid) return jid; + + // Remove LID suffix and convert to standard JID format + if (jid.includes(':lid')) { + return jid.split(':')[0] + '@s.whatsapp.net'; + } + + // Remove participant suffix from group messages + if (jid.includes(':') && jid.includes('@g.us')) { + return jid.split(':')[0] + '@g.us'; + } + + // Remove any other participant suffixes + if (jid.includes(':') && !jid.includes('@g.us')) { + return jid.split(':')[0] + '@s.whatsapp.net'; + } + + return jid; +} + +// Function to clear corrupted session data +async function clearCorruptedSessionData(instanceId: string, baileysCache: CacheService) { + try { + // Clear all baileys cache for this instance + await baileysCache.deleteAll(instanceId); + + // Clear session-related cache patterns + const patterns = [ + `${instanceId}_*`, + `*${instanceId}*`, + `*session*${instanceId}*`, + `*prekey*${instanceId}*` + ]; + + for (const pattern of patterns) { + await baileysCache.deleteAll(pattern); + } + + console.log(`Cleared corrupted session data for instance: ${instanceId}`); + } catch (error) { + console.error('Error clearing session data:', error); + } +} + // Adicione a função getVideoDuration no início do arquivo async function getVideoDuration(input: Buffer | string | Readable): Promise { const MediaInfoFactory = (await import('mediainfo.js')).default; @@ -383,6 +430,11 @@ export class BaileysStartupService extends ChannelStartupService { state: connection, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; + + this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`); + if (lastDisconnect?.error) { + this.logger.warn(`Connection error: ${JSON.stringify(lastDisconnect.error)}`); + } } if (connection === 'close') { @@ -662,6 +714,9 @@ export class BaileysStartupService extends ChannelStartupService { this.endSession = false; + // Clear any corrupted session data before connecting + await clearCorruptedSessionData(this.instanceId, this.baileysCache); + this.client = makeWASocket(socketConfig); if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) { @@ -1124,7 +1179,8 @@ export class BaileysStartupService extends ChannelStartupService { } } - const messageKey = `${this.instance.id}_${received.key.id}`; + const normalizedJid = normalizeJid(received.key.remoteJid); + const messageKey = `${this.instance.id}_${normalizedJid}_${received.key.id}`; const cached = await this.baileysCache.get(messageKey); if (cached && !editedMessage) { @@ -1151,8 +1207,9 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + const normalizedRemoteJid = normalizeJid(received.key.remoteJid); const existingChat = await this.prismaRepository.chat.findFirst({ - where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid }, + where: { instanceId: this.instanceId, remoteJid: normalizedRemoteJid }, select: { id: true, name: true }, }); @@ -1231,7 +1288,8 @@ export class BaileysStartupService extends ChannelStartupService { const { remoteJid } = received.key; const timestamp = msg.messageTimestamp; const fromMe = received.key.fromMe.toString(); - const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; + const normalizedRemoteJid = normalizeJid(remoteJid); + const messageKey = `${normalizedRemoteJid}_${timestamp}_${fromMe}`; const cachedTimestamp = await this.baileysCache.get(messageKey); @@ -1336,6 +1394,41 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + // Schedule automatic status update for PENDING sent messages + if (messageRaw.key.fromMe && messageRaw.status === 'PENDING') { + setTimeout(async () => { + try { + const stillPendingMessage = await this.prismaRepository.message.findFirst({ + where: { + instanceId: this.instanceId, + key: { path: ['id'], equals: messageRaw.key.id }, + status: 'PENDING' + } + }); + + if (stillPendingMessage) { + this.logger.warn(`Forcing status update for PENDING message after timeout: ${messageRaw.key.id}`); + await this.prismaRepository.message.update({ + where: { id: stillPendingMessage.id }, + data: { status: 'SERVER_ACK' } + }); + + // Emit webhook for the status change + this.sendDataWebhook(Events.MESSAGES_UPDATE, { + messageId: stillPendingMessage.id, + keyId: messageRaw.key.id, + remoteJid: messageRaw.key.remoteJid, + fromMe: messageRaw.key.fromMe, + status: 'SERVER_ACK', + instanceId: this.instanceId + }); + } + } catch (error) { + this.logger.error(`Error updating PENDING message status: ${error.message}`); + } + }, 30000); // 30 seconds timeout + } + await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, remoteJid: messageRaw.key.remoteJid, @@ -1344,13 +1437,13 @@ export class BaileysStartupService extends ChannelStartupService { }); const contact = await this.prismaRepository.contact.findFirst({ - where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, + where: { remoteJid: normalizedRemoteJid, instanceId: this.instanceId }, }); const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = { - remoteJid: received.key.remoteJid, + remoteJid: normalizedRemoteJid, pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName, - profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + profilePicUrl: (await this.profilePicture(normalizedRemoteJid)).profilePictureUrl, instanceId: this.instanceId, }; @@ -1481,32 +1574,34 @@ export class BaileysStartupService extends ChannelStartupService { continue; } else if (update.status !== undefined && status[update.status] !== findMessage.status) { - if (!key.fromMe && key.remoteJid) { - readChatToUpdate[key.remoteJid] = true; + const { remoteJid } = key; + const timestamp = findMessage.messageTimestamp; + const fromMe = key.fromMe.toString(); + const normalizedRemoteJid = normalizeJid(remoteJid); + const messageKey = `${normalizedRemoteJid}_${timestamp}_${fromMe}`; - const { remoteJid } = key; - const timestamp = findMessage.messageTimestamp; - const fromMe = key.fromMe.toString(); - const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; + const cachedTimestamp = await this.baileysCache.get(messageKey); - const cachedTimestamp = await this.baileysCache.get(messageKey); + if (!cachedTimestamp) { + // Handle read status for received messages + if (!key.fromMe && key.remoteJid && status[update.status] === status[4]) { + readChatToUpdate[key.remoteJid] = true; + this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`); + await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); + await this.baileysCache.set(messageKey, true, 5 * 60); + } - if (!cachedTimestamp) { - if (status[update.status] === status[4]) { - this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`); - await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); - await this.baileysCache.set(messageKey, true, 5 * 60); - } + // Update message status for all messages (sent and received) + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: status[update.status] }, + }); - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, - data: { status: status[update.status] }, - }); - } else { - this.logger.info( - `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, - ); - } + this.logger.log(`Message status updated from ${findMessage.status} to ${status[update.status]} for message ${key.id}`); + } else { + this.logger.info( + `Update messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, + ); } } @@ -1935,11 +2030,19 @@ export class BaileysStartupService extends ChannelStartupService { } if (message['conversation']) { - return await this.client.sendMessage( - sender, - { text: message['conversation'], mentions, linkPreview: linkPreview } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); + try { + this.logger.log(`Attempting to send conversation message to ${sender}: ${message['conversation']}`); + const result = await this.client.sendMessage( + sender, + { text: message['conversation'], mentions, linkPreview: linkPreview } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + this.logger.log(`Message sent successfully with ID: ${result.key.id}`); + return result; + } catch (error) { + this.logger.error(`Failed to send message to ${sender}: ${error.message || JSON.stringify(error)}`); + throw error; + } } if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') { @@ -3217,12 +3320,35 @@ export class BaileysStartupService extends ChannelStartupService { const cachedNumbers = await getOnWhatsappCache(numbersToVerify); console.log('cachedNumbers', cachedNumbers); - const filteredNumbers = numbersToVerify.filter( - (jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)), - ); + // Filter numbers that are not cached OR should be re-verified + const filteredNumbers = numbersToVerify.filter((jid) => { + const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(jid)); + // If not cached, we should verify + if (!cached) return true; + + // For Brazilian numbers, force verification if both formats exist in cache + // to ensure we're using the correct format + const isBrazilian = jid.startsWith('55') && jid.includes('@s.whatsapp.net'); + if (isBrazilian) { + const numberPart = jid.replace('@s.whatsapp.net', ''); + const hasDigit9 = numberPart.length === 13 && numberPart.slice(4, 5) === '9'; + const altFormat = hasDigit9 + ? numberPart.slice(0, 4) + numberPart.slice(5) + : numberPart.slice(0, 4) + '9' + numberPart.slice(4); + const altJid = altFormat + '@s.whatsapp.net'; + + // If both formats exist in cache, prefer the one with 9 + const altCached = cachedNumbers.find((c) => c.jidOptions.includes(altJid)); + if (cached && altCached && !hasDigit9) { + return true; // Force verification to get the correct format + } + } + + return false; // Use cached result + }); console.log('filteredNumbers', filteredNumbers); - const verify = await this.client.onWhatsApp(...filteredNumbers); + const verify = filteredNumbers.length > 0 ? await this.client.onWhatsApp(...filteredNumbers) : []; console.log('verify', verify); normalVerifiedUsers = await Promise.all( normalUsers.map(async (user) => { @@ -3318,7 +3444,6 @@ export class BaileysStartupService extends ChannelStartupService { .filter((user) => user.exists) .map((user) => ({ remoteJid: user.jid, - jidOptions: user.jid.replace('+', ''), lid: user.lid, })), ); @@ -4253,8 +4378,15 @@ export class BaileysStartupService extends ChannelStartupService { const contentType = getContentType(message.message); const contentMsg = message?.message[contentType] as any; + // Normalize JID to handle LID/JID conversion + const normalizedKey = { + ...message.key, + remoteJid: normalizeJid(message.key.remoteJid), + participant: message.key.participant ? normalizeJid(message.key.participant) : undefined, + }; + const messageRaw = { - key: message.key, + key: normalizedKey, pushName: message.pushName || (message.key.fromMe @@ -4269,8 +4401,17 @@ export class BaileysStartupService extends ChannelStartupService { source: getDevice(message.key.id), }; - if (!messageRaw.status && message.key.fromMe === false) { - messageRaw.status = status[3]; // DELIVERED MESSAGE + // Log for debugging PENDING status + if (message.key.fromMe && (!message.status || message.status === 1)) { + this.logger.warn(`Message sent with PENDING status - ID: ${message.key.id}, Instance: ${this.instance.id}, Status: ${message.status}, RemoteJid: ${message.key.remoteJid}`); + } + + if (!messageRaw.status) { + if (message.key.fromMe === false) { + messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages + } else { + messageRaw.status = status[2]; // SERVER_ACK for sent messages without status + } } if (messageRaw.message.extendedTextMessage) { diff --git a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts index 1d9910d1f..563567b0c 100644 --- a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts +++ b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts @@ -184,7 +184,7 @@ export class OpenaiController extends BaseChatbotController `${number}@${domain}`); + return numbersAvailable; } interface ISaveOnWhatsappCacheParams { remoteJid: string; - lid?: string; + lid?: string | null; } export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {