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
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Migration may overwrite intentional false values.

Currently, the migration updates both NULL and false values to true. If false values are intentional, you may want to update only NULL values instead.

2 changes: 1 addition & 1 deletion prisma/mysql-schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Migration may overwrite intentional false values.

This update will change all false or NULL values to true, potentially overriding intentional false settings. Please confirm if this aligns with the desired migration behavior.

2 changes: 1 addition & 1 deletion prisma/postgresql-schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
227 changes: 183 additions & 44 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCollectionsDto } from '@api/dto/business.dto';

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Run autofix to sort these imports!
import { OfferCallDto } from '@api/dto/call.dto';
import {
ArchiveChatDto,
Expand Down Expand Up @@ -139,6 +139,7 @@
import mimeTypes from 'mime-types';
import NodeCache from 'node-cache';
import cron from 'node-cron';
import dayjs from 'dayjs';

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

'dayjs' is defined but never used
import { release } from 'os';
import { join } from 'path';
import P from 'pino';
Expand All @@ -153,6 +154,52 @@

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;

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··`
// Remove LID suffix and convert to standard JID format
if (jid.includes(':lid')) {
return jid.split(':')[0] + '@s.whatsapp.net';
}

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··`
// Remove participant suffix from group messages
if (jid.includes(':') && jid.includes('@g.us')) {
return jid.split(':')[0] + '@g.us';
}

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··`
// Remove any other participant suffixes
if (jid.includes(':') && !jid.includes('@g.us')) {
return jid.split(':')[0] + '@s.whatsapp.net';
}

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

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

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `····`
// Clear session-related cache patterns
const patterns = [

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Replace `⏎······`${instanceId}_*`,⏎······`*${instanceId}*`,⏎······`*session*${instanceId}*`,⏎······`*prekey*${instanceId}*`⏎····` with ``${instanceId}_*`,·`*${instanceId}*`,·`*session*${instanceId}*`,·`*prekey*${instanceId}*``
`${instanceId}_*`,
`*${instanceId}*`,
`*session*${instanceId}*`,
`*prekey*${instanceId}*`
];

Check failure on line 192 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 pattern of patterns) {
await baileysCache.deleteAll(pattern);
}

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

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `····`
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<number> {
const MediaInfoFactory = (await import('mediainfo.js')).default;
Expand Down Expand Up @@ -383,6 +430,11 @@
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') {
Expand Down Expand Up @@ -662,6 +714,9 @@

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) {
Expand Down Expand Up @@ -1124,7 +1179,8 @@
}
}

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) {
Expand All @@ -1151,8 +1207,9 @@
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 },
});

Expand Down Expand Up @@ -1231,7 +1288,8 @@
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);

Expand Down Expand Up @@ -1336,6 +1394,41 @@

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,
Expand All @@ -1344,13 +1437,13 @@
});

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,
};

Expand Down Expand Up @@ -1481,35 +1574,35 @@
}

continue;
}

if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) {
if (!key.fromMe && key.remoteJid) {
readChatToUpdate[key.remoteJid] = true;
} else if (update.status !== undefined && status[update.status] !== findMessage.status) {
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}`,
);
}
}

Expand Down Expand Up @@ -1938,11 +2031,19 @@
}

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') {
Expand Down Expand Up @@ -3254,12 +3355,35 @@
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) => {
Expand Down Expand Up @@ -3355,7 +3479,6 @@
.filter((user) => user.exists)
.map((user) => ({
remoteJid: user.jid,
jidOptions: user.jid.replace('+', ''),
lid: user.lid,
})),
);
Expand Down Expand Up @@ -4303,8 +4426,15 @@
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
Expand All @@ -4319,8 +4449,17 @@
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) {
Expand Down
Loading