merging with develop

This commit is contained in:
pedro-php
2025-03-28 10:36:34 -03:00
50 changed files with 2129 additions and 934 deletions

View File

@@ -206,6 +206,20 @@ export class BusinessStartupService extends ChannelStartupService {
return content;
}
private messageLocationJson(received: any) {
const message = received.messages[0];
let content: any = {
locationMessage: {
degreesLatitude: message.location.latitude,
degreesLongitude: message.location.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
return content;
}
private messageContactsJson(received: any) {
const message = received.messages[0];
let content: any = {};
@@ -283,6 +297,9 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template':
messageType = 'conversation';
break;
case 'location':
messageType = 'locationMessage';
break;
default:
messageType = 'conversation';
break;
@@ -438,6 +455,17 @@ export class BusinessStartupService extends ChannelStartupService {
source: 'unknown',
instanceId: this.instanceId,
};
} else if (received?.messages[0].location) {
messageRaw = {
key,
pushName,
message: this.messageLocationJson(received),
contextInfo: this.messageLocationJson(received)?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else {
messageRaw = {
key,
@@ -800,6 +828,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
const isVideo = message['mimetype']?.startsWith('video/');
content = {
messaging_product: 'whatsapp',
@@ -809,7 +838,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }),
caption: message['caption'],
},
};
@@ -977,8 +1006,10 @@ export class BusinessStartupService extends ChannelStartupService {
private async getIdMedia(mediaMessage: any) {
const formData = new FormData();
const media = mediaMessage.media || mediaMessage.audio;
if (!media) throw new Error('Media or audio not found');
const fileStream = createReadStream(mediaMessage.media);
const fileStream = createReadStream(media);
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype);
@@ -1079,7 +1110,7 @@ export class BusinessStartupService extends ChannelStartupService {
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
audio,
};
if (isURL(audio)) {
@@ -1101,15 +1132,7 @@ export class BusinessStartupService extends ChannelStartupService {
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data };
if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else if (isURL(mediaData.audio)) {
// DO NOTHING
// mediaData.audio = mediaData.audio;
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
}
if (file) mediaData.audio = file.buffer.toString('base64');
const message = await this.processAudio(mediaData.audio, data.number);

View File

@@ -1,3 +1,4 @@
import { getCollectionsDto } from '@api/dto/business.dto';
import { OfferCallDto } from '@api/dto/call.dto';
import {
ArchiveChatDto,
@@ -91,6 +92,7 @@ import makeWASocket, {
BufferedEventData,
BufferJSON,
CacheStore,
CatalogCollection,
Chat,
ConnectionState,
Contact,
@@ -100,6 +102,7 @@ import makeWASocket, {
fetchLatestBaileysVersion,
generateWAMessageFromContent,
getAggregateVotesInPollMessage,
GetCatalogOptions,
getContentType,
getDevice,
GroupMetadata,
@@ -113,6 +116,7 @@ import makeWASocket, {
MiscMessageGenerationOptions,
ParticipantAction,
prepareWAMessageMedia,
Product,
proto,
UserFacingSocketConfig,
WABrowserDescription,
@@ -378,7 +382,7 @@ export class BaileysStartupService extends ChannelStartupService {
qrcodeTerminal.generate(qr, { small: true }, (qrcode) =>
this.logger.log(
`\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` +
qrcode,
qrcode,
),
);
@@ -1019,18 +1023,18 @@ export class BaileysStartupService extends ChannelStartupService {
const messagesRepository: Set<string> = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ??
(
await this.prismaRepository.message.findMany({
select: { key: true },
where: { instanceId: this.instanceId },
})
).map((message) => {
const key = message.key as {
id: string;
};
(
await this.prismaRepository.message.findMany({
select: { key: true },
where: { instanceId: this.instanceId },
})
).map((message) => {
const key = message.key as {
id: string;
};
return key.id;
}),
return key.id;
}),
);
if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
@@ -1128,37 +1132,74 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) {
const editedMessage =
received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage;
if (editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const editedMessage = received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
if (received.message?.protocolMessage?.editedMessage && editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const oldMessage = await this.getMessage(editedMessage.key, true);
if ((oldMessage as any)?.id) {
if (Long.isLong(editedMessage?.timestampMs)) {
editedMessage.timestampMs = editedMessage.timestampMs?.toNumber();
}
await this.prismaRepository.message.update({
where: { id: (oldMessage as any).id },
data: {
message: editedMessage.editedMessage as any,
messageTimestamp: editedMessage.timestampMs,
status: 'EDITED',
},
});
await this.prismaRepository.messageUpdate.create({
data: {
fromMe: editedMessage.key.fromMe,
keyId: editedMessage.key.id,
remoteJid: editedMessage.key.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
messageId: (oldMessage as any).id,
},
});
}
}
if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
// if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
// this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
await this.baileysCache.set(received.key.id, {
message: received,
retry: 0,
});
// await this.baileysCache.set(received.key.id, {
// message: received,
// retry: 0,
// });
// continue;
// }
// const retryCache = (await this.baileysCache.get(received.key.id)) || null;
// if (retryCache) {
// this.logger.info('Recovered message lost');
// await this.baileysCache.delete(received.key.id);
// }
// Cache to avoid duplicate messages
const messageKey = `${this.instance.id}_${received.key.id}`;
const cached = await this.baileysCache.get(messageKey);
if (cached && !editedMessage) {
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
continue;
}
const retryCache = (await this.baileysCache.get(received.key.id)) || null;
if (retryCache) {
this.logger.info('Recovered message lost');
await this.baileysCache.delete(received.key.id);
}
await this.baileysCache.set(messageKey, true, 30 * 60);
if (
(type !== 'notify' && type !== 'append') ||
@@ -1185,7 +1226,8 @@ export class BaileysStartupService extends ChannelStartupService {
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
received.pushName.trim().length > 0 &&
!received.key.remoteJid.includes('@g.us')
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
@@ -1421,6 +1463,17 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey);
if (cached) {
this.logger.info(`Message duplicated ignored: ${key.id}`);
continue;
}
await this.baileysCache.set(updateKey, true, 30 * 60);
if (status[update.status] === 'READ' && key.fromMe) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
@@ -2716,7 +2769,9 @@ export class BaileysStartupService extends ChannelStartupService {
if (file) mediaData.sticker = file.buffer.toString('base64');
const convert = await this.convertToWebP(data.sticker);
const convert = data?.notConvertSticker
? Buffer.from(data.sticker, 'base64')
: await this.convertToWebP(data.sticker);
const gifPlayback = data.sticker.includes('.gif');
const result = await this.sendMessageWithTyping(
data.number,
@@ -3572,6 +3627,18 @@ export class BaileysStartupService extends ChannelStartupService {
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,
});
} else {
await this.prismaRepository.message.deleteMany({
where: {
@@ -3898,28 +3965,75 @@ export class BaileysStartupService extends ChannelStartupService {
}
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');
}
const messageSent = await this.client.sendMessage(jid, {
...(options as any),
edit: data.key,
});
if (messageSent) {
const messageId = messageSent.message?.protocolMessage?.key?.id;
if (messageId) {
let message = await this.prismaRepository.message.findFirst({
where: {
key: {
path: ['id'],
equals: messageId,
},
},
});
if (!message) throw new NotFoundException('Message not found');
const updatedMessage =
messageSent.message?.protocolMessage || messageSent.message?.editedMessage?.message?.protocolMessage;
if (!(message.key.valueOf() as any).fromMe) {
new BadRequestException('You cannot edit others messages');
}
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 {
oldMessage.message[oldMessage.messageType].caption = data.text;
}
message = await this.prismaRepository.message.update({
where: { id: message.id },
data: {
message: oldMessage.message,
status: 'EDITED',
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 (updatedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'send.message.update',
{ instanceName: this.instance.name, instanceId: this.instance.id },
updatedMessage,
);
await this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, updatedMessage);
const editedMessage = messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
}
}
return messageSent;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.toString());
throw error;
}
}
@@ -4548,4 +4662,137 @@ export class BaileysStartupService extends ChannelStartupService {
return response;
}
//Business Controller
public async fetchCatalog(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit || 10;
const cursor = null;
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor });
let nextPageCursor = catalog.nextPageCursor;
let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
let pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
let productsCatalog = catalog.products || [];
let countLoops = 0;
while (fetcherHasMore && countLoops < 4) {
catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor });
nextPageCursor = catalog.nextPageCursor;
nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
productsCatalog = [...productsCatalog, ...catalog.products];
countLoops++;
}
return {
wuid: info?.jid || jid,
numberExists: info?.exists,
isBusiness: business.isBusiness,
catalogLength: productsCatalog.length,
catalog: productsCatalog,
};
} catch (error) {
console.log(error);
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCatalog({
jid,
limit,
cursor,
}: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor });
if (!catalog) {
return {
products: undefined,
nextPageCursor: undefined,
};
}
return catalog;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
public async fetchCollections(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque)
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
const collections = await this.getCollections(info?.jid, limit);
return {
wuid: info?.jid || jid,
name: info?.name,
numberExists: info?.exists,
isBusiness: business.isBusiness,
collectionsLength: collections?.length,
collections: collections,
};
} catch (error) {
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCollections(jid?: string | undefined, limit?: number): Promise<CatalogCollection[]> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const result = await this.client.getCollections(jid, limit);
if (!result) {
return [
{
id: undefined,
name: undefined,
products: [],
status: undefined,
},
];
}
return result.collections;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
}