import { ArchiveChatDto, BlockUserDto, DeleteMessage, getBase64FromMediaMessageDto, LastMessage, MarkChatUnreadDto, NumberBusiness, OnWhatsAppDto, PrivacySettingDto, ReadMessageDto, SendPresenceDto, UpdateMessageDto, WhatsAppNumberDto, } from '@api/dto/chat.dto'; import { AcceptGroupInvite, CreateGroupDto, GetParticipant, GroupDescriptionDto, GroupInvite, GroupJid, GroupPictureDto, GroupSendInvite, GroupSubjectDto, GroupToggleEphemeralDto, GroupUpdateParticipantDto, GroupUpdateSettingDto, } from '@api/dto/group.dto'; import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto'; import { HandleLabelDto, LabelDto } from '@api/dto/label.dto'; import { ContactMessage, MediaMessage, Options, SendAudioDto, SendContactDto, SendLocationDto, SendMediaDto, SendPollDto, SendReactionDto, SendStatusDto, SendStickerDto, SendTextDto, StatusMessage, } from '@api/dto/sendMessage.dto'; import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper'; import * as s3Service from '@api/integrations/storage/s3/libs/minio.server'; import { ProviderFiles } from '@api/provider/sessions'; import { PrismaRepository } from '@api/repository/repository.service'; import { chatbotController, waMonitor } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { CacheConf, Chatwoot, ConfigService, configService, ConfigSessionPhone, Database, Log, Openai, ProviderSession, QrCode, S3, } from '@config/env.config'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import { Instance } from '@prisma/client'; import { makeProxyAgent } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import axios from 'axios'; import makeWASocket, { AnyMessageContent, BufferedEventData, BufferJSON, CacheStore, Chat, ConnectionState, Contact, delay, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, generateWAMessageFromContent, getAggregateVotesInPollMessage, getContentType, getDevice, GroupMetadata, isJidBroadcast, isJidGroup, isJidNewsletter, isJidUser, makeCacheableSignalKeyStore, MessageUpsertType, MiscMessageGenerationOptions, ParticipantAction, prepareWAMessageMedia, proto, UserFacingSocketConfig, WABrowserDescription, WAMediaUpload, WAMessage, WAMessageUpdate, WAPresence, WASocket, } from 'baileys'; import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; import { spawn } from 'child_process'; import { isBase64, isURL } from 'class-validator'; import { randomBytes } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; import { readFileSync } from 'fs'; import Long from 'long'; import mime from 'mime'; import NodeCache from 'node-cache'; import cron from 'node-cron'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; import { PassThrough } from 'stream'; import { v4 } from 'uuid'; const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); export class BaileysStartupService extends ChannelStartupService { constructor( public readonly configService: ConfigService, public readonly eventEmitter: EventEmitter2, public readonly prismaRepository: PrismaRepository, public readonly cache: CacheService, public readonly chatwootCache: CacheService, public readonly baileysCache: CacheService, private readonly providerFiles: ProviderFiles, ) { super(configService, eventEmitter, prismaRepository, chatwootCache); this.instance.qrcode = { count: 0 }; this.authStateProvider = new AuthStateProvider(this.providerFiles); } private authStateProvider: AuthStateProvider; private readonly msgRetryCounterCache: CacheStore = new NodeCache(); private readonly userDevicesCache: CacheStore = new NodeCache(); private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; public stateConnection: wa.StateConnection = { state: 'close' }; public phoneNumber: string; public get connectionStatus() { return this.stateConnection; } public async logoutInstance() { await this.client?.logout('Log out instance: ' + this.instanceName); this.client?.ws?.close(); const sessionExists = await this.prismaRepository.session.findFirst({ where: { sessionId: this.instanceId }, }); if (sessionExists) { await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId, }, }); } } public async getProfileName() { let profileName = this.client.user?.name ?? this.client.user?.verifiedName; if (!profileName) { const data = await this.prismaRepository.session.findUnique({ where: { sessionId: this.instanceId }, }); if (data) { const creds = JSON.parse(JSON.stringify(data.creds), BufferJSON.reviver); profileName = creds.me?.name || creds.me?.verifiedName; } } return profileName; } public async getProfileStatus() { const status = await this.client.fetchStatus(this.instance.wuid); return status?.status; } public get profilePictureUrl() { return this.instance.profilePictureUrl; } public get qrCode(): wa.QrCode { return { pairingCode: this.instance.qrcode?.pairingCode, code: this.instance.qrcode?.code, base64: this.instance.qrcode?.base64, count: this.instance.qrcode?.count, }; } private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { if (qr) { if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { this.sendDataWebhook(Events.QRCODE_UPDATED, { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( Events.QRCODE_UPDATED, { instanceName: this.instance.name, instanceId: this.instanceId }, { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession, }, ); } this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, state: 'refused', statusReason: DisconnectReason.connectionClosed, }); this.endSession = true; return this.eventEmitter.emit('no.connection', this.instance.name); } this.instance.qrcode.count++; const color = this.configService.get('QRCODE').COLOR; const optsQrcode: QRCodeToDataURLOptions = { margin: 3, scale: 4, errorCorrectionLevel: 'H', color: { light: '#ffffff', dark: color }, }; if (this.phoneNumber) { await delay(1000); this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); } else { this.instance.qrcode.pairingCode = null; } qrcode.toDataURL(qr, optsQrcode, (error, base64) => { if (error) { this.logger.error('Qrcode generate failed:' + error.toString()); return; } this.instance.qrcode.base64 = base64; this.instance.qrcode.code = qr; this.sendDataWebhook(Events.QRCODE_UPDATED, { qrcode: { instance: this.instance.name, pairingCode: this.instance.qrcode.pairingCode, code: qr, base64, }, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( Events.QRCODE_UPDATED, { instanceName: this.instance.name, instanceId: this.instanceId }, { qrcode: { instance: this.instance.name, pairingCode: this.instance.qrcode.pairingCode, code: qr, base64, }, }, ); } }); 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, ), ); await this.prismaRepository.instance.update({ where: { id: this.instanceId }, data: { connectionStatus: 'connecting', }, }); } if (connection) { this.stateConnection = { state: connection, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection, }); } if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; const shouldReconnect = !codesToNotReconnect.includes(statusCode); if (shouldReconnect) { await this.connectToWhatsapp(this.phoneNumber); } else { this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, status: 'closed', disconnectionAt: new Date(), disconnectionReasonCode: statusCode, disconnectionObject: JSON.stringify(lastDisconnect), }); await this.prismaRepository.instance.update({ where: { id: this.instanceId }, data: { connectionStatus: 'close', disconnectionAt: new Date(), disconnectionReasonCode: statusCode, disconnectionObject: JSON.stringify(lastDisconnect), }, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( Events.STATUS_INSTANCE, { instanceName: this.instance.name, instanceId: this.instanceId }, { instance: this.instance.name, status: 'closed', }, ); } this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); this.client?.ws?.close(); this.client.end(new Error('Close connection')); } } if (connection === 'open') { this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); try { const profilePic = await this.profilePicture(this.instance.wuid); this.instance.profilePictureUrl = profilePic.profilePictureUrl; } catch (error) { this.instance.profilePictureUrl = null; } const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' '); const formattedName = this.instance.name; this.logger.info( ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ └──────────────────────────────┘`.replace(/^ +/gm, ' '), ); this.logger.info( ` wuid: ${formattedWuid} name: ${formattedName} `, ); await this.prismaRepository.instance.update({ where: { id: this.instanceId }, data: { ownerJid: this.instance.wuid, profileName: (await this.getProfileName()) as string, profilePicUrl: this.instance.profilePictureUrl, connectionStatus: 'open', }, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( Events.CONNECTION_UPDATE, { instanceName: this.instance.name, instanceId: this.instanceId }, { instance: this.instance.name, status: 'open', }, ); this.syncChatwootLostMessages(); } } } private async getMessage(key: proto.IMessageKey, full = false) { try { const webMessageInfo = (await this.prismaRepository.message.findMany({ where: { instanceId: this.instanceId, key: { path: ['id'], equals: key.id, }, }, })) as unknown as proto.IWebMessageInfo[]; if (full) { return webMessageInfo[0]; } if (webMessageInfo[0].message?.pollCreationMessage) { const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; if (typeof messageSecretBase64 === 'string') { const messageSecret = Buffer.from(messageSecretBase64, 'base64'); const msg = { messageContextInfo: { messageSecret, }, pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, }; return msg; } } return webMessageInfo[0].message; } catch (error) { return { conversation: '' }; } } private async defineAuthState() { const db = this.configService.get('DATABASE'); const cache = this.configService.get('CACHE'); const provider = this.configService.get('PROVIDER'); if (provider?.ENABLED) { return await this.authStateProvider.authStateProvider(this.instance.id); } if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) { this.logger.info('Redis enabled'); return await useMultiFileAuthStateRedisDb(this.instance.id, this.cache); } if (db.SAVE_DATA.INSTANCE) { return await useMultiFileAuthStatePrisma(this.instance.id, this.cache); } } private async createClient(number?: string): Promise { this.instance.authState = await this.defineAuthState(); const session = this.configService.get('CONFIG_SESSION_PHONE'); let browserOptions = {}; if (number || this.phoneNumber) { this.phoneNumber = number; this.logger.info(`Phone number: ${number}`); } else { const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; browserOptions = { browser }; this.logger.info(`Browser: ${browser}`); } let version; let log; if (session.VERSION) { version = session.VERSION.split('.'); log = `Baileys version env: ${version}`; } else { const baileysVersion = await fetchLatestBaileysVersion(); version = baileysVersion.version; log = `Baileys version: ${version}`; } this.logger.info(log); this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`); let options; if (this.localProxy?.enabled) { this.logger.info('Proxy enabled: ' + this.localProxy?.host); if (this.localProxy?.host?.includes('proxyscrape')) { try { const response = await axios.get(this.localProxy?.host); const text = response.data; const proxyUrls = text.split('\r\n'); const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const proxyUrl = 'http://' + proxyUrls[rand]; options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgent(proxyUrl), }; } catch (error) { this.localProxy.enabled = false; } } else { options = { agent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), fetchAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), }; } } const socketConfig: UserFacingSocketConfig = { ...options, version, logger: P({ level: this.logBaileys }), printQRInTerminal: false, auth: { creds: this.instance.authState.state.creds, keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), }, msgRetryCounterCache: this.msgRetryCounterCache, generateHighQualityLinkPreview: true, getMessage: async (key) => (await this.getMessage(key)) as Promise, ...browserOptions, markOnlineOnConnect: this.localSettings.alwaysOnline, retryRequestDelayMs: 350, maxMsgRetryCount: 4, fireInitQueries: true, connectTimeoutMs: 30_000, keepAliveIntervalMs: 30_000, qrTimeout: 45_000, emitOwnEvents: false, shouldIgnoreJid: (jid) => { const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid); const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid); const isNewsletter = isJidNewsletter(jid); // const isNewsletter = jid && jid.includes('newsletter'); return isGroupJid || isBroadcast || isNewsletter; }, syncFullHistory: this.localSettings.syncFullHistory, shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => { return this.historySyncNotification(msg); }, cachedGroupMetadata: this.getGroupMetadataCache, userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 }, }; this.endSession = false; this.client = makeWASocket(socketConfig); this.eventHandler(); this.phoneNumber = number; return this.client; } public async connectToWhatsapp(number?: string): Promise { try { this.loadChatwoot(); this.loadSettings(); this.loadProxy(); return await this.createClient(number); } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } public async reloadConnection(): Promise { try { return await this.createClient(this.phoneNumber); } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } private readonly chatHandle = { 'chats.upsert': async (chats: Chat[]) => { const existingChatIds = await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId }, select: { remoteJid: true }, }); const existingChatIdSet = new Set(existingChatIds.map((chat) => chat.remoteJid)); const chatsToInsert = chats .filter((chat) => !existingChatIdSet?.has(chat.id)) .map((chat) => ({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name })); this.sendDataWebhook(Events.CHATS_UPSERT, chatsToInsert); if (chatsToInsert.length > 0) { if (this.configService.get('DATABASE').SAVE_DATA.CHATS) await this.prismaRepository.chat.createMany({ data: chatsToInsert, skipDuplicates: true, }); } }, 'chats.update': async ( chats: Partial< proto.IConversation & { lastMessageRecvTimestamp?: number; } & { conditional: (bufferedData: BufferedEventData) => boolean; } >[], ) => { const chatsRaw = chats.map((chat) => { return { remoteJid: chat.id, instanceId: this.instanceId }; }); this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); for (const chat of chats) { await this.prismaRepository.chat.updateMany({ where: { instanceId: this.instanceId, remoteJid: chat.id, name: chat.name, }, data: { remoteJid: chat.id }, }); } }, 'chats.delete': async (chats: string[]) => { chats.forEach( async (chat) => await this.prismaRepository.chat.deleteMany({ where: { instanceId: this.instanceId, remoteJid: chat }, }), ); this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); }, }; private readonly contactHandle = { 'contacts.upsert': async (contacts: Contact[]) => { try { const contactsRaw: any = contacts.map((contact) => ({ remoteJid: contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, })); if (contactsRaw.length > 0) { this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) await this.prismaRepository.contact.createMany({ data: contactsRaw, skipDuplicates: true, }); const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp')); if (usersContacts) { await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); } } if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && this.localChatwoot.importContacts && contactsRaw.length ) { this.chatwootService.addHistoryContacts( { instanceName: this.instance.name, instanceId: this.instance.id }, contactsRaw, ); chatwootImport.importHistoryContacts( { instanceName: this.instance.name, instanceId: this.instance.id }, this.localChatwoot, ); } const updatedContacts = await Promise.all( contacts.map(async (contact) => ({ remoteJid: contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, instanceId: this.instanceId, })), ); if (updatedContacts.length > 0) { const usersContacts = updatedContacts.filter((c) => c.remoteJid.includes('@s.whatsapp')); if (usersContacts) { await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); } this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts); await Promise.all( updatedContacts.map(async (contact) => { const update = this.prismaRepository.contact.updateMany({ where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, data: { profilePicUrl: contact.profilePicUrl, }, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { const instance = { instanceName: this.instance.name, instanceId: this.instance.id }; const findParticipant = await this.chatwootService.findContact( instance, contact.remoteJid.split('@')[0], ); if (!findParticipant) { return; } this.chatwootService.updateContact(instance, findParticipant.id, { name: contact.pushName, avatar_url: contact.profilePicUrl, }); } return update; }), ); } } catch (error) { console.error(error); this.logger.error(`Error: ${error.message}`); } }, 'contacts.update': async (contacts: Partial[]) => { const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string; }[] = []; for await (const contact of contacts) { contactsRaw.push({ remoteJid: contact.id, pushName: contact?.name ?? contact?.verifiedName, profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, instanceId: this.instanceId, }); } this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); const updateTransactions = contactsRaw.map((contact) => this.prismaRepository.contact.upsert({ where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } }, create: contact, update: contact, }), ); await this.prismaRepository.$transaction(updateTransactions); const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp')); if (usersContacts) { await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid }))); } }, }; private readonly messageHandle = { 'messaging-history.set': async ({ messages, chats, contacts, isLatest, progress, syncType, }: { chats: Chat[]; contacts: Contact[]; messages: proto.IWebMessageInfo[]; isLatest?: boolean; progress?: number; syncType?: proto.HistorySync.HistorySyncType; }) => { try { if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { console.log('received on-demand history sync, messages=', messages); } console.log( `recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}`, ); const instance: InstanceDto = { instanceName: this.instance.name }; let timestampLimitToImport = null; if (this.configService.get('CHATWOOT').ENABLED) { const daysLimitToImport = this.localChatwoot?.enabled ? this.localChatwoot.daysLimitImportMessages : 1000; const date = new Date(); timestampLimitToImport = new Date(date.setDate(date.getDate() - daysLimitToImport)).getTime() / 1000; const maxBatchTimestamp = Math.max(...messages.map((message) => message.messageTimestamp as number)); const processBatch = maxBatchTimestamp >= timestampLimitToImport; if (!processBatch) { return; } } const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = []; const chatsRepository = new Set( ( await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId }, }) ).map((chat) => chat.remoteJid), ); for (const chat of chats) { if (chatsRepository?.has(chat.id)) { continue; } chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name, }); } this.sendDataWebhook(Events.CHATS_SET, chatsRaw); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true, }); } const messagesRaw: any[] = []; const messagesRepository = 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; }; return key.id; }), ); if (chatwootImport.getRepositoryMessagesCache(instance) === null) { chatwootImport.setRepositoryMessagesCache(instance, messagesRepository); } for (const m of messages) { if (!m.message || !m.key || !m.messageTimestamp) { continue; } if (Long.isLong(m?.messageTimestamp)) { m.messageTimestamp = m.messageTimestamp?.toNumber(); } if (this.configService.get('CHATWOOT').ENABLED) { if (m.messageTimestamp <= timestampLimitToImport) { continue; } } if (messagesRepository?.has(m.key.id)) { continue; } messagesRaw.push(this.prepareMessage(m)); } this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true, }); } if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && this.localChatwoot.importMessages && messagesRaw.length > 0 ) { this.chatwootService.addHistoryMessages( instance, messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)), ); } await this.contactHandle['contacts.upsert']( contacts .filter((c) => !!c.notify || !!c.name) .map((c) => ({ id: c.id, name: c.name ?? c.notify, })), ); contacts = undefined; messages = undefined; chats = undefined; } catch (error) { this.logger.error(error); } }, 'messages.upsert': async ( { messages, type, requestId, }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string; }, settings: any, ) => { try { for (const received of messages) { if (received.message?.conversation || received.message?.extendedTextMessage?.text) { const text = received.message?.conversation || received.message?.extendedTextMessage?.text; if (text == 'requestPlaceholder' && !requestId) { const messageId = await this.client.requestPlaceholderResend(received.key); console.log('requested placeholder resync, id=', messageId); } else if (requestId) { console.log('Message received from phone, id=', requestId, received); } if (text == 'onDemandHistSync') { const messageId = await this.client.fetchMessageHistory(50, received.key, received.messageTimestamp!); console.log('requested on-demand sync, id=', messageId); } } 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').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); } } 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, }); 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); } if ( (type !== 'notify' && type !== 'append') || received.message?.protocolMessage || received.message?.pollUpdateMessage || !received?.message ) { return; } if (Long.isLong(received.messageTimestamp)) { received.messageTimestamp = received.messageTimestamp?.toNumber(); } if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { return; } const messageRaw = this.prepareMessage(received); const isMedia = received?.message?.imageMessage || received?.message?.videoMessage || received?.message?.stickerMessage || received?.message?.documentMessage || received?.message?.documentWithCaptionMessage || received?.message?.audioMessage; if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') { await this.client.readMessages([received.key]); } if (this.localSettings.readStatus && received.key.id === 'status@broadcast') { await this.client.readMessages([received.key]); } if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !received.key.id.includes('@broadcast') ) { const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, { instanceName: this.instance.name, instanceId: this.instance.id }, messageRaw, ); if (chatwootSentMessage?.id) { messageRaw.chatwootMessageId = chatwootSentMessage.id; messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id; messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id; } } if (this.configService.get('OPENAI').ENABLED && received?.message?.audioMessage) { const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ where: { instanceId: this.instanceId, }, include: { OpenaiCreds: true, }, }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { messageRaw.message.speechToText = await this.openaiService.speechToText( openAiDefaultSettings.OpenaiCreds, received, this.client.updateMediaMessage, ); } } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { const msg = await this.prismaRepository.message.create({ data: messageRaw, }); if (isMedia) { if (this.configService.get('S3').ENABLE) { try { const message: any = received; const media = await this.getBase64FromMediaMessage( { message, }, true, ); const { buffer, mediaType, fileName, size } = media; const mimetype = mime.getType(fileName).toString(); const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName); await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, }); await this.prismaRepository.media.create({ data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype, }, }); 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]); } } } } if (isMedia && !this.configService.get('S3').ENABLE) { const buffer = await downloadMediaMessage( { key: received.key, message: received?.message }, 'buffer', {}, { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage, }, ); messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined; } this.logger.log(messageRaw); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, }); const contact = await this.prismaRepository.contact.findFirst({ where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, }); const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = { remoteJid: received.key.remoteJid, pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName, profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, instanceId: this.instanceId, }; if (contactRaw.remoteJid === 'status@broadcast') { return; } if (contact) { this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { await this.chatwootService.eventWhatsapp( Events.CONTACTS_UPDATE, { instanceName: this.instance.name, instanceId: this.instanceId }, contactRaw, ); } if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) await this.prismaRepository.contact.upsert({ where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId } }, create: contactRaw, update: contactRaw, }); return; } this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) await this.prismaRepository.contact.upsert({ where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId, }, }, update: contactRaw, create: contactRaw, }); if (contactRaw.remoteJid.includes('@s.whatsapp')) { await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]); } } } catch (error) { this.logger.error(error); } }, 'messages.update': async (args: WAMessageUpdate[], settings: any) => { const status: Record = { 0: 'ERROR', 1: 'PENDING', 2: 'SERVER_ACK', 3: 'DELIVERY_ACK', 4: 'READ', 5: 'PLAYED', }; for await (const { key, update } of args) { if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) { return; } if (status[update.status] === 'READ' && key.fromMe) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( 'messages.read', { instanceName: this.instance.name, instanceId: this.instanceId }, { key: key }, ); } } if (key.remoteJid !== 'status@broadcast') { let pollUpdates: any; if (update.pollUpdates) { const pollCreation = await this.getMessage(key); if (pollCreation) { pollUpdates = getAggregateVotesInPollMessage({ message: pollCreation as proto.IMessage, pollUpdates: update.pollUpdates, }); } } const findMessage = await this.prismaRepository.message.findFirst({ where: { instanceId: this.instanceId, key: { path: ['id'], equals: key.id, }, }, }); if (!findMessage) { return; } if (status[update.status] === 'READ' && !key.fromMe) return; if (update.message === null && update.status === undefined) { this.sendDataWebhook(Events.MESSAGES_DELETE, key); const message: any = { messageId: findMessage.id, keyId: key.id, remoteJid: key.remoteJid, fromMe: key.fromMe, participant: key?.remoteJid, status: 'DELETED', instanceId: this.instanceId, }; if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) await this.prismaRepository.messageUpdate.create({ data: message, }); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( Events.MESSAGES_DELETE, { instanceName: this.instance.name, instanceId: this.instanceId }, { key: key }, ); } return; } const message: any = { messageId: findMessage.id, keyId: key.id, remoteJid: key.remoteJid, fromMe: key.fromMe, participant: key?.remoteJid, status: status[update.status], pollUpdates, instanceId: this.instanceId, }; this.sendDataWebhook(Events.MESSAGES_UPDATE, message); if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) await this.prismaRepository.messageUpdate.create({ data: message, }); } } }, }; private readonly groupHandler = { 'groups.upsert': (groupMetadata: GroupMetadata[]) => { this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); }, 'groups.update': (groupMetadataUpdate: Partial[]) => { this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); groupMetadataUpdate.forEach((group) => { if (isJidGroup(group.id)) { this.updateGroupMetadataCache(group.id); } }); }, 'group-participants.update': (participantsUpdate: { id: string; participants: string[]; action: ParticipantAction; }) => { this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); this.updateGroupMetadataCache(participantsUpdate.id); }, }; private readonly labelHandle = { [Events.LABELS_EDIT]: async (label: Label) => { const labelsRepository = await this.prismaRepository.label.findMany({ where: { instanceId: this.instanceId }, }); const savedLabel = labelsRepository.find((l) => l.labelId === label.id); if (label.deleted && savedLabel) { await this.prismaRepository.label.delete({ where: { labelId_instanceId: { instanceId: this.instanceId, labelId: label.id } }, }); this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); return; } const labelName = label.name.replace(/[^\x20-\x7E]/g, ''); if (!savedLabel || savedLabel.color !== `${label.color}` || savedLabel.name !== labelName) { if (this.configService.get('DATABASE').SAVE_DATA.LABELS) { const labelData = { color: `${label.color}`, name: labelName, labelId: label.id, predefinedId: label.predefinedId, instanceId: this.instanceId, }; await this.prismaRepository.label.upsert({ where: { labelId_instanceId: { instanceId: labelData.instanceId, labelId: labelData.labelId, }, }, update: labelData, create: labelData, }); } this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); } }, [Events.LABELS_ASSOCIATION]: async ( data: { association: LabelAssociation; type: 'remove' | 'add' }, database: Database, ) => { if (database.SAVE_DATA.CHATS) { const chats = await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId }, }); const chat = chats.find((c) => c.remoteJid === data.association.chatId); if (chat) { const labelsArray = Array.isArray(chat.labels) ? chat.labels.map((event) => String(event)) : []; let labels = [...labelsArray]; if (data.type === 'remove') { labels = labels.filter((label) => label !== data.association.labelId); } else if (data.type === 'add') { labels = [...labels, data.association.labelId]; } await this.prismaRepository.chat.update({ where: { id: chat.id }, data: { labels, }, }); } } this.sendDataWebhook(Events.LABELS_ASSOCIATION, { instance: this.instance.name, type: data.type, chatId: data.association.chatId, labelId: data.association.labelId, }); }, }; private eventHandler() { this.client.ev.process(async (events) => { if (!this.endSession) { const database = this.configService.get('DATABASE'); const settings = await this.findSettings(); if (events.call) { const call = events.call[0]; if (settings?.rejectCall && call.status == 'offer') { this.client.rejectCall(call.id, call.from); } if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { const msg = await this.client.sendMessage(call.from, { text: settings.msgCall, }); this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify', }); } this.sendDataWebhook(Events.CALL, call); } if (events['connection.update']) { this.connectionUpdate(events['connection.update']); } if (events['creds.update']) { this.instance.authState.saveCreds(); } if (events['messaging-history.set']) { const payload = events['messaging-history.set']; this.messageHandle['messaging-history.set'](payload); } if (events['messages.upsert']) { const payload = events['messages.upsert']; this.messageHandle['messages.upsert'](payload, settings); } if (events['messages.update']) { const payload = events['messages.update']; this.messageHandle['messages.update'](payload, settings); } if (events['presence.update']) { const payload = events['presence.update']; if (settings?.groupsIgnore && payload.id.includes('@g.us')) { return; } this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); } if (!settings?.groupsIgnore) { if (events['groups.upsert']) { const payload = events['groups.upsert']; this.groupHandler['groups.upsert'](payload); } if (events['groups.update']) { const payload = events['groups.update']; this.groupHandler['groups.update'](payload); } if (events['group-participants.update']) { const payload = events['group-participants.update']; this.groupHandler['group-participants.update'](payload); } } if (events['chats.upsert']) { const payload = events['chats.upsert']; this.chatHandle['chats.upsert'](payload); } if (events['chats.update']) { const payload = events['chats.update']; this.chatHandle['chats.update'](payload); } if (events['chats.delete']) { const payload = events['chats.delete']; this.chatHandle['chats.delete'](payload); } if (events['contacts.upsert']) { const payload = events['contacts.upsert']; this.contactHandle['contacts.upsert'](payload); } if (events['contacts.update']) { const payload = events['contacts.update']; this.contactHandle['contacts.update'](payload); } if (events[Events.LABELS_ASSOCIATION]) { const payload = events[Events.LABELS_ASSOCIATION]; this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); return; } if (events[Events.LABELS_EDIT]) { const payload = events[Events.LABELS_EDIT]; this.labelHandle[Events.LABELS_EDIT](payload); return; } } }); } private historySyncNotification(msg: proto.Message.IHistorySyncNotification) { const instance: InstanceDto = { instanceName: this.instance.name }; if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && this.localChatwoot.importMessages && this.isSyncNotificationFromUsedSyncType(msg) ) { if (msg.chunkOrder === 1) { this.chatwootService.startImportHistoryMessages(instance); } if (msg.progress === 100) { setTimeout(() => { this.chatwootService.importHistoryMessages(instance); }, 10000); } } return true; } private isSyncNotificationFromUsedSyncType(msg: proto.Message.IHistorySyncNotification) { return ( (this.localSettings.syncFullHistory && msg?.syncType === 2) || (!this.localSettings.syncFullHistory && msg?.syncType === 3) ); } public async profilePicture(number: string) { const jid = this.createJid(number); try { const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image'); return { wuid: jid, profilePictureUrl, }; } catch (error) { return { wuid: jid, profilePictureUrl: null, }; } } public async getStatus(number: string) { const jid = this.createJid(number); try { return { wuid: jid, status: (await this.client.fetchStatus(jid))?.status, }; } catch (error) { return { wuid: jid, status: null, }; } } public async fetchProfile(instanceName: string, number?: string) { const jid = number ? this.createJid(number) : this.client?.user?.id; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); if (!onWhatsapp.exists) { throw new BadRequestException(onWhatsapp); } try { if (number) { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); const picture = await this.profilePicture(info?.jid); const status = await this.getStatus(info?.jid); const business = await this.fetchBusinessProfile(info?.jid); return { wuid: info?.jid || jid, name: info?.name, numberExists: info?.exists, picture: picture?.profilePictureUrl, status: status?.status, isBusiness: business.isBusiness, email: business?.email, description: business?.description, website: business?.website?.shift(), }; } else { const info: Instance = await waMonitor.instanceInfo(instanceName); const business = await this.fetchBusinessProfile(jid); return { wuid: jid, name: info?.profileName, numberExists: true, picture: info?.profilePicUrl, status: info?.connectionStatus, isBusiness: business.isBusiness, email: business?.email, description: business?.description, website: business?.website?.shift(), }; } } catch (error) { return { wuid: jid, name: null, picture: null, status: null, os: null, isBusiness: false, }; } } private async sendMessage( sender: string, message: any, mentions: any, linkPreview: any, quoted: any, messageId?: string, ephemeralExpiration?: number, // participants?: GroupParticipant[], ) { sender = sender.toLowerCase(); const option: any = { quoted, }; if (isJidGroup(sender)) { option.useCachedGroupMetadata = true; // if (participants) // option.cachedGroupMetadata = async () => { // return { participants: participants as GroupParticipant[] }; // }; } if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration; if (messageId) option.messageId = messageId; else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase(); if ( !message['audio'] && !message['poll'] && !message['sticker'] && !message['conversation'] && sender !== 'status@broadcast' ) { if (message['reactionMessage']) { return await this.client.sendMessage( sender, { react: { text: message['reactionMessage']['text'], key: message['reactionMessage']['key'], }, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } } if (message['conversation']) { return await this.client.sendMessage( sender, { text: message['conversation'], mentions, linkPreview: linkPreview, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') { return await this.client.sendMessage( sender, { forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message, }, mentions, }, option as unknown as MiscMessageGenerationOptions, ); } if (sender === 'status@broadcast') { let jidList; if (message['status'].option.allContacts) { const contacts = await this.prismaRepository.contact.findMany({ where: { instanceId: this.instanceId, remoteJid: { not: { endsWith: '@g.us', }, }, }, }); jidList = contacts.map((contact) => contact.remoteJid); } else { jidList = message['status'].option.statusJidList; } const batchSize = 10; const batches = Array.from({ length: Math.ceil(jidList.length / batchSize) }, (_, i) => jidList.slice(i * batchSize, i * batchSize + batchSize), ); let msgId: string | null = null; let firstMessage: WAMessage; const firstBatch = batches.shift(); if (firstBatch) { firstMessage = await this.client.sendMessage( sender, message['status'].content as unknown as AnyMessageContent, { backgroundColor: message['status'].option.backgroundColor, font: message['status'].option.font, statusJidList: firstBatch, } as unknown as MiscMessageGenerationOptions, ); msgId = firstMessage.key.id; } if (batches.length === 0) return firstMessage; await Promise.allSettled( batches.map(async (batch) => { const messageSent = await this.client.sendMessage( sender, message['status'].content as unknown as AnyMessageContent, { backgroundColor: message['status'].option.backgroundColor, font: message['status'].option.font, statusJidList: batch, messageId: msgId, } as unknown as MiscMessageGenerationOptions, ); return messageSent; }), ); return firstMessage; } return await this.client.sendMessage( sender, message as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } private async sendMessageWithTyping( number: string, message: T, options?: Options, isIntegration = false, ) { const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { const body = { key: { remoteJid: isWA.jid }, }; this.chatwootService.eventWhatsapp( 'contact.is_not_in_wpp', { instanceName: this.instance.name, instanceId: this.instance.id }, body, ); } throw new BadRequestException(isWA); } const sender = isWA.jid.toLowerCase(); this.logger.verbose(`Sending message to ${sender}`); try { if (options?.delay) { this.logger.verbose(`Typing for ${options.delay}ms to ${sender}`); if (options.delay > 20000) { let remainingDelay = options.delay; while (remainingDelay > 20000) { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); await delay(20000); await this.client.sendPresenceUpdate('paused', sender); remainingDelay -= 20000; } if (remainingDelay > 0) { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); await delay(remainingDelay); await this.client.sendPresenceUpdate('paused', sender); } } else { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender); await delay(options.delay); await this.client.sendPresenceUpdate('paused', sender); } } const linkPreview = options?.linkPreview != false ? undefined : false; let quoted: WAMessage; if (options?.quoted) { const m = options?.quoted; const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); if (msg) { quoted = msg; } } let messageSent: WAMessage; let mentions: string[]; if (isJidGroup(sender)) { let group; try { const cache = this.configService.get('CACHE'); if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner'); else group = await this.getGroupMetadataCache(sender); } catch (error) { throw new NotFoundException('Group not found'); } if (!group) { throw new NotFoundException('Group not found'); } if (options?.mentionsEveryOne) { mentions = group.participants.map((participant) => participant.id); } else if (options.mentioned?.length) { mentions = options.mentioned.map((mention) => { const jid = this.createJid(mention); if (isJidGroup(jid)) { return null; } return jid; }); } messageSent = await this.sendMessage( sender, message, mentions, linkPreview, quoted, null, group?.ephemeralDuration, // group?.participants, ); } else { messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted); } if (Long.isLong(messageSent?.messageTimestamp)) { messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber(); } const messageRaw = this.prepareMessage(messageSent); const isMedia = messageSent?.message?.imageMessage || messageSent?.message?.videoMessage || messageSent?.message?.stickerMessage || messageSent?.message?.documentMessage || messageSent?.message?.documentWithCaptionMessage || messageSent?.message?.audioMessage; if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) { this.chatwootService.eventWhatsapp( Events.SEND_MESSAGE, { instanceName: this.instance.name, instanceId: this.instanceId }, messageRaw, ); } if (this.configService.get('OPENAI').ENABLED && messageRaw?.message?.audioMessage) { const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ where: { instanceId: this.instanceId, }, include: { OpenaiCreds: true, }, }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { messageRaw.message.speechToText = await this.openaiService.speechToText( openAiDefaultSettings.OpenaiCreds, messageRaw, this.client.updateMediaMessage, ); } } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { const msg = await this.prismaRepository.message.create({ data: messageRaw, }); if (isMedia && this.configService.get('S3').ENABLE) { try { const message: any = messageRaw; const media = await this.getBase64FromMediaMessage( { message, }, true, ); const { buffer, mediaType, fileName, size } = media; const mimetype = mime.getType(fileName).toString(); const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName); await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, }); await this.prismaRepository.media.create({ data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype, }, }); 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]); } } } if (isMedia && !this.configService.get('S3').ENABLE) { const buffer = await downloadMediaMessage( { key: messageRaw.key, message: messageRaw?.message }, 'buffer', {}, { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage, }, ); messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined; } this.logger.log(messageRaw); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration) { await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, isIntegration, }); } return messageRaw; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } // Instance Controller public async sendPresence(data: SendPresenceDto) { try { const { number } = data; const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } const sender = isWA.jid; if (data?.delay && data?.delay > 20000) { let remainingDelay = data?.delay; while (remainingDelay > 20000) { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); await delay(20000); await this.client.sendPresenceUpdate('paused', sender); remainingDelay -= 20000; } if (remainingDelay > 0) { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); await delay(remainingDelay); await this.client.sendPresenceUpdate('paused', sender); } } else { await this.client.presenceSubscribe(sender); await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender); await delay(data?.delay); await this.client.sendPresenceUpdate('paused', sender); } return { presence: data.presence }; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } // Presence Controller public async setPresence(data: SetPresenceDto) { try { await this.client.sendPresenceUpdate(data.presence); return { presence: data.presence }; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } // Send Message Controller public async textMessage(data: SendTextDto, isIntegration = false) { const text = data.text; if (!text || text.trim().length === 0) { throw new BadRequestException('Text is required'); } return await this.sendMessageWithTyping( data.number, { conversation: data.text, }, { delay: data?.delay, presence: 'composing', quoted: data?.quoted, linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, isIntegration, ); } public async pollMessage(data: SendPollDto) { return await this.sendMessageWithTyping( data.number, { poll: { name: data.name, selectableCount: data.selectableCount, values: data.values, }, }, { delay: data?.delay, presence: 'composing', quoted: data?.quoted, linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, ); } private async formatStatusMessage(status: StatusMessage) { if (!status.type) { throw new BadRequestException('Type is required'); } if (!status.content) { throw new BadRequestException('Content is required'); } if (status.allContacts) { const contacts = await this.prismaRepository.contact.findMany({ where: { instanceId: this.instanceId }, }); if (!contacts.length) { throw new BadRequestException('Contacts not found'); } status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.remoteJid); } if (!status.statusJidList?.length && !status.allContacts) { throw new BadRequestException('StatusJidList is required'); } if (status.type === 'text') { if (!status.backgroundColor) { throw new BadRequestException('Background color is required'); } if (!status.font) { throw new BadRequestException('Font is required'); } return { content: { text: status.content, }, option: { backgroundColor: status.backgroundColor, font: status.font, statusJidList: status.statusJidList, }, }; } if (status.type === 'image') { return { content: { image: { url: status.content, }, caption: status.caption, }, option: { statusJidList: status.statusJidList, }, }; } if (status.type === 'video') { return { content: { video: { url: status.content, }, caption: status.caption, }, option: { statusJidList: status.statusJidList, }, }; } if (status.type === 'audio') { const convert = await this.processAudioMp4(status.content); if (Buffer.isBuffer(convert)) { const result = { content: { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus', }, option: { statusJidList: status.statusJidList, }, }; return result; } else { throw new InternalServerErrorException(convert); } } throw new BadRequestException('Type not found'); } public async statusMessage(data: SendStatusDto, file?: any) { const mediaData: SendStatusDto = { ...data }; if (file) mediaData.content = file.buffer.toString('base64'); const status = await this.formatStatusMessage(mediaData); const statusSent = await this.sendMessageWithTyping('status@broadcast', { status, }); return statusSent; } private async prepareMediaMessage(mediaMessage: MediaMessage) { try { const prepareMedia = await prepareWAMessageMedia( { [mediaMessage.mediatype]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'), } as any, { upload: this.client.waUploadToServer }, ); const mediaType = mediaMessage.mediatype + 'Message'; if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { const regex = new RegExp(/.*\/(.+?)\./); const arrayMatch = regex.exec(mediaMessage.media); mediaMessage.fileName = arrayMatch[1]; } if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { mediaMessage.fileName = 'image.png'; } if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { mediaMessage.fileName = 'video.mp4'; } let mimetype: string; if (mediaMessage.mimetype) { mimetype = mediaMessage.mimetype; } else { mimetype = mime.getType(mediaMessage.fileName); if (!mimetype && isURL(mediaMessage.media)) { let config: any = { responseType: 'arraybuffer', }; if (this.localProxy?.enabled) { config = { ...config, httpsAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), }; } const response = await axios.get(mediaMessage.media, config); mimetype = response.headers['content-type']; } } prepareMedia[mediaType].caption = mediaMessage?.caption; prepareMedia[mediaType].mimetype = mimetype; prepareMedia[mediaType].fileName = mediaMessage.fileName; if (mediaMessage.mediatype === 'video') { prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), ); prepareMedia[mediaType].gifPlayback = false; } return generateWAMessageFromContent( '', { [mediaType]: { ...prepareMedia[mediaType] } }, { userJid: this.instance.wuid }, ); } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString() || error); } } private async convertToWebP(image: string): Promise { try { let imageBuffer: Buffer; if (isBase64(image)) { const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); imageBuffer = Buffer.from(base64Data, 'base64'); } else { const timestamp = new Date().getTime(); const url = `${image}?timestamp=${timestamp}`; let config: any = { responseType: 'arraybuffer', }; if (this.localProxy?.enabled) { config = { ...config, httpsAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), }; } const response = await axios.get(url, config); imageBuffer = Buffer.from(response.data, 'binary'); } const webpBuffer = await sharp(imageBuffer).webp().toBuffer(); return webpBuffer; } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); throw error; } } public async mediaSticker(data: SendStickerDto, file?: any) { const mediaData: SendStickerDto = { ...data }; if (file) mediaData.sticker = file.buffer.toString('base64'); const convert = await this.convertToWebP(data.sticker); const gifPlayback = data.sticker.includes('.gif'); const result = await this.sendMessageWithTyping( data.number, { sticker: convert, gifPlayback, }, { delay: data?.delay, presence: 'composing', quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, ); return result; } public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) { const mediaData: SendMediaDto = { ...data }; if (file) mediaData.media = file.buffer.toString('base64'); const generate = await this.prepareMediaMessage(mediaData); const mediaSent = await this.sendMessageWithTyping( data.number, { ...generate.message }, { delay: data?.delay, presence: 'composing', quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, isIntegration, ); return mediaSent; } public async processAudioMp4(audio: string) { let inputStream: PassThrough; if (isURL(audio)) { const response = await axios.get(audio, { responseType: 'stream' }); inputStream = response.data; } else { const audioBuffer = Buffer.from(audio, 'base64'); inputStream = new PassThrough(); inputStream.end(audioBuffer); } return new Promise((resolve, reject) => { const ffmpegProcess = spawn(ffmpegPath.path, [ '-i', 'pipe:0', '-vn', '-ab', '128k', '-ar', '44100', '-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov', 'pipe:1', ]); const outputChunks: Buffer[] = []; let stderrData = ''; ffmpegProcess.stdout.on('data', (chunk) => { outputChunks.push(chunk); }); ffmpegProcess.stderr.on('data', (data) => { stderrData += data.toString(); this.logger.verbose(`ffmpeg stderr: ${data}`); }); ffmpegProcess.on('error', (error) => { console.error('Error in ffmpeg process', error); reject(error); }); ffmpegProcess.on('close', (code) => { if (code === 0) { this.logger.verbose('Audio converted to mp4'); const outputBuffer = Buffer.concat(outputChunks); resolve(outputBuffer); } else { this.logger.error(`ffmpeg exited with code ${code}`); this.logger.error(`ffmpeg stderr: ${stderrData}`); reject(new Error(`ffmpeg exited with code ${code}: ${stderrData}`)); } }); inputStream.pipe(ffmpegProcess.stdin); inputStream.on('error', (err) => { console.error('Error in inputStream', err); ffmpegProcess.stdin.end(); reject(err); }); }); } public async processAudio(audio: string): Promise { let inputAudioStream: PassThrough; if (isURL(audio)) { const timestamp = new Date().getTime(); const url = `${audio}?timestamp=${timestamp}`; const config: any = { responseType: 'stream', }; const response = await axios.get(url, config); inputAudioStream = response.data.pipe(new PassThrough()); } else { const audioBuffer = Buffer.from(audio, 'base64'); inputAudioStream = new PassThrough(); inputAudioStream.end(audioBuffer); } return new Promise((resolve, reject) => { const outputAudioStream = new PassThrough(); const chunks: Buffer[] = []; outputAudioStream.on('data', (chunk) => chunks.push(chunk)); outputAudioStream.on('end', () => { const outputBuffer = Buffer.concat(chunks); resolve(outputBuffer); }); outputAudioStream.on('error', (error) => { console.log('error', error); reject(error); }); ffmpeg.setFfmpegPath(ffmpegPath.path); ffmpeg(inputAudioStream) .outputFormat('ogg') .noVideo() .audioCodec('libopus') .addOutputOptions('-avoid_negative_ts make_zero') .audioChannels(1) .pipe(outputAudioStream, { end: true }) .on('error', function (error) { console.log('error', error); reject(error); }); }); } public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { const mediaData: SendAudioDto = { ...data }; if (file) mediaData.audio = file.buffer.toString('base64'); if (!data?.encoding && data?.encoding !== false) { data.encoding = true; } if (data?.encoding) { const convert = await this.processAudio(mediaData.audio); if (Buffer.isBuffer(convert)) { const result = this.sendMessageWithTyping( data.number, { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus', }, { presence: 'recording', delay: data?.delay }, isIntegration, ); return result; } else { throw new InternalServerErrorException('Failed to convert audio'); } } return await this.sendMessageWithTyping( data.number, { audio: isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'), ptt: true, mimetype: 'audio/ogg; codecs=opus', }, { presence: 'recording', delay: data?.delay }, isIntegration, ); } public async buttonMessage() { throw new BadRequestException('Method not available on WhatsApp Baileys'); } public async locationMessage(data: SendLocationDto) { return await this.sendMessageWithTyping( data.number, { locationMessage: { degreesLatitude: data.latitude, degreesLongitude: data.longitude, name: data?.name, address: data?.address, }, }, { delay: data?.delay, presence: 'composing', quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, }, ); } public async listMessage() { throw new BadRequestException('Method not available on WhatsApp Baileys'); } public async contactMessage(data: SendContactDto) { const message: proto.IMessage = {}; const vcard = (contact: ContactMessage) => { let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; if (contact.organization) { result += `ORG:${contact.organization};\n`; } if (contact.email) { result += `EMAIL:${contact.email}\n`; } if (contact.url) { result += `URL:${contact.url}\n`; } if (!contact.wuid) { contact.wuid = this.createJid(contact.phoneNumber); } result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; return result; }; if (data.contact.length === 1) { message.contactMessage = { displayName: data.contact[0].fullName, vcard: vcard(data.contact[0]), }; } else { message.contactsArrayMessage = { displayName: `${data.contact.length} contacts`, contacts: data.contact.map((contact) => { return { displayName: contact.fullName, vcard: vcard(contact), }; }), }; } return await this.sendMessageWithTyping(data.number, { ...message }, {}); } public async reactionMessage(data: SendReactionDto) { return await this.sendMessageWithTyping(data.key.remoteJid, { reactionMessage: { key: data.key, text: data.reaction, }, }); } // Chat Controller public async whatsappNumber(data: WhatsAppNumberDto) { const jids: { groups: { number: string; jid: string }[]; broadcast: { number: string; jid: string }[]; users: { number: string; jid: string; name?: string }[]; } = { groups: [], broadcast: [], users: [], }; data.numbers.forEach((number) => { const jid = this.createJid(number); if (isJidGroup(jid)) { jids.groups.push({ number, jid }); } else if (jid === 'status@broadcast') { jids.broadcast.push({ number, jid }); } else { jids.users.push({ number, jid }); } }); const onWhatsapp: OnWhatsAppDto[] = []; // BROADCAST onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number))); // GROUPS const groups = await Promise.all( jids.groups.map(async ({ jid, number }) => { const group = await this.findGroup({ groupJid: jid }, 'inner'); if (!group) { new OnWhatsAppDto(jid, false, number); } return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject); }), ); onWhatsapp.push(...groups); // USERS const contacts: any[] = await this.prismaRepository.contact.findMany({ where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid), }, }, }); const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', '')); const cachedNumbers = await getOnWhatsappCache(numbersToVerify); const filteredNumbers = numbersToVerify.filter( (jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)), ); const verify = await this.client.onWhatsApp(...filteredNumbers); const users: OnWhatsAppDto[] = await Promise.all( jids.users.map(async (user) => { let numberVerified: (typeof verify)[0] | null = null; const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', ''))); if (cached) { return { exists: true, jid: cached.remoteJid, name: contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName, number: user.number, }; } // Brazilian numbers if (user.number.startsWith('55')) { const numberWithDigit = user.number.slice(4, 5) === '9' && user.number.length === 13 ? user.number : `${user.number.slice(0, 4)}9${user.number.slice(4)}`; const numberWithoutDigit = user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5); numberVerified = verify.find( (v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`, ); } // Mexican/Argentina numbers // Ref: https://faq.whatsapp.com/1294841057948784 if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) { let prefix = ''; if (user.number.startsWith('52')) { prefix = ''; } if (user.number.startsWith('54')) { prefix = '9'; } const numberWithDigit = user.number.slice(2, 3) === prefix && user.number.length === 13 ? user.number : `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`; const numberWithoutDigit = user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3); numberVerified = verify.find( (v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`, ); } if (!numberVerified) { numberVerified = verify.find((v) => v.jid === user.jid); } const numberJid = numberVerified?.jid || user.jid; return { exists: !!numberVerified?.exists, jid: numberJid, name: contacts.find((c) => c.remoteJid === numberJid)?.pushName, number: user.number, }; }), ); await saveOnWhatsappCache(users.filter((user) => user.exists).map((user) => ({ remoteJid: user.jid }))); onWhatsapp.push(...users); return onWhatsapp; } public async markMessageAsRead(data: ReadMessageDto) { try { const keys: proto.IMessageKey[] = []; data.readMessages.forEach((read) => { if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { keys.push({ remoteJid: read.remoteJid, fromMe: read.fromMe, id: read.id, }); } }); await this.client.readMessages(keys); return { message: 'Read messages', read: 'success' }; } catch (error) { throw new InternalServerErrorException('Read messages fail', error.toString()); } } public async getLastMessage(number: string) { const where: any = { key: { remoteJid: number, }, instanceId: this.instance.id, }; const messages = await this.prismaRepository.message.findMany({ where, orderBy: { messageTimestamp: 'desc', }, take: 1, }); if (messages.length === 0) { throw new NotFoundException('Messages not found'); } let lastMessage = messages.pop(); for (const message of messages) { if (message.messageTimestamp >= lastMessage.messageTimestamp) { lastMessage = message; } } return lastMessage as unknown as LastMessage; } public async archiveChat(data: ArchiveChatDto) { try { let last_message = data.lastMessage; let number = data.chat; if (!last_message && number) { last_message = await this.getLastMessage(number); } else { last_message = data.lastMessage; last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); number = last_message?.key?.remoteJid; } if (!last_message || Object.keys(last_message).length === 0) { throw new NotFoundException('Last message not found'); } await this.client.chatModify( { archive: data.archive, lastMessages: [last_message], }, this.createJid(number), ); return { chatId: number, archived: true, }; } catch (error) { throw new InternalServerErrorException({ archived: false, message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], }); } } public async markChatUnread(data: MarkChatUnreadDto) { try { let last_message = data.lastMessage; let number = data.chat; if (!last_message && number) { last_message = await this.getLastMessage(number); } else { last_message = data.lastMessage; last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); number = last_message?.key?.remoteJid; } if (!last_message || Object.keys(last_message).length === 0) { throw new NotFoundException('Last message not found'); } await this.client.chatModify( { markRead: false, lastMessages: [last_message], }, this.createJid(number), ); return { chatId: number, markedChatUnread: true, }; } catch (error) { throw new InternalServerErrorException({ markedChatUnread: false, message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()], }); } } public async deleteMessage(del: DeleteMessage) { try { return await this.client.sendMessage(del.remoteJid, { delete: del }); } catch (error) { throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); } } public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { try { const m = data?.message; const convertToMp4 = data?.convertToMp4 ?? false; const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); if (!msg) { throw 'Message not found'; } for (const subtype of MessageSubtype) { if (msg.message[subtype]) { msg.message = msg.message[subtype].message; } } let mediaMessage: any; let mediaType: string; 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, }, ); const typeMessage = getContentType(msg.message); const ext = mime.getExtension(mediaMessage?.['mimetype']); const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`; if (convertToMp4 && typeMessage === 'audioMessage') { try { const convert = await this.processAudioMp4(buffer.toString('base64')); if (Buffer.isBuffer(convert)) { const result = { mediaType, fileName, caption: mediaMessage['caption'], size: { fileLength: mediaMessage['fileLength'], height: mediaMessage['height'], width: mediaMessage['width'], }, mimetype: 'audio/mp4', base64: convert.toString('base64'), buffer: getBuffer ? convert : null, }; return result; } } catch (error) { this.logger.error('Error converting audio to mp4:'); this.logger.error(error); throw new BadRequestException('Failed to convert audio to MP4'); } } return { mediaType, fileName, caption: mediaMessage['caption'], size: { fileLength: mediaMessage['fileLength'], height: mediaMessage['height'], width: mediaMessage['width'], }, mimetype: mediaMessage['mimetype'], base64: buffer.toString('base64'), buffer: getBuffer ? buffer : null, }; } catch (error) { this.logger.error('Error processing media message:'); this.logger.error(error); throw new BadRequestException(error.toString()); } } public async fetchPrivacySettings() { const privacy = await this.client.fetchPrivacySettings(); return { readreceipts: privacy.readreceipts, profile: privacy.profile, status: privacy.status, online: privacy.online, last: privacy.last, groupadd: privacy.groupadd, }; } public async updatePrivacySettings(settings: PrivacySettingDto) { try { await this.client.updateReadReceiptsPrivacy(settings.readreceipts); await this.client.updateProfilePicturePrivacy(settings.profile); await this.client.updateStatusPrivacy(settings.status); await this.client.updateOnlinePrivacy(settings.online); await this.client.updateLastSeenPrivacy(settings.last); await this.client.updateGroupsAddPrivacy(settings.groupadd); this.reloadConnection(); return { update: 'success', data: { readreceipts: settings.readreceipts, profile: settings.profile, status: settings.status, online: settings.online, last: settings.last, groupadd: settings.groupadd, }, }; } catch (error) { throw new InternalServerErrorException('Error updating privacy settings', error.toString()); } } public async fetchBusinessProfile(number: string): Promise { try { const jid = number ? this.createJid(number) : this.instance.wuid; const profile = await this.client.getBusinessProfile(jid); if (!profile) { const info = await this.whatsappNumber({ numbers: [jid] }); return { isBusiness: false, message: 'Not is business profile', ...info?.shift(), }; } return { isBusiness: true, ...profile, }; } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } public async updateProfileName(name: string) { try { await this.client.updateProfileName(name); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } public async updateProfileStatus(status: string) { try { await this.client.updateProfileStatus(status); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating profile status', error.toString()); } } public async updateProfilePicture(picture: string) { try { let pic: WAMediaUpload; if (isURL(picture)) { const timestamp = new Date().getTime(); const url = `${picture}?timestamp=${timestamp}`; let config: any = { responseType: 'arraybuffer', }; if (this.localProxy?.enabled) { config = { ...config, httpsAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), }; } pic = (await axios.get(url, config)).data; } else if (isBase64(picture)) { pic = Buffer.from(picture, 'base64'); } else { throw new BadRequestException('"profilePicture" must be a url or a base64'); } await this.client.updateProfilePicture(this.instance.wuid, pic); this.reloadConnection(); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating profile picture', error.toString()); } } public async removeProfilePicture() { try { await this.client.removeProfilePicture(this.instance.wuid); this.reloadConnection(); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error removing profile picture', error.toString()); } } public async blockUser(data: BlockUserDto) { try { const { number } = data; const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } const sender = isWA.jid; await this.client.updateBlockStatus(sender, data.status); return { block: 'success' }; } catch (error) { throw new InternalServerErrorException('Error blocking user', error.toString()); } } private async formatUpdateMessage(data: UpdateMessageDto) { try { const msg: any = await this.getMessage(data.key, true); if (msg?.messageType === 'conversation' || msg?.messageType === 'extendedTextMessage') { return { text: data.text, }; } if (msg?.messageType === 'imageMessage') { return { image: msg?.message?.imageMessage, caption: data.text, }; } if (msg?.messageType === 'videoMessage') { return { video: msg?.message?.videoMessage, caption: data.text, }; } return null; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } public async updateMessage(data: UpdateMessageDto) { const jid = this.createJid(data.number); const options = await this.formatUpdateMessage(data); if (!options) { this.logger.error('Message not compatible'); throw new BadRequestException('Message not compatible'); } try { return await this.client.sendMessage(jid, { ...(options as any), edit: data.key, }); } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } public async fetchLabels(): Promise { const labels = await this.prismaRepository.label.findMany({ where: { instanceId: this.instanceId, }, }); return labels.map((label) => ({ color: label.color, name: label.name, id: label.labelId, predefinedId: label.predefinedId, })); } public async handleLabel(data: HandleLabelDto) { const whatsappContact = await this.whatsappNumber({ numbers: [data.number] }); if (whatsappContact.length === 0) { throw new NotFoundException('Number not found'); } const contact = whatsappContact[0]; if (!contact.exists) { throw new NotFoundException('Number is not on WhatsApp'); } try { if (data.action === 'add') { await this.client.addChatLabel(contact.jid, data.labelId); return { numberJid: contact.jid, labelId: data.labelId, add: true }; } if (data.action === 'remove') { await this.client.removeChatLabel(contact.jid, data.labelId); return { numberJid: contact.jid, labelId: data.labelId, remove: true }; } } catch (error) { throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString()); } } // Group private async updateGroupMetadataCache(groupJid: string) { try { const meta = await this.client.groupMetadata(groupJid); const cacheConf = this.configService.get('CACHE'); if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { this.logger.verbose(`Updating cache for group: ${groupJid}`); await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta, }); } return meta; } catch (error) { this.logger.error(error); return null; } } private getGroupMetadataCache = async (groupJid: string) => { if (!isJidGroup(groupJid)) return null; const cacheConf = this.configService.get('CACHE'); if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { if (await groupMetadataCache?.has(groupJid)) { console.log(`Cache request for group: ${groupJid}`); const meta = await groupMetadataCache.get(groupJid); if (Date.now() - meta.timestamp > 3600000) { await this.updateGroupMetadataCache(groupJid); } return meta.data; } console.log(`Cache request for group: ${groupJid} - not found`); return await this.updateGroupMetadataCache(groupJid); } return await this.findGroup({ groupJid }, 'inner'); }; public async createGroup(create: CreateGroupDto) { try { const participants = (await this.whatsappNumber({ numbers: create.participants })) .filter((participant) => participant.exists) .map((participant) => participant.jid); const { id } = await this.client.groupCreate(create.subject, participants); if (create?.description) { await this.client.groupUpdateDescription(id, create.description); } if (create?.promoteParticipants) { await this.updateGParticipant({ groupJid: id, action: 'promote', participants: participants, }); } const group = await this.client.groupMetadata(id); return group; } catch (error) { this.logger.error(error); throw new InternalServerErrorException('Error creating group', error.toString()); } } public async updateGroupPicture(picture: GroupPictureDto) { try { let pic: WAMediaUpload; if (isURL(picture.image)) { const timestamp = new Date().getTime(); const url = `${picture.image}?timestamp=${timestamp}`; let config: any = { responseType: 'arraybuffer', }; if (this.localProxy?.enabled) { config = { ...config, httpsAgent: makeProxyAgent({ host: this.localProxy.host, port: this.localProxy.port, protocol: this.localProxy.protocol, username: this.localProxy.username, password: this.localProxy.password, }), }; } pic = (await axios.get(url, config)).data; } else if (isBase64(picture.image)) { pic = Buffer.from(picture.image, 'base64'); } else { throw new BadRequestException('"profilePicture" must be a url or a base64'); } await this.client.updateProfilePicture(picture.groupJid, pic); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error update group picture', error.toString()); } } public async updateGroupSubject(data: GroupSubjectDto) { try { await this.client.groupUpdateSubject(data.groupJid, data.subject); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating group subject', error.toString()); } } public async updateGroupDescription(data: GroupDescriptionDto) { try { await this.client.groupUpdateDescription(data.groupJid, data.description); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating group description', error.toString()); } } public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { try { const group = await this.client.groupMetadata(id.groupJid); if (!group) { this.logger.error('Group not found'); return null; } const picture = await this.profilePicture(group.id); return { id: group.id, subject: group.subject, subjectOwner: group.subjectOwner, subjectTime: group.subjectTime, pictureUrl: picture.profilePictureUrl, size: group.participants.length, creation: group.creation, owner: group.owner, desc: group.desc, descId: group.descId, restrict: group.restrict, announce: group.announce, participants: group.participants, }; } catch (error) { if (reply === 'inner') { return; } throw new NotFoundException('Error fetching group', error.toString()); } } public async fetchAllGroups(getParticipants: GetParticipant) { const fetch = Object.values(await this?.client?.groupFetchAllParticipating()); let groups = []; for (const group of fetch) { const picture = await this.profilePicture(group.id); const result = { id: group.id, subject: group.subject, subjectOwner: group.subjectOwner, subjectTime: group.subjectTime, pictureUrl: picture?.profilePictureUrl, size: group.participants.length, creation: group.creation, owner: group.owner, desc: group.desc, descId: group.descId, restrict: group.restrict, announce: group.announce, }; if (getParticipants.getParticipants == 'true') { result['participants'] = group.participants; } groups = [...groups, result]; } return groups; } public async inviteCode(id: GroupJid) { try { const code = await this.client.groupInviteCode(id.groupJid); return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; } catch (error) { throw new NotFoundException('No invite code', error.toString()); } } public async inviteInfo(id: GroupInvite) { try { return await this.client.groupGetInviteInfo(id.inviteCode); } catch (error) { throw new NotFoundException('No invite info', id.inviteCode); } } public async sendInvite(id: GroupSendInvite) { try { const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); const inviteUrl = inviteCode.inviteUrl; const numbers = id.numbers.map((number) => this.createJid(number)); const description = id.description ?? ''; const msg = `${description}\n\n${inviteUrl}`; const message = { conversation: msg, }; for await (const number of numbers) { await this.sendMessageWithTyping(number, message); } return { send: true, inviteUrl }; } catch (error) { throw new NotFoundException('No send invite'); } } public async acceptInviteCode(id: AcceptGroupInvite) { try { const groupJid = await this.client.groupAcceptInvite(id.inviteCode); return { accepted: true, groupJid: groupJid }; } catch (error) { throw new NotFoundException('Accept invite error', error.toString()); } } public async revokeInviteCode(id: GroupJid) { try { const inviteCode = await this.client.groupRevokeInvite(id.groupJid); return { revoked: true, inviteCode }; } catch (error) { throw new NotFoundException('Revoke error', error.toString()); } } public async findParticipants(id: GroupJid) { try { const participants = (await this.client.groupMetadata(id.groupJid)).participants; const contacts = await this.prismaRepository.contact.findMany({ where: { instanceId: this.instanceId, remoteJid: { in: participants.map((p) => p.id), }, }, }); const parsedParticipants = participants.map((participant) => { const contact = contacts.find((c) => c.remoteJid === participant.id); return { ...participant, name: participant.name ?? contact?.pushName, imgUrl: participant.imgUrl ?? contact?.profilePicUrl, }; }); const usersContacts = parsedParticipants.filter((c) => c.id.includes('@s.whatsapp')); if (usersContacts) { await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.id }))); } return { participants: parsedParticipants }; } catch (error) { console.error(error); throw new NotFoundException('No participants', error.toString()); } } public async updateGParticipant(update: GroupUpdateParticipantDto) { try { const participants = update.participants.map((p) => this.createJid(p)); const updateParticipants = await this.client.groupParticipantsUpdate( update.groupJid, participants, update.action, ); return { updateParticipants: updateParticipants }; } catch (error) { throw new BadRequestException('Error updating participants', error.toString()); } } public async updateGSetting(update: GroupUpdateSettingDto) { try { const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); return { updateSetting: updateSetting }; } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); } } public async toggleEphemeral(update: GroupToggleEphemeralDto) { try { await this.client.groupToggleEphemeral(update.groupJid, update.expiration); return { success: true }; } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); } } public async leaveGroup(id: GroupJid) { try { await this.client.groupLeave(id.groupJid); return { groupJid: id.groupJid, leave: true }; } catch (error) { throw new BadRequestException('Unable to leave the group', error.toString()); } } public async templateMessage() { throw new Error('Method not available in the Baileys service'); } private prepareMessage(message: proto.IWebMessageInfo): any { const contentType = getContentType(message.message); const contentMsg = message?.message[contentType] as any; const messageRaw = { key: message.key, pushName: message.pushName, status: message.status, message: { ...message.message }, contextInfo: contentMsg?.contextInfo, messageType: contentType || 'unknown', messageTimestamp: message.messageTimestamp as number, instanceId: this.instanceId, source: getDevice(message.key.id), }; if (messageRaw.message.extendedTextMessage) { messageRaw.messageType = 'conversation'; messageRaw.message.conversation = messageRaw.message.extendedTextMessage.text; delete messageRaw.message.extendedTextMessage; } if (messageRaw.message.documentWithCaptionMessage) { messageRaw.messageType = 'documentMessage'; messageRaw.message.documentMessage = messageRaw.message.documentWithCaptionMessage.message.documentMessage; delete messageRaw.message.documentWithCaptionMessage; } return messageRaw; } private async syncChatwootLostMessages() { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { const chatwootConfig = await this.findChatwoot(); const prepare = (message: any) => this.prepareMessage(message); this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); const task = cron.schedule('0,30 * * * *', async () => { this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare); }); task.start(); } } }