import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import makeWASocket, { AnyMessageContent, BufferedEventData, BufferJSON, CacheStore, Chat, ConnectionState, Contact, delay, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, generateWAMessageFromContent, getAggregateVotesInPollMessage, getContentType, getDevice, GroupMetadata, isJidBroadcast, isJidGroup, isJidUser, makeCacheableSignalKeyStore, MessageUpsertType, MiscMessageGenerationOptions, ParticipantAction, prepareWAMessageMedia, proto, useMultiFileAuthState, UserFacingSocketConfig, WABrowserDescription, WAMediaUpload, WAMessage, WAMessageUpdate, WASocket, } from '@whiskeysockets/baileys'; import axios from 'axios'; import { exec, execSync } from 'child_process'; import { arrayUnique, isBase64, isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import fs, { existsSync, readFileSync } from 'fs'; import Long from 'long'; import NodeCache from 'node-cache'; import { getMIMEType } from 'node-mime-types'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; import { ProxyAgent } from 'proxy-agent'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; import { v4 } from 'uuid'; import { Auth, CleanStoreConf, ConfigService, ConfigSessionPhone, Database, HttpServer, Log, QrCode, Redis, Sqs, Webhook, Websocket, } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; import { getAMQP, removeQueues } from '../../libs/amqp.server'; import { dbserver } from '../../libs/db.connect'; import { RedisCache } from '../../libs/redis.client'; import { getIO } from '../../libs/socket.server'; import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { ArchiveChatDto, DeleteMessage, getBase64FromMediaMessageDto, LastMessage, NumberBusiness, OnWhatsAppDto, PrivacySettingDto, ReadMessageDto, SendPresenceDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { CreateGroupDto, GetParticipant, GroupDescriptionDto, GroupInvite, GroupJid, GroupPictureDto, GroupSendInvite, GroupSubjectDto, GroupToggleEphemeralDto, GroupUpdateParticipantDto, GroupUpdateSettingDto, } from '../dto/group.dto'; import { ContactMessage, MediaMessage, Options, SendAudioDto, SendButtonDto, SendContactDto, SendListDto, SendLocationDto, SendMediaDto, SendPollDto, SendReactionDto, SendStatusDto, SendStickerDto, SendTextDto, StatusMessage, } from '../dto/sendMessage.dto'; import { ChamaaiRaw, ProxyRaw, RabbitmqRaw, SettingsRaw, SqsRaw, TypebotRaw } from '../models'; import { ChatRaw } from '../models/chat.model'; import { ChatwootRaw } from '../models/chatwoot.model'; import { ContactRaw } from '../models/contact.model'; import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; import { WebhookRaw } from '../models/webhook.model'; import { WebsocketRaw } from '../models/websocket.model'; import { ContactQuery } from '../repository/contact.repository'; import { MessageQuery } from '../repository/message.repository'; import { MessageUpQuery } from '../repository/messageUp.repository'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; import { ChamaaiService } from './chamaai.service'; import { ChatwootService } from './chatwoot.service'; import { TypebotService } from './typebot.service'; const retryCache = {}; export class WAStartupService { constructor( private readonly configService: ConfigService, private readonly eventEmitter: EventEmitter2, private readonly repository: RepositoryBroker, private readonly cache: RedisCache, ) { this.logger.verbose('WAStartupService initialized'); this.cleanStore(); this.instance.qrcode = { count: 0 }; } private readonly logger = new Logger(WAStartupService.name); public readonly instance: wa.Instance = {}; public client: WASocket; private readonly localWebhook: wa.LocalWebHook = {}; private readonly localChatwoot: wa.LocalChatwoot = {}; private readonly localSettings: wa.LocalSettings = {}; private readonly localWebsocket: wa.LocalWebsocket = {}; private readonly localRabbitmq: wa.LocalRabbitmq = {}; private readonly localSqs: wa.LocalSqs = {}; public readonly localTypebot: wa.LocalTypebot = {}; private readonly localProxy: wa.LocalProxy = {}; private readonly localChamaai: wa.LocalChamaai = {}; public stateConnection: wa.StateConnection = { state: 'close' }; public readonly storePath = join(ROOT_DIR, 'store'); private readonly msgRetryCounterCache: CacheStore = new NodeCache(); private readonly userDevicesCache: CacheStore = new NodeCache(); private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; private phoneNumber: string; private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); private chamaaiService = new ChamaaiService(waMonitor, this.configService); public set instanceName(name: string) { this.logger.verbose(`Initializing instance '${name}'`); if (!name) { this.logger.verbose('Instance name not found, generating random name with uuid'); this.instance.name = v4(); return; } this.instance.name = name; this.logger.verbose(`Instance '${this.instance.name}' initialized`); this.logger.verbose('Sending instance status to webhook'); this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, status: 'created', }); if (this.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.STATUS_INSTANCE, { instanceName: this.instance.name }, { instance: this.instance.name, status: 'created', }, ); } } public get instanceName() { this.logger.verbose('Getting instance name'); return this.instance.name; } public get wuid() { this.logger.verbose('Getting remoteJid of instance'); return this.instance.wuid; } public async getProfileName() { this.logger.verbose('Getting profile name'); let profileName = this.client.user?.name ?? this.client.user?.verifiedName; if (!profileName) { this.logger.verbose('Profile name not found, trying to get from database'); if (this.configService.get('DATABASE').ENABLED) { this.logger.verbose('Database enabled, trying to get from database'); const collection = dbserver .getClient() .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') .collection(this.instanceName); const data = await collection.findOne({ _id: 'creds' }); if (data) { this.logger.verbose('Profile name found in database'); const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); profileName = creds.me?.name || creds.me?.verifiedName; } } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { this.logger.verbose('Profile name found in file'); const creds = JSON.parse( readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { encoding: 'utf-8', }), ); profileName = creds.me?.name || creds.me?.verifiedName; } } this.logger.verbose(`Profile name: ${profileName}`); return profileName; } public async getProfileStatus() { this.logger.verbose('Getting profile status'); const status = await this.client.fetchStatus(this.instance.wuid); this.logger.verbose(`Profile status: ${status.status}`); return status.status; } public get profilePictureUrl() { this.logger.verbose('Getting profile picture url'); return this.instance.profilePictureUrl; } public get qrCode(): wa.QrCode { this.logger.verbose('Getting qrcode'); return { pairingCode: this.instance.qrcode?.pairingCode, code: this.instance.qrcode?.code, base64: this.instance.qrcode?.base64, count: this.instance.qrcode?.count, }; } private async loadWebhook() { this.logger.verbose('Loading webhook'); const data = await this.repository.webhook.find(this.instanceName); this.localWebhook.url = data?.url; this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); this.localWebhook.enabled = data?.enabled; this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); this.localWebhook.events = data?.events; this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); this.localWebhook.webhook_by_events = data?.webhook_by_events; this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); this.localWebhook.webhook_base64 = data?.webhook_base64; this.logger.verbose(`Webhook by webhook_base64: ${this.localWebhook.webhook_base64}`); this.logger.verbose('Webhook loaded'); } public async setWebhook(data: WebhookRaw) { this.logger.verbose('Setting webhook'); await this.repository.webhook.create(data, this.instanceName); this.logger.verbose(`Webhook url: ${data.url}`); this.logger.verbose(`Webhook events: ${data.events}`); Object.assign(this.localWebhook, data); this.logger.verbose('Webhook set'); } public async findWebhook() { this.logger.verbose('Finding webhook'); const data = await this.repository.webhook.find(this.instanceName); if (!data) { this.logger.verbose('Webhook not found'); throw new NotFoundException('Webhook not found'); } this.logger.verbose(`Webhook url: ${data.url}`); this.logger.verbose(`Webhook events: ${data.events}`); return { enabled: data.enabled, url: data.url, events: data.events, webhook_by_events: data.webhook_by_events, webhook_base64: data.webhook_base64, }; } private async loadChatwoot() { this.logger.verbose('Loading chatwoot'); const data = await this.repository.chatwoot.find(this.instanceName); this.localChatwoot.enabled = data?.enabled; this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); this.localChatwoot.account_id = data?.account_id; this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); this.localChatwoot.token = data?.token; this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); this.localChatwoot.url = data?.url; this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); this.localChatwoot.name_inbox = data?.name_inbox; this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); this.localChatwoot.sign_msg = data?.sign_msg; this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); this.localChatwoot.number = data?.number; this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); this.localChatwoot.reopen_conversation = data?.reopen_conversation; this.logger.verbose(`Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`); this.localChatwoot.conversation_pending = data?.conversation_pending; this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); this.logger.verbose('Chatwoot loaded'); } public async setChatwoot(data: ChatwootRaw) { this.logger.verbose('Setting chatwoot'); await this.repository.chatwoot.create(data, this.instanceName); this.logger.verbose(`Chatwoot account id: ${data.account_id}`); this.logger.verbose(`Chatwoot token: ${data.token}`); this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); Object.assign(this.localChatwoot, { ...data, sign_delimiter: data.sign_msg ? data.sign_delimiter : null }); this.logger.verbose('Chatwoot set'); } public async findChatwoot() { this.logger.verbose('Finding chatwoot'); const data = await this.repository.chatwoot.find(this.instanceName); if (!data) { this.logger.verbose('Chatwoot not found'); return null; } this.logger.verbose(`Chatwoot account id: ${data.account_id}`); this.logger.verbose(`Chatwoot token: ${data.token}`); this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); return { enabled: data.enabled, account_id: data.account_id, token: data.token, url: data.url, name_inbox: data.name_inbox, sign_msg: data.sign_msg, sign_delimiter: data.sign_delimiter || null, reopen_conversation: data.reopen_conversation, conversation_pending: data.conversation_pending, }; } private async loadSettings() { this.logger.verbose('Loading settings'); const data = await this.repository.settings.find(this.instanceName); this.localSettings.reject_call = data?.reject_call; this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); this.localSettings.msg_call = data?.msg_call; this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); this.localSettings.groups_ignore = data?.groups_ignore; this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); this.localSettings.always_online = data?.always_online; this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); this.localSettings.read_messages = data?.read_messages; this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); this.localSettings.read_status = data?.read_status; this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); this.logger.verbose('Settings loaded'); } public async setSettings(data: SettingsRaw) { this.logger.verbose('Setting settings'); await this.repository.settings.create(data, this.instanceName); this.logger.verbose(`Settings reject_call: ${data.reject_call}`); this.logger.verbose(`Settings msg_call: ${data.msg_call}`); this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); this.logger.verbose(`Settings always_online: ${data.always_online}`); this.logger.verbose(`Settings read_messages: ${data.read_messages}`); this.logger.verbose(`Settings read_status: ${data.read_status}`); Object.assign(this.localSettings, data); this.logger.verbose('Settings set'); this.client?.ws?.close(); } public async findSettings() { this.logger.verbose('Finding settings'); const data = await this.repository.settings.find(this.instanceName); if (!data) { this.logger.verbose('Settings not found'); return null; } this.logger.verbose(`Settings url: ${data.reject_call}`); this.logger.verbose(`Settings msg_call: ${data.msg_call}`); this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); this.logger.verbose(`Settings always_online: ${data.always_online}`); this.logger.verbose(`Settings read_messages: ${data.read_messages}`); this.logger.verbose(`Settings read_status: ${data.read_status}`); return { reject_call: data.reject_call, msg_call: data.msg_call, groups_ignore: data.groups_ignore, always_online: data.always_online, read_messages: data.read_messages, read_status: data.read_status, }; } private async loadWebsocket() { this.logger.verbose('Loading websocket'); const data = await this.repository.websocket.find(this.instanceName); this.localWebsocket.enabled = data?.enabled; this.logger.verbose(`Websocket enabled: ${this.localWebsocket.enabled}`); this.localWebsocket.events = data?.events; this.logger.verbose(`Websocket events: ${this.localWebsocket.events}`); this.logger.verbose('Websocket loaded'); } public async setWebsocket(data: WebsocketRaw) { this.logger.verbose('Setting websocket'); await this.repository.websocket.create(data, this.instanceName); this.logger.verbose(`Websocket events: ${data.events}`); Object.assign(this.localWebsocket, data); this.logger.verbose('Websocket set'); } public async findWebsocket() { this.logger.verbose('Finding websocket'); const data = await this.repository.websocket.find(this.instanceName); if (!data) { this.logger.verbose('Websocket not found'); throw new NotFoundException('Websocket not found'); } this.logger.verbose(`Websocket events: ${data.events}`); return { enabled: data.enabled, events: data.events, }; } private async loadRabbitmq() { this.logger.verbose('Loading rabbitmq'); const data = await this.repository.rabbitmq.find(this.instanceName); this.localRabbitmq.enabled = data?.enabled; this.logger.verbose(`Rabbitmq enabled: ${this.localRabbitmq.enabled}`); this.localRabbitmq.events = data?.events; this.logger.verbose(`Rabbitmq events: ${this.localRabbitmq.events}`); this.logger.verbose('Rabbitmq loaded'); } public async setRabbitmq(data: RabbitmqRaw) { this.logger.verbose('Setting rabbitmq'); await this.repository.rabbitmq.create(data, this.instanceName); this.logger.verbose(`Rabbitmq events: ${data.events}`); Object.assign(this.localRabbitmq, data); this.logger.verbose('Rabbitmq set'); } public async findRabbitmq() { this.logger.verbose('Finding rabbitmq'); const data = await this.repository.rabbitmq.find(this.instanceName); if (!data) { this.logger.verbose('Rabbitmq not found'); throw new NotFoundException('Rabbitmq not found'); } this.logger.verbose(`Rabbitmq events: ${data.events}`); return { enabled: data.enabled, events: data.events, }; } public async removeRabbitmqQueues() { this.logger.verbose('Removing rabbitmq'); if (this.localRabbitmq.enabled) { removeQueues(this.instanceName, this.localRabbitmq.events); } } private async loadSqs() { this.logger.verbose('Loading sqs'); const data = await this.repository.sqs.find(this.instanceName); this.localSqs.enabled = data?.enabled; this.logger.verbose(`Sqs enabled: ${this.localSqs.enabled}`); this.localSqs.events = data?.events; this.logger.verbose(`Sqs events: ${this.localSqs.events}`); this.logger.verbose('Sqs loaded'); } public async setSqs(data: SqsRaw) { this.logger.verbose('Setting sqs'); await this.repository.sqs.create(data, this.instanceName); this.logger.verbose(`Sqs events: ${data.events}`); Object.assign(this.localSqs, data); this.logger.verbose('Sqs set'); } public async findSqs() { this.logger.verbose('Finding sqs'); const data = await this.repository.sqs.find(this.instanceName); if (!data) { this.logger.verbose('Sqs not found'); throw new NotFoundException('Sqs not found'); } this.logger.verbose(`Sqs events: ${data.events}`); return { enabled: data.enabled, events: data.events, }; } public async removeSqsQueues() { this.logger.verbose('Removing sqs'); if (this.localSqs.enabled) { removeQueuesSQS(this.instanceName, this.localSqs.events); } } private async loadTypebot() { this.logger.verbose('Loading typebot'); const data = await this.repository.typebot.find(this.instanceName); this.localTypebot.enabled = data?.enabled; this.logger.verbose(`Typebot enabled: ${this.localTypebot.enabled}`); this.localTypebot.url = data?.url; this.logger.verbose(`Typebot url: ${this.localTypebot.url}`); this.localTypebot.typebot = data?.typebot; this.logger.verbose(`Typebot typebot: ${this.localTypebot.typebot}`); this.localTypebot.expire = data?.expire; this.logger.verbose(`Typebot expire: ${this.localTypebot.expire}`); this.localTypebot.keyword_finish = data?.keyword_finish; this.logger.verbose(`Typebot keyword_finish: ${this.localTypebot.keyword_finish}`); this.localTypebot.delay_message = data?.delay_message; this.logger.verbose(`Typebot delay_message: ${this.localTypebot.delay_message}`); this.localTypebot.unknown_message = data?.unknown_message; this.logger.verbose(`Typebot unknown_message: ${this.localTypebot.unknown_message}`); this.localTypebot.listening_from_me = data?.listening_from_me; this.logger.verbose(`Typebot listening_from_me: ${this.localTypebot.listening_from_me}`); this.localTypebot.sessions = data?.sessions; this.logger.verbose('Typebot loaded'); } public async setTypebot(data: TypebotRaw) { this.logger.verbose('Setting typebot'); await this.repository.typebot.create(data, this.instanceName); this.logger.verbose(`Typebot typebot: ${data.typebot}`); this.logger.verbose(`Typebot expire: ${data.expire}`); this.logger.verbose(`Typebot keyword_finish: ${data.keyword_finish}`); this.logger.verbose(`Typebot delay_message: ${data.delay_message}`); this.logger.verbose(`Typebot unknown_message: ${data.unknown_message}`); this.logger.verbose(`Typebot listening_from_me: ${data.listening_from_me}`); Object.assign(this.localTypebot, data); this.logger.verbose('Typebot set'); } public async findTypebot() { this.logger.verbose('Finding typebot'); const data = await this.repository.typebot.find(this.instanceName); if (!data) { this.logger.verbose('Typebot not found'); throw new NotFoundException('Typebot not found'); } return { enabled: data.enabled, url: data.url, typebot: data.typebot, expire: data.expire, keyword_finish: data.keyword_finish, delay_message: data.delay_message, unknown_message: data.unknown_message, listening_from_me: data.listening_from_me, sessions: data.sessions, }; } private async loadProxy() { this.logger.verbose('Loading proxy'); const data = await this.repository.proxy.find(this.instanceName); this.localProxy.enabled = data?.enabled; this.logger.verbose(`Proxy enabled: ${this.localProxy.enabled}`); this.localProxy.proxy = data?.proxy; this.logger.verbose(`Proxy proxy: ${this.localProxy.proxy}`); this.logger.verbose('Proxy loaded'); } public async setProxy(data: ProxyRaw, reload = true) { this.logger.verbose('Setting proxy'); await this.repository.proxy.create(data, this.instanceName); this.logger.verbose(`Proxy proxy: ${data.proxy}`); Object.assign(this.localProxy, data); this.logger.verbose('Proxy set'); if (reload) { this.reloadConnection(); } } public async findProxy() { this.logger.verbose('Finding proxy'); const data = await this.repository.proxy.find(this.instanceName); if (!data) { this.logger.verbose('Proxy not found'); throw new NotFoundException('Proxy not found'); } return { enabled: data.enabled, proxy: data.proxy, }; } private async loadChamaai() { this.logger.verbose('Loading chamaai'); const data = await this.repository.chamaai.find(this.instanceName); this.localChamaai.enabled = data?.enabled; this.logger.verbose(`Chamaai enabled: ${this.localChamaai.enabled}`); this.localChamaai.url = data?.url; this.logger.verbose(`Chamaai url: ${this.localChamaai.url}`); this.localChamaai.token = data?.token; this.logger.verbose(`Chamaai token: ${this.localChamaai.token}`); this.localChamaai.waNumber = data?.waNumber; this.logger.verbose(`Chamaai waNumber: ${this.localChamaai.waNumber}`); this.localChamaai.answerByAudio = data?.answerByAudio; this.logger.verbose(`Chamaai answerByAudio: ${this.localChamaai.answerByAudio}`); this.logger.verbose('Chamaai loaded'); } public async setChamaai(data: ChamaaiRaw) { this.logger.verbose('Setting chamaai'); await this.repository.chamaai.create(data, this.instanceName); this.logger.verbose(`Chamaai url: ${data.url}`); this.logger.verbose(`Chamaai token: ${data.token}`); this.logger.verbose(`Chamaai waNumber: ${data.waNumber}`); this.logger.verbose(`Chamaai answerByAudio: ${data.answerByAudio}`); Object.assign(this.localChamaai, data); this.logger.verbose('Chamaai set'); } public async findChamaai() { this.logger.verbose('Finding chamaai'); const data = await this.repository.chamaai.find(this.instanceName); if (!data) { this.logger.verbose('Chamaai not found'); throw new NotFoundException('Chamaai not found'); } return { enabled: data.enabled, url: data.url, token: data.token, waNumber: data.waNumber, answerByAudio: data.answerByAudio, }; } public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; const websocketLocal = this.localWebsocket.events; const rabbitmqLocal = this.localRabbitmq.events; const sqsLocal = this.localSqs.events; const serverUrl = this.configService.get('SERVER').URL; const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds const localISOTime = new Date(Date.now() - tzoffset).toISOString(); const now = localISOTime; const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; const tokenStore = await this.repository.auth.find(this.instanceName); const instanceApikey = tokenStore?.apikey || 'Apikey not found'; if (this.localRabbitmq.enabled) { const amqp = getAMQP(); if (amqp) { if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) { const exchangeName = this.instanceName ?? 'evolution_exchange'; amqp.assertExchange(exchangeName, 'topic', { durable: true, autoDelete: false, }); const queueName = `${this.instanceName}.${event}`; amqp.assertQueue(queueName, { durable: true, autoDelete: false, arguments: { 'x-queue-type': 'quorum', }, }); amqp.bindQueue(queueName, exchangeName, event); const message = { event, instance: this.instance.name, data, server_url: serverUrl, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { message['apikey'] = instanceApikey; } amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendData-RabbitMQ', event, instance: this.instance.name, data, server_url: serverUrl, apikey: (expose && instanceApikey) || null, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { logData['apikey'] = instanceApikey; } this.logger.log(logData); } } } } if (this.localSqs.enabled) { const sqs = getSQS(); if (sqs) { if (Array.isArray(sqsLocal) && sqsLocal.includes(we)) { const eventFormatted = `${event.replace('.', '_').toLowerCase()}`; const queueName = `${this.instanceName}_${eventFormatted}.fifo`; const sqsConfig = this.configService.get('SQS'); const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`; const message = { event, instance: this.instance.name, data, server_url: serverUrl, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { message['apikey'] = instanceApikey; } const params = { MessageBody: JSON.stringify(message), MessageGroupId: 'evolution', MessageDeduplicationId: `${this.instanceName}_${eventFormatted}_${Date.now()}`, QueueUrl: sqsUrl, }; sqs.sendMessage(params, (err, data) => { if (err) { this.logger.error({ local: WAStartupService.name + '.sendData-SQS', message: err?.message, hostName: err?.hostname, code: err?.code, stack: err?.stack, name: err?.name, url: queueName, server_url: serverUrl, }); } else { if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendData-SQS', event, instance: this.instance.name, data, server_url: serverUrl, apikey: (expose && instanceApikey) || null, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { logData['apikey'] = instanceApikey; } this.logger.log(logData); } } }); } } } if (this.configService.get('WEBSOCKET')?.ENABLED && this.localWebsocket.enabled) { this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name); if (Array.isArray(websocketLocal) && websocketLocal.includes(we)) { this.logger.verbose('Sending data to websocket on event: ' + event); const io = getIO(); const message = { event, instance: this.instance.name, data, server_url: serverUrl, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { message['apikey'] = instanceApikey; } this.logger.verbose('Sending data to socket.io in channel: ' + this.instance.name); io.of(`/${this.instance.name}`).emit(event, message); if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendData-Websocket', event, instance: this.instance.name, data, server_url: serverUrl, apikey: (expose && instanceApikey) || null, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { logData['apikey'] = instanceApikey; } this.logger.log(logData); } } } const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; if (local) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { this.logger.verbose('Sending data to webhook local'); let baseURL: string; if (this.localWebhook.webhook_by_events) { baseURL = `${this.localWebhook.url}/${transformedWe}`; } else { baseURL = this.localWebhook.url; } if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendDataWebhook-local', url: baseURL, event, instance: this.instance.name, data, destination: this.localWebhook.url, date_time: now, sender: this.wuid, server_url: serverUrl, apikey: (expose && instanceApikey) || null, }; if (expose && instanceApikey) { logData['apikey'] = instanceApikey; } this.logger.log(logData); } try { if (this.localWebhook.enabled && isURL(this.localWebhook.url, { require_tld: false })) { const httpService = axios.create({ baseURL }); const postData = { event, instance: this.instance.name, data, destination: this.localWebhook.url, date_time: now, sender: this.wuid, server_url: serverUrl, }; if (expose && instanceApikey) { postData['apikey'] = instanceApikey; } await httpService.post('', postData); } } catch (error) { this.logger.error({ local: WAStartupService.name + '.sendDataWebhook-local', message: error?.message, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, error: error?.errno, stack: error?.stack, name: error?.name, url: baseURL, server_url: serverUrl, }); } } } if (webhookGlobal.GLOBAL?.ENABLED) { if (webhookGlobal.EVENTS[we]) { this.logger.verbose('Sending data to webhook global'); const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; let globalURL; if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { globalURL = `${globalWebhook.URL}/${transformedWe}`; } else { globalURL = globalWebhook.URL; } const localUrl = this.localWebhook.url; if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendDataWebhook-global', url: globalURL, event, instance: this.instance.name, data, destination: localUrl, date_time: now, sender: this.wuid, server_url: serverUrl, }; if (expose && globalApiKey) { logData['apikey'] = globalApiKey; } this.logger.log(logData); } try { if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { const httpService = axios.create({ baseURL: globalURL }); const postData = { event, instance: this.instance.name, data, destination: localUrl, date_time: now, sender: this.wuid, server_url: serverUrl, }; if (expose && globalApiKey) { postData['apikey'] = globalApiKey; } await httpService.post('', postData); } } catch (error) { this.logger.error({ local: WAStartupService.name + '.sendDataWebhook-global', message: error?.message, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, error: error?.errno, stack: error?.stack, name: error?.name, url: globalURL, server_url: serverUrl, }); } } } } private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { this.logger.verbose('Connection update'); if (qr) { this.logger.verbose('QR code found'); if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { this.logger.verbose('QR code limit reached'); this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); this.sendDataWebhook(Events.QRCODE_UPDATED, { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession, }); if (this.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.QRCODE_UPDATED, { instanceName: this.instance.name }, { message: 'QR code limit reached, please login again', statusCode: DisconnectReason.badSession, }, ); } this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, state: 'refused', statusReason: DisconnectReason.connectionClosed, }); this.logger.verbose('endSession defined as true'); this.endSession = true; this.logger.verbose('Emmiting event logout.instance'); return this.eventEmitter.emit('no.connection', this.instance.name); } this.logger.verbose('Incrementing QR code count'); 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(2000); this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); } else { this.instance.qrcode.pairingCode = null; } this.logger.verbose('Generating QR code'); 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.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.QRCODE_UPDATED, { instanceName: this.instance.name }, { qrcode: { instance: this.instance.name, pairingCode: this.instance.qrcode.pairingCode, code: qr, base64, }, }, ); } }); this.logger.verbose('Generating QR code in terminal'); 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, ), ); } if (connection) { this.logger.verbose('Connection found'); this.stateConnection = { state: connection, statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, }; this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection, }); } if (connection === 'close') { this.logger.verbose('Connection closed'); const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; if (shouldReconnect) { this.logger.verbose('Reconnecting to whatsapp'); await this.connectToWhatsapp(); } else { this.logger.verbose('Do not reconnect to whatsapp'); this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, status: 'closed', }); if (this.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.STATUS_INSTANCE, { instanceName: this.instance.name }, { instance: this.instance.name, status: 'closed', }, ); } this.logger.verbose('Emittin event logout.instance'); this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); this.client?.ws?.close(); this.client.end(new Error('Close connection')); this.logger.verbose('Connection closed'); } } if (connection === 'open') { this.logger.verbose('Connection opened'); this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; 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} `); if (this.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.CONNECTION_UPDATE, { instanceName: this.instance.name }, { instance: this.instance.name, status: 'open', }, ); } } } private async getMessage(key: proto.IMessageKey, full = false) { this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); try { const webMessageInfo = (await this.repository.message.find({ where: { owner: this.instance.name, key: { id: key.id } }, })) as unknown as proto.IWebMessageInfo[]; if (full) { this.logger.verbose('Returning full message'); return webMessageInfo[0]; } if (webMessageInfo[0].message?.pollCreationMessage) { this.logger.verbose('Returning poll message'); 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; } } this.logger.verbose('Returning message'); return webMessageInfo[0].message; } catch (error) { return { conversation: '' }; } } private cleanStore() { this.logger.verbose('Cronjob to clean store initialized'); const cleanStore = this.configService.get('CLEAN_STORE'); const database = this.configService.get('DATABASE'); if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { this.logger.verbose('Cronjob to clean store enabled'); setInterval(() => { try { for (const [key, value] of Object.entries(cleanStore)) { if (value === true) { execSync( `rm -rf ${join(this.storePath, key.toLowerCase().replace('_', '-'), this.instance.name)}/*.json`, ); this.logger.verbose( `Cleaned ${join(this.storePath, key.toLowerCase().replace('_', '-'), this.instance.name)}/*.json`, ); } } } catch (error) { this.logger.error(error); } }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); } } private async defineAuthState() { this.logger.verbose('Defining auth state'); const db = this.configService.get('DATABASE'); const redis = this.configService.get('REDIS'); if (redis?.ENABLED) { this.logger.verbose('Redis enabled'); this.cache.reference = this.instance.name; return await useMultiFileAuthStateRedisDb(this.cache); } if (db.SAVE_DATA.INSTANCE && db.ENABLED) { this.logger.verbose('Database enabled'); return await useMultiFileAuthStateDb(this.instance.name); } this.logger.verbose('Store file enabled'); return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); } public async connectToWhatsapp(number?: string): Promise { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); this.loadChatwoot(); this.loadSettings(); this.loadWebsocket(); this.loadRabbitmq(); this.loadSqs(); this.loadTypebot(); this.loadProxy(); this.loadChamaai(); this.instance.authState = await this.defineAuthState(); const { version } = await fetchLatestBaileysVersion(); this.logger.verbose('Baileys version: ' + version); const session = this.configService.get('CONFIG_SESSION_PHONE'); const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; this.logger.verbose('Browser: ' + JSON.stringify(browser)); let options; if (this.localProxy.enabled) { this.logger.info('Proxy enabled: ' + this.localProxy.proxy); if (this.localProxy.proxy.includes('proxyscrape')) { const response = await axios.get(this.localProxy.proxy); 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: new ProxyAgent(proxyUrl as any), }; } else { options = { agent: new ProxyAgent(this.localProxy.proxy as any), }; } } const socketConfig: UserFacingSocketConfig = { ...options, auth: { creds: this.instance.authState.state.creds, keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), }, logger: P({ level: this.logBaileys }), printQRInTerminal: false, browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser, version, markOnlineOnConnect: this.localSettings.always_online, retryRequestDelayMs: 10, connectTimeoutMs: 60_000, qrTimeout: 40_000, defaultQueryTimeoutMs: undefined, emitOwnEvents: false, shouldIgnoreJid: (jid) => { const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); return isGroupJid || isBroadcast; }, msgRetryCounterCache: this.msgRetryCounterCache, getMessage: async (key) => (await this.getMessage(key)) as Promise, generateHighQualityLinkPreview: true, syncFullHistory: false, userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending: (message) => { const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); if (requiresPatch) { message = { viewOnceMessageV2: { message: { messageContextInfo: { deviceListMetadataVersion: 2, deviceListMetadata: {}, }, ...message, }, }, }; } return message; }, }; this.endSession = false; this.logger.verbose('Creating socket'); this.client = makeWASocket(socketConfig); this.logger.verbose('Socket created'); this.eventHandler(); this.logger.verbose('Socket event handler initialized'); this.phoneNumber = number; return this.client; } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } public async reloadConnection(): Promise { try { this.instance.authState = await this.defineAuthState(); const { version } = await fetchLatestBaileysVersion(); const session = this.configService.get('CONFIG_SESSION_PHONE'); const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; let options; if (this.localProxy.enabled) { this.logger.verbose('Proxy enabled'); options = { agent: new ProxyAgent(this.localProxy.proxy as any), fetchAgent: new ProxyAgent(this.localProxy.proxy as any), }; } const socketConfig: UserFacingSocketConfig = { ...options, auth: { creds: this.instance.authState.state.creds, keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), }, logger: P({ level: this.logBaileys }), printQRInTerminal: false, browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser, version, markOnlineOnConnect: this.localSettings.always_online, retryRequestDelayMs: 10, connectTimeoutMs: 60_000, qrTimeout: 40_000, defaultQueryTimeoutMs: undefined, emitOwnEvents: false, shouldIgnoreJid: (jid) => { const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); return isGroupJid || isBroadcast; }, msgRetryCounterCache: this.msgRetryCounterCache, getMessage: async (key) => (await this.getMessage(key)) as Promise, generateHighQualityLinkPreview: true, syncFullHistory: false, userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending: (message) => { const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); if (requiresPatch) { message = { viewOnceMessageV2: { message: { messageContextInfo: { deviceListMetadataVersion: 2, deviceListMetadata: {}, }, ...message, }, }, }; } return message; }, }; this.client = makeWASocket(socketConfig); return this.client; } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } private readonly chatHandle = { 'chats.upsert': async (chats: Chat[], database: Database) => { this.logger.verbose('Event received: chats.upsert'); this.logger.verbose('Finding chats in database'); const chatsRepository = await this.repository.chat.find({ where: { owner: this.instance.name }, }); this.logger.verbose('Verifying if chats exists in database to insert'); const chatsRaw: ChatRaw[] = []; for await (const chat of chats) { if (chatsRepository.find((cr) => cr.id === chat.id)) { continue; } chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); } this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); this.logger.verbose('Inserting chats in database'); this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); }, 'chats.update': async ( chats: Partial< proto.IConversation & { lastMessageRecvTimestamp?: number; } & { conditional: (bufferedData: BufferedEventData) => boolean; } >[], ) => { this.logger.verbose('Event received: chats.update'); const chatsRaw: ChatRaw[] = chats.map((chat) => { return { id: chat.id, owner: this.instance.wuid }; }); this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); }, 'chats.delete': async (chats: string[]) => { this.logger.verbose('Event received: chats.delete'); this.logger.verbose('Deleting chats in database'); chats.forEach( async (chat) => await this.repository.chat.delete({ where: { owner: this.instance.name, id: chat }, }), ); this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); }, }; private readonly contactHandle = { 'contacts.upsert': async (contacts: Contact[], database: Database) => { this.logger.verbose('Event received: contacts.upsert'); this.logger.verbose('Finding contacts in database'); const contactsRepository = await this.repository.contact.find({ where: { owner: this.instance.name }, }); this.logger.verbose('Verifying if contacts exists in database to insert'); const contactsRaw: ContactRaw[] = []; for await (const contact of contacts) { if (contactsRepository.find((cr) => cr.id === contact.id)) { continue; } contactsRaw.push({ id: contact.id, pushName: contact?.name || contact?.verifiedName, profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, owner: this.instance.name, }); } this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); this.logger.verbose('Inserting contacts in database'); this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); }, 'contacts.update': async (contacts: Partial[], database: Database) => { this.logger.verbose('Event received: contacts.update'); this.logger.verbose('Verifying if contacts exists in database to update'); const contactsRaw: ContactRaw[] = []; for await (const contact of contacts) { contactsRaw.push({ id: contact.id, pushName: contact?.name ?? contact?.verifiedName, profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, owner: this.instance.name, }); } this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); this.logger.verbose('Updating contacts in database'); this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); }, }; private readonly messageHandle = { 'messaging-history.set': async ( { messages, chats, isLatest, }: { chats: Chat[]; contacts: Contact[]; messages: proto.IWebMessageInfo[]; isLatest: boolean; }, database: Database, ) => { this.logger.verbose('Event received: messaging-history.set'); if (isLatest) { this.logger.verbose('isLatest defined as true'); const chatsRaw: ChatRaw[] = chats.map((chat) => { return { id: chat.id, owner: this.instance.name, lastMsgTimestamp: chat.lastMessageRecvTimestamp, }; }); this.logger.verbose('Sending data to webhook in event CHATS_SET'); this.sendDataWebhook(Events.CHATS_SET, chatsRaw); this.logger.verbose('Inserting chats in database'); this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); } const messagesRaw: MessageRaw[] = []; const messagesRepository = await this.repository.message.find({ where: { owner: this.instance.name }, }); for await (const [, m] of Object.entries(messages)) { if (!m.message) { continue; } if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { continue; } if (Long.isLong(m?.messageTimestamp)) { m.messageTimestamp = m.messageTimestamp?.toNumber(); } messagesRaw.push({ key: m.key, pushName: m.pushName, participant: m.participant, message: { ...m.message }, messageType: getContentType(m.message), messageTimestamp: m.messageTimestamp as number, owner: this.instance.name, }); } this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); messages = undefined; }, 'messages.upsert': async ( { messages, type, }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; }, database: Database, settings: SettingsRaw, ) => { try { this.logger.verbose('Event received: messages.upsert'); for (const received of messages) { if ( (type !== 'notify' && type !== 'append') || received.message?.protocolMessage || received.message?.pollUpdateMessage ) { this.logger.verbose('message rejected'); return; } if (Long.isLong(received.messageTimestamp)) { received.messageTimestamp = received.messageTimestamp?.toNumber(); } if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { this.logger.verbose('group ignored'); return; } let messageRaw: MessageRaw; if ( (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || received?.message?.imageMessage ) { const buffer = await downloadMediaMessage( { key: received.key, message: received?.message }, 'buffer', {}, { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage, }, ); messageRaw = { key: received.key, pushName: received.pushName, message: { ...received.message, base64: buffer ? buffer.toString('base64') : undefined, }, messageType: getContentType(received.message), messageTimestamp: received.messageTimestamp as number, owner: this.instance.name, source: getDevice(received.key.id), }; } else { messageRaw = { key: received.key, pushName: received.pushName, message: { ...received.message }, messageType: getContentType(received.message), messageTimestamp: received.messageTimestamp as number, owner: this.instance.name, source: getDevice(received.key.id), }; } if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { await this.client.readMessages([received.key]); } if (this.localSettings.read_status && received.key.id === 'status@broadcast') { await this.client.readMessages([received.key]); } this.logger.log(messageRaw); this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); if (this.localChatwoot.enabled && !received.key.id.includes('@broadcast')) { const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, { instanceName: this.instance.name }, messageRaw, ); if (chatwootSentMessage?.id) { messageRaw.chatwoot = { messageId: chatwootSentMessage.id, inboxId: chatwootSentMessage.inbox_id, conversationId: chatwootSentMessage.conversation_id, }; } } const typebotSessionRemoteJid = this.localTypebot.sessions?.find( (session) => session.remoteJid === received.key.remoteJid, ); if ((this.localTypebot.enabled && type === 'notify') || typebotSessionRemoteJid) { if (!(this.localTypebot.listening_from_me === false && messageRaw.key.fromMe === true)) { if (messageRaw.messageType !== 'reactionMessage') await this.typebotService.sendTypebot( { instanceName: this.instance.name }, messageRaw.key.remoteJid, messageRaw, ); } } if (this.localChamaai.enabled && messageRaw.key.fromMe === false && type === 'notify') { await this.chamaaiService.sendChamaai( { instanceName: this.instance.name }, messageRaw.key.remoteJid, messageRaw, ); } this.logger.verbose('Inserting message in database'); await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); this.logger.verbose('Verifying contact from message'); const contact = await this.repository.contact.find({ where: { owner: this.instance.name, id: received.key.remoteJid }, }); const contactRaw: ContactRaw = { id: received.key.remoteJid, pushName: received.pushName, profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, owner: this.instance.name, }; if (contactRaw.id === 'status@broadcast') { this.logger.verbose('Contact is status@broadcast'); return; } if (contact?.length) { this.logger.verbose('Contact found in database'); const contactRaw: ContactRaw = { id: received.key.remoteJid, pushName: contact[0].pushName, profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, owner: this.instance.name, }; this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); if (this.localChatwoot.enabled) { await this.chatwootService.eventWhatsapp( Events.CONTACTS_UPDATE, { instanceName: this.instance.name }, contactRaw, ); } this.logger.verbose('Updating contact in database'); await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); return; } this.logger.verbose('Contact not found in database'); this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); this.logger.verbose('Inserting contact in database'); this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); } } catch (error) { this.logger.error(error); } }, 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { this.logger.verbose('Event received: messages.update'); 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?.groups_ignore && key.remoteJid.includes('@g.us')) { this.logger.verbose('group ignored'); return; } if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { this.logger.verbose('Message update is valid'); let pollUpdates: any; if (update.pollUpdates) { this.logger.verbose('Poll update found'); this.logger.verbose('Getting poll message'); const pollCreation = await this.getMessage(key); this.logger.verbose(pollCreation); if (pollCreation) { this.logger.verbose('Getting aggregate votes in poll message'); pollUpdates = getAggregateVotesInPollMessage({ message: pollCreation as proto.IMessage, pollUpdates: update.pollUpdates, }); } } if (status[update.status] === 'READ' && !key.fromMe) return; if (update.message === null && update.status === undefined) { this.logger.verbose('Message deleted'); this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); this.sendDataWebhook(Events.MESSAGES_DELETE, key); const message: MessageUpdateRaw = { ...key, status: 'DELETED', datetime: Date.now(), owner: this.instance.name, }; this.logger.verbose(message); this.logger.verbose('Inserting message in database'); await this.repository.messageUpdate.insert( [message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE, ); if (this.localChatwoot.enabled) { this.chatwootService.eventWhatsapp( Events.MESSAGES_DELETE, { instanceName: this.instance.name }, { key: key }, ); } return; } const message: MessageUpdateRaw = { ...key, status: status[update.status], datetime: Date.now(), owner: this.instance.name, pollUpdates, }; this.logger.verbose(message); this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); this.sendDataWebhook(Events.MESSAGES_UPDATE, message); this.logger.verbose('Inserting message in database'); this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); } } }, }; private readonly groupHandler = { 'groups.upsert': (groupMetadata: GroupMetadata[]) => { this.logger.verbose('Event received: groups.upsert'); this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); }, 'groups.update': (groupMetadataUpdate: Partial[]) => { this.logger.verbose('Event received: groups.update'); this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); }, 'group-participants.update': (participantsUpdate: { id: string; participants: string[]; action: ParticipantAction; }) => { this.logger.verbose('Event received: group-participants.update'); this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); }, }; private eventHandler() { this.logger.verbose('Initializing event handler'); this.client.ev.process(async (events) => { if (!this.endSession) { const database = this.configService.get('DATABASE'); const settings = await this.findSettings(); if (events.call) { this.logger.verbose('Listening event: call'); const call = events.call[0]; if (settings?.reject_call && call.status == 'offer') { this.logger.verbose('Rejecting call'); this.client.rejectCall(call.id, call.from); } if (settings?.msg_call?.trim().length > 0 && call.status == 'offer') { this.logger.verbose('Sending message in call'); const msg = await this.client.sendMessage(call.from, { text: settings.msg_call, }); this.logger.verbose('Sending data to event messages.upsert'); this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify', }); } this.logger.verbose('Sending data to webhook in event CALL'); this.sendDataWebhook(Events.CALL, call); } if (events['connection.update']) { this.logger.verbose('Listening event: connection.update'); this.connectionUpdate(events['connection.update']); } if (events['creds.update']) { this.logger.verbose('Listening event: creds.update'); this.instance.authState.saveCreds(); } if (events['messaging-history.set']) { this.logger.verbose('Listening event: messaging-history.set'); const payload = events['messaging-history.set']; this.messageHandle['messaging-history.set'](payload, database); } if (events['messages.upsert']) { this.logger.verbose('Listening event: messages.upsert'); const payload = events['messages.upsert']; if (payload.messages.find((a) => a?.messageStubType === 2)) { const msg = payload.messages[0]; retryCache[msg.key.id] = msg; return; } this.messageHandle['messages.upsert'](payload, database, settings); } if (events['messages.update']) { this.logger.verbose('Listening event: messages.update'); const payload = events['messages.update']; payload.forEach((message) => { if (retryCache[message.key.id]) { this.client.ev.emit('messages.upsert', { messages: [message], type: 'notify', }); delete retryCache[message.key.id]; return; } }); this.messageHandle['messages.update'](payload, database, settings); } if (events['presence.update']) { this.logger.verbose('Listening event: presence.update'); const payload = events['presence.update']; if (settings.groups_ignore && payload.id.includes('@g.us')) { this.logger.verbose('group ignored'); return; } this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); } if (!settings?.groups_ignore) { if (events['groups.upsert']) { this.logger.verbose('Listening event: groups.upsert'); const payload = events['groups.upsert']; this.groupHandler['groups.upsert'](payload); } if (events['groups.update']) { this.logger.verbose('Listening event: groups.update'); const payload = events['groups.update']; this.groupHandler['groups.update'](payload); } if (events['group-participants.update']) { this.logger.verbose('Listening event: group-participants.update'); const payload = events['group-participants.update']; this.groupHandler['group-participants.update'](payload); } } if (events['chats.upsert']) { this.logger.verbose('Listening event: chats.upsert'); const payload = events['chats.upsert']; this.chatHandle['chats.upsert'](payload, database); } if (events['chats.update']) { this.logger.verbose('Listening event: chats.update'); const payload = events['chats.update']; this.chatHandle['chats.update'](payload); } if (events['chats.delete']) { this.logger.verbose('Listening event: chats.delete'); const payload = events['chats.delete']; this.chatHandle['chats.delete'](payload); } if (events['contacts.upsert']) { this.logger.verbose('Listening event: contacts.upsert'); const payload = events['contacts.upsert']; this.contactHandle['contacts.upsert'](payload, database); } if (events['contacts.update']) { this.logger.verbose('Listening event: contacts.update'); const payload = events['contacts.update']; this.contactHandle['contacts.update'](payload, database); } } }); } // Check if the number is MX or AR private formatMXOrARNumber(jid: string): string { const countryCode = jid.substring(0, 2); if (Number(countryCode) === 52 || Number(countryCode) === 54) { if (jid.length === 13) { const number = countryCode + jid.substring(3); return number; } return jid; } return jid; } // Check if the number is br private formatBRNumber(jid: string) { const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); if (regexp.test(jid)) { const match = regexp.exec(jid); if (match && match[1] === '55') { const joker = Number.parseInt(match[3][0]); const ddd = Number.parseInt(match[2]); if (joker < 7 || ddd < 31) { return match[0]; } return match[1] + match[2] + match[3]; } return jid; } else { return jid; } } private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net or @lid'); return number; } if (number.includes('@broadcast')) { this.logger.verbose('Number already contains @broadcast'); return number; } number = number ?.replace(/\s/g, '') .replace(/\+/g, '') .replace(/\(/g, '') .replace(/\)/g, '') .split(':')[0] .split('@')[0]; if (number.includes('-') && number.length >= 24) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } number = number.replace(/\D/g, ''); if (number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); return `${number}@s.whatsapp.net`; } public async profilePicture(number: string) { const jid = this.createJid(number); this.logger.verbose('Getting profile picture with jid: ' + jid); try { this.logger.verbose('Getting profile picture url'); return { wuid: jid, profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), }; } catch (error) { this.logger.verbose('Profile picture not found'); return { wuid: jid, profilePictureUrl: null, }; } } public async getStatus(number: string) { const jid = this.createJid(number); this.logger.verbose('Getting profile status with jid:' + jid); try { this.logger.verbose('Getting status'); return { wuid: jid, status: (await this.client.fetchStatus(jid))?.status, }; } catch (error) { this.logger.verbose('Status not found'); return { wuid: jid, status: null, }; } } public async fetchProfile(instanceName: string, number?: string) { const jid = number ? this.createJid(number) : this.client?.user?.id; this.logger.verbose('Getting profile with jid: ' + jid); try { this.logger.verbose('Getting profile info'); 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 = await waMonitor.instanceInfo(instanceName); const business = await this.fetchBusinessProfile(jid); return { wuid: jid, name: info?.instance?.profileName, numberExists: true, picture: info?.instance?.profilePictureUrl, status: info?.instance?.profileStatus, isBusiness: business.isBusiness, email: business?.email, description: business?.description, website: business?.website?.shift(), }; } } catch (error) { this.logger.verbose('Profile not found'); return { wuid: jid, name: null, picture: null, status: null, os: null, isBusiness: false, }; } } private async sendMessageWithTyping( number: string, message: T, options?: Options, isChatwoot = false, ) { this.logger.verbose('Sending message with typing'); this.logger.verbose(`Check if number "${number}" is WhatsApp`); const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } const sender = isWA.jid; try { if (options?.delay) { this.logger.verbose('Delaying message'); await this.client.presenceSubscribe(sender); this.logger.verbose('Subscribing to presence'); await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); await delay(options.delay); this.logger.verbose('Set delay: ' + options.delay); await this.client.sendPresenceUpdate('paused', sender); this.logger.verbose('Sending presence update: paused'); } 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) { throw 'Message not found'; } quoted = msg; this.logger.verbose('Quoted message'); } let mentions: string[]; if (isJidGroup(sender)) { try { const group = await this.findGroup({ groupJid: sender }, 'inner'); if (!group) { throw new NotFoundException('Group not found'); } if (options?.mentions) { this.logger.verbose('Mentions defined'); if (options.mentions?.everyOne) { this.logger.verbose('Mentions everyone'); this.logger.verbose('Getting group metadata'); mentions = group.participants.map((participant) => participant.id); this.logger.verbose('Getting group metadata for mentions'); } else if (options.mentions?.mentioned?.length) { this.logger.verbose('Mentions manually defined'); mentions = options.mentions.mentioned.map((mention) => { const jid = this.createJid(mention); if (isJidGroup(jid)) { return null; } return jid; }); } } } catch (error) { throw new NotFoundException('Group not found'); } } const messageSent = await (async () => { const option = { quoted, }; if ( !message['audio'] && !message['poll'] && !message['sticker'] && !message['conversation'] && sender !== 'status@broadcast' ) { if (message['reactionMessage']) { this.logger.verbose('Sending reaction'); 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['audio']) { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, { forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message, }, mentions, }, option as unknown as MiscMessageGenerationOptions, ); } } if (message['conversation']) { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, { text: message['conversation'], mentions, linkPreview: linkPreview, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } if (sender === 'status@broadcast') { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, message['status'].content as unknown as AnyMessageContent, { backgroundColor: message['status'].option.backgroundColor, font: message['status'].option.font, statusJidList: message['status'].option.statusJidList, } as unknown as MiscMessageGenerationOptions, ); } this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, message as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); })(); const messageRaw: MessageRaw = { key: messageSent.key, pushName: messageSent.pushName, message: { ...messageSent.message }, messageType: getContentType(messageSent.message), messageTimestamp: messageSent.messageTimestamp as number, owner: this.instance.name, source: getDevice(messageSent.key.id), }; this.logger.log(messageRaw); this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); if (this.localChatwoot.enabled && !isChatwoot) { this.chatwootService.eventWhatsapp(Events.SEND_MESSAGE, { instanceName: this.instance.name }, messageRaw); } this.logger.verbose('Inserting message in database'); await this.repository.message.insert( [messageRaw], this.instance.name, this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, ); return messageSent; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } // Instance Controller public get connectionStatus() { this.logger.verbose('Getting connection status'); return this.stateConnection; } public async sendPresence(data: SendPresenceDto) { try { const { number } = data; this.logger.verbose(`Check if number "${number}" is WhatsApp`); const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } const sender = isWA.jid; this.logger.verbose('Sending presence'); await this.client.presenceSubscribe(sender); this.logger.verbose('Subscribing to presence'); await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing'); await delay(data.options.delay); this.logger.verbose('Set delay: ' + data.options.delay); await this.client.sendPresenceUpdate('paused', sender); this.logger.verbose('Sending presence update: paused'); } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } // Send Message Controller public async textMessage(data: SendTextDto, isChatwoot = false) { this.logger.verbose('Sending text message'); return await this.sendMessageWithTyping( data.number, { conversation: data.textMessage.text, }, data?.options, isChatwoot, ); } public async pollMessage(data: SendPollDto) { this.logger.verbose('Sending poll message'); return await this.sendMessageWithTyping( data.number, { poll: { name: data.pollMessage.name, selectableCount: data.pollMessage.selectableCount, values: data.pollMessage.values, }, }, data?.options, ); } private async formatStatusMessage(status: StatusMessage) { this.logger.verbose('Formatting status message'); if (!status.type) { throw new BadRequestException('Type is required'); } if (!status.content) { throw new BadRequestException('Content is required'); } if (status.allContacts) { this.logger.verbose('All contacts defined as true'); this.logger.verbose('Getting contacts from database'); const contacts = await this.repository.contact.find({ where: { owner: this.instance.name }, }); if (!contacts.length) { throw new BadRequestException('Contacts not found'); } this.logger.verbose('Getting contacts with push name'); status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); this.logger.verbose(status.statusJidList); } if (!status.statusJidList?.length && !status.allContacts) { throw new BadRequestException('StatusJidList is required'); } if (status.type === 'text') { this.logger.verbose('Type defined as 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') { this.logger.verbose('Type defined as image'); return { content: { image: { url: status.content, }, caption: status.caption, }, option: { statusJidList: status.statusJidList, }, }; } if (status.type === 'video') { this.logger.verbose('Type defined as video'); return { content: { video: { url: status.content, }, caption: status.caption, }, option: { statusJidList: status.statusJidList, }, }; } if (status.type === 'audio') { this.logger.verbose('Type defined as audio'); this.logger.verbose('Processing audio'); const convert = await this.processAudio(status.content, 'status@broadcast'); if (typeof convert === 'string') { this.logger.verbose('Audio processed'); const audio = fs.readFileSync(convert).toString('base64'); const result = { content: { audio: Buffer.from(audio, 'base64'), ptt: true, mimetype: 'audio/mp4', }, option: { statusJidList: status.statusJidList, }, }; fs.unlinkSync(convert); return result; } else { throw new InternalServerErrorException(convert); } } throw new BadRequestException('Type not found'); } public async statusMessage(data: SendStatusDto) { this.logger.verbose('Sending status message'); const status = await this.formatStatusMessage(data.statusMessage); return await this.sendMessageWithTyping('status@broadcast', { status, }); } private async prepareMediaMessage(mediaMessage: MediaMessage) { try { this.logger.verbose('Preparing media message'); 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'; this.logger.verbose('Media type: ' + mediaType); if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { this.logger.verbose('If media type is document and file name is not defined then'); const regex = new RegExp(/.*\/(.+?)\./); const arrayMatch = regex.exec(mediaMessage.media); mediaMessage.fileName = arrayMatch[1]; this.logger.verbose('File name: ' + mediaMessage.fileName); } 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 { if (isURL(mediaMessage.media)) { const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' }); mimetype = response.headers['content-type']; } else { mimetype = getMIMEType(mediaMessage.fileName); } } this.logger.verbose('Mimetype: ' + mimetype); prepareMedia[mediaType].caption = mediaMessage?.caption; prepareMedia[mediaType].mimetype = mimetype; prepareMedia[mediaType].fileName = mediaMessage.fileName; if (mediaMessage.mediatype === 'video') { this.logger.verbose('Is media type video then set gif playback as false'); prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), ); prepareMedia[mediaType].gifPlayback = false; } this.logger.verbose('Generating wa message from content'); 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, number: string) { try { this.logger.verbose('Converting image to WebP to sticker'); let imagePath: string; const hash = `${number}-${new Date().getTime()}`; this.logger.verbose('Hash to image name: ' + hash); const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; this.logger.verbose('Output path: ' + outputPath); if (isBase64(image)) { this.logger.verbose('Image is base64'); const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); const imageBuffer = Buffer.from(base64Data, 'base64'); imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; this.logger.verbose('Image path: ' + imagePath); await sharp(imageBuffer).toFile(imagePath); this.logger.verbose('Image created'); } else { this.logger.verbose('Image is url'); const timestamp = new Date().getTime(); const url = `${image}?timestamp=${timestamp}`; this.logger.verbose('including timestamp in url: ' + url); const response = await axios.get(url, { responseType: 'arraybuffer' }); this.logger.verbose('Getting image from url'); const imageBuffer = Buffer.from(response.data, 'binary'); imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; this.logger.verbose('Image path: ' + imagePath); await sharp(imageBuffer).toFile(imagePath); this.logger.verbose('Image created'); } await sharp(imagePath).webp().toFile(outputPath); this.logger.verbose('Image converted to WebP'); fs.unlinkSync(imagePath); this.logger.verbose('Temp image deleted'); return outputPath; } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); } } public async mediaSticker(data: SendStickerDto) { this.logger.verbose('Sending media sticker'); const convert = await this.convertToWebP(data.stickerMessage.image, data.number); const result = await this.sendMessageWithTyping( data.number, { sticker: { url: convert }, }, data?.options, ); fs.unlinkSync(convert); this.logger.verbose('Converted image deleted'); return result; } public async mediaMessage(data: SendMediaDto, isChatwoot = false) { this.logger.verbose('Sending media message'); const generate = await this.prepareMediaMessage(data.mediaMessage); return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options, isChatwoot); } public async processAudio(audio: string, number: string) { this.logger.verbose('Processing audio'); let tempAudioPath: string; let outputAudio: string; const hash = `${number}-${new Date().getTime()}`; this.logger.verbose('Hash to audio name: ' + hash); if (isURL(audio)) { this.logger.verbose('Audio is url'); outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; this.logger.verbose('Output audio path: ' + outputAudio); this.logger.verbose('Temp audio path: ' + tempAudioPath); const timestamp = new Date().getTime(); const url = `${audio}?timestamp=${timestamp}`; this.logger.verbose('Including timestamp in url: ' + url); const response = await axios.get(url, { responseType: 'arraybuffer' }); this.logger.verbose('Getting audio from url'); fs.writeFileSync(tempAudioPath, response.data); } else { this.logger.verbose('Audio is base64'); outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; this.logger.verbose('Output audio path: ' + outputAudio); this.logger.verbose('Temp audio path: ' + tempAudioPath); const audioBuffer = Buffer.from(audio, 'base64'); fs.writeFileSync(tempAudioPath, audioBuffer); this.logger.verbose('Temp audio created'); } this.logger.verbose('Converting audio to mp4'); return new Promise((resolve, reject) => { exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { fs.unlinkSync(tempAudioPath); this.logger.verbose('Temp audio deleted'); if (error) reject(error); this.logger.verbose('Audio converted to mp4'); resolve(outputAudio); }); }); } public async audioWhatsapp(data: SendAudioDto, isChatwoot = false) { this.logger.verbose('Sending audio whatsapp'); if (!data.options?.encoding && data.options?.encoding !== false) { data.options.encoding = true; } if (data.options?.encoding) { const convert = await this.processAudio(data.audioMessage.audio, data.number); if (typeof convert === 'string') { const audio = fs.readFileSync(convert).toString('base64'); const result = this.sendMessageWithTyping( data.number, { audio: Buffer.from(audio, 'base64'), ptt: true, mimetype: 'audio/mp4', }, { presence: 'recording', delay: data?.options?.delay }, isChatwoot, ); fs.unlinkSync(convert); this.logger.verbose('Converted audio deleted'); return result; } else { throw new InternalServerErrorException(convert); } } return await this.sendMessageWithTyping( data.number, { audio: isURL(data.audioMessage.audio) ? { url: data.audioMessage.audio } : Buffer.from(data.audioMessage.audio, 'base64'), ptt: true, mimetype: 'audio/ogg; codecs=opus', }, { presence: 'recording', delay: data?.options?.delay }, isChatwoot, ); } public async buttonMessage(data: SendButtonDto) { this.logger.verbose('Sending button message'); const embeddedMedia: any = {}; let mediatype = 'TEXT'; if (data.buttonMessage?.mediaMessage) { mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; } const btnItems = { text: data.buttonMessage.buttons.map((btn) => btn.buttonText), ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), }; if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); } return await this.sendMessageWithTyping( data.number, { buttonsMessage: { text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, footerText: data.buttonMessage?.footerText, buttons: data.buttonMessage.buttons.map((button) => { return { buttonText: { displayText: button.buttonText, }, buttonId: button.buttonId, type: 1, }; }), headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], [embeddedMedia?.mediaKey]: embeddedMedia?.message, }, }, data?.options, ); } public async locationMessage(data: SendLocationDto) { this.logger.verbose('Sending location message'); return await this.sendMessageWithTyping( data.number, { locationMessage: { degreesLatitude: data.locationMessage.latitude, degreesLongitude: data.locationMessage.longitude, name: data.locationMessage?.name, address: data.locationMessage?.address, }, }, data?.options, ); } public async listMessage(data: SendListDto) { this.logger.verbose('Sending list message'); return await this.sendMessageWithTyping( data.number, { listMessage: { title: data.listMessage.title, description: data.listMessage.description, buttonText: data.listMessage?.buttonText, footerText: data.listMessage?.footerText, sections: data.listMessage.sections, listType: 1, }, }, data?.options, ); } public async contactMessage(data: SendContactDto) { this.logger.verbose('Sending contact message'); const message: proto.IMessage = {}; const vcard = (contact: ContactMessage) => { this.logger.verbose('Creating vcard'); let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; if (contact.organization) { this.logger.verbose('Organization defined'); result += `ORG:${contact.organization};\n`; } if (contact.email) { this.logger.verbose('Email defined'); result += `EMAIL:${contact.email}\n`; } if (contact.url) { this.logger.verbose('Url defined'); result += `URL:${contact.url}\n`; } if (!contact.wuid) { this.logger.verbose('Wuid defined'); contact.wuid = this.createJid(contact.phoneNumber); } result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; this.logger.verbose('Vcard created'); return result; }; if (data.contactMessage.length === 1) { message.contactMessage = { displayName: data.contactMessage[0].fullName, vcard: vcard(data.contactMessage[0]), }; } else { message.contactsArrayMessage = { displayName: `${data.contactMessage.length} contacts`, contacts: data.contactMessage.map((contact) => { return { displayName: contact.fullName, vcard: vcard(contact), }; }), }; } return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); } public async reactionMessage(data: SendReactionDto) { this.logger.verbose('Sending reaction message'); return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { reactionMessage: { key: data.reactionMessage.key, text: data.reactionMessage.reaction, }, }); } // Chat Controller public async whatsappNumber(data: WhatsAppNumberDto) { this.logger.verbose('Getting whatsapp number'); const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { let jid = this.createJid(number); if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); if (!group) throw new BadRequestException('Group not found'); onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); } else { jid = !jid.startsWith('+') ? `+${jid}` : jid; const verify = await this.client.onWhatsApp(jid); const result = verify[0]; if (!result) { onWhatsapp.push(new OnWhatsAppDto(jid, false)); } else { onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); } } } return onWhatsapp; } public async markMessageAsRead(data: ReadMessageDto) { this.logger.verbose('Marking message as read'); try { const keys: proto.IMessageKey[] = []; data.read_messages.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 messages = await this.fetchMessages({ where: { key: { remoteJid: number, }, owner: this.instance.name, }, }); 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) { this.logger.verbose('Archiving chat'); 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 deleteMessage(del: DeleteMessage) { this.logger.verbose('Deleting message'); 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) { this.logger.verbose('Getting base64 from media message'); 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)); } this.logger.verbose('Downloading media 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); if (convertToMp4 && typeMessage === 'audioMessage') { this.logger.verbose('Converting audio to mp4'); const number = msg.key.remoteJid.split('@')[0]; const convert = await this.processAudio(buffer.toString('base64'), number); if (typeof convert === 'string') { const audio = fs.readFileSync(convert).toString('base64'); this.logger.verbose('Audio converted to mp4'); const result = { mediaType, fileName: mediaMessage['fileName'], caption: mediaMessage['caption'], size: { fileLength: mediaMessage['fileLength'], height: mediaMessage['height'], width: mediaMessage['width'], }, mimetype: 'audio/mp4', base64: Buffer.from(audio, 'base64').toString('base64'), }; fs.unlinkSync(convert); this.logger.verbose('Converted audio deleted'); this.logger.verbose('Media message downloaded'); return result; } } this.logger.verbose('Media message downloaded'); return { mediaType, fileName: mediaMessage['fileName'], caption: mediaMessage['caption'], size: { fileLength: mediaMessage['fileLength'], height: mediaMessage['height'], width: mediaMessage['width'], }, mimetype: mediaMessage['mimetype'], base64: buffer.toString('base64'), }; } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } } public async fetchContacts(query: ContactQuery) { this.logger.verbose('Fetching contacts'); if (query?.where) { query.where.owner = this.instance.name; if (query.where?.id) { query.where.id = this.createJid(query.where.id); } } else { query = { where: { owner: this.instance.name, }, }; } return await this.repository.contact.find(query); } public async fetchMessages(query: MessageQuery) { this.logger.verbose('Fetching messages'); if (query?.where) { if (query.where?.key?.remoteJid) { query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); } query.where.owner = this.instance.name; } else { query = { where: { owner: this.instance.name, }, limit: query?.limit, }; } return await this.repository.message.find(query); } public async fetchStatusMessage(query: MessageUpQuery) { this.logger.verbose('Fetching status messages'); if (query?.where) { if (query.where?.remoteJid) { query.where.remoteJid = this.createJid(query.where.remoteJid); } query.where.owner = this.instance.name; } else { query = { where: { owner: this.instance.name, }, limit: query?.limit, }; } return await this.repository.messageUpdate.find(query); } public async fetchChats() { this.logger.verbose('Fetching chats'); return await this.repository.chat.find({ where: { owner: this.instance.name } }); } public async fetchPrivacySettings() { this.logger.verbose('Fetching privacy settings'); 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) { this.logger.verbose('Updating privacy settings'); try { await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); this.logger.verbose('Read receipts privacy updated'); await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); this.logger.verbose('Profile picture privacy updated'); await this.client.updateStatusPrivacy(settings.privacySettings.status); this.logger.verbose('Status privacy updated'); await this.client.updateOnlinePrivacy(settings.privacySettings.online); this.logger.verbose('Online privacy updated'); await this.client.updateLastSeenPrivacy(settings.privacySettings.last); this.logger.verbose('Last seen privacy updated'); await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); this.logger.verbose('Groups add privacy updated'); this.reloadConnection(); return { update: 'success', data: { readreceipts: settings.privacySettings.readreceipts, profile: settings.privacySettings.profile, status: settings.privacySettings.status, online: settings.privacySettings.online, last: settings.privacySettings.last, groupadd: settings.privacySettings.groupadd, }, }; } catch (error) { throw new InternalServerErrorException('Error updating privacy settings', error.toString()); } } public async fetchBusinessProfile(number: string): Promise { this.logger.verbose('Fetching business profile'); try { const jid = number ? this.createJid(number) : this.instance.wuid; const profile = await this.client.getBusinessProfile(jid); this.logger.verbose('Trying to get business profile'); if (!profile) { const info = await this.whatsappNumber({ numbers: [jid] }); return { isBusiness: false, message: 'Not is business profile', ...info?.shift(), }; } this.logger.verbose('Business profile fetched'); return { isBusiness: true, ...profile, }; } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } public async updateProfileName(name: string) { this.logger.verbose('Updating profile name to ' + name); 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) { this.logger.verbose('Updating profile status to: ' + status); 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) { this.logger.verbose('Updating profile picture'); try { let pic: WAMediaUpload; if (isURL(picture)) { this.logger.verbose('Picture is url'); const timestamp = new Date().getTime(); const url = `${picture}?timestamp=${timestamp}`; this.logger.verbose('Including timestamp in url: ' + url); pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; this.logger.verbose('Getting picture from url'); } else if (isBase64(picture)) { this.logger.verbose('Picture is base64'); pic = Buffer.from(picture, 'base64'); this.logger.verbose('Getting picture from base64'); } else { throw new BadRequestException('"profilePicture" must be a url or a base64'); } await this.client.updateProfilePicture(this.instance.wuid, pic); this.logger.verbose('Profile picture updated'); this.reloadConnection(); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating profile picture', error.toString()); } } public async removeProfilePicture() { this.logger.verbose('Removing profile picture'); try { await this.client.removeProfilePicture(this.instance.wuid); this.reloadConnection(); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error removing profile picture', error.toString()); } } // Group public async createGroup(create: CreateGroupDto) { this.logger.verbose('Creating group: ' + create.subject); 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); this.logger.verbose('Group created: ' + id); if (create?.description) { this.logger.verbose('Updating group description: ' + create.description); await this.client.groupUpdateDescription(id, create.description); } if (create?.promoteParticipants) { this.logger.verbose('Prometing group participants: ' + participants); await this.updateGParticipant({ groupJid: id, action: 'promote', participants: participants, }); } this.logger.verbose('Getting group metadata'); 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) { this.logger.verbose('Updating group picture'); try { let pic: WAMediaUpload; if (isURL(picture.image)) { this.logger.verbose('Picture is url'); const timestamp = new Date().getTime(); const url = `${picture.image}?timestamp=${timestamp}`; this.logger.verbose('Including timestamp in url: ' + url); pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; this.logger.verbose('Getting picture from url'); } else if (isBase64(picture.image)) { this.logger.verbose('Picture is base64'); pic = Buffer.from(picture.image, 'base64'); this.logger.verbose('Getting picture from base64'); } else { throw new BadRequestException('"profilePicture" must be a url or a base64'); } await this.client.updateProfilePicture(picture.groupJid, pic); this.logger.verbose('Group picture updated'); return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error update group picture', error.toString()); } } public async updateGroupSubject(data: GroupSubjectDto) { this.logger.verbose('Updating group subject to: ' + data.subject); 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) { this.logger.verbose('Updating group description to: ' + data.description); 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') { this.logger.verbose('Fetching group'); try { return await this.client.groupMetadata(id.groupJid); } catch (error) { if (reply === 'inner') { return; } throw new NotFoundException('Error fetching group', error.toString()); } } public async fetchAllGroups(getParticipants: GetParticipant) { this.logger.verbose('Fetching all groups'); try { const fetch = Object.values(await this.client.groupFetchAllParticipating()); const groups = fetch.map((group) => { const result = { id: group.id, subject: group.subject, subjectOwner: group.subjectOwner, subjectTime: group.subjectTime, 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; } return result; }); return groups; } catch (error) { throw new NotFoundException('Error fetching group', error.toString()); } } public async inviteCode(id: GroupJid) { this.logger.verbose('Fetching invite code for group: ' + 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) { this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); try { return await this.client.groupGetInviteInfo(id.inviteCode); } catch (error) { throw new NotFoundException('No invite info', id.inviteCode); } } public async sendInvite(id: GroupSendInvite) { this.logger.verbose('Sending invite for group: ' + id.groupJid); try { const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); const inviteUrl = inviteCode.inviteUrl; this.logger.verbose('Invite url: ' + 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); } this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); return { send: true, inviteUrl }; } catch (error) { throw new NotFoundException('No send invite'); } } public async revokeInviteCode(id: GroupJid) { this.logger.verbose('Revoking invite code for group: ' + 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) { this.logger.verbose('Fetching participants for group: ' + id.groupJid); try { const participants = (await this.client.groupMetadata(id.groupJid)).participants; return { participants }; } catch (error) { throw new NotFoundException('No participants', error.toString()); } } public async updateGParticipant(update: GroupUpdateParticipantDto) { this.logger.verbose('Updating participants'); 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) { this.logger.verbose('Updating setting for group: ' + update.groupJid); 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) { this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); 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) { this.logger.verbose('Leaving group: ' + 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()); } } }