Merge branch 'develop' into feat/add-async-lock

This commit is contained in:
Davidson Gomes
2025-07-16 19:22:44 -03:00
committed by GitHub
32 changed files with 3397 additions and 2082 deletions

View File

@@ -455,39 +455,46 @@ export class EvolutionStartupService extends ChannelStartupService {
if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}

View File

@@ -429,107 +429,114 @@ export class BusinessStartupService extends ChannelStartupService {
try {
const message: any = received;
const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers });
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
const buffer = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
let mediaType;
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
mediaType = 'video';
}
const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers });
const mimetype = result.data?.mime_type || result.headers['content-type'];
const buffer = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+?)"/);
if (match) {
fileName = match[1];
let mediaType;
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
} else {
mediaType = 'video';
}
}
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
const mimetype = result.data?.mime_type || result.headers['content-type'];
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+?)"/);
if (match) {
fileName = match[1];
}
}
}
const size = result.headers['content-length'] || buffer.data.byteLength;
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
}
}
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
const size = result.headers['content-length'] || buffer.data.byteLength;
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
await this.prismaRepository.media.create({
data: {
messageId: createdMessage.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
await this.prismaRepository.media.create({
data: {
messageId: createdMessage.id,
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
type: mediaType,
fileName: fullName,
mimetype,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
}

View File

@@ -99,6 +99,7 @@ import makeWASocket, {
Contact,
delay,
DisconnectReason,
downloadContentFromMessage,
downloadMediaMessage,
generateWAMessageFromContent,
getAggregateVotesInPollMessage,
@@ -122,7 +123,7 @@ import makeWASocket, {
WABrowserDescription,
WAMediaUpload,
WAMessage,
WAMessageUpdate,
WAMessageKey,
WAPresence,
WASocket,
} from 'baileys';
@@ -894,7 +895,7 @@ export class BaileysStartupService extends ChannelStartupService {
}: {
chats: Chat[];
contacts: Contact[];
messages: proto.IWebMessageInfo[];
messages: WAMessage[];
isLatest?: boolean;
progress?: number;
syncType?: proto.HistorySync.HistorySyncType;
@@ -980,6 +981,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (m.key.remoteJid?.includes('@lid') && m.key.senderPn) {
m.key.remoteJid = m.key.senderPn;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
@@ -1037,11 +1042,29 @@ export class BaileysStartupService extends ChannelStartupService {
},
'messages.upsert': async (
{ messages, type, requestId }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string },
{ messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string },
settings: any,
) => {
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;
received.key.remoteJid = received.key.senderPn;
}
if (
received?.messageStubParameters?.some?.((param) =>
[
'No matching sessions found for message',
'Bad MAC',
'failed to decrypt message',
'SessionError',
'Invalid PreKey ID',
].some((err) => param?.includes?.(err)),
)
) {
this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`);
continue;
}
if (received.message?.conversation || received.message?.extendedTextMessage?.text) {
const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
@@ -1233,33 +1256,41 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const message: any = received;
const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
await this.prismaRepository.media.create({
data: {
messageId: msg.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true);
const mediaUrl = await s3Service.getObjectUrl(fullName);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.media.create({
data: {
messageId: msg.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
@@ -1363,7 +1394,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
},
'messages.update': async (args: WAMessageUpdate[], settings: any) => {
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
@@ -1373,6 +1404,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (key.remoteJid?.includes('@lid') && key.senderPn) {
key.remoteJid = key.senderPn;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey);
@@ -2130,31 +2165,39 @@ export class BaileysStartupService extends ChannelStartupService {
if (isMedia && this.configService.get<S3>('S3').ENABLE) {
try {
const message: any = messageRaw;
const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
const mimetype = mimeTypes.lookup(fileName).toString();
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true);
const fullName = join(
`${this.instance.id}`,
messageRaw.key.remoteJid,
`${messageRaw.key.id}`,
mediaType,
fileName,
);
const { buffer, mediaType, fileName, size } = media;
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
const mimetype = mimeTypes.lookup(fileName).toString();
await this.prismaRepository.media.create({
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
});
const fullName = join(
`${this.instance.id}`,
messageRaw.key.remoteJid,
`${messageRaw.key.id}`,
mediaType,
fileName,
);
const mediaUrl = await s3Service.getObjectUrl(fullName);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.media.create({
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
});
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
@@ -3422,6 +3465,18 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
public async mapMediaType(mediaType) {
const map = {
imageMessage: 'image',
videoMessage: 'video',
documentMessage: 'document',
stickerMessage: 'sticker',
audioMessage: 'audio',
ptvMessage: 'video',
};
return map[mediaType] || null;
}
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
try {
const m = data?.message;
@@ -3446,28 +3501,76 @@ export class BaileysStartupService extends ChannelStartupService {
let mediaMessage: any;
let mediaType: string;
for (const type of TypeMediaMessage) {
mediaMessage = msg.message[type];
if (mediaMessage) {
mediaType = type;
break;
}
}
if (msg.message?.templateMessage) {
const template =
msg.message.templateMessage.hydratedTemplate || msg.message.templateMessage.hydratedFourRowTemplate;
if (!mediaMessage) {
throw 'The message is not of the media type';
for (const type of TypeMediaMessage) {
if (template[type]) {
mediaMessage = template[type];
mediaType = type;
msg.message = { [type]: { ...template[type], url: template[type].staticUrl } };
break;
}
}
if (!mediaMessage) {
throw 'Template message does not contain a supported media type';
}
} else {
for (const type of TypeMediaMessage) {
mediaMessage = msg.message[type];
if (mediaMessage) {
mediaType = type;
break;
}
}
if (!mediaMessage) {
throw 'The message is not of the media type';
}
}
if (typeof mediaMessage['mediaKey'] === 'object') {
msg.message = JSON.parse(JSON.stringify(msg.message));
}
const buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message },
'buffer',
{},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
let buffer: Buffer;
try {
buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message },
'buffer',
{},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
} catch (err) {
this.logger.error('Download Media failed, trying to retry in 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
if (!mediaType) throw new Error('Could not determine mediaType for fallback');
try {
const media = await downloadContentFromMessage(
{
mediaKey: msg.message?.[mediaType]?.mediaKey,
directPath: msg.message?.[mediaType]?.directPath,
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
},
await this.mapMediaType(mediaType),
{},
);
const chunks = [];
for await (const chunk of media) {
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
this.logger.info('Download Media with downloadContentFromMessage was successful!');
} catch (fallbackErr) {
this.logger.error('Download Media with downloadContentFromMessage also failed!');
throw fallbackErr;
}
}
const typeMessage = getContentType(msg.message);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);